이성열

가져오고 해석하고 실행하고

지난 글에서 프로그램을 데이터로 메모리에 저장하는 폰 노이만 구조를 살펴보았다. 메모리에 명령어가 준비되었고, 실행 결과를 관찰할 입출력장치에 대해서도 배웠으니, 이제 그 명령어를 실행하는 장치인 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 — 레지스터 간 값 복사
메모리
레지스터
R00
R10
R20
R30
  1. LOAD를 선택하고 메모리 주소를 클릭해 값을 R0에 가져온다.
  2. STORE로 R0의 값을 다른 메모리 주소에 저장해보자.
  3. LDI를 선택하고 Rd와 Imm을 조절해 원하는 레지스터에 즉시값을 넣어보자.
  4. MOVE를 선택해 한 레지스터의 값을 다른 레지스터로 복사해보자.

Fetch

메모리에는 명령어가 여러 개 저장되어 있다. CPU는 어디서부터 가져와야 할까?

책을 읽을 때 손가락으로 줄을 짚듯이, CPU에도 “지금 어디를 읽고 있는지” 가리키는 장치가 있다. Program Counter(PC) 라는 레지스터다. CPU는 PC가 가리키는 주소에서 명령어를 읽어온 뒤, PC를 다음 주소로 한 칸 옮긴다.

보통 PC는 한 칸씩 순서대로 증가하지만, PC를 직접 바꾸는 명령어도 있다:

  • JUMP Addr — PC를 지정한 주소로 변경
  • JZ Addr — 직전 연산 결과가 0일 때만 점프 (뒤에서 다시 다룬다)

이 명령어들 덕분에 반복문이나 조건 분기 같은 프로그램 흐름 제어가 가능해진다.

주소이진수어셈블리
001100001LDI R0, 1
101100110LDI R1, 2
200010001ADD R0, R1
310010101STORE 5
411000010JUMP 2
500000000데이터
PC0
  1. “다음”을 선택하고 “PC + 1”을 눌러 PC를 하나씩 증가시켜 보자.
  2. JUMP를 선택하고 목적지를 2로 설정한 뒤 실행해보자. PC가 주소 2로 이동한다.
  3. JZ를 선택하고 Z 플래그를 0으로 두고 실행해보자. 아무 일도 일어나지 않는다. Z 플래그를 1로 바꾸고 다시 실행해보자.

Decode

가져온 명령어는 01100011 같은 이진수일 뿐이다. 이걸 어떻게 해석할까?

이 시리즈의 8비트 ISA에서는 명령어를 두 부분으로 나눠서 해석한다. 앞 4비트는 opcode, 즉 “무엇을 하라”는 명령이다. 뒤 4비트는 operand, 즉 “어디에/무엇에 대해”라는 대상이다. 예를 들어 0110 0011이면 opcode 0110(LOAD) 에 operand 0011(주소 3)이므로, “주소 3의 값을 레지스터로 가져와라”는 뜻이 된다.

이 해석을 담당하는 것이 제어 장치다. 제어 장치는 opcode를 보고 어떤 부품이 무엇을 해야 하는지 판단하여 신호를 보낸다.

0110
Rd
Imm
LDI R0, 3

즉시값을 Rd에 로드한다.

레지스터즉시값 → Rd
메모리사용 안 함
ALU사용 안 함
  1. 각 비트를 클릭해 0과 1을 토글해보자.
  2. opcode에 따라 레지스터, 메모리, ALU 각각에 어떤 신호가 가는지 확인해보자.

Execute

제어 장치가 신호를 보내면 실제 연산이 수행된다.

덧셈, 뺄셈 같은 산술 연산과 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 — 레지스터에서 숫자를 바로 빼기
레지스터
R05
R13
R20
R30
Rd
Rs
결과
Z 플래그0
  1. ADD를 선택하고 Rd와 Rs를 골라 실행해보자. Rd에 결과가 저장된다.
  2. ADDI를 선택하면 Rs 대신 즉시값(0-3)을 직접 지정할 수 있다.
  3. 같은 값이 든 두 레지스터를 골라 SUB를 실행해보자. 결과가 0이면 Zero Flag(Z) 가 1이 된다.

Zero Flag는 왜 필요할까? 앞서 본 JZ(Jump if Zero)가 “직전 연산 결과가 0이면 점프하라”는 판단을 내릴 때 이 플래그를 본다. 실행이 끝나면 PC가 다음 주소로 증가하고, 다시 Fetch로 돌아간다.

모아보기

지금까지 본 명령어를 한눈에 정리한 목록이다. 각 명령어를 클릭해 구조와 동작을 확인해보자.

LOAD Addr

메모리에서 값을 읽어 R0에 저장한다.

10004bit
Addr4bit
LOAD 1010001010

이 부품들을 하나로 조립한 폰 노이만 시뮬레이터에서 Fetch-Decode-Execute 사이클을 한 단계씩 실행해보자.

마무리

0과 1에서 시작해 논리 게이트, 가산기, 래치, 튜링 머신, 폰 노이만 구조를 거쳐 동작하는 컴퓨터를 조립했다.

그런데 한 가지 문제가 있다. 오늘날 CPU는 1초에 수십억 개의 명령어를 처리할 수 있지만, 메모리에서 데이터를 가져오는 속도는 이에 비해 수백 배 느리다. Fetch 단계에서 매번 메모리에 접근해야 하니, CPU가 아무리 빨라도 메모리를 기다리는 시간이 병목이 된다. 다음 글에서는 이 속도 차이를 어떻게 해결하는지 살펴보자.