[CS300 #086] 폰 노이만 구조 — 프로그램도 메모리에 담긴 데이터다
컴퓨터공학 300 주제 시리즈의 086번째 글이다. 전체 지도는 여기.
한 줄 요약
폰 노이만 구조는 명령어와 데이터를 같은 메모리에 두고, CPU 가 프로그램 카운터가 가리키는 명령어를 하나씩 꺼내 실행하는 “저장 프로그램” 컴퓨터의 설계다. 오늘날 거의 모든 범용 컴퓨터가 이 모델 위에 서 있고, 그 장점과 한계(메모리 병목)를 함께 물려받았다.
왜 필요한가
초기 전자 계산기 중에는 프로그램을 바꾸려면 배선을 다시 꽂아야 하는 기계도 있었다. 1945년 존 폰 노이만이 쓴 EDVAC 보고서 초안은 프로그램을 숫자로 부호화해 데이터와 같은 메모리에 넣자는 구조를 정리했다. 이 발상 덕분에 다음이 가능해졌다.
- 프로그램을 바꾸는 일이 메모리에 쓰는 일이 된다. 하드웨어를 건드릴 필요가 없다.
- 프로그램이 다른 프로그램을 데이터로 다룰 수 있다. 컴파일러, 로더, 운영체제, 인터프리터, JIT 가 모두 여기서 나온다.
반대로 이 구조의 한계도 매일 마주친다. CPU 는 빨라졌는데 메모리에서 명령어와 데이터를 가져오는 통로는 하나라서, 실제 성능은 자주 메모리가 정한다. 캐시와 메모리 계층(#092~#093)은 이 문제를 완화하려는 장치다.
핵심 개념
다섯 구성 요소
EDVAC 보고서는 기계를 몇 개의 기관으로 나눠 설명했다. 현대 교과서는 이를 보통 다섯으로 정리한다.
┌───────────────── CPU ─────────────────┐
│ ┌───────────┐ ┌─────────────┐ │
입력 ───►│ │ 제어 장치 │◄─────►│ 연산 장치 │ │───► 출력
│ │ (PC, IR) │ │ (ALU, 레지스터)│ │
│ └─────┬─────┘ └──────┬──────┘ │
└────────┼────────────────────┼─────────┘
│ 버스(주소·데이터·제어)
┌────────▼────────────────────▼─────────┐
│ 메모리: 명령어와 데이터가 함께 │
└───────────────────────────────────────┘
| 구성 요소 | 역할 |
|---|---|
| 연산 장치(ALU) | 덧셈, 비교, 논리 연산 |
| 제어 장치 | 명령어를 해독하고 각 부품에 제어 신호를 보냄 |
| 메모리 | 명령어와 데이터를 주소로 구분 없이 저장 |
| 입력 장치 | 외부에서 데이터를 받음 |
| 출력 장치 | 결과를 내보냄 |
핵심 아이디어 세 가지
- 저장 프로그램: 명령어도 비트열이다. 메모리의 어느 칸이 명령어이고 어느 칸이 데이터인지는 비트만 봐서는 알 수 없고, PC 가 그 칸을 가리키느냐로 정해진다.
- 순차 실행: 프로그램 카운터(PC)가 다음에 실행할 명령어 주소를 갖고 있다. 기본은 PC 를 하나씩 올리고, 분기 명령만 PC 를 다른 값으로 바꾼다.
- 주소로 접근하는 단일 메모리: 모든 칸은 번호(주소)로 읽고 쓴다.
인출–해독–실행 주기
CPU 는 전원이 꺼질 때까지 이 루프를 돈다.
loop:
IR ← Memory[PC] ; 인출(fetch)
PC ← PC + 명령어 길이
해독(decode): IR 의 연산 코드·피연산자 해석
실행(execute): ALU 연산, 메모리 읽기/쓰기, 또는 PC 변경
(인터럽트가 있으면 처리)
IR(명령어 레지스터)은 방금 가져온 명령어를 담는다. 이 단순한 루프를 여러 단계로 겹쳐 돌리는 것이 파이프라이닝이다.
폰 노이만 병목
명령어와 데이터가 같은 통로로 오가므로 CPU 는 한 번에 둘 중 하나밖에 가져오지 못한다. 그리고 메모리 접근은 CPU 연산보다 훨씬 느리다. 존 배커스는 1977년 튜링상 강연에서 이 통로를 “폰 노이만 병목” 이라고 불렀다. 대응책은 다음과 같다.
- 캐시: 자주 쓰는 명령어·데이터를 CPU 가까이에 복사해 둔다.
- 명령어 캐시와 데이터 캐시 분리: 아래 수정 하버드 구조.
- 프리페치, 넓은 버스, 메모리 채널 다중화.
하버드 구조와 수정 하버드 구조
하버드 구조는 명령어 메모리와 데이터 메모리를 물리적으로 나눈다. 둘을 동시에 가져올 수 있어 빠르지만, 프로그램을 데이터처럼 다루기는 어렵다. 일부 마이크로컨트롤러와 DSP 가 이 구조를 쓴다.
현대 범용 CPU 는 절충형이다. 주기억 장치와 주소 공간은 하나(폰 노이만)이고, CPU 안쪽 L1 캐시만 명령어용과 데이터용으로 나눈다(하버드). 이것을 흔히 수정 하버드 구조라고 부른다.
“프로그램도 데이터” 의 그림자
명령어와 데이터가 같은 메모리에 있다는 것은 공격자가 데이터로 넣은 바이트를 CPU 가 명령어로 실행할 수도 있다는 뜻이다. 버퍼 오버플로로 셸코드를 실행하는 고전적인 공격이 그렇다. 그래서 현대 운영체제는 페이지 단위로 “쓰기 가능” 과 “실행 가능” 을 동시에 허용하지 않는 정책(W^X, NX 비트)을 쓴다. JIT 컴파일러는 이 제약 아래에서 코드를 쓰고 나서 권한을 바꾸는 방식으로 동작한다.
직접 해 보기
명령어와 데이터가 같은 리스트(메모리)에 있는 누산기 기계를 만든다. 1 부터 n 까지 더한다. python3 로 실행해 확인했다.
mem = [
("LOAD", 21), # 0: acc = sum
("ADD", 20), # 1: acc += n
("STORE", 21), # 2: sum = acc
("LOAD", 20), # 3: acc = n
("SUB", 22), # 4: acc -= 1
("STORE", 20), # 5: n = acc
("JNZ", 0), # 6: acc != 0 이면 0 번지로
("PRINT", 21), # 7
("HALT", 0), # 8
] + [None] * 11 + [
5, # 20: n
0, # 21: sum
1, # 22: 상수 1
]
def run(mem, limit=200):
pc, acc, steps = 0, 0, 0
while steps < limit:
op, addr = mem[pc] # 인출: PC 가 가리키는 메모리에서 명령어를 읽는다
pc += 1 # PC 증가
steps += 1
if op == "LOAD": acc = mem[addr] # 해독 후 실행
elif op == "ADD": acc += mem[addr]
elif op == "SUB": acc -= mem[addr]
elif op == "STORE": mem[addr] = acc
elif op == "JNZ": pc = addr if acc != 0 else pc
elif op == "PRINT": print("out:", mem[addr])
elif op == "HALT": break
return steps
print("steps:", run(list(mem)))
m2 = list(mem)
m2[1] = ("SUB", 20) # 더하기를 빼기로 바꿔 넣는다
print("steps:", run(m2))
print(mem[1], "->", m2[1])
출력:
out: 15
steps: 37
out: -15
steps: 37
('ADD', 20) -> ('SUB', 20)
0~8 번지는 명령어, 20~22 번지는 데이터지만 둘 다 같은 mem 에 있다. 메모리 한 칸을 바꾸자 다른 프로그램이 됐다. 로더가 실행 파일을 메모리에 올리는 일도 본질적으로 이것이다. steps 37 중 절반 가까이가 LOAD/STORE 라는 점도 눈여겨볼 만하다. 연산보다 메모리 왕복이 많다. 병목의 축소판이다.
현업에서는
- 성능 분석의 출발점: 프로파일러에서 CPU 사용률이 높은데 실제로는 메모리를 기다리는 시간(stall)이 대부분인 경우가 흔하다. “CPU 바운드” 라고 판단하기 전에 메모리 바운드인지 확인한다.
- 인터프리터와 JIT: 파이썬 바이트코드, JVM 바이트코드는 소프트웨어로 구현한 인출–해독–실행 루프다. JIT 는 그 바이트코드를 실제 기계어로 바꿔 메모리에 쓰고 PC 를 그곳으로 옮긴다. 저장 프로그램 개념이 없으면 불가능한 일이다.
- 보안 설정: 컨테이너 런타임과 커널의 메모리 보호(NX, ASLR)는 “데이터를 코드로 실행하지 못하게” 하는 장치다. 일부 JIT 기반 런타임이 seccomp·SELinux 정책과 충돌하는 이유도 쓰기 가능한 메모리를 실행해야 하기 때문이다.
확인 문제
- 저장 프로그램 개념을 한 문장으로 설명하라.
- 메모리 칸의 내용이 명령어인지 데이터인지는 무엇으로 정해지는가.
- 폰 노이만 병목이란 무엇이고 대표적인 완화책은.
- 현대 CPU 가 “수정 하버드 구조” 라고 불리는 이유는.
풀이
- 프로그램을 숫자로 부호화해 데이터와 같은 메모리에 저장하고 실행한다는 개념이다.
- 비트 자체가 아니라 CPU 가 그 주소를 PC 로 인출하느냐로 정해진다.
- CPU 와 메모리 사이의 단일 통로가 처리 속도를 제한하는 현상이다. 캐시, 명령어·데이터 캐시 분리, 프리페치가 완화책이다.
- 주기억 장치와 주소 공간은 하나지만 L1 캐시를 명령어용과 데이터용으로 나눠 동시에 접근하기 때문이다.
더 읽을거리 (References)
- John von Neumann, First Draft of a Report on the EDVAC, 1945 (MIT 강의 자료 사본)
- John Backus, “Can Programming Be Liberated from the von Neumann Style?”, Communications of the ACM 21(8), 1978 (1977 튜링상 강연)
- Computer Systems: A Programmer’s Perspective — 저자 공식 사이트
- David A. Patterson, John L. Hennessy, Computer Organization and Design RISC-V Edition, 2nd ed., Morgan Kaufmann, 2020. 1장.