포스트

CPU 동작 원리

CPU 동작 원리

기계어(Machine Code)

CPU가 직접 해독하고 실행할 수 있는 유일한 언어로, 0과 1의 조합으로 이루어진 저수준 명령어입니다.

어셈블리어(Assembly Language)

인간이 읽고 작성할 수 있는 코드(C, Java, Python)와 컴퓨터가 이해할 수 있는 기계어(0 또는 1)의 중간 단계 언어이며 기계어와 1:1로 대응되는 기호(Symbolic) 언어입니다.
MOV AL, 1처럼 사람이 읽을 수 있는 형태로 표현되어, 데이터 이동, 레지스터 조작, 메모리 접근 등 하드웨어를 직접 제어하는 데 사용됩니다.

명령어의 구조(Instruction Format)

CPU가 이해하는 명령어(기계어)는 연산 코드(Opcode)오퍼랜드(Operand) 로 구성된 비트 덩어리입니다.

연산 코드(Operation Code, Opcode)

CPU가 수행해야 할 연산의 종류를 나타냅니다. (LOAD, STORE, ADD, JUMP 등) Opcode 필드의 비트 수가 n개라면, 해당 CPU는 최대 $2^{n}$ 개의 서로 다른 명령어를 가질 수 있습니다. 예를 들어 4비트 Opcode는 16개의 명령어를 정의할 수 있습니다.

오퍼랜드(Operand)

연산에 사용될 데이터 또는 데이터가 저장된 위치(메모리 주소, 레지스터 번호 등)를 지정합니다. 하나의 명령어는 오퍼랜드의 개수에 따라 0-주소, 1-주소, 2-주소, 3-주소 명령어 형식으로 나뉩니다.

1
2
3
4
5
6
7
ADD R1, R2, R3 // R2와 R3를 더해 R1에 저장
연산 코드 - ADD
오퍼랜드 1 - R1 레지스터 // 목적지
오퍼랜드 2 - R2 레지스터 // 소스
오퍼랜드 3 - R3 레지스터 // 소스

오퍼랜드가 3개 이므로 3-주소 명령어

명령어의 종류

데이터 전송(Data Transfer)

레지스터와 메모리, 또는 레지스터와 레지스터 간에 데이터를 옮깁니다.

  • LOAD : 메모리에서 레지스터로 데이터를 가져옵니다.
  • STORE : 레지스터의 데이터를 메모리에 저장합니다.
  • MOVE : 레지스터 간, 혹은 레지스터와 메모리 간 데이터를 복사합니다.
1
2
LOAD R1, 100 // 메모리 주소 100에서 데이터를 레지스터 R1으로 로드
STORE R1, 200 // 레지스터 R1의 데이터를 메모리 주소 200에 저장

산술 및 논리 연산(Arithmetic/Logic)

ALU에서 실제 계산을 수행합니다.

  • 산술 연산 : ADD(덧셈), SUB(뺄셈), MUL(곱셈), INC(1 증가) 등
  • 논리 연산 : AND, OR, NOT, XOR (비트 단위 논리곱, 논리합 등)
  • 시프트/회전 : SHIFT, ROTATE (비트를 왼쪽이나 오른쪽으로 이동시켜 곱셈/나눗셈을 빠르게 수행하거나 특정 비트를 추출)
1
2
SUB R1, R2 // R1에 R2의 값을 뺌
AND R1, R2 // R1과 R2의 비트를 AND 연산

제어 흐름 변경(Control Flow)

프로그램 카운터(PC)의 값을 변경하여 프로그램의 실행 순서를 제어합니다. 기본적으로 차례대로 실행되던 흐름을 바꾸는 역할을 합니다.

  • JUMP : 특정 주소로 무조건 실행 위치를 이동시킵니다.
  • BRANCH (or Conditional JUMP) : 특정 조건(결과가 0인가?, 음수인가?)이 만족할 때만 지정된 주소로 이동합니다. (if-else, loop 구현의 핵심)
  • CALL : 함수(서브루틴)를 호출할 때 사용하며, 돌아올 주소(현재 PC+1)를 스택에 저장한 후 함수 위치로 점프합니다.
  • RET : 호출했던 함수(서브루틴)의 실행이 끝나고 원래 위치로 복귀합니다. 스택에서 돌아올 주소를 꺼내 PC에 로드합니다.
1
2
JMP 400 // 프로그램을 주소 400으로 이동
BEQ R1, R2, 500 // R1과 R2가 같으면 주소 500으로 이동

입출력(Input/Output)

CPU가 입출력 장치와 데이터를 교환합니다. (IN, OUT 등)

  • IN : 데이터를 읽어옵니다.
  • OUT : 데이터를 전송합니다.
1
2
IN R1, 0xFF // 입력 장치 0xFF에서 데이터를 읽어 R1에 저장
OUT R1, 0xFF // R1의 데이터를 출력 장치 0xFF로 보냄

오퍼랜드 주소 지정 방식(Addressing Modes)

주소 지정 방식이란, 명령어가 연산에 사용할 데이터(오퍼랜드)를 어떻게 찾아갈 것인지를 결정하는 규칙입니다. CPU 설계에 따라 다양한 방식이 존재하며, 프로그램의 유연성과 효율성에 큰 영향을 미칩니다.

정의 내용표기 방법
유효 주소(데이터의 최종 주소)EA
명령어에 명시된 주소 값A
레지스터 번호R
기억장치 A 번지의 내용(A)
레지스터 R 번지의 내용(R)

유효 주소(Effective Address, EA)는 데이터에 접근하기 위한 최종적인 실제 주소를 의미합니다.

직접 주소 지정 방식(Direct Addressing)

오퍼랜드 필드에 데이터가 저장된 메모리의 유효 주소가 직접 명시됩니다. (EA = A 가장 기본적인 방식)

데이터 인출을 위해 메모리에 1번만 접근하면 된다는 장점이 있지만, 오퍼랜드 필드의 비트 수에 의해 지정할 수 있는 기억 장소의 수가 제한됩니다.

오퍼랜드 하나에 12비트가 할당되어 있다면 0 ~ $2^{12}$(=4096) 즉 범위를 벗어나는 주소에 접근해야 한다면 12비트로는 접근할 수 없음

1
LOAD R1, 100 // 메모리 100번지에 있는 데이터를 R1으로 가져옴

간접 주소 지정 방식(Indirect Addressing)

오퍼랜드 필드에 있는 값은 데이터의 주소가 아니라, 데이터의 주소를 담고 있는 또 다른 메모리 주소입니다. (EA = (A) 포인터와 유사)

주소를 동적으로 변경할 수 있어 유연성이 높고 직접 주소 방식보다 더 넓은 메모리 영역에 접근할 수 있습니다. (데이터의 주소가 바뀌어도 A의 주소 값만 바꾸면 되고 명령어를 변경할 필요가 없음)

메모리에 두 번(주소를 읽기 위해, 실제 데이터를 얻기 위해) 접근해야 해서 속도가 느립니다.

실제 데이터가 비트값을 넘어가는 B 주소에 저장되어 있다면, 비트값으로 접근할 수 있는 A 주소에 B 주소를 저장하여 접근

1
LOAD R1, (100) // 메모리 100번지로 가서, 그곳에 저장된 주소를 읽어, 해당 주소의 데이터를 R1으로 가져옴

직접 주소 지정 방식과 간접 주소 지정 방식의 비교

직접 주소 지정 방식간접 주소 지정 방식
LOAD R1, 100LOAD R1, (200)
ADD R2, 100ADD R2, (200)
STORE 100, R3STORE (200), R3
  • 직접 주소 지정 방식은 데이터가 주소 100에 저장되어 있으며 데이터 위치가 주소 200으로 변경되면, 모든 명령어에서 주소를 수정해야 합니다.
  • 간접 주소 지정 방식은 주소 200에는 실제 데이터가 저장된 주소(300)가 들어 있으므로 데이터의 위치가 300에서 400으로 변경될 경우 주소 200의 값만 수정하면 됩니다. (200에 400 저장)

즉시 주소 지정 방식(Immediate Addressing)

오퍼랜드 필드에 데이터 값 자체가 직접 포함되어 있습니다.

데이터를 찾아가기 위해 메모리 접근이 필요 없어 속도가 매우 빠르지만, 값의 크기가 오퍼랜드 필드의 비트 수로 제한됩니다.

1
ADD R1, #10 // R1 레지스터에 상수 10을 더함

레지스터 주소 지정 방식(Register Addressing)

데이터가 메모리가 아닌 CPU 내부 레지스터에 저장되어 있습니다. 오퍼랜드 필드에는 해당 레지스터의 번호가 명시됩니다. (EA = R)

기억 장치에 접근할 필요가 없어 속도가 빠릅니다. (기억 장치 접근 속도보다 레지스터 접근 속도가 훨씬 빠름)

데이터를 저장할 수 있는 공간이 CPU 내부의 레지스터로 제한됩니다.

1
ADD R1, R2 // R2 레지스터의 값을 R1 레지스터에 더함

레지스터 간접 주소 지정 방식(Register Indirect Addressing)

오퍼랜드로 지정된 레지스터에 데이터의 메모리 주소가 저장되어 있습니다.(EA = (R))

간접 주소 방식보다 빠르면서(메모리 접근 1회) 주소 계산의 유연성을 제공합니다.

1
LOAD R1, (R2) // R2 레지스터에 저장된 값을 주소로 삼아, 해당 메모리 위치의 데이터를 R1으로 로드

PC-상대 주소 지정 방식 (PC-Relative Addressing)

오퍼랜드 필드의 값을 프로그램 카운터(PC)의 현재 값에 더하여 유효 주소를 계산하는 방식입니다. (EA = PC + A)

JUMPBRANCH 명령어에서 주로 사용됩니다. 프로그램 코드가 메모리의 어느 위치에 로드되더라도 상관없이 실행될 수 있는 ‘위치 독립적인 코드(Position-Independent Code)’ 작성을 가능하게 합니다.

인덱스 주소 지정 (Indexed Addressing)

베이스 주소에 동적인 인덱스 값을 더해 주소를 계산합니다. EA = (Base Register) + (Index Register)

반복문 안에서 배열의 각 원소에 순차적으로 접근할 때 매우 유용합니다.

베이스-오프셋 주소 지정 방식(Base-plus-offset Addressing)

메모리에 있는 데이터 구조에 접근할 때 가장 보편적으로 사용됩니다. (EA = (Base Register) + Offset 직접/간접 방식의 실용적인 진화 형태)

현대의 CPU는 연산을 할 땐 레지스터 방식을, 상수를 다룰 땐 즉시 방식을, 메모리의 데이터(변수)를 가져올 땐 베이스-오프셋 방식을, 코드의 분기를 결정할 땐 PC-상대 방식을 사용하는 등, 각자의 역할에 따라 최적의 주소 지정 방식을 선택하여 사용합니다.

명령어 집합 구조(Instruction Set Architecture, ISA)

ISA는 CPU가 이해하고 실행할 수 있는 모든 명령어의 집합과 그 사용법을 정의한 규약 또는 명세입니다. 개발자가 CPU의 하드웨어 구조를 몰라도 어셈블리어나 컴파일러를 통해 CPU를 제어할 수 있게 해주는 인터페이스 역할을 합니다.

ISA는 설계 철학에 따라 크게 CISC와 RISC로 나뉩니다.

CISC(Complex Instruction Set Computer)

복잡하고 기능이 많은 명령어를 제공하여, 적은 수의 명령어로 복잡한 작업을 처리하는 것을 목표로 합니다.

초기에는 고급 언어의 구문과 유사한 복잡한 명령어가 있어 컴파일러 개발이 용이한 측면이 있었습니다. 또한, 하나의 명령어로 많은 일을 할 수 있어 메모리 사용량이 적다는 장점이 있었습니다.

현대에는 오히려 RISC의 단순한 명령어를 타켓으로 하는 컴파일러 최적화가 더 발전했습니다.

명령어마다 실행 시간이 달라 명령어 해석에 시간이 오래 걸려 CPU 내부 회로가 복잡해지고 성능 향상에 불리합니다.

RISC(Reduced Instruction Set Computer)

자주 쓰이는 간단한 명령어들만 만들고, 이들을 조합하여 복잡한 작업을 처리하는 것을 목표로 합니다.

하드웨어 구현이 단순하고, 명령어마다 실행 시간이 대부분 일정해 해석이 빨라 고성능을 내기 쉽습니다. 하지만 명령어 수가 많아져 프로그램의 크기가 커질 수 있고, 컴파일러의 최적화가 중요합니다.

현대의 CPU는 RISC와 CISC의 장점을 결합한 혼합 설계를 주로 사용합니다.

예를 들어, Intel의 x86-64 CPU(CISC)는 내부적으로 CISC 명령어를 마이크로 옵스(Micro-operations) 라는 여러 개의 단순한 RISC 형태의 명령어로 변환합니다. 그리고 CPU 코어는 이 마이크로 옵스를 RISC 파이프라인에서 실행합니다.

즉, 외부적으로는 CISC의 명령어 집합을 사용해 하위 호환성을 지키면서, 내부적으로는 RISC의 장점인 높은 성능을 취하는 방식입니다.

특징CISCRISC
명령어 구조복잡하고 다양한 명령어간단하고 적은 수의 명령어
명령어 길이가변(Variable-length)고정(Fixed-length)
명령어 실행 시간여러 클럭 사이클 소요 가능대부분 1클럭 사이클
파이프라이닝어려움최적화 쉬움
대표 CPUIntel x86, AMD x86-64ARM, MIPS, Apple Silicon (M-series)
응용 분야- 데스크톱 PC
- 서버(데이터 센터)
- 스마트폰
- 임베디드 시스템(가전제품, IoT 디바이스)
- 랩탑(Apple M, Qualcomm Snapdragon X)

명령어 비교 예시

고급 언어의 a = (b + c) * d 연산을 각각의 방식으로 어셈블리어로 변환

1
2
3
4
5
6
7
8
9
10
11
12
13
// CISC 방식
MOV AX, [B]  ; 메모리 주소 B의 값을 AX 레지스터로 로드
ADD AX, [C]  ; 메모리 주소 C의 값을 AX에 더함(B + C)
IMUL AX, [D] ; 메모리 주소 D의 값을 AX와 곱함((B + C) * D)
MOV [A], AX  ; AX의 값을 메모리 주소 A에 저장

// RISC 방식
LDR R1, [B]    ; 메모리 주소 B의 값을 R1 레지스터에 로드
LDR R2, [C]    ; 메모리 주소 C의 값을 R2 레지스터로 로드
ADD R3, R1, R2 ; R1 + R2 결과를 R3에 저장 (B + C)
LDR R4, [D]    ; 메모리 주소 D의 값을 R4 레지스터로 로드
MUL R5, R3, R4 ; R3 * R4 결과를 R5에 저장 ((B + C) * D)
STR R5, [A]    ; R5의 값을 메모리 주소 A에 저장

RISC 방식의 가장 큰 특징은 메모리 접근은 LDR(Load), STR(Store) 명령어로만 수행하고, 실제 연산(ADD, MUL 등)은 반드시 레지스터 간에만 이루어진다는 점입니다.

이는 모든 명령어가 단순하고 빠르게 실행되도록 하여, 명령어 파이프라이닝 성능을 극대화하기 위한 핵심적인 설계입니다.

이 기사는 저작권자의 CC BY-NC 4.0 라이센스를 따릅니다.