This post is not yet available in English and is shown in Korean.
지난 글에서 프로그램을 데이터로 메모리에 저장하는 폰 노이만 구조를 살펴보았다. 메모리에 명령어가 준비되었고, 실행 결과를 관찰할 입출력장치에 대해서도 배웠으니, 이제 그 명령어를 실행하는 장치인 CPU(Central Processing Unit) 를 알아보자.
CPU가 명령어를 처리하려면 무엇을 해야 할까? 메모리에서 명령어를 가져오고 (Fetch), 그 의미를 해석하고(Decode), 해석한 대로 실행(Execute) 한다. 이 세 단계를 끊임없이 반복하는 것이 CPU가 하는 일의 전부다. 지난 글에서 설계한 8비트 ISA를 사용하여 각 단계를 따라가 보자.
본격적으로 들어가기 전에, CPU 내부의 작업 공간부터 알아야 한다.
메모리는 용량이 크지만 CPU에서 멀리 떨어져 있어 접근이 느리다. 매번 메모리까지 왕복하면 시간이 낭비된다. 그래서 CPU 내부에 레지스터라는 소규모 저장소를 둔다. 서랍장에서 재료를 꺼내 작업대 위에 올려놓고 요리하듯이, CPU도 메모리에서 데이터를 레지스터로 꺼내 놓고 연산한 뒤 결과를 다시 메모리에 넣는다.
이 시리즈의 ISA에서 데이터를 옮기는 명령어는 네 가지다:
LOAD Addr — 메모리에서 값을 읽어 R0에 저장STORE Addr — R0의 값을 메모리에 저장LDI Rd, Imm — 숫자를 레지스터에 바로 넣기MOVE Rd, Rs — 레지스터 간 값 복사메모리에는 명령어가 여러 개 저장되어 있다. CPU는 어디서부터 가져와야 할까?
책을 읽을 때 손가락으로 줄을 짚듯이, CPU에도 “지금 어디를 읽고 있는지” 가리키는 장치가 있다. Program Counter(PC) 라는 레지스터다. CPU는 PC가 가리키는 주소에서 명령어를 읽어온 뒤, PC를 다음 주소로 한 칸 옮긴다.
보통 PC는 한 칸씩 순서대로 증가하지만, PC를 직접 바꾸는 명령어도 있다:
JUMP Addr — PC를 지정한 주소로 변경JZ Addr — 직전 연산 결과가 0일 때만 점프 (뒤에서 다시 다룬다)이 명령어들 덕분에 반복문이나 조건 분기 같은 프로그램 흐름 제어가 가능해진다.
| 주소 | 이진수 | 어셈블리 |
|---|---|---|
| 0 | 01100001 | LDI R0, 1 |
| 1 | 01100110 | LDI R1, 2 |
| 2 | 00010001 | ADD R0, R1 |
| 3 | 10010101 | STORE 5 |
| 4 | 11000010 | JUMP 2 |
| 5 | 00000000 | 데이터 |
가져온 명령어는 01100011 같은 이진수일 뿐이다. 이걸 어떻게 해석할까?
이 시리즈의 8비트 ISA에서는 명령어를 두 부분으로 나눠서 해석한다. 앞 4비트는
opcode, 즉 “무엇을 하라”는 명령이다. 뒤 4비트는 operand, 즉
“어디에/무엇에 대해”라는 대상이다. 예를 들어 0110 0011이면 opcode 0110(LOAD)
에 operand 0011(주소 3)이므로, “주소 3의 값을 레지스터로 가져와라”는 뜻이
된다.
이 해석을 담당하는 것이 제어 장치다. 제어 장치는 opcode를 보고 어떤 부품이 무엇을 해야 하는지 판단하여 신호를 보낸다.
즉시값을 Rd에 로드한다.
제어 장치가 신호를 보내면 실제 연산이 수행된다.
덧셈, 뺄셈 같은 산술 연산과 AND, OR, NOT 같은 논리 연산을 담당하는 장치가 ALU(Arithmetic Logic Unit) 다. 시리즈 초반에 논리 게이트를 조합하여 만든 가산기가 바로 ALU의 일부다.
ALU를 사용하는 연산 명령어는 네 가지다:
ADD Rd, Rs — 두 레지스터를 더해 Rd에 저장SUB Rd, Rs — Rd에서 Rs를 빼서 Rd에 저장ADDI Rd, Imm — 레지스터에 숫자를 바로 더하기SUBI Rd, Imm — 레지스터에서 숫자를 바로 빼기Zero Flag는 왜 필요할까? 앞서 본 JZ(Jump if Zero)가 “직전 연산 결과가 0이면
점프하라”는 판단을 내릴 때 이 플래그를 본다. 실행이 끝나면 PC가 다음 주소로
증가하고, 다시 Fetch로 돌아간다.
지금까지 본 명령어를 한눈에 정리한 목록이다. 각 명령어를 클릭해 구조와 동작을 확인해보자.
메모리에서 값을 읽어 R0에 저장한다.
이 부품들을 하나로 조립한 폰 노이만 시뮬레이터에서 Fetch-Decode-Execute 사이클을 한 단계씩 실행해보자.
0과 1에서 시작해 논리 게이트, 가산기, 래치, 튜링 머신, 폰 노이만 구조를 거쳐 동작하는 컴퓨터를 조립했다.
그런데 한 가지 문제가 있다. 오늘날 CPU는 1초에 수십억 개의 명령어를 처리할 수 있지만, 메모리에서 데이터를 가져오는 속도는 이에 비해 수백 배 느리다. Fetch 단계에서 매번 메모리에 접근해야 하니, CPU가 아무리 빨라도 메모리를 기다리는 시간이 병목이 된다. 다음 글에서는 이 속도 차이를 어떻게 해결하는지 살펴보자.