성열의 컴퓨터공학

CPU와 대화하기

지난 글에서 프로그램을 데이터로 메모리에 저장하는 폰 노이만 구조를 살펴보았다. 01100011 같은 이진수가 R0에 3을 저장해라라는 명령이 될 수 있다는 것도 배웠다. CPU 설계자와 프로그래머 사이의 약속, 즉 명령어 집합(ISA)이라는 개념이었다.

그런데 약속의 목록은 아직 보지 못했다. CPU에게 일을 시키려면 구체적으로 어떤 명령어들이 있을까? 이 글에서 이 시리즈의 8비트 ISA에 포함된 열 가지 명령어를 하나씩 알아보자.

레지스터

요리사가 냉장고에서 재료를 꺼내 바로 조리대 위에서 일하듯이, CPU도 메모리에서 데이터를 꺼내 가까운 곳에 놓고 작업한다. 이 조리대가 바로 레지스터다.

왜 메모리에서 바로 작업하지 않을까? 메모리는 용량이 크지만 CPU에서 물리적으로 떨어져 있어 접근이 느리다. 반면 레지스터는 CPU 내부에 있어서 접근이 매우 빠르다. 대신 그만큼 비싸기 때문에 개수가 적다. 이 시리즈의 ISA에는 R0, R1, R2, R3 네 개의 레지스터가 있다.

레지스터와 메모리 사이에서 데이터를 옮기는 명령어는 네 가지다:

  • LOAD Addr — 메모리 주소의 값을 R0에 가져오기
  • STORE Addr — R0의 값을 메모리 주소에 저장
  • LDI Rd, Imm — 숫자를 레지스터에 바로 넣기
  • MOVE Rd, Rs — 레지스터 간 값 복사

LOAD와 STORE는 메모리와 R0 사이에서만 값을 주고받는다. 다른 레지스터에 값을 넣고 싶다면? MOVE로 R0의 값을 옮기거나, LDI(Load Immediate)로 원하는 레지스터에 직접 값을 넣으면 된다. 여기서 Immediate(즉시값)란 명령어 자체에 포함된 숫자를 말한다. 메모리까지 갈 필요 없이 명령어 안에 값이 들어 있어 바로 사용할 수 있다.

메모리
레지스터
R00
R10
R20
R30
  1. LOAD를 선택하고 메모리 주소를 클릭해 값을 R0에 가져온다.
  2. STORE로 R0의 값을 다른 메모리 주소에 저장해보자.
  3. LDI를 선택하고 Rd와 Imm을 조절해 원하는 레지스터에 즉시값을 넣어보자.
  4. MOVE를 선택해 한 레지스터의 값을 다른 레지스터로 복사해보자.

데이터를 옮길 수 있게 되었다. 그런데 옮기기만 해서는 의미가 없다. 데이터로 무엇을 할 수 있을까?

연산

시리즈 초반에 논리 게이트를 조합하여 가산기를 만들었다. 이 가산기가 포함된 장치가 ALU(Arithmetic Logic Unit)다. ALU는 덧셈, 뺄셈 같은 산술 연산을 담당한다.

ALU를 사용하는 연산 명령어는 네 가지다:

  • ADD Rd, Rs — Rd와 Rs를 더해 Rd에 저장
  • SUB Rd, Rs — Rd에서 Rs를 빼서 Rd에 저장
  • ADDI Rd, Imm — Rd에 즉시값을 더하기
  • SUBI Rd, Imm — Rd에서 즉시값을 빼기

여기서 Rd는 destination, 즉 결과가 저장되는 레지스터이고, Rs는 source, 즉 값을 가져오는 레지스터다. ADDI와 SUBI는 레지스터 대신 즉시값을 사용하는 버전이다.

레지스터
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는 직전 연산 결과가 0인지를 기록하는 1비트 표시등이다. 왜 필요할까? 두 값이 같은가?라는 질문에 답할 수 있기 때문이다. 두 레지스터의 값을 빼서 결과가 0이면 두 값은 같다. 이 표시등은 바로 다음에 나올 제어 흐름 명령어에서 결정적인 역할을 한다.

제어 흐름

지금까지의 명령어만으로는 프로그램이 위에서 아래로 한 줄씩 실행된다. 하지만 현실의 프로그램은 그렇지 않다. 조건이 맞으면 이쪽으로(if), 맞을 때까지 반복(while) 같은 흐름 제어가 필요하다.

이를 위한 명령어가 두 가지 있다:

  • JUMP Addr — 다음에 실행할 주소를 지정한 주소로 변경
  • JZ Addr — 직전 연산 결과가 0일 때만 점프

JUMP는 무조건 지정한 주소로 이동한다. 예를 들어 프로그램의 마지막에 JUMP 0을 넣으면 처음으로 돌아가 같은 코드를 무한히 반복한다.

하지만 무한 반복만으로는 부족하다. 특정 조건이 충족되면 반복을 멈춰라를 표현하려면 조건부 점프가 필요하다. 그것이 바로 JZ(Jump if Zero)다. JZ는 Zero Flag가 1일 때, 즉 직전 연산 결과가 0일 때만 점프한다.

구체적인 예를 들어보자. R0에서 1씩 빼면서 0이 되면 멈추는 카운트다운 프로그램을 생각해보자:

주소 0: SUBI R0, 1    ← R0에서 1을 뺀다
주소 1: JZ 3          ← 결과가 0이면 주소 3으로 점프
주소 2: JUMP 0        ← 아니면 주소 0으로 돌아가 반복
주소 3: (다음 작업)

R0이 3이었다면 SUBI를 세 번 반복한 뒤 결과가 0이 되어 JZ가 작동하고, 반복이 끝난다. JUMP가 반복을, JZ가 멈출 조건을 담당하는 것이다. 이 두 명령어의 조합만으로 반복문과 조건 분기를 모두 표현할 수 있다.

모아보기

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

MOVE Rd, Rs

Rs의 값을 Rd로 복사한다.

00004bit
Rd2bit
Rs2bit
MOVE R1, R200000110

데이터를 옮기는 명령어 4개, 연산 명령어 4개, 흐름을 제어하는 명령어 2개. 총 열 가지뿐이다. 적어 보이지만 이것만으로도 카운트다운, 두 수의 곱셈, 조건 분기 등 다양한 프로그램을 만들 수 있다. 다음 글에서 만들어볼 시뮬레이터에서 직접 확인해보자.

마무리

CPU와 대화하는 언어를 배웠다. 데이터를 옮기고, 연산하고, 흐름을 제어하는 것. 이 세 종류의 명령어가 CPU가 이해하는 전부다.

그런데 명령어를 안다고 해서 CPU가 어떻게 동작하는지까지 아는 것은 아니다. 메모리에 줄지어 놓인 이 명령어들을 CPU는 어떻게 하나씩 꺼내서 실행할까? 다음 글에서 그 과정을 따라가 보자.