명령어

임정빈·2024년 4월 11일

명령어는 컴퓨터를 실질적으로 작동시키는 매우 중요한 정보다.
모든 소스 코드는 컴퓨터 내부에서 명령어로 변환되는데, 이번 포스팅에서는 프로그래밍 언어가 어떻게 명령어가 되어 실행되는지 알아보겠다.




1. 소스 코드와 명령어

1.1 고급 언어와 저급 언어

컴퓨터는 C, C++, Java, Python과 같은 프로그래밍 언어를 이해할 수 있을까?
→ 그렇지 않다.

고급 언어(high-level programming language):
우리가 프로그램을 만들 때 사용하는 사람이 이해하고 작성하기 쉽게 만들어진 언어

저급 언어(low-level programming language):
컴퓨터가 직접 이해하고 실행할 수 있는 언어

고급 언어로 작성된 소스 코드가 실행되려면 반드시 저급 언어(명령어)로 변환되어야 한다.

저급 언어에는 기계어와 어셈블리어가 있다.

기계어(machine code): 0과 1의 명령어 비트로 이루어진 언어

컴퓨터만을 위해 만들어진 언어이기 때문에 사람이 이해하기 어렵다.
그래서 등장한 저급 언어가 어셈블리어(assembly language)이다.

저급언어를 알아야 할까?
→ 어셈블리어를 작성하거나 관찰할 일이 거의 없는 개발자도 있다.
→ 하드웨어와 밀접하게 맞닿아 있는 프로그램을 개발하는 임베디드 개발자, 게임 개발자, 정보 보안 분야 등의 개발자는 어셈블리어를 많이 이용한다.



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

고급 언어를 저급 언어로 변환하는 방식은 컴파일방식과 인터프리터방식이 있다.

컴파일 언어

컴파일 언어: 컴파일러에 의해 소스 코드 전체가 저급 언어로 변환되어 실행되는 고급 언어

  • 컴파일(compile): 컴파일 언어로 작성된 코드를 저급 언어로 변환하는 과정
  • 컴파일러 : 컴파일을 수행해 주는 도구
  • 대표적인 컴파일 언어로는 C
  • 소스 코드내에서 오류를 하나라도 발견하면 컴파일에 실패한다.


인터프리터 언어

인터프리터(interpreter) 언어: 인터프리터에 의해 소스 코드가 한 줄씩 실행되는 고급 언어

  • 대표적인 인터프리터 언어로 Python
  • 인터프리터: 소스 코드를 한 줄씩 저급 언어로 변환하여 실행해 주는 도구
  • 소스 코드 전체가 저급 언어로 변환되는 컴파일러와 달리, 인터프리터 언어는 소스코드가 한 줄씩 차례로 실행된다.
    • 소스 코드 전체를 저급 언어로 변환하는 시간을 기다릴 필요가 없다.
    • 소스 코드 N번째 줄에 문법 오류가 있더라도 N-1번째 줄까지는 올바르게 수행된다.

일반적으로 인터프리터 언어는 컴파일 언어보다 느리다.

  • 목적 코드(컴파일을 통해 나온 결과물)는 컴퓨터가 이해하고 실행할 수 있는 저급 언어인 반면, 인터프리터 언어는 소스 코드 마지막에 이를 때 까지 한 줄씩 저급 언어로 해석하며 실행해야 하기 때문이다.

컴파일 언어와 인터프리터 언어의 구분
→ C, C++와 같이 명확한 컴파일 언어가 있으나 현대의 많은 프로그래밍 언어 중에는 컴파일 언어와 인터프리터 언어 간의 경계가 모호한 경우가 많다.
→ Python도 컴파일을 하지 않는 것은 아니다.
→ Java는 저급 언어가 되는 과정에서 컴파일과 인터프리트를 동시에 수행한다.

하나의 프로그램밍 언어가 반드시 둘 중 하나의 방식만으로 작동하지는 않는다.



1.3 목적 파일과 실행 파일

목적 파일: 목적 코드로 이루어진 파일
실행 파일: 실행 코드로 이루어진 파일

  • 윈도우의 .exe 확장자를 가진 파일이 대표적인 실행 파일

목적 코드가 실행 파일이 되기 위해서는 링킹이라는 작업이 필요하다.

링킹 작업이 필요한 이유

  1. helper.c 안에는 'HELPER_더하기'라는 기능이 구현되어 있다.
  2. main.c는 helper.c에 구현된 'HELPER_더하기'기능과 프로그래밍 언어가 기본으로 제공하는 '화면 출력'이라는 기능을 가져다 사용한다.
  3. 이들을 컴파일 한 목적 파일을 helper.o와 main.o라고 칭한다.

  1. main.o(목적 파일)는 main.c 내용을 그대로 저급 언어로 변환된 파일일 뿐 main.c에 없는 'HELPER더하기'나 '화면출력'은 어떻게 실행하는지 모른다.
  2. main.o가 실행되려면 main.o에 없는 외부 기능들을 main.o와 연결 짓는 작업이 필요하다.
  • 이러한 직업을 링킹(linking)이라고 한다.

링킹 작업까지 거치면 비로소 하나의 실행 파일이 만들어진다.




2. 명령어의 구조

2.1 연산 코드와 오퍼랜드

명령어는 연산 코드와 오퍼랜드로 구성되어 있다.

  • 연산 코드(operation code): 명령어가 수행해야할 연산, 연산자
  • 오퍼랜드(operand): 연산에 사용할 데이터 또는 연산에 사용할 데이터의 위치(피연산자)

붉은 글씨가 연산 코드, 검은 글씨가 오퍼랜드다.

오퍼랜드

  • 오퍼랜드 필드에는 숫자와 문자 등을 나타내는 데이터 또는 메모리나 레지스터 주소가 올 수 있다.
  • 오퍼랜드 필드에는 연산에 사용할 데이터를 직접 명시하기보다는, 많은 경우 연산에 사용할 데이터가 저장된 위치, 즉 메모리 주소나 레지스터 이름이 담긴다.
    • 오퍼랜드 필드를 주소 필드라고 부르기도 한다.
  • 오퍼랜드는 명령어 안에 하나도 없을 수도 있고, 여러개 있을 수도 있다.

오퍼랜드가 하나도 없는 명령어를 0-주소 명령어라고 하고, 오퍼랜드가 하나인 명령어를 1-주소 명령어, 두 개인 명령어를 2-주소 명령어, 세 개인 명령어를 3-주소 명령어라고 한다.


연산 코드

연산 코드 종류는 많지만, 연산 코드 유형은 크게 네 가지로 나눌 수 있다.

  • 데이터 전송
  • 산술/논리 연산
  • 제어 흐름 변경
  • 입출력 제어

명령어의 종류와 생김새는 CPU마다 다르기 때문에 연산 코드의 종류와 생김새 또한 CPU마다 다르다.

  • CPU가 공통으로 이해하는 대표적인 연산 코드의 종류 정도로만 이해해도 무방하다.

데이터 전송

  • MOVE: 데이터를 옮겨라
  • STORE: 메모리에 저장해라
  • LOAD(FETCH): 메모리에 CPU로 데이터를 가져와라
  • PUSH: 스택에 데이터를 저장해라
  • POP: 스택의 최상단 데이터를 가져와라

산술/논리 연산

  • ADD / SUBTRACT / MULTIPLY / DIVIDE: 사칙연산을 수행해라
  • INCREMENT / DECREMENT: 오퍼랜드에 1을 더하라 / 빼라
  • AND / OR / NOT: AND / OR / NOT 연산을 수행하라
  • COMPARE: 두 개의 숫자 또는 TRUE / FALSE 값을 비교하라

제어 흐름 변경

  • JUMP: 특정 주소로 실행 순서를 옮겨라
  • CONDITIONAL JUMP: 조건에 부합할 때 특정 주소로 실행 순서를 옮겨라
  • HALT: 프로그램의 실행을 멈춰라
  • CALL: 되돌아올 주소를 저장한 채 특정 주소로 실행 순서를 옮겨라
  • RETURN: CALL을 호출할 때 저장했던 주소로 돌아가라

입출력 제어

  • READ(INPUT): 특정 입출력 장치로부터 데이터를 읽어라
  • WRITE(OUTPUT): 특정 입출력 장치로 데이터를 써라
  • START IO: 입출력 장치를 시작하라
  • TEST IO: 입출력 장치의 상태를 확인하라


2.2 주소 지정 방식

오퍼랜드 필드에 메모리나 레지스터의 주소를 담는 이유
→ 명령어의 길이 때문이다.

명령어의 크기가 16비트, 연산 코드 필드가 4비트인 3-주소 명령어에서는 오퍼랜드 필드당 4비트 정도밖에 남지 않는다.

하지만 만약 한 주소에 16비트를 저장할 수 있는 메모리 안에 데이터를 저장하고, 오퍼랜드 필드 안에 해당 메모리 주소를 명시한다면 16비트로 늘어난다.

유효 주소(effecttive address): 연산 코드에 사용할 데이터가 저장된 위치
주소 지정 방식(addressing mode): 연산에 사용할 데이터 위치를 찾는 방법


즉시 주소 지정 방식(immediate addressing mode)

연산에 사용할 데이터를 오퍼랜드 필드에 직접 명시하는 방식

  • 가장 간단한 형태의 주소 지정 방식
  • 표현할 수 있는 데이터의 크기가 작아지는 단점이 있다.
  • 다른 주소 지정 방식들보다 빠르다.


직접 주소 지정 방식(direct addressing mode)

오퍼랜드 필드에 유효 주소를 직접적으로 명시하는 방식

  • 즉시 주소 지정 방식보다는 데이터의 크기가 커졌다.
  • 표현할 수 있는 유효 주소에 제한이 생길 수 있다.


간접 주소 지정 방식(indirect addressing mode)

유효 주소의 주소를 오퍼랜드 필드에 명시한다.

  • 직접 주소 지정 방식보다 표현할 수 있는 유효 주소의 범위가 늘어났다.
  • 두 번의 메모리 접근이 필요하기 때문에 주소 지정 방식들보다 일반적으로 느리다.


레지스터 주소 지정 방식(register addressing mode)

직접 주소 지정 방식과 비슷하게 연산에 사용할 데이터를 저장한 레지스터를 오퍼랜드 필드에 직접 명시하는 방법

  • 일반적으로 CPU 외부에 있는 메모리에 접근하는 것 보다 CP 내부에 있는 레지스터에 접근하는 것이 더 빠르다.
    • 직접 주소 지정 방식보다 빠르게 데이터에 접근할 수 있다.
  • 직접 주소 지정 방식과 비슷한 문제로, 표현할 수 있는 레지스터 크기에 제한이 생길 수 있다.


레지스터 간접 주소 지정 방식(register indirect addressing mode)

연산에 사용할 데이터를 메모리에 저장하고, 유효 주소를 저장한 레지스터를 오퍼랜드 필드에 명시하는 방법

  • 간접 주소 지정 방식보다 빠르다.
  • 메모리에 접근하는 것이 레지스터에 접근하는 것보다 더 느리기 때문에 접근 횟수가 한 번으로 줄어든다.




마치며

책에서 설명했듯이 나는 고급 언어의 지식만 갖고 있었는데, 저급 언어로서의 명령어와 다양한 주소 지정 방식의 구조를 알게되어서 흥미로웠다.

출처
혼자 공부하는 컴퓨터 구조+운영체제

profile
신입개발자

0개의 댓글