[밑바닥부터 만드는 컴퓨팅] 04장 기계어

9 minute read

개요

해당 내용은 ‘밑바닥부터 만드는 컴퓨팅 시스템’ 책의 4장 내용을 정리하였습니다.

1. 배경

해당 장에서는 언어에 대해 설명합니다. 하드웨어 플랫폼에 대한 자세한 설명은 다음장에서 설명합니다. 기계어를 일반적으로 설명하는데는 프로세서, 메모리, 레지스터로 설명할 수 있겠습니다.

1.1 기계

기계어는 프로세서와 레지스터들을 이용하여 메모리를 조작할 수 있도록 미리 정의한 규칙이라 할 수 있습니다.

1.1.1 메모리

메모리는 컴퓨터에서 데이터와 명령어를 저장하는 하드웨어 장치들을 대략적으로 통칭하는 장치입니다. (주로 RAM을 사용합니다) 메모리는 word나 location이라 불리는 정해진 폭의 셀들이 연속적으로 배열되어 있으며, 이 셀은 각각의 주소를 가지고 있습니다. 그렇기에 각각의 word는 address를 통해 특정할 수 있습니다. 앞으로 이러한 단어들을 memory, RAM, M 과 같은 용어로 표현됩니다.

1.1.2 프로세서

중앙처리장치 또는 CPU라고 불리는 프로세서는 기초 연산을 수행하는 장치입니다. 산술 및 논리연산, 메모리 접근 연산, 제어, 분기와 같은 연산이 포함된다. 이와 같은 연산의 피연산자들은 선택된 메모리 위치와 레지스터에 있는 바이너리 값이 됩니다.. 연산의 결과값은 선택된 메모리 주소 또는 레지스터에 저장됩니다.

1.1.3 레지스터

메모리 접근 연산은 상대적으로 느리고 긴 명령어가 필요합니다. 그렇기 때문에 대부분의 프로세서는 값 하나를 저장할 수 있는 레지스터를 여러개 두고 있습니다. 레지스터는 프로세서의 바로 옆에 위치합니다. 그렇기때문에 프로세서가 명령어와 데이터를 빠르게 처리할 수 있는 로컬 고속 메모리 역할을 합니다. 레지스터가 있기 때문에 메모리 접근 단계를 줄일 수 있으며, 이로인해 프로그램의 실행 속도가 올라가게 됩니다.

1.2 언어

기계어 프로그램은 명령어를 코드화한 것입니다. 예를 들어 16비트 컴퓨터라고 하면 명령어는 1001010110100111 과 같이 16비트의 바이너리로 구성되어 있습니다. 이러한 명령어가 무엇을 뜻하는지 알려면 하드웨어 플랫폼의 명령 집합을 알아야 합니다.

예를들어 어떤 기계어가 4개의 필드로 명령이 구성되어 있고, 맨 왼쪽에는 CPU 연산 코드, 나머지 세개는 피연산자라고 생각해봅시다. 16비트를 4비트로 나누었을때 맨 처음의 4비트로 CPU의 연산을 결정하고, 나머지 세부분은 피연산자가 될 수 있습니다. CPU 연산을 해석하여 R1 + R2를 수행하여 R3에 저장하라는 해석이 될 수 있습니다.

2진 코드는 암호같아보이기 때문에 보통은 연상기호를 사용하도록 되어 있습니다. 예를들어 연산코드 1010이 ADD라고 하고 레지스터들을 R1, R2와 같은 기호로 표기할 수 있습니다. 그러면 10100001001000111010, 0001, 0002, 0003으로 나눌 수 있으며, 이는 연상기호로 ADD R1 R2 R3 으로 표현할 수 있습니다.

이 표기법을 더 추상화한다면 기호를 읽는것 뿐만 아니라 2진 명령어 대신 기호를 텍스트로 입력하여 프로그램을 작성할 수도 있습니다. 또한 이 명령어들을 텍스트 처리 프로그램으로 분석하여 2진 코드로 변환할 수 있을 것입니다. 이러한 기호 표기법을 어셈블리 언어 또는 어셈블리라고 부르며, 어셈블리를 2진 코드로 번역하는 프로그램을 어셈블러 라고 합니다.

CPU마다 연산이나 레지스터의 종류, 어셈블리 문법 등이 다를 수 있습니다. 하지만 문법이 다르더라도 일반적으로는 비슷한 명령으로 되어있습니다.

1.3 명령

1.3.1 산술 및 논리연산

컴퓨터에서 산술 연산은 더하기, 빼기, 곱하기, 나누기 와 같은 연산을 의미합니다. 뿐만 아니라 컴퓨터는 And, Or, Shift, 반전 등와 같은 비트 논리연산도 필요합니다. 다음은 일반적인 기계어 문법으로 작성된 예시입니다.

ADD R2, R1, R3 // R2<-R1+R3, 여기서 R1,R2,R3는 레지스터다.
ADD R2, R1, foo // R2<-R1+foo, 여기서 foo는 사용자가 정의한 이름으로,
               // foo가 가리키는 메모리 주소의 값을 뜻한다.
AND R1, R1, R2 // R1<-R1과 R2을 비트 단위로 And 연산한 결과

1.3.2 메모리 접근

메모리 접근 명령은 두 부류로 나뉩니다. 산술 및 논리연산 명령은 레지스터 외에도 특정 메모리 주소에 접근할 수 있습니다. 또는 어느 컴퓨터나 지원하는 load, store 명령을 이용해 레지스터와 메모리 사이에 데이터를 이동시키는 명령이 있습니다.

메모리 접근 명령에는 주소 지정 모드가 있습니다. 컴퓨터 CPU 마다 다를 수 있지만 일반적으로 아래의 방법들을 지원합니다.

  • 직접 주소 지정 방식
    • 특정 주소에 직접 쓰거나, 그 주소를 나타내는 기호를 활용
      LOAD R1, 67 // R1<-Memory [67]
              // 또는 bar가 에모리 주소 67을 나타낸다고 가정하면
      LOAD R1, bar // R1<-Memory (67)
      
  • 즉시 주소 지정 방식
    • 명령어 코드에 있는 상수를 불러오는데 쓰임
      LOAD R1,67 // R1<-67
      
  • 간접 주소 지정 방식
    • 명령어에 메모리 주소가 하드코딩되지 않고, 필요한 주소값을 저장하고 있는 메모리 위치를 참조하는 방법
    • 이 주소방식은 poointer을 사용할 때 쓰임
      // x = foo[j] 또는 x = *(foo + j)
      ADD R1, foo, j // R1 <- foo+j
      LOAD R2, R1    // R2 <- Memory[R1]
      STR R2, x      // x <- R2
      

1.3.3 제어 흐름

프로그램은 보통 명령어를 순차작으로 실행합니다. 하지만 상황에 따라 다음 명령어가 아닌 특정 위치의 명령어를 수행하기도 합니다. if, switch 문과 같은경우 상황에 맞게 명령어를 분기해줘야합니다. 또한 for, while 과 같은 반복문은 loop의 끝에 오면 다시 위의 명령어를 실행하도록 해야합니다.

모든 기계어들은 이런 프로그래밍 구조를 지원하기 위해 프로그램 내에 선택된 주소의 명령으로 점프하는 기능을 가지고 있습니다.

2. 핵 기계어 명세

2.1 개요

핵 컴퓨터는 폰 노이만 플랫폼입니다.

CPU, 명령용과 데이터용으로 분리된 두 개의 메모리 모듈, 두개의 메모리 매핑 I/O 장치로 구성된 16비트의 장치입니다.

2.1.1 메모리 주소 공간

핵에서는 주소 공간이 명령어 메모리와 데이터 메모리라는 두 부분으로 분리되있습니다. 폭이 16비트인 두 메모리 공간은 15비트로 최대 32K 개의 16비트 워드를 주소에 할당할 수 있습니다.

핵의 CPU는 명령어 메모리에 존재하는 프로그램만을 수행합니다. 명령어 메모리는 읽기전용입니다. 내부적인 방법으로는 프로그램을 로드할 수 없고 외부적인 방법으로만 가능합니다. 프로그램을 미리 ROM에 입력하는 방식입니다.

핵의 하드웨어 시뮬레이터에서는 이런 ROM 교체 방식을 시뮬레이션하기 위해 텍스트파일에 있는 기계어 프로그램을 명령어 메모리 로드하는 기능을 제공하게 됩니다.

2.1.2 레지스터

핵 시스템에는 D와 A라 불리는 16비트의 레지스터 두개가 있습니다. 이 레지스터들은 A=D-1과 같은 산술 명령이나 D=!A같은 논리 명령으로 직접 조작됩니다. D는 데이터 값을 저장하는 용도로 쓰이는 반면 A 는 데이터 레지스터와 주소 레지스터라는 두 역할을 합니다.

2.2 A-명령어

A-명령어는 A레지스터에 15비트 값을 설정하는데 씁니다.

이 명령어는 컴퓨터가 A 레지스터에 특정 값을 저장하도록 명령합니다. 예를들어 @5 명령어는 0000000000000101 와 동일한데, A 레지스터에 2진수로 표현된 5를 저장하라는 뜻을 의미합니다.

A-명령어는 세가지 용도로 쓰입니다.

  • 상수 입력 용도
  • C 명령어에서 주소값을 참조할 수 있도록 A 레지스터에 데이터 주소 입력
  • A 레지스터에 점프할 주소를 미리 입력

image

2.3 C-명령어

C-명령어는 핵 플랫폼에서 거의모든 일을 수행하는 명령어입니다. C-명령어는 A명령어와 함께 쓰이면 본 책에서 만들 컴퓨터에서 가능한 모든 연산을 수행합니다.

맨 왼쪽 비트 값이 1이면 명령어가 C-명령어임을 나타내는 코드입니다. 그 다음의 두 비트는 사용되지 않습니다. 나머지 비트들은 명령어에 있는 세가지 필드를 뜻합니다. comp 필드는 ALU 가 할 연산이 무엇인지를 뜻합니다. dest 필드는 계산된 값을 어디에 저장할지 가리킵니다. jump 필드는 점프 조건으로 다음에 불러와서 실행할 명령어가 무엇인지를 의미합니다.

image

2.3.1 계산 필드

핵 ALU 는 D와 A 및 M 레지스터 상에서 미리 정해진 함수들을 계산합니다. 계산할 함수는 명령어의 comp 필드에서 정의되는데, 1개의 a-비트 6개의 c-비트로 총 7비트로 표현됩니다. 이 7비트 패턴으로는 최대 128개의 서로 다른 함수를 코드화할 수 있습니다. 아래의 그림은 그 중 28개의 패턴만 언어 명세에 포함됩니다.

image

C-명령어의 형식은 111a cccc ccdd djjj로 구성되어 있습니다. ALU에 D-1연산, 즉 D레지스터의 값에 1을 빼는 연산을 할 떄의 명령어를 확인해보겠습니다. 그림에서 보았을 때 D-1의 C-명령어 계산 필드는 001110 입니다. 따라서 D-11110 0011 1000 0000 로 표현할 수 있습니다. D|M1111 0101 0100 0000이며, 상수-1 연산은 1110 1110 1000 0000 이 됩니다.

2.3.2 목적지 필드

C-명령어의 comp 부분에서 계산된 값은 3비트 dest 부분이 가리키는 몇가지 목적지에 저장됩니다. 첫번째와 두번째 d-비트는 계산된 값을 각각 A 레지스터와 D레지스터에 저장할지를 결정하는 코드입니다. 세번째 d-비트는 계산된 값을 M에 저장할지를 결정하는 코드입니다.

C-명령어의 형식은 111a cccc ccdd djjj 입니다. 만약 메모리의 [7] 의 값을 1만큼 증가시키고 그 결과를 D레지스터에 저장하는 명령어는 아래와 같습니다.

0000 0000 0000 0111  // @7
1111 1101 1101 1000  // MD = M + 1

[그림 추가 예정]

2.3.3 점프 필드

C-명령어의 jump 필드는 컴퓨터가 다음에 수행할 일을 지시합니다. 컴퓨터가 하게될 일은 둘중 하나가 됩니다.

  • 프로그램에서 다음번 명령을 불러와서 실행
  • 프로그램 내 다른곳에 위치한 명령을 불러와서 실행 (A레지스터에 점프할 주소가 저장된경우)

실제로 점프할지 아닐지는 jump 필드의 세개의 j비트와 ALU 출력 값에 달렸습니다. ALU 출력값이 음수일 때는 첫 번째 j-비트, 0일 때는 두번째 j-비트, 양수일 때는 세번째 j-비트를 보고 점프 여부를 결정합니다.

  • 논리
    if Memory[3] = 5 then goto 100
    else goto 200
    
  • 구현
    @3
    D=M     // D = Memory[3]
    @5
    D=D-A   // D = D - 5
    @100
    D;JEQ   // If D = 0 goto 100
    @200
    0;JMP   // Goto 200
    

[그림 추가 예정]

마지막 명령어(0;JMP)는 무조건 점프를 뜻합니다. C-명령어 문법에 따르면 항상 어떠한 계산이라도 해야하기 떄문에, ALU가 0을 계산하고 그 결과값을 그냥 무시하기 위함입니다.

2.4 기호

어셈블리 명령은 상수나 기호를 이용하여 메모리 주소를 참조할 수 있습니다. 어셈블리는 세가지 방식으로 기호를 활용합니다.

2.4.1 미리 정의된 기호

RAM 내의 특정 주소들은 다음과 같이 미리 정의된 기호를 사용하며, 이 기호를 통해 어떤 어셈블리 프로그램에서도 주소를 참조합니다.

  • 가상 레지스터 : 어셈블리 프로그램을 단순화하기 위해 기호 R0 ~ R15 는 RAM 주소 0~15를 가리키도록 정의함
  • 미리 정의된 포인터 : SP, LCL, ARG, THIS, THAT 기호는 각각 RAM 주소 0 ~ 4를 참조하도록 정의된 기호임, 이 메모리 주소들은 이름이 둘임
  • I/O 포인터 : SCREEN과 KBD 기호는 각각 RAM 주소 16384(0x4000)와 24576(0x6000)를 참조하도록 미리 정의된 기호로, 이 주소들은 스크린 및 키보드의 메모리 매핑 시작 주소가 됨

2.4.2 레이블 기호

goto 명령어의 목적지를 나타내는 레이블 기호는 사용자가 (Xxx) 라는 의사 명령으로 직접 선언합니다. 이 명령은 다음에 실행할 명령을 담고 있는 명령어 메모리 주소를 기소 Xxx로 선언하라는 뜻입니다. 레이블은 한번 선언되면 어셈블리 프로그램 내 어디서든지 쓸 수 있으며, 심지어 선언된 라인의 앞쪽 라인에서도 사용할 수 있습니다.

2.4.3 변수 기호

어셈블리 프로그램 내에서 미리 정의된 기호가 아니거나, (Xxx) 명령으로 선언되지 않은 모든 사용자 정의 기호 Xxx 는 변수로 취급되며, 어셈블러는 RAM 주소 16(0x0010)에서부터 차례대로 변수마다 유일한 메모리 주소를 할당합니다.

2.5 입력 / 출력 조작

핵 플랫폼은 스크린과 키보드, 두 단말 장치에 연결될 수 있습니다. 두 장치는 메모리맵을 통해 컴퓨터 플랫폼과 통신하게 됩니다. 메모리 세그먼트에 2진 값을 쓰면 그에 대응하는 스크린 위에 픽셀이 그려지는 방식이 되겠습니다. 키보드 입력은 해당 키 입력에 대응하는 메모리 위치를 읽어 들이는 식으로 처리하게 됩니다. 물리적 I/O 장치와 메모리 맵은 계속 갱신되는 루프를 통해 동기화된다.

2.5.1 스크린

핵 컴퓨터의 스크린은 512개의 열과 256개의 행의 흑백 픽셀로 구성됩니다. 그리고 스크린에 표시되는 픽셀은 RAM 주소 16384(0x4000)부터 시작하는 8K의 메모리 맵에 대응됩니다. 물리적 스크린에서 각 행은 화면 맨 왼쪽부터 시작하며, RAM 내에서는 32개의 연속된 16비트 단어로 표현됩니다. 물리적 스크린에 픽셀을 쓰거나 읽으려면 RAM 내 메모리 맵에 해당 픽셀에 대응하는 비트에 읽거나 쓰면 됩니다. (1 = 검정, 0 = 흰색)

2.5.2 키보드

핵 컴퓨터는 RAM 주소 24576(0x6000)에 위치한 1워드의 메모리 맵을 통해 물리적 키보드 장치와 통신합니다. 실제 키보드에서 키가 눌릴 때마다, 눌린 키의 16비트 ASCII 코드가 RAM[24576]에 기록되게 됩니다. 키가 눌리지 않는 동안에는 해당 메모리에 값은 0이 기록됩니다. 핵 키보드에서 일반적인 문자 ASCII 코드 외의 특수 입력은 다음과 같습니다.

눌린 키 코드
새 라인 128
백스페이스 129
왼쪽 화살표 130
위쪽 화살표 131
오른쪽 화살표 132
아래쪽 화살표 133
홈(home) 134
엔드(end) 135
페이지 업 (page up) 136
페이지 다운 (page down) 137
인서트 (insert) 138
딜리트 (delete) 139
이스케이프 (esc) 140
f1 ~ f12 141 ~ 152

2.6 구문 규칙과 파일 형식

2.6.1 2진 코드 파일

2진 코드 파일은 텍스트 라인들로 구성됩니다. 각 라인은 16개의 ASCII 문자 ‘0’ 과 ‘1’로 되어있으며, 하나의 기계어 명령어를 부호화한것으로 보시면 됩니다. 그리고 파일 내 모든 라인들이 모여 하나의 기계어 프로그램을 이루게 됩니다. 컴퓨터 멍령어 메모리에 이 기계어 프로그램이 로드될 떄는 파일의 n번째 라인에 있는 2진 코드가 명령어 메모리의 주소 n에 저장되는 규칙을 따릅니다. 규칙에 따라 기계어 프로그램은 ‘hack’확장자를 가지는 텍스트파일에 저장됩니다.

2.6.2 어셈블리 언어 파일

어셈블리 언어 프로그램은 ‘asm’ 확장자를 가지는 텍스트 파일에 저장됩니다. 어셈플리 언어 파일은 텍스트 라인으로 구성되며, 각 텍스트 라인은 명령어나 기호를 뜻합니다.

  • 명령어 : A-명령어나 C-명령어
  • Symbol : 이 의사명령은 다음번 명령이 저장되는 메모리 symbol 레이블을 할당하라고 어셈블러에게 지시합니다.

2.6.3 상수와 기호

상수는 음수가 아니어야 하며, 항상 10진법으로 표기됩니다. 맨 앞 문자가 숫자만 아니면 문자, 숫자, 밑줄, 마침표, 달러기호, 콜론으로 이뤄진 어떤 문자열이라도 사용자 정의 기호로 선언 가능합니다.

2.6.4 주석

슬래시 두개(//) 에서 라인 끝까지의 텍스트는 주석으로 간주되어 무시됩니다.

2.6.5 공백

공백 문자는 무시됩니다. 빈 라인이라도 무시됩니다.

2.6.6 대소문자 규칙

모든 어셈블리 연상기호는 대문자로 써야합니다. 그 외 사용자 정의 레이블과 변수명은 대소문자를 구분합니다.

Leave a comment