
유저 모드 vs 커널 모드: 이중 보호 장치
개발자가 만든 프로그램이 커널 모드에 진입하려고 하면 CPU가 막아섭니다. 왜 컴퓨터는 모드를 두 개로 나눴을까요?

개발자가 만든 프로그램이 커널 모드에 진입하려고 하면 CPU가 막아섭니다. 왜 컴퓨터는 모드를 두 개로 나눴을까요?
내 서버는 왜 걸핏하면 뻗을까? OS가 한정된 메모리를 쪼개 쓰는 처절한 사투. 단편화(Fragmentation)와의 전쟁.

미로를 탈출하는 두 가지 방법. 넓게 퍼져나갈 것인가(BFS), 한 우물만 팔 것인가(DFS). 최단 경로는 누가 찾을까?

프론트엔드 개발자가 알아야 할 4가지 저장소의 차이점과 보안 이슈(XSS, CSRF), 그리고 언제 무엇을 써야 하는지에 대한 명확한 기준.

이름부터 빠릅니다. 피벗(Pivot)을 기준으로 나누고 또 나누는 분할 정복 알고리즘. 왜 최악엔 느린데도 가장 많이 쓰일까요?

옛날 윈도우 95 시절을 떠올려봅니다. 블루스크린이 밥 먹듯이 떴습니다.
이유는 단순했습니다. 일반 프로그램이 실수로 운영체제 메모리 영역을 건드렸기 때문입니다. 한 놈이 실수했는데 컴퓨터 전체가 죽어버리는 거죠. 프로그램 하나가 버그를 일으키면 전체 시스템이 다운됩니다. 저장 안 한 문서? 날아갑니다.
이 참사를 경험하면서 나는 이렇게 이해했습니다. 프로그램 간의 경계가 없으면 시스템은 무너진다는 것을.
이런 재앙을 막기 위해 CPU는 모드(Mode)라는 걸 하드웨어 레벨에 구현했습니다. 소프트웨어가 아니라 실리콘 위에 새긴 보안 장치입니다.
나는 이 개념을 처음에 이해하지 못했습니다. "왜 프로그램이 직접 디스크를 읽으면 안 되지?" 제한을 둔다는 게 비효율적이라고 생각했습니다.
하지만 이 비유가 와닿았습니다.
도시를 상상해봅니다.일반 시민은 자기 집에서 무엇이든 할 수 있습니다. 요리하고, TV를 보고, 코드를 짭니다. 하지만 경찰서에 침입해서 범죄 기록 데이터베이스를 직접 수정할 수는 없습니다. 그걸 하려면 경찰서 창구에 가서 "정식 절차"를 밟아야 합니다.
Hello World, 웹 브라우저, VS Code, 게임)는 다 여기서 돌아갑니다.HLT(CPU 정지), CLI(인터럽트 차단), I/O 포트 제어결국 이거였습니다. 권한 분리(Privilege Separation). 신뢰할 수 없는 코드와 신뢰할 수 있는 코드를 물리적으로 격리하는 것.
처음에 나는 "유저 모드 / 커널 모드" 두 개만 있는 줄 알았습니다. 틀렸습니다.
x86 CPU는 실제로는 4개의 Protection Ring을 정의합니다.
Ring 0: Kernel (OS 커널)
↓
Ring 1: Device Drivers (이론상, 실제로는 거의 안 씀)
↓
Ring 2: Device Drivers (이론상, 실제로는 거의 안 씀)
↓
Ring 3: Applications (우리 프로그램)
대부분의 현대 OS는 Ring 0과 Ring 3만 사용합니다. Ring 1, 2는 사실상 버려진 땅입니다. 왜냐하면 복잡성 대비 보안 이득이 미미했기 때문입니다.
그런데 가상화 기술이 등장하면서 새로운 레벨이 생겼습니다.
Ring -1: Hypervisor (VMware, KVM, Xen)
↓
Ring 0: Guest OS Kernel (가상 머신 안의 Linux)
↓
Ring 3: Apps inside VM
나는 이렇게 정리해봤습니다. Ring 번호가 낮을수록 하드웨어에 가깝고 권한이 크다. 0에 가까울수록 신이고, 3에 가까울수록 죄수입니다.
나는 처음에 시스템 콜이 "함수 호출"이라고 착각했습니다.
아니었습니다.
시스템 콜은 소프트웨어 인터럽트(Software Interrupt)입니다.일반 함수 호출:
int result = add(3, 5); // 같은 권한 레벨에서 실행
시스템 콜:
int fd = open("/etc/passwd", O_RDONLY); // CPU 모드 전환 발생
open() 함수를 호출하면 내부적으로 이런 일이 벌어집니다.
mov eax, 5 ; syscall number (open = 5 in x86)
mov ebx, filename ; 첫 번째 인자
mov ecx, O_RDONLY ; 두 번째 인자
int 0x80 ; <- 이게 핵심! 소프트웨어 인터럽트
int 0x80 명령어가 실행되면:
system_call() 함수로 점프이제 와닿았습니다. 시스템 콜은 점프가 아니라 "트랩(Trap)"입니다. 자발적으로 감옥에 들어가서 간수(커널)에게 부탁하는 것.
나는 이 세 개를 자주 헷갈렸습니다. 이렇게 받아들였습니다.
| 종류 | 트리거 | 예시 |
|---|---|---|
| Trap | 프로그램이 의도적으로 발생 | 시스템 콜 (int 0x80, syscall) |
| Interrupt | 하드웨어가 발생 | 키보드 입력, 타이머, 네트워크 패킷 도착 |
| Exception | CPU가 비정상 상황 감지 | Divide by zero, Page Fault, Segmentation Fault |
모두 커널 모드로 전환을 유발합니다. 차이는 "누가 방아쇠를 당기냐"입니다.
나는 개념만 알고 실제로는 본 적이 없었습니다. 그래서 strace를 써봤습니다.
strace ls
출력:
execve("/bin/ls", ["ls"], [/* 환경 변수 */]) = 0
brk(NULL) = 0x55b8f0a0e000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=95788, ...}) = 0
mmap(NULL, 95788, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f8c9c0a0000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0"..., 832) = 832
...
write(1, "file1.txt\nfile2.txt\n", 20) = 20
close(1) = 0
exit_group(0) = ?
+++ exited with 0 +++
충격이었습니다. ls 명령어 하나가 수십 개의 시스템 콜을 호출합니다.
openat(), fstat(), mmap(), read(), write(), close()... 각각이 유저 → 커널 → 유저 전환을 일으킵니다.
나는 이렇게 이해했습니다. 프로그램은 하드웨어와 직접 대화하지 않는다. 커널을 통해서만 대화한다.
이제 나는 왜 시스템 콜이 비싼지 알았습니다.
모드 전환 한 번에 수백 ~ 수천 CPU 사이클이 소비됩니다.
왜?
이걸 한 번 하는 건 괜찮습니다. 하지만 1초에 100만 번 한다면?
// 비효율적인 코드
for (int i = 0; i < 1000000; i++) {
write(fd, &i, sizeof(i)); // 시스템 콜 100만 번!
}
이 코드는 끔찍하게 느립니다. 왜냐하면 매 루프마다 유저 → 커널 → 유저 전환이 일어나기 때문입니다.
해결책: Buffered I/O
// 효율적인 코드
char buffer[4096];
int pos = 0;
for (int i = 0; i < 1000000; i++) {
memcpy(&buffer[pos], &i, sizeof(i));
pos += sizeof(i);
if (pos >= 4096) {
write(fd, buffer, pos); // 버퍼가 찰 때만 시스템 콜
pos = 0;
}
}
나는 이렇게 정리했습니다. 시스템 콜을 줄이는 게 성능 최적화의 핵심이다.
나는 Docker를 쓰면서 이런 의문이 들었습니다. "컨테이너는 격리된 환경인데, 왜 가상 머신보다 빠르지?"
답은 커널을 공유하기 때문이었습니다.
[VM 구조]
App A (Ring 3)
↓
Guest OS Kernel (Ring 0)
↓
Hypervisor (Ring -1)
↓
Host OS Kernel (Ring 0)
↓
Hardware
[Container 구조]
App A (Ring 3)
↓
Host OS Kernel (Ring 0) <- 직접 시스템 콜
↓
Hardware
VM은 2단계 모드 전환이 필요합니다 (Guest → Hypervisor → Host). 컨테이너는 1단계입니다 (App → Host Kernel).
그래서 Docker는 빠릅니다. 하지만 덜 안전합니다. 왜냐하면 커널 취약점이 있으면 컨테이너 탈출이 가능하기 때문입니다.
나는 이렇게 받아들였습니다. Docker는 격리가 아니라 네임스페이스(Namespace)와 cgroup을 이용한 "착각"이다. 실제로는 같은 커널을 공유합니다.
나는 Linux 커널 모듈을 처음 만들어봤습니다.
// hello.c - 커널 모듈
#include <linux/module.h>
#include <linux/kernel.h>
int init_module(void) {
printk(KERN_INFO "Hello Kernel!\n");
return 0;
}
void cleanup_module(void) {
printk(KERN_INFO "Bye Kernel!\n");
}
컴파일 후:
sudo insmod hello.ko
이 순간 내 코드가 Ring 0에서 실행됩니다.
나는 이제 신입니다. 모든 메모리를 읽고 쓸 수 있고, 모든 프로세스를 죽일 수 있고, 키보드 입력을 가로챌 수 있습니다.
한 줄만 잘못 써도:
*(int*)0 = 42; // NULL 포인터 역참조
Kernel Panic. 시스템 전체가 죽습니다.
나는 이렇게 이해했습니다. 커널 모드는 절대 권력이고, 절대 권력은 절대 위험하다.
그래서 Linux는 커널 모듈을 설치할 때 sudo를 요구합니다. 관리자 권한 없이는 Ring 0 코드를 주입할 수 없습니다.
2018년, 나는 Spectre와 Meltdown 취약점 뉴스를 봤습니다. 처음에는 이해하지 못했습니다.
"CPU 버그가 뭔 상관이야?"
하지만 이게 심각했습니다. 유저 모드 프로그램이 커널 메모리를 읽을 수 있었습니다.
원리를 간단히 정리해봤습니다.
// Spectre 공격 예시 (단순화)
char kernel_memory[4096]; // 커널 영역 (접근 불가)
int secret = kernel_memory[0]; // <- 여기서 예외 발생해야 함
// 하지만 CPU의 "추측 실행(Speculative Execution)"이 이미 실행함
// 예외 발생 전에 캐시에 데이터가 로드됨
// 공격자는 캐시 타이밍 공격으로 secret 값을 추론 가능
CPU는 성능을 위해 예측해서 미리 실행합니다. 나중에 "아 이거 실행하면 안 되는 거였네?"라고 롤백하지만, 이미 CPU 캐시에는 흔적이 남았습니다.
결국 이거였습니다. 하드웨어 최적화가 보안 구멍이 됐습니다.
해결책: KPTI (Kernel Page Table Isolation). 유저 모드일 때는 커널 메모리를 페이지 테이블에서 아예 제거합니다. 하지만 이게 시스템 콜 비용을 10~30% 증가시켰습니다.
나는 이렇게 받아들였습니다. 보안과 성능은 트레이드오프다.
나는 커널이 얼마나 자주 모드 전환을 하는지 궁금했습니다.
cat /proc/interrupts
출력:
CPU0 CPU1 CPU2 CPU3
0: 142 0 0 0 IO-APIC 2-edge timer
1: 9 0 0 0 IO-APIC 1-edge i8042
8: 0 0 0 0 IO-APIC 8-edge rtc0
9: 0 0 0 0 IO-APIC 9-fasteoi acpi
12: 155 0 0 0 IO-APIC 12-edge i8042
...
NMI: 123 456 789 101 Non-maskable interrupts
LOC: 5234567 5234568 5234569 5234570 Local timer interrupts
LOC (Local timer interrupts)를 보세요. 500만 번 이상 발생했습니다.
타이머 인터럽트는 보통 1ms마다 발생합니다 (1000 Hz). 즉, 컴퓨터가 부팅한 지 약 5000초 (1시간 반) 지났다는 뜻입니다.
1초에 1000번, 커널 모드로 강제 전환됩니다. 프로그램이 원하든 말든.
나는 이렇게 이해했습니다. 인터럽트는 CPU에게 있어 강제 소환장이다.
나는 유저 모드와 커널 모드를 이렇게 정리해봤습니다.
결국 이거였습니다. 컴퓨터는 신뢰의 계층 구조다. Ring 3은 Ring 0을 믿고, Ring 0은 하드웨어를 믿고, 하드웨어는... 설계자를 믿습니다.