Chapter 04

CPU: 명령을 수행하는 두뇌

CPU는 똑똑해 보이지만 실제로 하는 일은 놀랄 만큼 단순하다. 메모리에서 명령 하나를 가져와서, 무슨 뜻인지 해석하고, 실행한다. 그리고 다음 명령으로 넘어간다. 이것을 1초에 수십억 번 반복할 뿐이다. 이 장에서는 명령어 8개짜리 장난감 CPU를 직접 한 박자씩 돌려 보며 그 반복을 눈으로 확인한다.

CPU 안의 네 식구

CPU 내부는 몇 개의 핵심 부품으로 나뉜다. 1장의 주방 비유를 이어 가면, 요리사 한 명의 머리와 손을 나눠 보는 셈이다.

CPU 제어 장치명령을 해석하고 지시한다 ALU (산술 논리 장치)더하기·빼기·비교 레지스터 (아주 빠른 소형 기억 장소) PC IR A 플래그 메모리 0: LOAD 14 1: ADD 15 2: STORE 13 ⋮ 15: 7 (데이터) 버스
그림 4-1. CPU의 기본 구성. 제어 장치가 메모리에서 명령을 가져와 해석하고, ALU와 레지스터에게 일을 시킨다.
부품하는 일주방에서는
제어 장치
Control Unit
명령을 해석해서 각 부품에 “지금 이걸 해”라고 신호를 보낸다.레시피를 읽고 손에게 지시하는 요리사의 머리
ALU덧셈, 뺄셈, 비교, AND/OR 같은 실제 계산을 한다(3장의 덧셈기).칼질하고 섞는 손놀림
프로그램 카운터 (PC)다음에 실행할 명령이 메모리 몇 번지에 있는지 기억한다.레시피의 “지금 몇 번째 줄” 손가락
명령 레지스터 (IR)방금 가져온 명령을 붙잡아 둔다.지금 읽고 있는 레시피 한 줄
범용 레지스터 (A)계산에 쓰는 값을 잠시 쥐고 있다. 장난감 CPU는 A 하나, 실제 CPU는 16~32개.손에 쥔 재료
플래그“방금 결과가 0이었나?” 같은 짧은 상태를 기록한다.“냄비가 끓었나?” 확인 표시

이 부품들은 두 갈래의 길로 메모리와 이어진다. “몇 번지?”를 실어 나르는 주소 버스와 실제 값을 실어 나르는 데이터 버스다. 제어 장치는 박자마다 “지금은 PC가 주소 버스에 값을 올려”, “지금은 ALU가 더해”처럼 각 부품에 짧은 신호(제어 신호)를 보낸다. 이렇게 데이터가 다니는 길 전체를 데이터 경로(datapath)라 한다.

CPU 주소 버스 (몇 번지?) 데이터 버스 (값) PC IR A 레지스터 ALU 결과 제어 장치해독 · 지시 점선 = 제어 신호 메모리 0: LOAD 14 1: ADD 15 ⋮ 15: 7
그림 4-2. 장난감 CPU의 데이터 경로. 주소 버스로 “어디”를, 데이터 버스로 “무엇”을 주고받는다. 명령을 가져올 때는 PC가, 데이터를 읽을 때는 IR의 주소 부분이 주소 버스를 쓴다.

명령어도 숫자다

1장에서 프로그램도 데이터처럼 메모리에 저장된다고 했다. 메모리에는 숫자만 들어가니 명령어도 숫자여야 한다. CPU 설계자는 “이 숫자는 이 명령”이라는 약속표를 만드는데, 이것을 명령어 집합(ISA, Instruction Set Architecture)이라 한다.

이 장의 장난감 CPU는 명령어가 8비트다. 앞 4비트는 무엇을 할지(연산 코드, opcode), 뒤 4비트는 메모리 몇 번지를 대상으로 할지(주소)를 나타낸다. 예를 들어 ADD 15는 0010 1111이다. “0010 = 더하기”, “1111 = 15번지”.

0 0 1 0 1 1 1 1 연산 코드 0010 = ADD 무엇을 할까? (최대 16가지) 주소 1111 = 15번지 어디의 값으로? (0~15번지) 주소 칸이 4비트뿐이라 이 CPU는 메모리 16칸까지만 가리킬 수 있다. 64비트 CPU가 거대한 메모리를 쓰는 이유가 여기에 있다.
그림 4-3. 명령어 ADD 15의 8비트 구조. 앞부분은 동작, 뒷부분은 대상이다.
명령코드뜻
LOAD n0001 nnnn메모리 n번지의 값을 A로 가져온다
ADD n0010 nnnnA ← A + (n번지 값)
SUB n0011 nnnnA ← A − (n번지 값)
STORE n0100 nnnnA의 값을 메모리 n번지에 저장한다
JUMP n0101 nnnn다음 명령을 n번지에서 가져온다 (PC ← n)
JZ n0110 nnnnA가 0이면 n번지로 점프, 아니면 그냥 다음 명령
OUT1110 0000A의 값을 화면에 출력한다
HALT1111 0000멈춘다
같은 숫자, 다른 의미

메모리에 00100111(39)이 있다면 이것은 “ADD 7”이라는 명령일까, 그냥 숫자 39일까? 메모리는 모른다. PC가 가리켜서 명령으로 가져가면 명령이고, LOAD로 읽으면 데이터다. 2장에서 말한 “모든 것은 해석의 약속”이 여기서도 그대로 적용된다.

SIMULATOR

기계어 조립기 & 해독 퍼즐

① 조립하기: 명령을 고르면 숫자가 된다

② 해독하기: 이 숫자는 어떤 명령일까?
 
맞힌 문제0
푼 문제0
결과—
요령: 앞 4비트만 보고 명령 이름을, 뒤 4비트로 주소를 읽는다. 0001 = LOAD, 0010 = ADD, 0011 = SUB, 0100 = STORE, 0101 = JUMP, 0110 = JZ. CPU의 해석기(디코더)도 정확히 이 일을 회로로 한다.

명령어 사이클: 가져오기 → 해석 → 실행

CPU는 전원이 켜져 있는 동안 다음 세 단계를 끝없이 반복한다.

  1. 가져오기(Fetch): PC가 가리키는 메모리 번지에서 명령을 읽어 IR에 넣는다. 그리고 PC를 1 늘려 다음 줄을 가리키게 한다.
  2. 해석하기(Decode): 제어 장치가 IR의 앞 4비트를 보고 무슨 명령인지, 뒤 4비트로 대상 주소가 어디인지 알아낸다.
  3. 실행하기(Execute): ALU로 계산하거나, 메모리에 쓰거나, PC를 바꾸는(점프) 등 실제 동작을 한다.
① 가져오기IR ← 메모리[PC] ② 해석무슨 명령? ③ 실행계산·저장·점프 PC + 1(가져올 때마다) 이 고리가 1초에 장난감 CPU (손가락): 몇 번 1970년대 칩: 수십만 번 요즘 CPU 코어 하나: 수십억 번 이상 점프는? ③ 실행에서 PC를 다른 값으로 바꿔 고리를 다른 줄로 잇는다.
그림 4-4. 명령어 사이클. 전원이 켜져 있는 한 이 세 단계가 끝없이 돈다. 4장의 모든 내용은 이 고리를 자세히 들여다본 것이다.

이제 직접 돌려 보자. 한 단계 버튼을 누를 때마다 위 세 단계 중 하나가 진행된다. 무엇이 어디로 움직이는지 반짝이는 칸을 따라가 보자.

SIMULATOR

장난감 CPU

예제 프로그램
실행 0 사이클

CPU

PC 다음 명령 주소
0
0000
A 계산용
0
00000000
IR 가져온 명령
—
 
Z 플래그 A가 0인가
1
상태
준비
① 가져오기② 해석③ 실행
ALU 대기 중
출력:

메모리 (16칸, 0~15번지)

한 단계를 눌러 시작하세요. 먼저 PC가 가리키는 0번지에서 명령을 가져옵니다.
해볼 것: “두 수 더하기”를 한 단계씩 끝까지 실행해 보자. PC는 가져오기 단계마다 1씩 늘어나고, JUMP·JZ만이 PC를 다른 값으로 바꾼다. “1부터 N까지 합”에서는 12번지 값(N)을 바꾸려면 “직접 작성” 탭에서 수정하면 된다.

현미경으로 본 명령 하나

위 시뮬레이터에서 “실행” 한 단계로 보였던 일도, 회로 안에서는 더 잘게 쪼개진 동작의 연속이다. 값이 어느 버스를 타고 어디로 가는지 ADD 15 한 명령을 따라가 보자. 그림 4-2의 데이터 경로가 실제로 움직이는 모습이다.

SIMULATOR

ADD 15의 미시 단계

진행 0
한 명령이 9개의 작은 동작(마이크로 연산)으로 나뉜다. 실제 CPU는 이 동작들을 여러 명령에 걸쳐 겹쳐서 진행한다(5장의 파이프라인). 그래서 한 명령에 여러 단계가 필요해도 평균적으로는 한 박자에 명령 하나 이상을 끝낸다.

점프 하나로 판단과 반복을

“카운트다운” 프로그램을 다시 보자. CPU에는 “반복하라”는 명령이 없다. 대신 JUMP로 앞쪽 줄로 되돌아가면 반복이 되고, JZ(0이면 점프)로 반복을 빠져나간다.

// 카운트다운: 5, 4, 3, 2, 1, 0 을 출력 0 LOAD 15 // A ← 5 1 OUT // A 출력 ◀──┐ 2 SUB 14 // A ← A − 1 │ 3 JZ 5 // A가 0이면 5번지로 │ (탈출) 4 JUMP 1 // 아니면 1번지로 ────┘ (반복) 5 OUT // 0 출력 6 HALT

우리가 프로그래밍 언어에서 쓰는 if(만약), while(~하는 동안), for(~번 반복)는 모두 결국 이런 조건 점프로 번역된다(13장). 아무리 복잡한 앱도 CPU에게는 “계산하고, 저장하고, 조건에 따라 다른 줄로 점프하기”의 반복일 뿐이다.

튜링 완전

놀랍게도 “메모리 읽기/쓰기, 더하기/빼기, 조건 점프” 정도만 있으면 (메모리가 충분하다는 가정하에) 어떤 계산이든 할 수 있다는 것이 증명되어 있다. 이런 성질을 튜링 완전이라 한다. 장난감 CPU도 원리적으로는 슈퍼컴퓨터와 같은 문제를 풀 수 있다. 다만 엄청나게 느리고 메모리가 작을 뿐이다.

예측해 보기

카운트다운 프로그램에서 3번지의 JZ 5를 조건 없는 JUMP 4(바로 다음 줄로 점프)로 바꾸면 어떻게 될까?

장난감 CPU의 ‘직접 작성’ 탭에 들어 있는 ‘1부터 N까지 합’에서 JZ 8 줄을 JUMP 7로 바꿔 실행해 보면 같은 일이 생긴다.

반복을 끝내는 유일한 문이 조건 점프였다. 그 문이 사라지면 JUMP 1이 영원히 되돌아가 무한 반복이 된다. 이 CPU의 8비트 값은 −128 다음에 127로 돌아가므로 값 자체가 끝없이 작아지는 것은 아니다. 시뮬레이터는 일정 단계 뒤 멈추지만 실제 컴퓨터에서는 프로그램이 ‘응답 없음’ 상태가 되고, 운영체제가 타이머 인터럽트로 CPU를 되찾아야 다른 프로그램이 돈다(8장).

함수 호출: 갔다가 돌아오기

프로그램에는 여러 곳에서 되풀이해 쓰는 코드 묶음, 즉 함수가 있다. 예를 들어 “화면에 글자 찍기”는 프로그램 곳곳에서 부른다. 함수로 점프하는 것은 JUMP로 할 수 있다. 문제는 돌아오는 길이다. 함수가 끝났을 때 원래 어디로 돌아가야 할까? 부른 곳이 매번 다르다.

예를 들어 x86 계열에는 다음 두 명령이 있다. CALL n은 “돌아올 주소(지금 PC 값)를 스택이라는 메모리 영역에 적어 두고 n번지로 점프”한다. 함수 끝의 RET은 “스택에 적어 둔 주소를 꺼내 PC에 넣는다”. 책을 읽다가 각주를 보러 갈 때 지금 쪽에 손가락을 끼워 두는 것과 같다.

본 프로그램 20: LOAD 3 21: CALL 50 22: STORE 9 ◀ 돌아올 곳 23: … 함수 (50번지부터) 50: ADD 7 51: OUT 52: RET ① 점프 ③ 돌아오기 스택 … 22 (돌아올 주소) ② CALL이 적어 둠 ③ RET이 꺼내 PC로 함수 안에서 또 함수를 부르면? 돌아올 주소가 스택 위에 한 칸씩 더 쌓인다. RET은 늘 맨 위의 것부터 꺼내므로 가장 나중에 부른 함수부터 차례로 정확히 되돌아간다. 13장에서 이 스택을 직접 쌓아 본다.
그림 4-5. CALL과 RET. 돌아올 주소를 스택에 적어 두는 덕분에 같은 함수를 어디서 불러도 제자리로 돌아온다.

장난감 CPU에는 CALL과 RET이 없지만, 실제 ISA는 다른 방식으로도 호출과 복귀를 지원한다. Arm64에서는 BL이 복귀 주소를 링크 레지스터(X30)에 넣고 RET이 그 주소로 돌아간다. 다른 함수를 다시 부를 때 등 필요하면 소프트웨어가 링크 레지스터를 스택에 보관한다. 스택의 맨 위를 가리키는 전용 레지스터(스택 포인터)도 함께 둔다. 운영체제, 앱, 게임 모두 수천 개의 함수가 서로를 부르며 실행된다.

실제 CPU는 무엇이 다를까

원리는 장난감 CPU와 같다. 하지만 규모가 다르다.

장난감 CPU요즘 실제 CPU
한 번에 다루는 비트864
범용 레지스터1개16~32개 (+ 벡터 레지스터)
명령어 종류8개수백~수천 개
메모리 주소16칸2⁴⁸ 칸 이상 (수백 TB)
클럭손가락3~5.5 GHz
한 박자에 처리하는 명령1/3개여러 개 (파이프라인·슈퍼스칼라, 5장)
코어(CPU 두뇌) 수18~24개 이상

실제 명령어 집합으로는 PC와 서버에서 쓰는 x86-64(인텔, AMD)와 스마트폰과 최신 맥에서 쓰는 ARM이 대표적이다. 같은 일도 ISA에 따라 다른 숫자로 적는다. 그래서 윈도우용 프로그램이 그대로 스마트폰에서 돌아가지 않는다. 같은 레시피라도 다른 언어로 써야 하는 것이다.

// a = b + c 를 각 ISA로 적으면 (개념적 예) 장난감: LOAD b ADD c STORE a x86-64: mov eax, [b] add eax, [c] mov [a], eax ARM64: ldr w0, [b] ldr w1, [c] add w0, w0, w1 str w0, [a]
CISC와 RISC

x86은 한 명령이 복잡한 일(메모리에서 읽어 더하기까지)을 한꺼번에 하는 CISC 전통에서, ARM은 단순한 명령만 두고 대신 빠르게 많이 실행하는 RISC 전통에서 출발했다. 요즘은 x86도 내부에서 복잡한 명령을 단순한 조각으로 쪼개 실행하기 때문에 둘의 차이는 많이 줄었다. 전력 효율이 중요한 스마트폰에서는 ARM이 우세하다.

핵심 정리

  1. CPU는 제어 장치(해석·지시), ALU(계산), 레지스터(초고속 소형 기억)로 이루어진다.
  2. PC(프로그램 카운터)는 다음 명령의 주소를 가리킨다. 레시피를 짚는 손가락이다.
  3. 명령어도 숫자다. 앞부분은 연산 종류(opcode), 뒷부분은 대상(주소·레지스터)을 담는다. 이 약속표가 ISA다.
  4. CPU는 가져오기 → 해석 → 실행을 끝없이 반복한다.
  5. 반복과 조건 판단은 모두 점프와 조건 점프로 만든다.
  6. x86의 함수 호출은 CALL(돌아올 주소를 스택에 적고 점프)과 RET(적어 둔 주소로 돌아가기)으로 한다.
  7. 실제 CPU는 원리는 같지만 비트 폭, 레지스터, 명령어 수, 속도, 코어 수가 훨씬 크다. x86과 ARM은 서로 다른 ISA다.

확인 퀴즈

프로그램 카운터(PC)가 저장하는 것은?

이름에 “카운터”가 들어가지만 개수를 세는 것이 아니라 “다음 줄 번호”를 가리킨다. 가져오기 단계마다 1씩 늘어난다.

장난감 CPU에서 0011 1110은 어떤 명령인가?

앞 4비트 0011은 SUB, 뒤 4비트 1110은 14. A에서 14번지 값을 뺀다.

명령어 사이클의 올바른 순서는?

메모리에서 명령을 가져와야(Fetch) 해석(Decode)하고 실행(Execute)할 수 있다.

CPU가 “반복문”을 실행할 수 있게 해 주는 것은?

점프로 PC를 이전 주소로 바꾸면 같은 명령들이 다시 실행된다. 조건 점프로 빠져나올 시점을 정한다.

윈도우 PC용 프로그램이 스마트폰에서 그대로 실행되지 않는 근본 이유 중 하나는?

기계어는 특정 ISA의 약속표로 적혀 있다. 다른 ISA의 CPU는 그 숫자를 다른 뜻으로 해석한다(운영체제 차이도 또 다른 이유다).

장난감 CPU의 명령어에서 주소 칸이 4비트다. 이 CPU가 가리킬 수 있는 메모리 칸은 몇 개인가?

4비트로 만들 수 있는 수는 2⁴ = 16가지(0~15)다. 주소 비트가 늘어날수록 더 큰 메모리를 쓸 수 있다.

CALL 명령이 점프하기 전에 스택에 적어 두는 것은?

RET이 그 주소를 꺼내 PC에 넣으므로, 함수를 어디서 불렀든 정확히 다음 줄로 돌아온다.