기계어는 대상 하드웨어 플랫폼에서 직접적으로 명령을 실행하고, 하드웨어를 완전히 제어하는 것이 목적이다. 새로운 컴퓨터 시스템을 이해려면 그 시스템의 기계어 프로그램을 먼저 살펴보는 것이 많은 도움이 될 것이다.
기계어는 프로세서와 레지스터들을 이용해서 메모리를 조작할 수 있도록 미리 정의된 규칙이다.
메모리는 컴퓨터에서 데이터와 명령어를 저장하는 하드웨어 장치이다.
보통 중앙 처리 장치(CPU)라고 불리는 프로세서는 미리 정해진 기초 연산들을 수행하는 장치다. 메모리와 레지스터에 있는 2진 값을 피연산자로 두고, 연산을 출력값은 선택된 메모리 주소나 레지스터에 저장된다.
항상 CPU와 거리가 있는 메모리와 데이터를 주고 받기에는 시간이 상대적으로 많이 걸리기 때문에 대부분의 프로세서는 값을 하나 저장할 수 있는 레지스터를 바로 옆에 여러 개 두고 있다.
기계어 프로그램은 명령어들을 2진 코드화한 것이다. 예를 들어 16비트 컴퓨터에서 명령은 보통 1010 0011 0001 1001 같은 식으로 표현된다. 이 기계어 닽은 경우에는 4bit 씩 4개의 필드로 구성되어 있고 맨 왼쪽 필드는 CPU연산 종류를, 나머지는 피 연산자를 나타낸다.
2진 코드만 보고서는 해석을 하기 어렵기 때문에, 기계어는 보통 2진 코드와 연산기호를 둘 다 사용하도록 되어 있다. 예를 들어, 1010은 ADD, 나머지 필드는 각각 R3, R1, R9을 의미한다.
이런 연상기호들을 사용한 표기법을 '어셈블리 언어' 또는 간단히 '어셈블리'라고 부르며, 어셈블리를 2진 기계어 코드로 번역하는 프로그램을 '어셈블러'라 한다.
산술 및 논리 연산
모든 컴퓨터에는 덧셈이나 뺄셈 같은 기초 산술 연산 외에도 비트 반전, 비트 이동 등의 불연산, 논리 연산도 가능해야 한다.
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 연산한 결과
메모리 접근
메모리 접근은 산술 및 논리 명령에 자연스럽게 포함되어 있고, 레지스터와 메모리 사이에 데이터를 이동시키는 로드, 저장 명령이 있다.
메모리 접근 명령은 몇 가지 종류의 주소 지정 모드를 가지고 있다.
// 직접 주소 지정 방식
LOAD R1, 67 // R1<-Memory[67]
// 또는 bar가 메모리 주소 67을 나타낸다고 가정하면
LOAD R1, bar // R1<-Memory[67]
// 즉시 주소 지정 방식
LOAD R1, 67 // R1<-67
// 간접 주소 지정 방식
// x=foo[j] 또는 x=*(foo+j)의 번역
ADD R1, foo, j // R1<-foo+j
LOAD* R2, R1 // R2<-Memory[R1]
STR R2, x // x<-R2
제어 흐름
프로그램의 명령들은 보통 순서대로 실행되지만, 갑자기 다른 위치로 분기해서 실행되기도 한다. 분기에는 반복, 조건 실행, 서브루틴 호출 등의 몇 가지 종류가 있다 .
//고수준
// while 루프
while (R1>=0) {
code segmnet 1
}
code segumnet 2
// ---------------------------------------------
// 저수준
beginWhile:
JNG R1, endWhile // If R1<0 goto endWhile
// 세그먼트 1의 번역은 여기에 위치한다.
JMP beginWhile // goto beginWhile
endWhile:
// 세그먼트 2의 번역은 여기에 위치한다.
기본적으로 핵 컴퓨터는 폰 노이만 플랫폼이고 CPU, 명령어 메모리, 데이터 메모리, 스크린과 키보드에 해당되는 메모리 매핑 I/O장치로 구성된 16비트 장치이다.
메모리 주소 공간
핵에서는 명령어를 저장하는 메모리, 데이터를 저장하는 메모리로 주소 공간이 분리 되어 있다. 폭이 16비트인 두 메모리의 주소 공간은 15비트로, 최대 32K개의 16비트 단어들에 주소를 할당할 수 있다. 그리고 핵의 CPU는 명령어 메모리(ROM)에 존재하는 프로그램만 수행할 수 있다.
레지스터
핵 시스템에는 데이터 값만을 저장하는 D 레지스터와 데이터 값과 주소 값을 저장할 수 있는 A 16비트 레지스터가 있다. 예를 들어, D=M+1에서 M은 항상 A에 저장된 값을 주소로 하는 메모리 데이터를 의미한다. 또한 분기명령어의 도착 위치는 항상 A에 저장된 값을 목적지로 한다.
A-명령어는 A레지스터에 15비트 값을 설정하는 데 쓰인다.
사용법 : @value
value는 음수가 아닌 10진수, 또는 그 숫자를 참조하는 기호
C-명령어는 핵 플랫폼에서 거의 모든 일을 수행한다. 이 명령어는 다음 질문에 대한 답이 된다.
C-명령어 : dest = comp;jump // dest나 jump 필드는 공란일 수 있다.
// dest가 공란이면 '='가 생략된다.
// jump가 공란이면 ';'가 생략된다.
최상위 비트 값 1은 이 명령어가 C-명령어임을 나타내는 코드다. 그다음 두 비트는 사용되지 않는다. comp 필드는 ALU가 할 연산이 무엇인지를 뜻하고 dest 필드는 계산된 값을 어디에 저장할지 가리킨다. jump 필드는 점프 조건을 의미한다.
계산 필드(computation field)
계산 필드의 a비트는 A레지스터와 메모리의 값 중 어느 것을 계산할 것인지를 결정해준다. 그리고 나머지 c1~6은 어떤 계산을 할지 결정하는데, 이는 Chap2에 등장한 ALU와 흡사하다. 그 이유는 다음장에 설명될 것이다.
목적지 필드(destination field)
목적지 필드는 comp 부분에서 계산된 값을 어디에 저장할지를 나타낸다. 여기에서 d1은 A레지스터, d2는 D레지스터, d3는 Memory[A]를 담당하는데, 활성화된 비트가 담당하는 장소에 값을 저장한다.
점프 필드(jump field)
점프 필드는 명령을 수행한 컴퓨터가 이제 어느 위치의 명령을 수행할지 결정한다.
하지만 실제로 점프할지는 ALU의 출력값에 달렸는데, 출력값이 음수라면 j1, 0이라면 j2, 양수일 때는 j3 비트를 보고 점프 여부를 결정한다.
어셈블리 명령은 상수나 기호를 이용해서 메모리 주소를 참조할 수 있다.
미리 정의된 기호
- 가상 레지스터 : R0~R15는 RAM 주소 0~15를 가리키도록 정의한다.
- 미리 정의된 포인터 : SP, LCL, ARG, THIS, THAT 기호는 각각 RAM 주소 0~4를 참조하도록 정의한다. 이 규칙은 가상머신을 구현할 때 유용하게 사용된다.
- I/O 포인터 : SCREEN과 KBD 기호는 각각 RAM 주소 16384(0x4000)과 24576(0x6000)을 참조한다. 이 주소들은 스크린 및 키보드의 메모리 매핑 시작 주소가 된다.
레이블 기호
분기 명령어의 목적지를 나타내는 레이블 기호는 사용자가 '(XXX)'라는 의사명령으로 직접 선언한다.
변수 기호
이전 두 가지 기호가 모두 아닌 경우다. 어셈블러는 RAM 주소 16에서부터 차례대로 변수마다 유일한 메모리 주소를 할당한다.
핵 플랫폼은 키보드와 스크린 장치에 연결될 수 있다. 두 장치는 메모리 맵을 통해 컴퓨터와 통신한다. 키보드입력은 해당 메모리 맵에 저장되고, 스크린에 해당하는 메모리 위치에 값을 쓰면 그에 대응하는 스크린 위에 픽셀이 쓰여진다.
스크린
핵 컴퓨터의 스크린은 512개 열 X 256개 행의 흑백 픽셀로 구성된다. 그리고 스크린의 픽셀은 RAM[16384]부터 시작하는 8K 메모리 맵에 대응한다. 각 열마다 32개의 16비트 단어로 표현된다.
키보드
핵 컴퓨터는 RAM[24576]에 위치한 1단어 메모리 맵을 통해 키보드와 통신한다. 실제 키보드가 눌릴 때마다, 눌린 키의 16비트 ASCII 코드가 기록되는 식이다. 키가 눌리지 않는 동안에는 0이 기록된다.
C 언어
// 1+..+100 덧셈
int i = 1;
int sum = 0;
While (i <= 100) {
sum += i;
i++;
}
핵 기계어
// Adds 1+...+100.
@i // i는 어떤 메모리 위치를 참조함
M=1 // i=1
@sum // sum은 어떤 메모리 위치를 참조함
M=0 // sum=0
(LOOP)
@i
D=M // D=i
@100
D=D-A // D=i-100
@END
D;JGT // if (i-100)>0 goto END
@i
D=M // D=i
@sum
M=D+M // sum=sum+i
@i
M=M+1 // i=i+1
@LOOP
0;JMP // Goto LOOP
(END)
@END
0;JMP // 무한루프(남은 실행횟수 소모)
@2
M=0 // R2를 0으로 초기화한다. R2에는 결과값이 저장된다.
(LOOP)
@0
D=M // R0의 값을 D에 저장한다. R0은 몇번 곱할지 남은 횟수를 나타낸다.
@END
D;JEQ // D의 값이 0이 되었다면 계산을 종료한다.
@1
D=M // R1의 값을 D에 저장한다.
@2
M=M+D // R2 += D
@0
M=M-1 // 남은 곱셈 횟수를 1감소시킨다.
@LOOP
0;JMP // 무조건 LOOP으로 점프한다.
(END)
@END
0;JMP
@i
M=0 // i = 0 초기화
(LOOP)
@KBD
D=M // 현재 키보드에 입력된 값을 D에 대입
@NOINPUT
D;JEQ // D == 0 jump
@INPUT
D;JGT // D > 0 jump
(NOINPUT)
@16384 //
D=A //
@i // 현재 스크린 위치 -> A
D=D+M // 16384+i(1)
A=D //
M=0 // 현재 스크린위치에 0000 0000 0000 0000 대입
@END
0;JMP
(INPUT)
@16384 //
D=A //
@i // 현재 스크린 위치 -> A
D=D+M //
A=D //
M=-1 // 현재 스크린 위치에 1111 1111 1111 1111 대입
@END
0;JMP
(END)
@i
M=M+1 // i = i + 1
@LOOP
0;JMP // LOOP로 jump