본문 바로가기
Computer Science/컴퓨터구조(Computer Architecture)

컴퓨터 정보 표현(명령어)

by 컴돈AI 2024. 3. 27.

목차

    고급 언어(high-level)와 저급 언어(low-level)

    • 고급 언어(high-level)
      • 사람을 위한 언어
      • 우리가 알고 있는 대부분의 프로그래밍 언어
      • C, C++, Java, Python 등
      • 모든 소스 코드는 컴퓨터 내부에서 명령어로 변환됨.
        • 고급 언어 -> 저급 언어로 변환
    • 저급 언어(low-level)
      • 저급 언어는 명령어로 이루어져 있음.
      • 컴퓨터가 이해하고 실행할 수 있는 언어는 오직 저급 언어뿐
      • 저급 언어 종류
        • 기계어
          • 0과 1의 명령어 비트로 이루어진 언어.
          • 실제로는 0과 1로만 처리되지만, 가독성으로 인해 표현할 때는 십육진수로 표현하기도 함
        • 어셈블리어
          • 0과 1로 표현된 기계어를 읽기 편한 형태로 번역한 언어
          • 예시
            • (기계어) 0101 0101 -> (어셈블리어) push rbp
            • (기계어) 0101 1101 -> (어셈블리어) pop rbp
          • 하드웨어와 밀접하게 맞닿아 있는 프로그램을 개발하는 임베디드 개발자, 게임 개발자, 정보 보안 분야 등의 개발자는 어셈블리어를 많이 이용합니다. 

    컴파일 언어와 인터프리터 언어

    • 고급 언어가 저급 언어로 변환되는 대표적인 방법
      • 컴파일 방식, 인터프리트 방식
    • 컴파일 언어
      • 컴파일 언어는 컴파일러에 의해 소스 코드 전체가 저급 언어로 변환되어 실행되는 고급 언어
        • 컴파일러 : 컴파일을 수행해 주는 도구
        • 컴파일 : 컴파일 언어로 작성된 소스 코드가 저급 언어로 변환되는 과정
      • 소스 코드(고급언어) -> 컴파일러(컴파일) -> 목적 코드(저급 언어)
        • 링킹 과정을 통해 하나 이상의 목적 파일들과 필요한 라이브러리를 결합하여 단일 실행 파일을 생성
      • 소스 코드 전체가 한번에 저급 언어로 변환되어 소스 코드 내에 오류가 하나라도 있으면 컴파일이 불가능함.
      • 대표적인 컴파일 언어
        • C, C++
    • 인터프리터 언어
      • 인터프리터에 의해 소스 코드가 한 줄씩 실행되는 고급 언어
        • 인터프리터 : 소스 코드를 한 줄씩 저급 언어로 변환하여 실행해 주는 도구
      • 인터프리터 언어는 컴퓨터와 대화하듯 소스 코드를 한 줄씩 실행하기 때문에 소스 코드 전체를 저급 언어로 변환하는 시간을 기다릴 필요가 없음. 
      • 소스 코드를 한 줄씩 실행하기 때문에 소스 코드 N번째 줄에 문법 오류가 있더라도 N-1번째 줄까지는 올바르게 수행됨.
      • 대표적인 인터프리터 언어
        • Python
    • 컴파일 언어의 특성과 인터프리터 언어의 특성을 결합한 언어
      • 대표적 예시
        • Java
      • Java 소스 코드는 먼저 바이트코드라는 중간 형태로 컴파일되며, 이 바이트코드는 Java 가상머신(JVM) 위에서 실행됨.
      • JVM은 바이트코드를 해당 운영 체제에서 실행할 수 있는 코드로 변환(인터프리터 혹은 JIT 컴파일러를 통해)
      • Java는 이러한 방식으로 다양한 플랫폼에서 동일한 코드를 실행할 수 있는 "한 번 작성하면 어디서나 실행(WORA, Write Once, Run Anywhere)"의 장점을 가지고 있음.
    • C/C++ , Python, Java 비교
      • C/C++의 경우 다양한 플랫폼에서 실행될 수 있는 능력을 가지고 있지만, 이를 위해선 각 플랫폼(운영체제, 하드웨어 아키텍처)에 맞는 컴파일 과정이 필요. 즉 소스 코드는 동일하나 실행 파일은 플랫폼마다 다르게 생성됩니다. (플랫폼 종속적)
        • C/C++는 컴파일되면, 컴파일된 특정 OS와 하드웨어에서만 실행가능. (OS마다 다른 실행 파일 생성)
      • Python과 같은 인터프리터 언어는 플랫폼 독립적
        • Python 인터프리터가 설치된 어떤 시스템에서도 python 소스 코드(.py 파일) 실행 가능
      • Java는 한번 컴파일하면, 그 바이트코드를 어떤 플랫폼의 JVM에서도 실행할 수 있기 때문에 진정한 의미에서의 "플랫폼 독립적 실행"이 가능
        • 즉, java는 한번 컴파일하면, 어떤 OS에서도 실행 가능한 바이트코드 생성(실행은 JVM을 통해 이루어짐)
    • 컴파일 언어와 인터프리터 언어의 속도 차이 
      • 컴파일 언어가 인터프리터 언어보다 빠름
      • 컴파일을 통해 나온 결과물(실행 파일), 즉 목적 코드는 컴퓨터가 이해하고 실행할 수 있는 저급 언어인 반면, 인터프리터 언어는 소스 코드 마지막에 이를 때까지 한 줄 한 줄 저급언어로 해석하며 실행해야 하기 때문

    목적 파일 VS 실행 파일 (컴파일언어)

    • 목적 파일(Object File)
      • 목적 파일은 컴파일러에 의해 소스 코드가 컴파일된 후 생성되는 중간 파일.
        • 다른 목적 파일이나 라이브러리와 링크될 준비가 되어 있는 상태
      • 이 파일은 기계어 코드를 포함하지만, 아직 실행 가능한 프로그램으로 완성되지 않음.
      • 목적 파일에는 함수, 변수 등의 심볼과 함께 그 심볼들이 실제 메모리에서 어디에 위치해야 할지에 대한 정보가 불완전한 상태로 존재.
      • 일반적으로 .o 또는 .obj 확장자를 가짐
    • 실행 파일(Executable File)
      • 실행 파일은 링커(Linker)에 의해 하나 이상의 목적 파일과 필요한 라이브러리가 함께 링크된 최종 결과물
        • 최종 사용자가 직접 실행할 수 있는 완성된 프로그램
      • 이 파일은 운영 체제가 직접 실행할 수 있는 완전한 형태의 프로그램을 포함
      • 형식은 운영 체제에 따라 다르며, Windows에서는 .exe, Linux 및 macOS에서는 특정 확장자 없이 실행 권한이 있는 파일이 됨
    • 링킹(Linking) 과정 
      • 링킹은 목적 파일들과 라이브러리들을 결합하여 실행 파일을 생성하는 과정.
      • 링킹에는 두 가지 주요 유형이 있습니다:
        • 정적 링킹(Static Linking)
          • 링킹 시점에 모든 필요한 코드와 데이터를 하나의 실행 파일 안에 포함시킴.
          • 이 방식은 실행 파일이 독립적으로 실행될 수 있도록 하지만, 파일 크기가 커질 수 있음.
        • 동적 링킹(Dynamic Linking)
          • 실행 파일이 생성될 때 필요한 코드의 일부를 외부 라이브러리에 두고, 프로그램이 실행될 때 해당 라이브러리를 불러오는 방식
          • 이 방식은 파일 크기를 줄이고, 라이브러리를 공유할 수 있게 하지만, 실행 시 모든 필요한 라이브러리가 시스템에 존재해야 함

    명령어 구조와 주소 지정 방식

    • 저급 언어(기계어, 어셈블리어)는 명령어들로 이루어져 있음.
      • 기계어
        • 0과 1의 명령어 비트로 이루어진 언어.
        • 실제로는 0과 1로만 처리되지만, 가독성으로 인해 표현할때는 십육진수로 표현하기도 함
      • 어셈블리어
        • 0과 1로 표현된 기계어를 읽기 편한 형태로 번역한 언어

    명령어 구조

    • 명령어 구조
      • 연산 코드(연산자)와 오퍼랜드(피연산자)로 구성되어 있음.
        • 연산코드(operation code) : 명령어가 수행할 연산
          • 기본적인 연산 코드 : 데이터 전송, 산술/논리 연산, 제어 흐름 변경, 입출력 제어
        • 오퍼랜드(operand) : 연산에 사용할 데이터(또는 연산에 사용할 데이터가 저장된 위치)
          • 데이터 또는 메모리 주소나 레지스터 이름(주소)
          • 오퍼랜드는 명령어 안에 하나도 없을 수도 있고, 한 개만 있을 수도 있고, 두 개 또는 세 개 등 여러 개가 있을 수 있음
            • 오퍼랜드가 하나도 없는 명령어 : 0-주소 명령어
            • 오퍼랜드가 1개인 명령어 : 1-주소 명령어
            • 오퍼랜드가 2개인 명령어 : 2-주소 명령어
            • 오퍼랜드가 3개인 명령어 : 3-주소 명령어
      • 예시
        • mov eax, 5
          • eax 레지스터에 5를 저장.
          • 여기서 'mov'는 연산 코드, 'eax'와 '5'는 오퍼랜드
            • "exe"는 CPU 레지스터 중 하나
        • add eax, 3
          • eax 레지스터의 값을 3만큼 증가
          • 'add'가 연산 코드이며, 'eax'와 '3'은 오퍼랜드

    주소 지정 방식

    • 주소 지정 방식
      • 오퍼랜드에는 데이터를 직접 저장하기보다 메모리 주소나 레지스터 이름을 담는 경우가 많음.
        • 그 이유는 명령어 크기를 효율적으로 사용하기 위함.
        • 한 주소에 16비트를 저장할 수 있는 메모리가 있다고 할 경우, 하나의 명령어의 16비트 중 연산 코드가 4비트라고 가정한다면, 1-주소 명령어의 경우 오퍼랜드 필드는 12비트, 3-주소 명령어의 경우 오퍼랜드 필드가 4비트로 표현할 수 있는 정보의 가짓수가 작아짐.
          • 이를 해결하기 위해 오퍼랜드 필드 안에는 데이터 보다 메모리 주소나 레지스터 이름을 담는 경우가 많음.
            • 오퍼랜드 필드에 메모리 주소를 담을 경우 한 주소에 16비트를 저장할 수 있기 때문에 훨씬더 많은 정보를 지정할 수 있음.
          • 대신 메모리나 레지스터에 접근해야하기때문에 직접 데이터를 명령어에 포함하는 방식보다 느림
        • 예시
          • 명령어의 크기 16비트, 연산 코드 필드가 4비트인 3-주소 명령어의 경우 오퍼랜드 필드당 4비트 정도밖에 남지 않음. 이경우 하나의 오퍼랜드 필드로 표현할 수 있는 정보의 가짓수는 2^4=16개밖에 없음.
          • 만약 한 주소에 16비트를 저장할 수 있는 메모리가 있다고 할 경우, 하나의 오퍼랜드 필드 안에서 해당 메모리 주소를 명시한다면 표현할 수 있는 정보의 가짓수가 2^16으로 확 커지게 됨.
          • 마찬가지로 데이터 대신 레지스터 이름을 명시하게 되면 표현할 수 있는 가짓수가 해당 레지스터가 저장할 수 있는 공간만큼 커짐.
        • 위 예시에서 연산의 대상이 되는 데이터가 저장된 위치를 유효 주소라고 함.
      • 명령어의 경우 연산 코드로 인해 데이터를 표현하는 오퍼랜드 크기 작아짐. 
    • 주소 지정 방식 종류
      • (한 주소에 16비트를 저장할 수 있는 메모리, 4비트의 연산 코드를 가지는 3-주소명령어 가정) 
      • 즉시 주소 지정 방식
        • 연산에 사용할 데이터를 오퍼랜드에 직접 명시하는 방식
        • 데이터를 메모리나 레지스터로부터 찾는 과정이 없기 때문에 속도가 빠름.
        • 하나의 데이터는 4비트로 표현 가능
      • 직접 주소 지정 방식
        • 메모리에 있는 데이터의 유효 주소를 오퍼랜드에 직접 명시하는 방식
        • 하나의 데이터는 16비트로 표현가능 (단, 접근 가능한 메모리 유효 주소 범위는 4비트)
      • 간접 주소 지정 방식
        • 메모리에 있는 데이터의 유효주소의 주소를 오퍼랜드에 명시하는 방식
          • 오퍼랜드(원하는 데이터의 메모리 유효주소의 주소)-> 원하는 데이터의 메모리 유효주소 -> 원하는 데이터 
        • 하나의 데이터는 16비트로 표현가능 (단, 접근 가능한 메모리 유효 주소 범위는 16비트)
      • 레지스터 주소 지정 방식
        • 직접 주소 지정 방식과 비슷하게 데이터를 저장한 레지스터를 오퍼랜드에 직접 명시하는 방식
        • 하나의 데이터는 레지스터크기로 표현가능 (단, 접근 가능한 레지스터 주소 범위는 4비트)
      • 레지스터 간접 주소 지정 방식
        • 간접 주소 지정 방식과 유사하게 연산에 사용할 데이터를 메모리에 저장하고, 그 주소(유효 주소)를 저장한 레지스터를 오퍼랜드 필드에 명시하는 방법
          • 오퍼랜드(원하는 데이터의 메모리 유효주소의 레지스터)-> 원하는 데이터의 메모리 유효주소 -> 원하는 데이터 
        • 하나의 데이터는 16비트로 표현가능 (단, 접근 가능한 메모리 유효 주소 범위는 레지스터크기)
      • 스택 주소 지정 방식
        • 스택 주소 지정 방식은 스택과 스택 포인터를 이용한 주소 지정 방식.
        • 스택 포인터란 스택의 꼭대기를 가리키는 레지스터
          • 스택은 메모리 안에 존재.
          • (정확히는 메모리 안에 스택처럼 사용할 영역이 정해져 있음 = 스택 영역(이 영역은 다른 주소 공간과는 다르게 스택처럼 사용하기로 암묵적으로 약속된 영역)
      • 변위 주소 지정 방식
        • 변위 주소 지정 방식이란 오퍼랜드 필드의 값(변위)과 특정 레지스터의 값을 더하여 유효 주소를 얻어내는 주소 지정 방식
        • 그래서 변위 주소 지정 방식을 사용하는 명령어는 연산 코드 필드, 어떤 레지스터의 값을 더할지를 나타내는 레지스터 필드, 그리고 주소를 담고 있는 오퍼랜드 필드가 있음.
        • 변위 주소 지정 방식은 오퍼랜드 필드의 주소와 어떤 레지스터를 더하는지에 따라 상대 주소 지정 방식, 베이스 레지스터 주소 지정 방식 등으로 나뉨.
          • 상대 주소 지정방식
            • 오퍼랜드와 프로그램 카운터의 값을 더하여 유효 주소를 얻는 방식
            • 만약 오퍼랜드가 음수, 가령 -3 이였다면 CPU는 읽어들이기로 한 명령어로부터 세 번째 이전 번지로 접근 (한마디로 실행하려는 명령어의 세 칸 이전 번지 명령어를 실행)
            • 만약 3 이였다면 세번째 이후 번지로 접근
            • 상대 주소 지정 방식은 프로그래밍 언어의 if문과 유사하게 모든 코드를 실행하는 것이 아닌, 분기하여 특정 주소의 코드를 실행할 때 사용됨.
          • 베이스 레지스터 주소 지정 방식
            • 오퍼랜드와 베이스 레지스터의 값을 더하여 유효 주소를 얻는 방식
            • 여기서 베이스 레지스터는 "기준 주소", 오퍼랜드는 "기준 주소로부터 떨어진 거리"로서의 역할을 함.
            • 베이스 레지스터에 200이라는 값이 있고 오퍼랜드가 40이라면 이는 기준 주소 200번지로부터 40만큼 떨어진 240번지로 접근하라는 의미

    출처