4장 : 컴퓨터 내부 구조
컴퓨터 하드웨어는 어떻게 구성되는가
이제까지 배운 내용을 바탕으로 실제로 컴퓨터를 구성하는 전체 구조를 다루는 거라서, 지금까지 배운 비트, 논리회로, 메모리 등이 실제로 어떻게 유기적으로 연결되는지를 이해하는 게 핵심
🖥️ 4장. 컴퓨터는 어떻게 구성되어 있을까?
1) 비트 → 회로 → 컴퓨터
우리가 지금까지 배운 흐름을 잠깐 복습해볼게:
1장: 비트와 이진수, 전기 신호로 정보를 표현하는 방법을 배웠고
2장: 조합 논리를 통해 ‘계산하는 회로’를 만들었고
3장: 순차 논리를 통해 ‘기억하는 회로’를 만들었지?
이제는 이 회로들을 조합해서 '컴퓨터'라는 하나의 시스템을 만든다는 거야. 즉, 지금부터는 진짜로 전기 신호로 작동하는 컴퓨터 전체의 뼈대를 만들기 시작하는 거지.
2) 컴퓨터의 세 가지 핵심 구성 요소
컴퓨터를 크게 나누면 이렇게 세 가지 구성으로 설명할 수 있어:
CPU (Central Processing Unit)
계산과 판단을 담당하는 '두뇌' 역할
Memory (메모리)
데이터를 '기억'해 두는 공간
I/O (Input/Output)
바깥세상과 소통하는 창구 (입력장치와 출력장치)
이 세 가지가 유기적으로 연결되면, 우리가 아는 컴퓨터가 되는 거야.
3) 이 장에서는 ‘간단한 컴퓨터’를 만든다
이번 4장에서는 설명이 쉬운 구조의 간단한 컴퓨터를 만드는 게 목적이야. 이 컴퓨터가 시장에서 팔리는 제품처럼 강력하거나 최적화되어 있는 건 아니야. 하지만 컴퓨터의 핵심 개념은 담겨 있기 때문에, 이 구조를 이해하면 어떤 컴퓨터든 구조가 눈에 들어오기 시작할 거야.
"간단한 구조로 본질을 꿰뚫고, 이후 복잡한 최적화를 이해하는 것" 이게 우리가 이 장에서 추구하는 방향성이야.
4) 컴퓨터 구조의 중심: 도심(CPU)
이 책에서는 CPU를 도심(City Center)
에 비유하고 있어.
모든 정보와 계산이 집중되는 중심지라는 의미지.
CPU는 메모리에서 정보를 받아와서 계산하고, 그 결과를 다시 메모리나 I/O로 보내줘.
도심(CPU)이 메모리(창고)와 I/O(도로망, 창구)를 통해 정보들을 모으고, 계산해서 다시 내보내는 구조라고 생각하면 돼.
1. 메모리 : 메모리는 어떻게 쓰일까?
메모리는 물리적으로 어떻게 구성되나
메모리 주소체계
여러 bit를 어떤 식의 주소로 쓰나
페이징할 때의 주소는 어떤 형식
프로그램 -> 메모리로 주소가 어떻게 변환되나
메모리 할당
protection memory
메모리를 할당하는 방법과 단편화 문제
페이징 시 주소변환, 효율적인 변환방법들
프로세스 context switching 때 페이징 관리 , demanding page보완
페이징 교체 알고리즘
1) 메모리는 물리적으로 어떻게 구성되나?
메모리(메인 메모리)는 ram(random access memory)이라고도 하며 디스크보다 빠른 속도의 i/o가 가능하지만 휘발되는 저장공간이다. (주로 d-ram이 사용됨)
아래와 같이 생겼으며, 저 칩 하나하나에 몇 억 개의 셀이 존재한다.
2) 메모리는 ‘정렬된 집들’이다
컴퓨터에서 메모리는 마치 일렬로 정렬된 집들 같아.
각각의 ‘집’은 고정된 크기(보통 1바이트)를 가지고 있고 정해진 개수만큼 비트를 저장할 수 있는 방이 있는 거지
모든 집에는 주소(address)가 붙어 있어!
예를 들어 컴퓨터에 64MiB의 메모리가 있으면,
주소는 0번
부터 67,108,863번
까지 차례대로 붙어 있는 거지.
👉 이걸 우리는 메모리 주소 공간이라고 불러.
3) 메모리는 바이트 단위로 나뉘지만, CPU는 더 크게 읽기도 해
컴퓨터는 실제로 1 바이트 하나만 읽기보다는, 더 큰 단위로 데이터를 다루는 경우가 많아:
32비트 시스템은 보통 4바이트씩 (긴 워드)
64비트 시스템은 8바이트씩 데이터를 다뤄
그 이유는?
🚗 도로에 비유하자면, 한 차선으로 가는 것보다 4차선, 8차선 도로가 훨씬 빠르게 데이터를 전송할 수 있기 때문이야.
메모리를 주소로 지정할 때는 어떤 대상(1바이트, 4바이트, 8바이트 등)을 원하는지 지정해야 한다
4) 포플렉스(Pourplex) 구조로 생각해보자!
바이트 → 듀플렉스(2바이트) → 포플렉스(4바이트) → 8바이트 구조까지…
이렇게 집을 묶어서 더 큰 단위의 블록으로 생각하면, CPU는 한 번에 포플렉스(4바이트) 단위로 메모리에 접근한다고 보면 돼!
32비트 컴퓨터의 메모리 구조를 포플렉스fourplex(집이 4개 연달아 붙어 있는 땅 콩집)가 늘어서 있는 길로 생각할 수도 있어
각 포플렉스에는 다시
듀플렉스(집이 2개 붙어 있는 땅콩집)
가 2개 들어 있고, 각 듀플렉스에는 유닛(한 세대)이 2개 들어 있어.이 말은 우리가 각 유 닛이나 듀플렉스 또는 건물 전체(포플렉스)의 주소를 지정할 수 있다는 뜻
5) 정렬된 접근 vs 정렬되지 않은 접근
메모리를 읽을 때도 규칙이 있어.
📌 정렬된 접근 (Aligned Access)
예: 4바이트 데이터를 주소 0번에서 읽는다 → 0,1,2,3번 바이트 딱 맞게 읽기
빠르고 효율적
❌ 정렬되지 않은 접근 (Non-Aligned Access)
예: 4바이트 데이터를 주소 5번부터 읽는다 → 5,6,7,8번 바이트인데 건물 2개에 걸침
비효율적 & 전통적으로 금지되는 경우가 많았음
📌 요약: “정렬된 접근이 빠르고 안전하다”는 개념이 컴퓨터 설계의 중요한 원칙이야!
🔄 5) 리틀 엔디안 vs 빅 엔디안
도심을 오가는 버스의 각 자리에는 누가 앉을까? 긴 단어의 경우 가장 왼쪽 자리에 0번 바이트 가 들어갈까, 3번 바이트가 들어갈까?
사용하는 프로세서에 따라 답이 달라져.
두 방식이 모 두 사용되기 때문에, 어느 방식을 사용하느냐는 프로세서를 설계하는 사람의 마음에 달렸음
큰 데이터를 여러 바이트에 나눠서 저장할 때, 순서를 어떻게 저장할까?
📌 리틀 엔디안 (Little Endian) – 인텔 방식
인텔 프로세서
덜 중요한 바이트가 먼저
예:
0x12345678
→ 저장 순서:78 56 34 12
📌 빅 엔디안 (Big Endian) – 모토로라 방식
모토로라Motorola 프로 세서
더 중요한 바이트가 먼저
예:
0x12345678
→ 저장 순서:12 34 56 78
이 차이 때문에 시스템 간 데이터 전송 시에는 엔디안 변환을 신경 써야 해. . 엔디안을 무 시하면 데이터 순서가 뒤섞일 수 있어.
유닉스를 다른 컴퓨터로 옮겼다가
Unix
가nUxi
로 출력된 걸 "눅시 신드롬(Nuxi syndrome)"이라고 불러 😅
🧭 6) 메모리 주소는 프로그램이 직접 지정하지 않는다!
우리가 코드를 작성할 때는 int a = 12345;
처럼 변수 이름만 쓰잖아?
근데 컴퓨터 입장에서는 “a가 어디 있는지” 알아야 해.
이 과정은 다음과 같이 진행돼:
Symbolic Address
사람이 쓴 변수 이름 (예: a
)
Relocatable Address
컴파일러가 만든 가상의 주소
Absolute Address
링커가 배치한 프로그램 내의 실제 주소
Physical Address
운영체제가 RAM 안에서 배정한 물리 주소
이 주소 변환을 실제로 수행하는 하드웨어가 MMU (Memory Management Unit)야!
프로그램은 0x00000000 ... 0xFFFFFFFF의 물리주소를 그대로 사용할까? 결론은 아니야
이는 실행되기 전까지는 디스크에 저장된 일종의 .exe파일일 뿐이야.
이게 실행될 때 a라는 변수가 메모리의 어느 주소에 있는지를 할당하는 것은 정해져있지 않아.(symbolic할 뿐이다)
이를 지정은 컴파일러가 수행
코드의 변수 a와 같은 symbolic한 변수를 컴파일러가 메모리에 할당가능한 가상의 주소로 바꿔서 어셈블리어로 서술하는 것야.
이 relocable한 주소는 링커(linker) 를 통해 absolute address가 정해지고, 로더(loader)를 통해 프로세스가 물리적으로 어디부터 어디의 메모리 주소(physical address)를 사용할지 할당을 os의 커널을 통해 수행해
symblic address -> relocable address -> absolute address -> physical address
즉 이 logical한 가상의 주소를 물리주소로 바꿔서 할당하고 접근하는 작업을 os가 수행하면서 주소변환,프로세스간의 메모리 보호 기능도 수행해
🛡️ 5) 메모리는 보호되어야 한다
mmu(memory management unit)이라고 하는 물리적인 장치를 이용해 수행해
한 도시 안에 여러 사람이 살아도 서로 집 안에 무단 침입하면 안 되잖아? 컴퓨터도 마찬가지야!
메모리 보호 기능: 프로세스가 서로 별도의 메모리 공간을 사용해야하므로 이를 위한 제약을 구현해야 해
프로세스 A가 프로세스 B의 메모리를 건드리면 안 돼
이를 막기 위해 CPU는 base register와 limit register를 사용해
base: 시작 주소
limit: 사용 가능한 범위
base register와 limit register를 통해 cpu에서 메모리로의 접근 범위를 제한해서 가능하게 해.
메모리 보호 기능의 핵심이야! (이 개념은 페이징에서도 다시 나와)
📌 요약 정리
메모리 주소
바이트 단위로 나열된 ‘집’에 붙은 번호
워드 단위 접근
32비트 → 4바이트, 64비트 → 8바이트 단위
정렬된 접근
메모리 블록 경계에 맞게 읽는 것 (속도 ↑)
리틀 vs 빅 엔디안
바이트를 저장하는 순서 차이
주의사항
다른 시스템과의 통신이나 네트워크 전송 시 엔디안 고려 필수!
2. 입력과 출력
🧃 컴퓨터는 왜 I/O가 필요할까?
컴퓨터가 혼자 계산만 하고 외부와 소통하지 않으면 무슨 의미가 있을까? 우리에게 필요한 건…
키보드, 마우스, 센서처럼 정보를 입력하는 장치
화면, 스피커, 프린터처럼 정보를 출력하는 장치
👉 이런 장치들을 통틀어 I/O Device (입출력 장치)라고 해! 그리고 컴퓨터와 I/O 장치가 소통하는 방법을 I/O (Input/Output)이라고 부르지.
이들은 컴퓨터의 주변부에 위치하기 때문에 주변장치peripheral device라고 부르며, 영어로는 퍼리퍼럴peripheral이라고 해.
🛣️ I/O도 메모리처럼 '거리'가 있다!
이제 도시 비유로 설명해볼게!
📍 도시 중심: CPU (도심이라고 했지!)
🏠 메모리 거리: RAM 주소들이 줄줄이 있는 구역
🏭 I/O 거리: 주변기기들이 위치한 별도의 산업 지구 같은 곳
과거엔 컴퓨터의 물리적 구조가 커서, 메모리 거리와 I/O 거리를 분리해서 관리했어. → 각자의 버스를 탔던 셈이지!
그림 4-5처럼 이렇게 나눠져 있었지:
과거에는 메모리 거리에 집이 많지 않았기 때문에, 제한된 주소를 I/O를 지원하느 라고 낭비하는 일은 바람직하지 않았어!
💡 그런데 요즘은 왜 I/O도 메모리 거리 안에 들어오게 됐을까?
이유는 바로… “주소 공간이 너무 많아졌기 때문”!
요즘 컴퓨터는 32비트, 64비트라서 주소 공간이 엄청 넓어졌어.
그래서 메모리 거리에 빈 집(주소)
이 많아졌고,
그 빈 집 일부를 I/O 장치에게 할당해버린 거야.
이걸 Memory-Mapped I/O (메모리 매핑 입출력)이라고 해.
🧭 Memory-Mapped I/O vs Port-Mapped I/O
Memory-Mapped I/O
I/O 장치도 메모리처럼 주소를 가짐
산업 지구를 도시 안에 포함시킨 느낌
Port-Mapped I/O
I/O 주소를 따로 관리 (예: x86의 in
, out
명령어)
메모리와 I/O를 아예 다른 거리로 구분한 것
요즘 대부분의 시스템은 메모리 맵 방식으로 설계돼. 효율적이니까!
🧱 I/O 슬롯 = I/O를 꽂는 자리
컴퓨터에는 표준화된
I/O 슬롯(slot)
이 있어.이 슬롯마다 일정한 주소 범위가 할당되고,
그 범위 내에서만 해당 장치가 활동 가능해!
마치 도시의 산업 구역을 “공장용”, “발전소용”처럼 구역을 나눈 셈이야.
🧩 정리하면!
컴퓨터는 외부와 통신하기 위해 I/O 장치가 필요하다.
과거에는 I/O와 메모리 주소 공간을 분리했지만,
요즘은 메모리 공간에 I/O 장치를 포함시켜 관리한다.
이를 Memory-Mapped I/O라고 하며,
표준화된 I/O 슬롯 구조를 통해 일관성 있게 연결된다.
3. 중앙 처리 장치(CPU)
중앙 처리 장치(CPU)는 실제 계산을 처리하는 컴퓨터 부품이야. 우리가 사용하는 비유에
CPU는 도심에 해당
다른 모든 요소 : CPU를 지원하는 역할
1️⃣ 산술 논리 장치(ALU) : 컴퓨터 도심 한가운데의 계산소
이제 우리가 드디어 도달한 곳은 바로 CPU의 심장부, 계산의 중심인 ALU(산술 논리 장치)야!
✅ ALU란?
ALU(Arithmetic Logic Unit)는 산술 연산(+, -, shift)과 논리 연산(AND, OR, XOR 등)을 담당하는 장치야.
🧮 쉽게 말해, 컴퓨터 안의 계산기! → 연산을 수행하는 건 이 ALU가 도맡아 해.
🎯 ALU는 어떤 일을 할까?
ALU는 기본적으로 다음 3가지를 입력으로 받아:
피연산자 A : 수를 표현하는 비트
피연산자 B
연산 코드 (opcode): 어떤 연산을 할지 지정, 연산자 지정
A와 B는 어디서 오냐면?
보통은 레지스터나 메모리에 저장된 값을 A와 B에 넣어서 계산해.
CPU 내부에 있는
레지스터들(R1, R2, ...)
에서 값을 꺼내서A ← R1
B ← R2 이런 식으로 ALU에 넣는다고 보면 돼.
그리고 두 가지 출력을 만들어:
연산 결과 (result)
조건 코드 (condition code) : 결과에 대한 추가 정보
보통 조건 코드 레지스터라는 레지스터에 조건 코드 저장
🛠️ 연산 코드: ALU에게 내리는 명령
ALU는 연산 코드를 보고 어떤 연산을 수행할지 결정해. 이건 마치 요리사에게 “볶아줘”, “끓여줘” 같은 지시를 내리는 것과 같아.
예제 연산 코드들 (표 4-1 요약):
0000
clr
결과를 전부 0으로 (초기화)
0001
set
결과를 전부 1로
0010
not
A의 모든 비트 반전
0011
neg
A의 2의 보수 → -A
0100
shl
A를 B만큼 왼쪽으로 쉬프트
0101
shr
A를 B만큼 오른쪽으로 쉬프트
1000
load
B값을 결과로 복사
1001
and
A AND B
1010
or
A OR B
1011
xor
A XOR B
1100
add
A + B
1101
sub
A - B
1110
cmp
B - A → 결과는 버리고 조건 코드만 설정
📦 조건 코드 레지스터 (CCR, 그림 4-8)
ALU는 결과 외에도 “이 연산 결과가 어떤 특성을 가졌는지” 알려줘. 이게 바로 조건 코드이고, 조건 코드 레지스터에 저장돼. 레지스터는 일종의 메모리이긴 하지만 메모리 거리에 존재하지 않는 메모리이야. 메모리 거리에 있는 일반적인 집보다는 더 특별하고 비싼 좋은 집이라고 할 수 있어.
조건 코드 레지스터는 아래처럼 표현할 수 있다:
이 중 오른쪽 3개 비트(2, 1, 0)가 조건 코드 비트이다.
나머지 비트(3~7)는 사용하지 않는 비트이다.
조건 코드 레지스터에서 조건을 표현하는 비트 설명:
N 비트: 마지막으로 수행한 연산 결과가 음수인 경우 1로 설정된다.
Z 비트: 마지막 연산 결과가 0인 경우 1로 설정된다.
O 비트: 마지막 연산에서 **오버플로(또는 언더플로)**가 생긴 경우 1로 설정된다.
N
(Negative)
음수면 1
결과가 음수면 1
Z
(Zero)
0이면 1
결과가 0이면 1
O
(Overflow)
오버플로 시 1
덧셈/뺄셈 중 자리 넘침 발생 시
💡 조건 코드는 if
, jump
같은 조건 분기문에서 사용돼!
🧩 그림 4-9: ALU 내부를 들여다보면?
사실 ALU는 그렇게 신비한 존재가 아니야.
그냥 논리 게이트 + 셀렉터의 조합일 뿐이야!
그림을 보면 이렇게 구성되어 있어:
명령코드에 따라 특정 회로만 활성화되고,
선택된 회로가 A, B에 대해 연산을 수행하고,
결과가 출력돼!
예를 들어:
clr
: 무조건 0을 출력하는 회로not
: A를 반전시키는 회로and
,or
: A와 B를 논리 연산하는 회로
→ 이 중 하나만 선택돼서 출력선으로 연결되는 구조야!
🧠 요약하면?
피연산자 A, B
계산에 사용될 입력 비트
연산 코드
어떤 연산을 할지 지정하는 코드
결과
연산 후 나오는 비트
조건 코드
연산 결과에 대한 추가 정보 (음수? 0? 오버플로?)
ALU 내부
논리 게이트 + 셀렉터 조합
🎓 Q&A로 정리해볼까?
Q: ALU는 무슨 일을 해? A: 숫자 계산, 논리 연산 등 CPU가 계산해야 하는 모든 걸 수행해.
Q: 조건 코드 레지스터는 왜 필요해? A: 결과가 0인지, 음수인지, 오버플로가 났는지를 기록해서 분기문에 쓰기 위함이야!
Q: ALU가 신비해 보여. 복잡한 회로일까? A: 아니, 사실은
AND
,OR
,NOT
같은 논리 게이트를 잘 조합한 구조일 뿐이야!
읽어 보면 좋은 자료 : https://techblog-history-younghunjo1.tistory.com/507
2️⃣ 시프트
시프트는 말 그대로 비트들을 좌우로 밀어내는 연산이야.
✅ 왼쪽 시프트 (Left Shift)
비트를 왼쪽으로 한 칸씩 밀고, 가장 오른쪽엔
0
을 넣는다.맨 왼쪽 비트(MSB)는 버려진다.
2의 배수 곱셈 효과가 있다!
✅ 오른쪽 시프트 (Right Shift)
비트를 오른쪽으로 한 칸씩 밀고, 가장 왼쪽엔
0
을 넣는다.맨 오른쪽 비트(LSB)는 버려진다.
2로 나눈 몫(버림 나눗셈)이 된다.
💡 시프트 연산에서 중요한 것!
시프트하면서 버려지는 비트가 중요한 경우가 있어.
왼쪽 시프트 →
MSB (가장 왼쪽 비트)
가 버려짐오른쪽 시프트 →
LSB (가장 오른쪽 비트)
가 버려짐이 비트들을 O 비트(Overflow) 같은 조건 코드 레지스터에 저장해서 나중에 참조할 수 있어
🧰 시프트 회로 구성 방식 2가지
1) ⏱ 순차적 시프트 레지스터 (Shift Register)
한 번에 1비트씩만 시프트 가능
클록 신호가 있어야 작동 (순차적 구조)
여러 비트를 시프트하려면 시간이 많이 걸림
2) ⚡ 조합 논리 기반 배럴 시프터 (Barrel Shifter)
조합 회로로 즉시 결과를 산출 (훨씬 빠름!)
여러 비트를 한 번에 시프트 가능
입력 비트마다 8:1 실렉터(MUX)를 연결해 구현
실렉터 8개를 병렬로 배치한 구조
입력에 따라 바로 0~7비트까지 시프트된 결과를 만들어냄
✨ 응용
시프트 연산은 간단한 곱셈/나눗셈을 빠르게 처리할 수 있는 방법이야!
특히 부동소수점 연산에서도 소수점을 정렬할 때 꼭 필요해.
곱셈기도 시프트 + 덧셈 조합으로 만들 수 있어!
🧩 요약 정리
왼쪽 시프트
2의 곱 (LSB에 0 삽입, MSB 버림)
오른쪽 시프트
2로 나눈 몫 (MSB에 0 삽입, LSB 버림)
순차 시프터
클록 따라 1비트씩 밀기
배럴 시프터
실렉터 조합으로 여러 비트 즉시 시프트
조건 코드
시프트 중 버려지는 비트는 O 비트에 저장 가능
방금 본 간단한 ALU에 곱셈과 나눗셈 연산이 없어. 그 이 유는 이런 연산이 더 복잡하고, 실제로 새로운 내용을 알려주지도 않기 때문이야.
곱셈은 덧셈 을 반복하면 되고, 이 방식을 택하면 순차 논리로 곱셈을 구현할 수 있어.
또는
왼쪽 시프트
는 어떤 수를 2로 곱하는 것과 같다는 점에 착안해, 배럴 시프터와 가산기를 조합하는 조합 논리 곱 셈기를 만들 수도 있지
3️⃣ ALU에게 명령을 수행하는 주체 : 실행장치
🧠 실행 장치 (Execution Unit, Control Unit)는 컴퓨터의 '대장'
ALU는 계산을 해주는 똑똑한 도구지만, 혼자선 아무것도 못 해. ALU가 뭘 계산해야 할지 알려주는 존재, 바로 실행 장치 (또는 제어 장치)가 필요해.
실행 장치는 ALU에게 “얘랑 얘 더해봐!”라고 명령하고, 계산 결과를 다시 메모리에 잘 넣어주는 감독관 같은 역할이야.
실행 장치는 메모리
에서 정해진 장소로부터 명령코드와 피연산자들을 가져와서 ALU에게 어떤 연산을 수행할지 알려주고, ALU가 연산을 수행한 결과를 내놓으면 이것을 메모리
로 돌려줌
🧾 명령어란?
실행장치가 위의 일을 할 수 있는 이유는?
실행 장치는 스스로 생각하지 않아. 대신, 명령어(instruction)라는 쪽지를 하나하나 읽고 그 지시에 따라 움직이는 거야. 예를 들면:
"주소 10에 있는 값을, 주소 12에 있는 값과 더해서, 주소 14에 저장해!"
이런 쪽지가 바로 명령어야. 그리고 이 명령어들은 메모리 안에 저장돼 있어.특정 주소 안에 저장되어 있지.
🧠 Stored-Program Computer = 프로그램 저장 방식 컴퓨터
이 구조를 Stored-program computer라고 해. 즉, 프로그램(=명령어 모음)을 메모리에 저장하고, CPU가 한 줄씩 읽어가며 실행하는 방식이야.
이 개념의 시초는 앨런 튜링이고,
이 아이디어를 전자식 컴퓨터에 적용한 사람은 존 폰 노이만이야.
그래서 지금 우리가 쓰는 컴퓨터 구조를 폰 노이만 구조라고 불러!
🧭 프로그램 카운터 (Program Counter, PC) = 현재 읽을 쪽지 위치
실행 장치가 명령어를 어떻게 읽냐고?
프로그램 카운터(PC)
라는 레지스터가 현재 다음 읽을 명령어의 주소를 가리키고 있어.
CPU는 PC가 가리키는 메모리 주소에서 명령어를 가져오고,
명령어 하나를 읽고 나면 PC를 자동으로 증가시켜서 다음 명령어를 준비하는 거야.
🧭 비유하자면: 컴퓨터는 ‘보물찾기’하듯 명령어 쪽지를 차례차례 따라가며 일을 수행해.
🧱 프로그램 카운터의 특징
PC도 레지스터다 (특별한 기억 장소).
프로그램 카운터는 메모리 위치를 가르키며 참조라고 해
실행장치는 프로그램 카운터가 가리키는 주소에서 명령어를 읽어와
CPU마다 정해진 초기 프로그램 카운터 값이 있고 CPU 전원이 들어오면 PC는 이 값으로 설정돼
내부적으로는 카운터 회로로 되어 있어서, 명령어를 하나 실행할 때마다 +1 증가되는 구조야.
특수한 명령어(JMP, CALL 등)가 PC 값을 바꾸면 프로그램 흐름을 바꿀 수도 있어.
💬 정리하면!
ALU
계산, 논리 연산
실행 장치 (제어 장치)
ALU에게 일을 시키고, 명령어를 읽고, 실행 흐름 제어
명령어
"무엇을 어떻게 할지"를 적은 쪽지
메모리
명령어들이 저장된 장소
프로그램 카운터(PC)
다음 읽을 명령어의 주소를 저장하는 레지스터
4. 명령어 집합
컴퓨터가 보물찾기를 하는 중에 메모리에서 찾는 쪽지를 명령어
라고 불러
📍 1. 메모리 주소 공간이 부족해지면 어떻게 할까?
컴퓨터가 사용하는 주소는 비트 수로 표현돼. 예를 들어, 32비트 주소는 **최대 4GiB(2³²)**까지만 메모리를 쓸 수 있어.
그런데 컴퓨터 성능이 좋아지면서 4GiB보다 훨씬 많은 메모리를 다뤄야 하는 상황이 생겼어. 그러면 주소가 부족하지 않을까? 그렇지! 그래서 해결책이 필요했어.
1️⃣ 명령어 : 컴퓨터에게 주는 '작업 지시서'
CPU에서 명령어를 어떻게 구성하고 처리하는지, 그리고 왜 명령어를 단순화해야 하는지에
컴퓨터가 무언가를 계산하려면, 사람처럼 명확한 지시가 필요해. 바로 명령어(instruction)라는 형태로 말이야.
📦 명령어는 보통 다음과 같은 정보들을 포함해
명령코드
어떤 연산을 할 건지 (ex. 더하기, 빼기)
피연산자 A
연산 대상이 되는 첫 번째 값의 위치
피연산자 B
연산 대상이 되는 두 번째 값의 위치
결과
결과를 저장할 위치
그래서 초기에는 다음과 같은 3주소 명령어 구조를 썼어:
❗️ 그런데 이 필드를 나누는 구조는 현실적으로 문제가 있어!
주소 공간이 너무 작다
4비트로 표현하면 한 필드당 16개의 주소밖에 못 씀
요즘 컴퓨터는 수십~수백 기가 메모리를 쓰기 때문에 주소 부족 현상 발생
3개의 메모리 위치에 동시에 접근해야 함
CPU는 주소 버스와 데이터 버스가 하나씩만 있어
한 번에 하나의 메모리 위치에만 접근할 수 있어
하드웨어 구현이 복잡해지고 느려짐
하드웨어 구조가 비현실적 → 동시에 3개의 메모리 주소에 접근하려면
주소 버스 3개
데이터 버스 3개
입출력 핀 수 3배 이걸 CPU가 다 감당할 수 없어
결국 같은 핀을 나눠 써야 하고, 동작은 순차적으로 해야 함 → 그러면 굳이 3주소 명령어를 쓸 필요가 없어짐
🧰 또 다른 해결책: 주소 확장 레지스터 (Address Extension Register)
상위 주소 비트를 따로 보관해두는 레지스터
예시: PAE (Physical Address Extension)
인텔이 32비트 CPU에서 4GiB 이상 메모리를 다루기 위해 사용한 기법
추가적인 명령어로 상위 주소 비트를 먼저 설정하고, 그 후 기존 명령어로 메모리에 접근함
동적 RAM인DRAM 주소 지정 기법을 활용하는 것이다
장점
기존 32비트 명령어 구조를 바꾸지 않고도 64GiB 이상 접근 가능
단점
주소 확장 값을 레지스터에 넣는 데 시간 소모
메모리 접근이 여러 번의 작업으로 분할됨 → 복잡하고 느림
❌ 즉, 3주소 명령어는 왜 실용적이지 않을까?
✅ 먼저, 3주소 명령어란?
명령어 안에 피연산자 A, B, 그리고 결과 주소까지 3개의 메모리 주소가 들어 있는 구조야.
❗️문제점
세 가지의 메모리 블록은 각기 다른 장치에 존재하는데 이렇게 하면 하나의 명령어 수행하는데도 회로가 복잡해짐...
🔁 그래서 대안은? → 누산기(Accumulator) 구조 사용!
피연산자 A와 누산기에 있는 값을 연산해서, 다시 누산기에 결과를 넣는 방식!
한 번에 한 메모리 위체에만 접근할 수 있다는 사실에 맞춰 레지스터 거리에 누산기라는 레지스터를 하나 추가하는 시도를 하는 거야
🏗 누산기란?
ALU 옆에 붙어 있는 특별한 레지스터 하나야. 계산의 결과를 잠깐 담아두고, 다음 연산에 이어서 쓰는 작은 창고 같은 느낌이지!
누산기는 ALU가 계산한 결과를 저 장해. 우리는 두 메모리 위치에 있는 값 사이에 연산을 수행해서 결과를 다른 메모리에 넣는 대신, 한 메모리 위치에 있는 값과 누산기에 있는 값에 대해 연산을 수행하고 결과를 누산기에 넣어.
물론 이 과정에서 누산기에 있는 값을 메모리에 저장하기 위한 저장 명령어
를 추가해줘야 해
🧱 1주소 명령어 구조의 장점
단 하나의 주소만 지정하면 됨
나머지는 누산기가 알아서 처리
하드웨어가 단순해지고, 주소 공간을 훨씬 넓게 쓸 수 있어!
💡 예제 비교
예시: C = A + B
계산
C = A + B
계산3주소 명령어
C = A + B
← 한 줄로 끝
1주소 명령어
누산기 = A
누산기 = 누산기 + B
C = 누산기
← 3줄 필요
이러면 더 비효율적인 것처럼 보이지만…!
예시: D = A + B + C
처럼 피연산자가 3개 이상일 경우
D = A + B + C
처럼 피연산자가 3개 이상일 경우네 가지 주소가 연관되기 때문에, 3주소 명령어를 사용해도 이 식을 한 명령어로 처리할 수 없 어.
3주소
중간값 = A + B D = 중간값 + C
2개 명령어 × 40비트 = 80비트
세 가지 주소와 명령코드를 저장하기 위해
피연산자 A 주소 : 12비트 피연산자 B 주소 : 12비트 결과 주소 : 12비트 명령 코드 : 4비트
주소 하나 지정하는 데 12비트 필요
피연산자 2개 + 결과 1개 = 12비트 × 3
명령코드는 예를 들어 4비트 정도 필요
총합 = 36 + 4 = 40비트가 되는 거지!
1주소
누산기 = A 누산기 = 누산기 + B 누산기 = 누산기 + C D = 누산기
4개 명령어 × 16비트(1주소 명령어비트 수) = 64비트
4개의 명령어
명령코드 : 4비트 주소 : 12비트
1주소 명령어 방식 (16비트짜리 명령어 4개 필요)
LOAD A
ADD B
ADD C
STORE D
→ 명령어 4개, 총 64비트
➡️ 오히려 1주소 명령어가 더 효율적일 수 있음!
📌 마무리 요약
명령어는 명령코드 + 피연산자 + 결과로 구성됨
3주소 구조는 표현은 간단하지만 주소 제한 & 하드웨어 복잡성 문제 있음
1주소 구조 + 누산기 레지스터를 쓰면 더 많은 주소를 활용 가능
실제 명령어 실행은 1줄을 여러 단계로 나눠 수행하고, 프로그램 카운터와 함께 쭉 실행됨
누진기는 왜 피연산자가 필요없는걸까?
명령어에 모든 피연산자를 다 안 써도 되도록 만들어진 구조
1주소 명령어 시스템에서는 명령어 안에 피연산자 하나만 지정해.
바로 나머지 하나의 피연산자는 항상 ‘누산기’에 있기 때문이야.
즉, 연산은 이렇게 되는 거지:
여기서:
누산기 = 항상 존재하는 내부 피연산자
주소 B의 값 = 명령어로 지정된 외부 피연산자
2️⃣ 주소 지정 모드
이건 "명령어 안에 들어 있는 피연산자 정보를 어떻게 해석해서 데이터를 가져올지"를 정하는 방식
🧠 주소 지정 방식이란?
CPU가 명령어를 읽고 실행할 때, "어디서 값을 가져올까?" 또는 "값을 어떻게 해석할까?"를 결정하는 방식이야.
위처럼 명령어에 12
라는 숫자가 들어 있을 때,
이걸 어떻게 해석하느냐에 따라 결과가 달라져!
🔧 대표적인 주소 지정 방식 3가지
직접 주소 지정 (Direct)
주소 12
에 있는 값을 읽는다
명령어에 적힌 숫자를 주소로 사용함
주소 12의 값인 4321
을 로드
간접 주소 지정 (Indirect)
주소 12
에 있는 값을 주소로 해석해서, 그 주소에 있는 값을 읽는다
주소 안에 주소가 들어 있는 방식
주소 12의 값이 4321
, 4321의 값이 345
→ 345
를 로드
즉시 주소 지정 (Immediate)
명령어에 있는 숫자를 그대로 값으로 사용
주소 아님, 그냥 '값'
12
를 그대로 로드
1) 직접 주소 지정 (Direct Addressing)
의미: 명령어 안에 직접 사용할 메모리 주소가 포함되어 있어.
예시:
load 12
→ 메모리 주소 12에 있는 값을 가져와서 누산기에 넣는다.명령어에 들어있는 숫자(345)는 주소야!
즉, "메모리 345번지를 열어봐!"
그 주소 안에 들어 있는 값을 누산기에 넣는 방식이야.
특징:
직관적이고 빠르지만
명령어 비트 수가 한정돼 있기 때문에 주소 공간이 제한됨 (ex. 12비트면 0~4095까지만 가능)
2) 간접 주소 지정 (Indirect Addressing)
의미: 명령어 안의 값은 실제 주소가 아니라, 진짜 주소를 담고 있는 메모리 주소를 의미해.
예시:
load @12
(12는 간접 주소)메모리 주소 12에 저장된 값이 4,321이라면
최종적으로 메모리 주소 4,321에 있는 값을 누산기에 가져와
명령어에 들어있는 **숫자(4321)**는 주소를 가리키는 주소야!
즉, "메모리 4321번지를 열어봐!" → 거기엔 345라는 값
그 다음, 345번지를 열어봐! → 거기엔 12
그래서 누산기에는 최종적으로 12가 들어가!
비유:
“열쇠가 있는 상자의 위치(12번)”를 알려주고
상자를 열면 진짜 금고(주소 4321)의 위치가 나오는 느낌.
장점:
메모리 공간을 훨씬 많이 사용할 수 있음
단점:
메모리에 세 번 접근해야 하므로 느림 (명령어 읽기 + 주소 얻기 + 값 읽기)
3) 즉시 주소 지정 (Immediate Addressing)
의미: 명령어 안의 값이 주소가 아니라 값 자체(상수)야!
예시:
load #12
→ 숫자 12 자체를 누산기에 넣는다.이때는 메모리를 접근하지 않고, 명령어에 있는 숫자 자체를 쓰는 거야.
명령어 안의 값(12)을 그냥 값 자체로 해석해!
특징:
메모리를 거치지 않기 때문에 가장 빠름
상수 값을 다룰 때 유용
🔁 핵심 정리
즉시
12는 값 자체
누산기에 12 저장
직접
12는 메모리 주소
메모리[12]의 값을 누산기에 저장
간접
12는 주소를 담고 있는 주소
메모리[메모리[12]]의 값을 누산기에 저장
🧠 비유로 정리하자면:
즉시: “쪽지에 있는 번호가 답이야!”
직접: “쪽지에 적힌 집 주소로 가면 답이 있어!”
간접: “쪽지에 적힌 집 주소에 또 다른 주소가 있고, 거기로 가야 진짜 답이 있어!"
⚡ 속도 비교
메모리에 몇 번 접근하느냐에 따라 속도가 달라져!
직접
주소를 명령어에 직접 포함
간단하고 빠름 ⭐ 가장 빠름
주소 공간 제한
1번 (명령어만 읽음)
간접
주소를 담은 주소 사용
주소 공간 확장 가능 보통
느림 (3단계 접근)
2번 (명령어 + 데이터)
즉시
값 자체를 명령어에 포함
빠름 ⛔ 가장 느림
값 크기에 제한
3번 (명령어 + 주소 해석 + 데이터)
🧩 이걸 왜 쓰냐면?
각 방식은 상황에 따라 유용해:
즉시 모드: 상수 값 (ex:
MOV A, 10
)직접 모드: 일반적인 변수 접근
간접 모드: 포인터처럼 간접 참조할 때 (동적 구조 등)
🔖 정리하면!
주소 지정 방식은 "값을 어디서 읽을지 결정하는 규칙"이다.
명령어에 숫자 하나가 있어도, 해석 방식에 따라 완전히 다른 결과를 낸다.
각각의 모드는 성능, 유연성, 사용 목적이 다르다.
3️⃣ 조건 코드 명령어
조건 코드 레지스터를 직접 다루는 명령어
우리는 앞에서 덧셈(add), 뺄셈(sub), 비교(cmp) 등을 수행하면 조건 코드 레지스터(N, Z, O)가 자동으로 설정된다는 걸 배웠어.
하지만 때때로 프로그래머가 직접 조건 코드를 보고, 바꾸고 싶을 때가 있어. 그럴 때 필요한 명령어가 바로 이거야:
1. cca
(Condition Code to Accumulator)
cca
(Condition Code to Accumulator)조건 코드 레지스터 → 누산기(A 레지스터) 로 복사하는 명령어
즉, N, Z, O 비트를 읽어서 누산기에 담는다.
조건 분기 전 확인용으로 자주 쓰일 수 있음!
📌 예: 조건 코드가
N=1, Z=0, O=0
이면cca
실행 후 누산기에는100
(즉, 4) 가 들어감.
2. acc
(Accumulator to Condition Code)
acc
(Accumulator to Condition Code)누산기 → 조건 코드 레지스터로 복사하는 명령어
즉, 누산기 안의 값을 조건 코드 레지스터의 N, Z, O 비트에 복사하는 거야.
조건 코드 테스트를 흉내내거나 강제 조건 설정할 때 사용 가능!
📌 예: 누산기에
001
이 들어있었다면acc
실행 후 조건 코드는N=0, Z=0, O=1
🔁 간단 요약표
cca
조건 코드 값을 누산기로 복사
조건 코드 확인용
acc
누산기 값을 조건 코드로 복사
조건 코드 강제 설정
4️⃣ 분기 명령어
이제 CPU가 조건에 따라 프로그램의 흐름을 바꾸는 방법, 즉 "분기(branch)" 에 대해 배우고 있어!
🧭 분기 명령어란?
지금까지의 CPU는 명령어를 무조건 순서대로 실행했어. 하지만 현실의 프로그램은 항상 이렇지 않지?
조건에 따라 다른 동작을 하기도 하고
같은 명령어를 반복해서 실행하기도 해!
👉 이런 걸 가능하게 하는 게 바로 분기(branch) 명령어야!
분기 명령어의 핵심
분기란? ➡ 현재 실행 중인 명령어 다음으로 가지 않고 ➡ 다른 메모리 주소로 "점프"하는 것
이걸 하려면 CPU 안에 있는 프로그램 카운터(PC)의 값을 바꿔야 해! 그 역할을 하는 명령어들이 바로 분기 명령어들이야.
🔁 분기 명령어 종류 (표 4-2 기준)
분기 명령어에는 조건을 저장하기 위한 비 트
000
bra
검사 안 함
항상 분기
001
bov
O (오버플로)
O = 1일 때
010
beq
Z (Zero)
Z = 1일 때
011
bne
Z
Z = 0일 때
100
blt
N, Z
N = 1 AND Z = 0 (음수)
101
ble
N, Z
N = 1 OR Z = 1 (음수 또는 0)
110
bgt
N, Z
N = 0 AND Z = 0 (양수)
111
bge
N, Z
N = 0 OR Z = 1 (0 또는 양수)
🤔 왜 조건 코드(Z, N, O)를 보는 걸까? 바로 이전에 했던 연산의 결과(덧셈, 뺄셈 등)를 기반으로 분기할 수 있기 때문이야!
🧩 PC를 직접 다루는 명령어
CPU가 흐름을 바꾸는 또 다른 방법은 프로그램 카운터 자체를 직접 조작하는 거야.
pca
현재 PC 값을 누산기(A)에 복사
apc
누산기 값을 PC로 설정 (점프 효과!)
📌 예를 들어,
pca
는 “지금 어디 실행 중이야”를 알게 해주고apc
는 “지금부터 이 주소로 가!” 라는 직접적인 점프 명령이야.
✨ 요약
분기 명령어는 프로그램 흐름을 바꾼다.
조건 코드(
N
,Z
,O
) 값을 보고 조건에 따라 점프함bra
는 항상 점프하는 무조건 분기pca
/apc
는 PC(프로그램 카운터)를 직접 다루는 명령어
5️⃣ 최종 명령어 구성
위에서 ‘모드’는 2비트를 추가적으로 차지한다.
이때의 모드는 위에서 배운 *주소 지정 모드’이다.
2비트는 4가지로 표현이 가능하고, 이 중 3가지는 위에서 배운 즉시 / 직접 / 간접 주소 지정 모드이다.
마지막으로 표현할 수 있는 1가지 방식이 남는데, 이는 메모리와 관계없는 연산을 표현한다고 한다.
15~14
주소 지정 모드 (2비트)
어떤 방식으로 주소를 사용할지 선택
13~10
명령어 코드 (4비트)
무슨 연산을 할지 결정
9~0
주소 or 상수 (10비트)
주소 값 또는 상수 값
🧭 주소 지정 모드 3가지
직접 Direct
00
명령어에 들어있는 주소에서 데이터를 직접 읽음
간접 Indirect
01
명령어의 주소에 들어있는 값이 진짜 주소
즉시 Immediate
10
주소 칸에 있는 값을 그냥 상수로 사용
없음 None
11
주소가 필요 없는 명령어 (ex: set
, shl
)
🧠 명령코드 정리 (표 4-3)
0000
load
✅
✅
✅
❌
0001
and
✅
✅
✅
❌
0010
or
✅
✅
✅
❌
0011
xor
✅
✅
✅
❌
0100
add
✅
✅
✅
❌
0101
sub
✅
✅
✅
❌
0110
cmp
✅
✅
✅
❌
0111
store
✅
✅
❌
cca
(조건코드 → 누산기)
1000
~1111
분기 명령
✅
✅
✅
apc
, pca
등
모드 11은 주소 없이 작동하는 ALU나 분기 보조 명령들을 포함!
🔁 시프트 연산 (왼쪽/오른쪽)
시프트 명령은 shl
, shr
이고,
주소 필드(하위 10비트)를 시프트할 비트 수로 사용해.
예: shl 3
이면 3비트 왼쪽 시프트한다는 뜻
🎯 정리하면
이제 주소 지정 모드 + 명령어 코드로 굉장히 유연한 구조가 됐어!
간단한 연산부터 조건 분기, 반복문까지 모두 구현 가능!
ALU, 누산기, 프로그램 카운터 등 CPU 구조를 조합해서 "진짜 프로그램"을 만들 수 있게 됨
5. 마지막 설계
1️⃣ 명령어 레지스터
컴퓨터가 명령어를 실제로 실행할 때 내부에서 어떤 일이 벌어지는지 설명하고 있어. 많이 들었던 ‘페치-실행 사이클(Fetch-Execute Cycle)’
🧠 컴퓨터는 명령어를 어떻게 실행할까?
혹시 이렇게 생각하지 않았어?
“컴퓨터는 프로그램을 그냥 한 줄씩 착착 실행하겠지?”
실제로는 훨씬 더 복잡한 과정이 내부에서 진행돼. 이걸 한마디로 요약하면 바로:
👉 “페치-실행 사이클 (Fetch-Execute Cycle)”
🌀 페치(Fetch) 단계:
프로그램 카운터(PC)
가 가리키는 메모리 주소에 접근해서 명령어를 하나 가져온다.
⚙️ 실행(Execute) 단계:
가져온 명령어를 해석하고, ALU나 메모리를 이용해 해당 명령을 실행한다.
이 두 단계가 계속 반복되면서 프로그램이 한 줄 한 줄 실행돼!
💾 명령어는 어디에 저장될까?
명령어를 실행하려면 잠시 기억해 둘 공간이 필요해. 왜냐하면 실행 도중에도 메모리를 다시 접근해야 할 수도 있으니까.
그래서 등장한 장치가 바로:
📍 명령어 레지스터 (IR, Instruction Register)
현재 실행 중인 명령어를 저장해 두는 곳이야.
PC가 명령어를 메모리에서 가져오면, 그 명령어는 명령어 레지스터(IR)에 저장돼.
그리고 CPU는 IR에 저장된 명령어를 해석해서 실행해!
PC는 다음 실행할 명령어의 주소를 가지고 있고,
그 주소를 따라가서 메모리에서 명령어를 읽고,
읽어온 명령어를 IR에 저장한 후 실행하는 구조야!
이제 진짜 컴퓨터가 명령어를 한 줄씩 실행할 때, 그냥 ‘줄 따라 실행’하는 게 아니라:
PC → 메모리 → IR → 실행 → PC 증가 → 다음 명령어
2️⃣ 데이터 경로와 제어 신호
이제 컴퓨터 내부의 진짜 복잡한 연결 구조가 등장했어!
🧭 우리가 지금까지 배운 컴퓨터 부품들
전부 한 도시의 구성요소라고 보면 돼!
프로그램 카운터 (PC)
우편배달부의 지도
다음 명령어의 주소를 기억
명령어 레지스터 (IR)
작업지시서
현재 실행 중인 명령어 저장
데이터 버스
도로
데이터가 오가는 통로
주소 버스
우편주소 전선망
어디로 갈지를 알려주는 주소 전달 통로
ALU
계산기
연산 수행
누산기
계산기 옆 노트
결과를 임시로 저장
조건 코드 레지스터
조건 기록장
연산 결과가 어떤 상태였는지 저장
메모리
창고 거리
실제 데이터나 명령어들이 저장됨
간접 주소 레지스터
보조 주소 기록장
메모리 주소를 한 번 더 저장해야 할 때 사용
이제 우리가 만든 모든 부품들을 어떻게 연결할지 결정해야 해. 그게 바로 그림 4-22: 데이터 경로와 제어 신호야!
즉, 컴퓨터가 ‘명령어를 가져와서 실행하는 모든 경로’를 그려놓은 거야.
🛣️ 이 구조의 핵심 포인트
프로그램 카운터(PC)
현재 실행할 명령어의 주소를 주소 버스에 보냄
→ 메모리에서 명령어를 가져옴
명령어 레지스터(IR)
메모리에서 가져온 명령어를 임시 저장
→ 연산 코드(opcode)와 피연산자 주소, 모드 등을 해석
피연산자 A / B
명령어에 나오는 주소를 따라가서 값을 읽어올 버스 라인
각각의 데이터를 ALU로 보내서 계산에 활용
ALU + 누산기
A와 B 값을 받아서 계산 수행
결과는 누산기에 임시 저장하고
조건 코드 레지스터에 상태(N/Z/O 등)를 기록
간접 주소 레지스터
간접 주소 지정을 위해 필요!
예: 명령어가 가리키는 주소 안에 또 다른 주소가 있을 때
메모리 → 레지스터에 임시로 저장 → 다시 메모리 접근
📌 클록과 제어 신호
컴퓨터는 모든 동작을 클록(tick)에 맞춰 실행해.
하지만 그림 4-22에서는 간단히 하려고 클록 선을 생략했을 뿐이야.
대부분의 레지스터, 메모리 등은 클록에 따라 데이터 저장/수정이 일어나.
✨ 정리하면!
주소 실렉터
명령어에서 가져온 주소를 기준으로 어디로 갈지 정함 (3방향 교차로)
데이터 실렉터
어떤 데이터를 보낼지 선택 (4방향 교차로)
간접 주소 레지스터
간접 주소 모드를 위해 주소를 한 번 더 저장해두는 역할
명령어 레지스터
현재 실행할 명령어 저장소
누산기 / ALU
계산 + 결과 임시 저장 + 조건 기록
프로그램 카운터
다음 명령어 위치 알려줌
데이터 흐름 제어
CPU의 핵심 두뇌, 바로 '제어 장치(Control Unit)' 이야.
여기선 명령어 실행을 위한 전체 흐름, 즉 명령어를 가져오고(Fetch), 실행(Execute)하는 과정을 실제로 어떻게 제어 신호로 만드는지 설명하고 있어.
컴퓨터가 명령어를 실행하기 위해 필요한 단계
1. Fetch (명령어 가져오기 단계)
CPU가 명령어를 실행하려면, 먼저 메모리에서 명령어를 읽어와야 해.
이때 필요한 제어 신호들은 다음과 같아:
address source
주소 버스에 어디의 주소를 넣을지 결정 (이 경우는 프로그램 카운터
)
메모리 enable
메모리 활성화
r/w
읽기(1)인지 쓰기(0)인지 설정 (여기선 읽기니까 1)
명령어 레지스터 enable
읽어온 명령어를 저장할 레지스터(IR)를 활성화
➡ 그리고 클록이 ‘틱!’ 하고 발생하면 IR(명령어 레지스터)에 명령어가 저장돼. 이게 Fetch 단계야!
🧩 실행 단계 예시: 누산기 값을 간접 주소에 저장하기
우리가 예로 든 동작은 이거야:
"명령어가 가리키는 메모리 주소 안에 들어있는 주소에, 누산기의 값을 저장하라!"
간접 주소 지정을 활용하는 복잡한 케이스야! 단계별로 제어 신호를 어떻게 설정하는지 보자 👇
2. 간접 주소 불러오기 단계
명령어의 주소 필드 = 100번지 → 그곳에 들어 있는 값 = 4321 → 여기에 실제 데이터를 쓸 거야
address source = IR
명령어의 주소 부분을 주소 버스로 보냄
메모리 enable = 1
메모리 읽기
r/w = 1
읽기 모드
간접 주소 레지스터 enable = 1
메모리에서 읽어온 주소를 저장
➡ 이렇게 하면 간접 주소 레지스터에 4321이 들어감
3. 누산기 값 쓰기 단계
이제 실제 주소인 4321에 누산기 값을 저장하자!
address source = 간접 주소 레지스터
실제 주소 4321로 설정
data bus enable = 1
누산기 값이 데이터 버스에 나가도록
r/w = 0
쓰기 모드
메모리 enable = 1
메모리에 쓰기 수행
프로그램 카운터 증가(ld/cnt)
다음 명령어 실행 준비
🔄 그런데 이걸 어떻게 자동으로 하게 만들까?
🤖 상태 기계 (State Machine)!
각 단계(Fetch, Execute)를 나누고
그때마다 어떤 제어 신호를 줄지 지정해주면
자동으로 명령어를 실행할 수 있어!
이걸 구현하는 방식은 두 가지:
🧠 방법 1: 랜덤 논리(random logic)
게이트와 논리 회로를 복잡하게 얽어서 하드코딩된 방식으로 제어 신호를 만드는 방법
➡ 그림 4-23: 랜덤 논리로 구현된 제어 장치 회로 회로 복잡도는 높지만 빠르고 하드웨어적으로 직관적임
💾 방법 2: 마이크로코드 방식
제어 신호를 메모리에 저장해두고, 그걸 읽어서 제어하는 방식 (일종의 ‘작은 프로그램’이 큰 프로그램을 실행하게 돕는 구조)
카운터 + 명령어 + 모드 ⇒ 주소
해당 주소에 저장된 제어 신호를 메모리에서 읽음
각 제어 신호는 19비트로 구성됨
➡ 그림 4-24~26은 이 마이크로코드 방식의 예시야
🎯 마이크로코드 방식의 장점과 단점
명령어 집합을 소프트웨어처럼 정의 가능
속도가 느릴 수 있음
CPU 구조를 더 유연하게 바꿀 수 있음
보안/안정성 문제로 사용자 변경 불가
예: 인텔 CPU는 버그 패치를 위해 마이크로코드 업데이트 기능을 일부 모델에 탑재함!
✨ 마무리 정리
컴퓨터는 명령어마다 어떤 신호를 언제 줄지 정해야 하고
그걸 상태 기계나 마이크로코드로 구현한다
오늘날 CPU는 대부분 마이크로코드 방식을 사용하며,
CPU 안에 또 다른 작은 컴퓨터가 들어있는 셈이다!
6. RISC와 CISC 명령어 집합
💡 명령어 집합 설계: RISC vs CISC
1) 왜 명령어가 단순한 게 좋을까?
과거에는 많은 CPU 설계자들이 복잡한 명령어를 만들고 싶어했어. 왜냐면, 한 줄에 많은 일을 하면 더 "똑똑한 컴퓨터"처럼 보였거든.
그런데 1980년대 초, 미국의 데이비드 패터슨과 존 헤네시라는 과학자들이 통계 분석을 통해 놀라운 사실을 밝혀냈어:
“복잡한 명령어의 대부분은 실제로 거의 쓰이지 않는다!”
그래서 두 사람은 새로운 아이디어를 제안했어: 👉 자주 쓰는 단순한 명령어만 남기자! 이 아이디어가 바로 RISC의 시작이야.
🔍 RISC란? (Reduced Instruction Set Computer)
자주 쓰이는 단순한 명령어만 사용
복잡한 명령어는 여러 단순한 명령어로 쪼개서 처리
모든 연산은 레지스터 안에서 수행 (load-store architecture)
명령어는 고정된 길이, 디코딩이 쉽고 빠름
즉, "간단한 것만 빠르게 하자" 철학이야!
🔎 CISC란? (Complex Instruction Set Computer)
복잡하고 다양한 명령어가 있음 (예: 메모리 접근 + 연산을 한 번에)
레지스터/메모리 등 어디서든 연산 가능
디코딩이 복잡하고, 명령어 길이도 제각각
즉, "한 줄로 똑똑하게 많은 걸 하자" 철학이야!
📚 예시: 메모리 복사 프로그램 (PDP-11)
과거 CISC 시스템인 PDP-11에서는 아래 같은 구조도 가능했어:
이처럼 PDP-11은 자동증가 / 감소 모드를 지원해서 단순한 루프를 아주 짧은 명령어로 구현할 수 있었어!
💬 왜 C 언어에 영향을 줬을까?
C언어는 PDP-11을 위해 처음 개발된 언어야!
그래서 C의 포인터(pointer) 개념도 PDP-11의 간접 주소 지정과 잘 어울려
자동증가/감소 모드도 C의 배열 처리와 찰떡!
그래서 C는 자연스럽게 하드웨어 구조와 잘 맞는 언어가 됐고, 이후 C++ → Java → JavaScript 등 수많은 언어에 영향을 줬어.
💡 요즘 RISC는 정말 단순할까?
그렇지 않아!
시간이 지나면서…
RISC에도 복잡한 명령어들이 일부 도입되고
반대로 CISC도 내부적으로 단순한 방식(RISC 방식)으로 동작하기도 해
즉, 이제는 완벽하게 RISC vs CISC로 나누기보다는
혼합형 구조(Hybrid architecture)
가 많아졌어.
✅ 요약 정리
RISC: 자주 쓰이는 단순 명령어만, 레지스터 중심, 빠르고 효율적
CISC: 복잡한 명령어로 한 번에 많은 일, 메모리 접근 연산도 가능
PDP-11: CISC의 대표, 자동증가/감소 모드로 효율적인 코드 작성 가능
C 언어: PDP-11을 위해 설계되어 포인터나 주소 지정을 자연스럽게 추상화
요즘은 RISC와 CISC의 구분보다 성능과 유연성을 섞은 구조가 많아짐
7. GPU
🧠 GPU란? (그래픽 처리 장치의 본질)
GPU(Graphics Processing Unit)는 요즘 가장 핫한 장치 중 하나야. 원래는 그래픽 처리에 특화되어 만들어졌지만, 지금은 병렬 연산을 수행하는 데도 널리 사용되고 있어.
🎨 GPU는 '초대형 컬러링북 색칠기'!
GPU의 그래픽 작업은 마치 컬러링북에 번호에 맞춰 색칠하는 작업과 비슷해.
번호가 적힌 칸들이 수백만, 수천만 개나 있고,
각 칸에 정해진 색깔을 칠해야 해.
이 수많은 칸들을 동시에 색칠해야 빠르게 그림이 완성되지!
즉, GPU는 매초 수억 번 이상 메모리에 접근해서, 동시에 많은 계산을 수행하는 구조가 필요한 거야!
✅ GPU가 CPU와 다른 이유 (병렬성과 메모리 접근)
목적
병렬 연산에 최적화 (그래픽, AI 등)
일반 연산에 최적화
구조
단순한 계산 유닛이 엄청 많이 있음
복잡한 계산 유닛이 소수 존재
메모리 버스
폭이 넓음 (데이터 이동 빠름)
비교적 좁음
비유
컬러링북에서 동시에 색칠
색연필 하나로 한 칸씩 칠함
그래서 GPU는 병렬화하기 아주 좋은 작업, 예를 들어:
3D 그래픽 렌더링
이미지 처리
딥러닝 학습과 추론
과학/공학 계산 에 탁월하게 쓰이게 된 거야!
💥 GPU의 두 가지 큰 특징 요약
수많은 단순한 처리 장치로 구성되어 있어 → 동시에 여러 작업을 처리하는 데 적합!
메모리 버스의 폭이 넓다 → 많은 데이터를 빠르게 주고받을 수 있어! (예: 소방 호스 비유)
📉 그런데 왜 CPU처럼 범용으로 못 쓰였을까?
GPU는 원래는 그래픽 전용으로 등장했지만, 너무 특화된 구조 때문에 다음과 같은 제약이 있었어:
정수 연산, 조건 분기, 복잡한 논리 연산에 약함
유연한 메모리 접근이 어려움
그래서 범용 계산용으로는 초기엔 잘 안 쓰였음
하지만 요즘은 CUDA, OpenCL, ROCm 등 덕분에 Python이나 C 언어 등으로 GPU도 쉽게 프로그래밍할 수 있게 되었고, AI, 머신러닝, 비트코인 채굴 등 범용 계산에도 폭넓게 쓰이고 있어.
🎯 요약 정리
GPU는 대규모 병렬 작업에 특화된 장치다.
그림 색칠처럼 수많은 점을 동시에 처리하는 그래픽에 특히 유용함.
단순한 처리 유닛이 많고, 메모리 버스 폭이 넓어 처리량이 크다.
컬러링북 비유는 GPU의 동작 방식을 아주 잘 설명해준다!
요즘은 그래픽뿐 아니라 AI, 공학 연산 등에도 광범위하게 사용된다.
딥다이브
1) 정렬되지 않은 접근이 문제가 되는 이유
🏙️ 비유로 이해하는 메모리 정렬
🏘️ 건물(포플렉스) = 메모리 블록
컴퓨터 메모리는 일정 크기의 블록(= 건물)으로 나뉘어 있어. 예를 들어, 32비트 컴퓨터라면 하나의 건물(블록)은 4바이트 = 32비트로 되어 있고, 이게 하나의 워드라고 보면 돼!
즉,
주소 0 ~ 3번 바이트 → 0번 건물 (포플렉스 0)
주소 4 ~ 7번 바이트 → 1번 건물 (포플렉스 1)
주소 8 ~ 11번 바이트 → 2번 건물 (포플렉스 2) 이런 식으로 나뉘는 거죠.
🚌 버스 = 데이터 버스
“도심을 오가는 버스”는 CPU와 메모리 사이를 오가는 데이터 버스를 말해
한 번 버스를 태우면 한 건물(포플렉스)에서 4바이트를 한꺼번에 실어올 수 있어 = 정렬이 맞을 때의 이상적인 상황!
🧩 문제 상황: 정렬이 맞지 않을 때
이제, 어떤 프로그램이 주소 5번부터 8번까지 4바이트를 읽으려 한다고 해보자
주소 58은 1번 건물(47)과 2번 건물(8~11)에 걸쳐 있어 즉, 한 건물에서만 데이터를 읽을 수 없어.
그래서 CPU는…
1번 건물에서 5~7번 바이트를 읽고
2번 건물에서 8번 바이트를 다시 읽어서
두 번을 나눠 읽고 조립해야 해.
🐢 왜 이게 문제일까?
메모리 접근이 두 번 일어나서 느려짐
CPU 내부에서도 조립 과정이 필요 → 복잡함
일부 아키텍처는 이런 접근 자체를 허용하지 않음
그래서 요즘 대부분의 시스템은 다음과 같은 규칙을 따른다:
"4바이트 데이터를 읽을 땐 반드시 4의 배수 주소에서 시작해야 한다!"
= 즉, 주소가 0, 4, 8, 12… 여야 한다는 거
이걸 정렬(alignment)
이라고 부른다.
📌 요약하면!
메모리는 워드 단위(4바이트 등)로 정렬된 구조를 가짐
버스는 한 번에 하나의 포플렉스(4바이트 블록)만 접근 가능
정렬이 맞지 않으면 버스를 두 번 왕복해야 하므로 비효율적
→ 정렬된 접근이 더 빠르고 안정적이다!
Last updated