CSAPP Wiki

2026-10-04 · CSAPP STUDY · CHAPTER 8

8.1–8.2.4예외와 프로세스

Exceptional Control Flow. 예외, 프로세스, 가상 주소 공간과 권한 전환을 Linux 실습으로 연결한다.

CSAPP 3e §8.1–§8.2.4ARM64 · x86-64 LAB확인 기준 2026-10-25 KST
강의 목록

요약과 학습 지도#

프로세스는 실행 흐름과 가상 주소 공간을 제공한다. CPU의 예외 처리와 권한 검사는 운영체제가 이 실행 환경을 관리하는 기반이다.

이 문서의 목차

범위: CSAPP 3판 §8.1~§8.2.4. 스터디 세션: 2026-10-04. 확인 기준일: 2026-10-25 KST.

HTML 강의노트 · 실습 소스 · 검증 보고서

읽는 기준: [CSAPP]는 교재 범위·모델, [LECTURE]는 기존 강의 메모에 명시된 확장 범위, [OFFICIAL]은 공식 문서·구현, [COMMENTARY]는 작성자 설명이다. [사실], [미확인], [추론], [제안]은 근거 상태다. LAB의 EXPECTED는 예상이며 OBSERVED는 실제 관측이다.

기초 개념과 교재 대응#

ELI10: 실행 순서를 잠시 맡기기#

[COMMENTARY] 도서관에서 책을 읽다가 대출을 요청하려면, 읽던 곳에 책갈피를 끼우고 직원에게 간다. 처리가 끝나면 책갈피를 펼쳐 이어 읽는다. 예외 처리에서도 CPU와 운영체제는 재개에 필요한 상태를 보존하고 handler로 제어를 옮긴다. 실제 복귀 위치와 종료 여부는 사건 종류에 따라 달라진다.

용어 의미 확인할 대상
Control flow 명령어 실행 순서 program counter와 명령어
Exception handler 사건을 처리하는 OS 코드 vector·handler·복귀 상태
Process 실행 중인 프로그램의 인스턴스 PID·실행 상태·주소 공간
Virtual address 프로세스가 사용하는 주소 mapping·접근 권한
Privilege mode CPU 코드 실행 권한 user/kernel 경계
프로그램에서 하드웨어까지의 실행 계층과 syscall 경계
프로그램에서 하드웨어까지의 실행 계층과 syscall 경계

[OFFICIAL][사실] libc와 언어 runtime은 사용자 공간에서 동작할 수 있다. syscall은 커널 진입 인터페이스다. CPU exception, Linux signal, 언어 exception·panic은 각 계층의 메커니즘이다. syscall을 직접 호출하는 경로도 있다. [S3][S7][S10]

교재 절 대응표#

[CSAPP][사실] 북미판과 스터디 교재 Global Edition은 절 순서가 같다. 목차는 각각 xiv쪽(PDF 14쪽), xv쪽(PDF 15쪽)에 있다. 아래 표는 각 판의 인쇄 쪽수다. §8.2.3·§8.2.4는 §8.2의 하위 절이다. [S1][S30]

절 북미판 Global 교재 주제 학습 연결
8.1 p.723 p.759 Exceptions 사건과 제어 이전
8.1.1 p.724 p.760 Exception Handling handler 선택·상태 보존
8.1.2 p.726 p.762 Classes of Exceptions 분류·재개 위치
8.1.3 p.729 p.765 Exceptions in Linux/x86-64 Systems CPU 예외·Linux 처리·system call
8.2 p.732 p.768 Processes 실행 흐름·주소 공간
8.2.1 p.732 p.768 Logical Control Flow 프로세스별 명령어 흐름
8.2.2 p.733 p.769 Concurrent Flows 타임라인·병렬 실행
8.2.3 p.734 p.770 Private Address Space mapping·보호
8.2.4 p.734 p.770 User and Kernel Modes 권한·커널 진입

[CSAPP][사실] Chapter 8은 북미판 p.721 / Global p.757에서 시작한다. §8.2.4 이후에는 문맥 교환, 프로세스 제어, signal로 이어진다. [S1][S30]

다음 절 주제 북미판 Global
8.2.5 문맥 교환 p.736 p.772
8.3 syscall 오류 처리 p.737 p.773
8.4 프로세스 제어 p.738 p.774
8.5 signal p.756 p.792
8.6 비지역 점프 p.781 p.817
8.7 프로세스 도구 p.786 p.822
8.8 요약 p.787 p.823

[COMMENTARY] LAB의 fork·waitpid는 §8.4의 프로세스 제어를, /proc·pagemap은 Chapter 9의 주소 변환을 미리 살펴보는 실습이다.

8.1 Exceptions#

8.1.1 Exception Handling#

[CSAPP][사실] 프로그램 실행 중에는 외부 장치의 통지나 명령어 처리 중의 오류가 발생한다. CSAPP의 exception 모델은 이런 사건을 처리할 운영체제 handler로 제어를 옮긴다. [S2][S18]

  1. CPU가 사건을 인식한다.
  2. 아키텍처에 정의된 진입 규칙으로 상태를 저장하고 handler를 선택한다.
  3. 커널 handler가 원인을 처리한다.
  4. 복귀 가능한 경우 저장된 상태로 실행을 재개한다. 종료가 필요한 경우 해당 실행을 끝낸다.
사건 인식에서 handler와 재개 또는 종료까지의 예외 흐름
사건 인식에서 handler와 재개 또는 종료까지의 예외 흐름

[OFFICIAL][사실] x86의 interrupt/exception vector와 IDT는 handler 선택에 사용된다. 64비트 SYSCALL 명령은 MSR에 설정된 경로로 커널에 진입한다. [S4]

8.1.2 Classes of Exceptions#

CSAPP 분류 시점·원인 재개 모델 예
Interrupt 비동기 외부 사건 중단된 흐름 재개 타이머·장치 통지
Trap 명령어와 동기인 의도적 사건 사건 이후 실행 계속 syscall의 교재 모델
Fault 명령어와 동기인 처리 필요 상태 해결되면 해당 명령 재시도 page fault
Abort 심각한 하드웨어 상태 오류 원래 프로그램으로 복귀하지 않음 하드웨어 오류

[CSAPP][사실] 이 표는 교재의 넓은 exception 분류다. [S2][S18]

[OFFICIAL][사실] Intel은 interrupt와 exception을 구분하고, exception을 fault·trap·abort로 분류한다. fault의 저장된 명령어 위치는 원인 명령어를, trap은 해당 명령 실행 이후 이어질 위치를 나타낸다. [S4] (Vol.3A §7.5~§7.6)

REMIND Fault의 재시도 가능성은 원인이 해결됐을 때의 복귀 모델이다. 접근 위반처럼 프로세스 종료로 이어지는 fault도 있다. [S4][S7]

8.1.3 Exceptions in Linux/x86-64 Systems#

[CSAPP][사실] 스터디 교재 Global p.765~767의 이 절은 divide error, 일반 보호 예외, page fault, machine check와 system call을 다룬다. CMU 동반 강의도 처리 가능한 page fault의 명령 재실행과 유효하지 않은 메모리 참조의 signal 전달을 비교한다. [S30][S18] (슬라이드 15~17)

사례 커널 처리 이어지는 실행
0 나눗셈 등 산술 예외 Linux의 signal 전달 signal의 설정·기본 동작에 따름
일반 보호 예외 보호 위반 처리 사용자 프로세스의 signal 처리로 연결
Machine check 교재 모델의 치명적 하드웨어 오류 처리 원래 프로그램으로 복귀하지 않음
처리 가능한 page fault 필요한 페이지 상태 준비 원인 명령 재실행
유효하지 않은 메모리 참조 SIGSEGV 전달 기본 동작은 프로세스 종료
System call 요청된 커널 서비스 실행 반환 가능한 호출은 다음 사용자 명령으로 복귀

[OFFICIAL][사실] Linux signal의 처리 방식은 기본 동작과 handler 설정에 따라 결정된다. Page fault 처리에 디스크 I/O가 필요한지는 해당 페이지의 상태에 달려 있다. [S7][S18]

[OFFICIAL][사실] x86의 DIV·IDIV는 제수가 0이거나 몫이 목적지 범위에 들어가지 않으면 divide-error fault #DE를 발생시킨다. Linux는 산술 오류를 SIGFPE 같은 signal로 사용자 프로세스에 전달한다. [S4] (Event 0)[S7]

[COMMENTARY] 오류를 조사할 때는 명령어 → CPU 예외 → 커널 처리 → signal → 프로세스 동작 순서로 추적한다. C의 정수 0 나눗셈은 undefined behavior이므로, fault 재현에는 컴파일 옵션과 생성된 명령어 확인이 필요하다. [S19]

공식 보충: System call과 함수 호출 ABI#

[OFFICIAL][사실] ABI는 호출 경계에서 인자·반환값을 전달하는 규약이다. Linux syscall 규약은 아키텍처별로 정의된다. 다음 표의 x86-64는 LP64, arm64는 AArch64 Linux 기준이다. [S3]

항목 x86-64 syscall arm64 syscall
진입 명령 syscall svc #0
syscall 번호 eax / rax w8 / x8
인자 1~3 rdi, rsi, rdx x0, x1, x2
인자 4~6 r10, r8, r9 x3, x4, x5
반환 레지스터 rax x0

[OFFICIAL][사실] System V AMD64 C 함수 호출의 4번째 정수·포인터 인자는 rcx다. Linux x86-64 syscall의 4번째 인자는 r10이다. SYSCALL 명령은 rcx와 r11을 사용해 복귀 위치·flags를 저장한다. libc wrapper는 커널 오류를 -1 반환과 errno로 변환한다. [S3][S4][S20]

실습 연결: stdin·stdout·stderr와 strace#

[OFFICIAL][사실] 표준 입력·출력·오류에 할당되는 file descriptor는 각각 0·1·2다. libc의 stdin·stdout·stderr는 FILE stream이며 버퍼링을 제공한다. write(fd, ...)는 fd를 사용한다. [S8][S9]

[COMMENTARY] printf() 호출 횟수와 write syscall 횟수는 버퍼링에 따라 달라진다. strace로 syscall·signal을 관측하면 실제 커널 호출을 확인할 수 있다.

[EXPECTED] LAB의 예상 trace:

실행 출력
write(1, "hello, world\n", 13) = 13
exit_group(0)                  = ?

[OFFICIAL][사실] exit_group은 호출 프로세스의 thread group을 종료하고 복귀하지 않는다. [S6]

8.2 Processes#

[CSAPP][사실] 프로세스는 실행 중인 프로그램의 인스턴스다. 프로그램은 프로세스의 논리적 제어 흐름과 가상 주소 공간을 통해 CPU와 메모리를 사용한다. [S18]

8.2.1 Logical Control Flow#

[CSAPP][사실] 한 프로세스의 program counter가 가리킨 명령어들을 시간순으로 보면 그 프로세스의 logical control flow를 얻는다. CPU가 다른 프로세스를 실행하는 동안에도 이 흐름의 상태가 보존되어 다음 실행으로 이어진다. [S18]

[COMMENTARY] 책 두 권에 각각 책갈피를 끼우면 번갈아 읽어도 각 책의 순서를 이어갈 수 있다. 프로세스도 실행 상태를 보존해 두었다가 이어서 실행한다. 이 상태에는 레지스터·메모리·OS 자원이 포함된다.

8.2.2 Concurrent Flows#

단일 코어 교대 실행과 두 코어 병렬 실행의 타임라인
단일 코어 교대 실행과 두 코어 병렬 실행의 타임라인

[CSAPP][사실] 교재 Global p.769~770은 두 흐름의 실행 기간이 겹치면 concurrent flow로 정의한다. 프로세스가 CPU를 번갈아 사용하는 multitasking의 각 실행 구간을 time slice라고 부른다. 다른 코어나 컴퓨터에서 함께 진행하는 흐름은 parallel flow다. [S30]

[COMMENTARY] 타임라인의 색 막대는 CPU가 해당 흐름을 실행한 구간이다. 동시성은 각 흐름의 시작부터 종료까지의 기간이 겹치는지로 판단한다. 서로 다른 코어의 병렬 실행을 확인하려면 코어별 실행 시점이 필요하다.

8.2.3 Private Address Space#

[CSAPP][사실] 프로세스는 자신의 가상 주소 공간을 사용한다. [S18]

[OFFICIAL][사실] CPU의 MMU와 운영체제가 관리하는 page table이 주소 변환과 접근 보호를 지원한다. [S4]

ELI10: 건물마다 같은 방 번호#

[COMMENTARY] 서로 다른 건물에 모두 101호가 있을 수 있다. 가상 주소는 건물 안의 방 번호에, 주소 변환 상태는 어느 건물인지 알려주는 정보에 비유할 수 있다. 같은 가상 주소도 프로세스의 mapping에 따라 서로 다른 물리 페이지로 이어진다. 공유 mapping은 두 프로세스가 같은 물리 페이지를 사용하는 예다.

구분 프로세스 A 프로세스 B
가상 주소 예시 0x1000 0x1000
변환 상태 A의 page table B의 page table
물리 페이지 예시 X Y 또는 공유 X
프로세스별 가상 주소가 다른 물리 페이지 또는 공유 페이지로 연결되는 구조
프로세스별 가상 주소가 다른 물리 페이지 또는 공유 페이지로 연결되는 구조

[OFFICIAL][사실] MAP_PRIVATE는 copy-on-write mapping을, MAP_SHARED는 공유 mapping을 지정한다. private mapping도 fork 직후 등에는 물리 페이지를 공유할 수 있다. [S15]

교재 주소 공간과 실제 mapping#

[CSAPP][사실] 공식 북미판 정오표는 p.735 그림 8.13의 stack 주소 상한 표기와 %esp를 정정한다. 실제 stack 주소는 프로세스마다 달라지고 x86-64 stack pointer 표기는 %rsp다. 스터디 Global판의 같은 그림은 p.771에 있다. [S1][S30]

영역 학습할 역할 Linux 관측
코드·읽기 전용 데이터 명령어·상수 실행 파일의 r-xp, r--p 등
쓰기 가능한 데이터 전역·정적 상태 실행 파일의 rw-p, anonymous 영역 등
Heap 동적 할당 [heap]과 별도 mmap 영역 가능
Mapped region 라이브러리·파일·anonymous mapping 경로·offset·권한
Stack 함수 실행 상태 [stack] 등
Kernel 영역 커널 자원·보호 교재 개념도와 실제 page-table 구성 구분

[OFFICIAL][사실] /proc/PID/maps는 가상 주소 구간과 접근 권한을 보여준다. r/w/x는 읽기·쓰기·실행, s/p는 공유·private copy-on-write를 나타낸다. [S10]

[OFFICIAL][사실] pagemap은 가상 페이지 단위 정보를 제공한다. Linux 4.2부터 CAP_SYS_ADMIN이 없으면 PFN 필드가 0으로 가려질 수 있다. 파일 접근 자체에도 ptrace 권한 검사가 적용된다. kernel 문서와 man-pages의 비트 설명은 버전별로 대조해야 한다. [S11]

[COMMENTARY] 주소 공간 그림은 주소 변환의 개념도다. 실제 배치에는 ASLR·공유 영역·KPTI 설정이 영향을 주며, 프로세스의 mapping은 /proc/PID/maps에서 확인한다.

8.2.4 User and Kernel Modes#

[CSAPP][사실] CPU의 실행 권한은 사용자 코드의 접근과 특권 동작을 제한한다. 운영체제는 커널 진입·복귀 경계를 관리한다. [S18]

동작 권한과 경계 예
사용자 함수 호출 사용자 공간의 호출 규약 main → helper
syscall 진입 커널 코드 실행으로 전환 write
handler에서 복귀 저장된 사용자 상태 복원 반환 가능한 syscall
context switch 실행할 task 변경 다음 절 §8.2.5의 주제
같은 task의 syscall 진입과 사용자 공간 복귀
같은 task의 syscall 진입과 사용자 공간 복귀

[COMMENTARY] syscall은 커널 코드로 진입할 때 권한이 전환된다. 같은 task로 돌아올 수도 있고, 스케줄러가 다른 task를 실행할 수도 있다.

x86 특권 명령과 조건#

[OFFICIAL][사실] control register에 쓰는 mov %rax, %cr3와 hlt에는 특권 조건이 있다. cli·sti는 CPL·IOPL과 가상화 관련 제어 조건에 따라 동작한다. 일반 Linux 사용자 프로그램은 허용된 커널 인터페이스로 권한 동작을 요청한다. [S4] (Vol.2A/2B)

x86-64 Assembly
mov %rax, %cr3    # 주소 변환 상태 변경
hlt               # 특권 조건에서 CPU 대기

[OFFICIAL][사실] 전원 끄기 syscall은 CAP_SYS_BOOT 등 권한 조건을 검사하며, power-off 결과는 플랫폼에 의존한다. orderly shutdown은 사용자 공간 서비스를 정리하는 절차를 포함한다. raw reboot(POWER_OFF) 호출 전에는 sync로 데이터를 기록해야 하며, man-pages는 이를 생략할 때의 데이터 손실 가능성을 명시한다. [S5]

WARNING 실습에서는 host의 reboot·shutdown을 실행하지 않는다.

보충: 서버 동시성과 런타임#

교재 밖 보충: Go·Erlang·Rust의 runtime 구현과 다른 저자의 동시성 정의.

다른 자료의 동시성 정의#

자료 동시성의 관점 병렬성과의 연결
CSAPP·CMU 두 흐름의 시작~종료 시간 구간이 겹침 서로 다른 코어에서 동시에 진행 가능
Rob Pike 독립 실행 부분을 조합하는 프로그램 구조 구조를 실제 동시 실행으로 실현 가능
OSTEP §26 한 프로세스 안의 여러 thread와 공유 상태 실행 순서·동기화 문제로 연결

[OFFICIAL][사실] Pike는 구조와 실행의 관점을 제시한다. OSTEP은 thread마다 PC·레지스터·stack이 있고 주소 공간을 공유한다는 모델에서 동기화로 전개한다. [S12][S13]

[LECTURE] 강의 메모의 확장 주제는 C10K, blocking I/O, green thread, Go/Erlang이다. 구현 설명과 Rust 보충의 출처는 공식 문서와 작성자 해설이다.

많은 요청이 I/O를 기다릴 때#

[OFFICIAL][사실] C10K 원문은 많은 client connection을 다루는 I/O 전략과 OS 설정을 비교한다. 이 명칭은 서버 아키텍처를 설명하는 역사적 문제다. [S14]

설계 대기 처리 조사할 비용·조건
Thread-per-request OS thread가 blocking 대기 가능 stack·thread 수·스케줄링
Event loop readiness 통지 뒤 작업 재개 긴 handler·blocking 호출
Runtime task runtime이 task와 thread를 연결 runtime 지원 I/O·실행 정책
blocking worker와 readiness 기반 task 재개의 비교
blocking worker와 readiness 기반 task 재개의 비교

[COMMENTARY][추론] 동시 connection을 늘리는 실험에서는 thread 수·stack·대기 시간·CPU 사용량을 함께 측정한다. thread-per-request가 감당할 수 있는 요청 수는 workload와 실행 환경에 따라 달라진다.

Scheduling과 function coloring#

[COMMENTARY] Cooperative scheduling은 task의 yield·대기 지점에서 실행 기회를 넘기는 방식이다. Preemptive scheduling은 scheduler가 실행 중인 작업을 중단할 수 있는 방식이다. green thread는 runtime이 관리하는 실행 단위를 설명할 때 쓰는 용어이며 구체적인 선점·I/O 동작은 구현별로 확인한다.

[COMMENTARY] function coloring은 async 함수와 호출 체인의 작성 제약을 설명하는 비공식 표현이다. Rust에서는 async 함수가 Future를 만들고 .await로 결과를 기다린다. executor가 future를 poll하며, poll 중 긴 blocking 작업을 하면 해당 worker가 점유된다. [S17]

Go G/M/P: 실행 중인 G와 I/O 대기 G#

[OFFICIAL][사실] 아래 구조는 Go go1.26.0 runtime 문서·소스를 기준으로 한다. [S16]

이름 의미 연결
G goroutine과 실행 상태 실행·runnable·대기 상태
M OS thread Go·runtime·syscall 실행 가능
P 사용자 Go 실행에 필요한 scheduler·allocator 자원 수는 GOMAXPROCS
실행 중인 G P M과 netpoll에서 대기하는 G의 관계
실행 중인 G P M과 netpoll에서 대기하는 G의 관계

[OFFICIAL][사실] 사용자 Go 코드를 실행하는 M에는 P가 필요하다. 지원되는 network I/O는 netpoll로 G를 대기시킬 수 있다. syscall 진입·복귀와 P 회수는 entersyscall, entersyscallblock, retake 등의 경로에서 관리한다. blocking call의 처리 경로는 호출 구현에 따라 달라진다. [S16]

[OFFICIAL][사실] Go 1.14는 지원 플랫폼에서 비동기 선점을 도입했다. Go scheduler에는 함수 호출·협력적 대기 지점과 비동기 선점에 의한 실행 전환이 있다. [S21]

Erlang/BEAM과 native blocking#

[OFFICIAL][사실] Erlang/OTP 29.1.2 문서는 긴 NIF 작업이 일반 scheduler의 응답성을 떨어뜨릴 수 있다고 설명한다. 긴 native 작업은 dirty scheduler에 배치할 수 있다. CPU-bound와 I/O-bound 분류를 맞춰야 하며, CPU 작업을 dirty I/O로 잘못 분류하면 일반 scheduler가 영향을 받을 수 있다. [S22]

[COMMENTARY] runtime task의 수와 OS thread 수, native 작업의 배치 위치를 각각 확인한다. Go·BEAM·Rust의 실행 단위를 비교할 때 메모리 격리·선점·I/O 지원도 함께 비교한다.

LAB와 적용 로드맵#

환경과 재현 명령#

[제안] Linux/x86-64에서 syscall과 mapping을 확인한 뒤 프로그램의 요청 대기 구조를 조사한다. 실습 소스는 hello.c, flows.c, maps.c다.

Shell
# 레포 root에서 실행
# macOS ARM64에서는 Docker의 amd64 실행 지원이 필요하다.
docker build --platform linux/amd64 -t csapp-ch8-lab:amd64 \
  examples/0002-exceptional-control-flow
docker run --rm --platform linux/amd64 --cap-add=SYS_PTRACE \
  csapp-ch8-lab:amd64

LAB 1: syscall·종료 경계#

  1. hello.c를 gcc -std=c11 -Wall -Wextra -Werror -O0 -g로 컴파일한다.
  2. strace -qq -e trace=write,exit_group ./hello를 실행한다.
  3. fd·길이·반환값과 exit_group을 읽는다.

[EXPECTED] write(1, "hello, world\n", 13) = 13과 종료 호출이 보인다. 요청 길이와 반환값이 모두 13이면 해당 호출의 출력이 완료된 것이다. 출력 루프에는 partial write와 EINTR 처리도 필요하다. [S8]

LAB 2: 두 프로세스 흐름#

  1. flows.c가 fork로 만든 부모·자식을 확인한다.
  2. 각 PID의 step=0,1,2 순서를 읽는다.
  3. 전체 출력 순서를 다시 실행해 비교한다.

[EXPECTED] 부모·자식이 각각 세 줄을 출력한다. 각 프로세스 안에서는 step 순서가 유지되고, 두 프로세스의 출력이 섞이는 순서는 실행마다 달라질 수 있다.

LAB 3: 주소와 mapping#

  1. maps.c가 MAP_PRIVATE|MAP_ANONYMOUS로 만든 주소와 PID를 출력한다.
  2. 실행 중 /proc/PID/maps에서 그 주소를 포함한 구간을 찾는다.
  3. rw-p와 anonymous mapping을 확인한다.
  4. pagemap의 present 비트를 읽고 PFN 접근 조건을 기록한다.

[EXPECTED] 쓰기 후 해당 가상 페이지의 mapping과 상태가 보인다. PFN이 0이면 접근 권한에 따른 마스킹 여부를 먼저 확인한다. [S10][S11]

관측과 다음 적용#

[사실][OBSERVED] 관측일은 2026-10-25다. OrbStack 컨테이너는 Debian bookworm, GCC 12.2.0, strace 6.1을 사용했다. 기존 Ubuntu amd64 머신과 QEMU guest의 환경은 아래 디버깅 사례에 명시했다.

실행 환경 검사 관측
Linux ARM64 native 전체 LAB write 13바이트·exit_group, 부모/자식 각 3줄, rw-p, present=1·PFN=0
Linux x86-64 에뮬레이션 세 C 예제 컴파일·실행 성공. syscall register 직접 관측 없음
OrbStack amd64 userspace strace syscall trace 불완전. 전체 LAB 실패
QEMU full-system x86-64 전체 LAB write·exit_group·부모/자식·rw-p·pagemap PASS

QEMU 검증 환경은 Linux 6.18.58-0-lts x86-64 커널, Debian userspace, GCC 12.2.0, strace 6.1이다. 전체 시스템 에뮬레이션에서 LAB의 기능을 검사했으며, macOS에서 python3 tools/ch8/qemu-lab.py로 재현할 수 있다.

ARM64 전체 LAB을 재현하려면 위 두 명령의 linux/amd64와 이미지 tag를 각각 linux/arm64, csapp-ch8-lab:arm64로 바꾼다. syscall ABI는 해당 아키텍처의 공식 규약을 따른다.

검증 보고서에는 환경·명령·전체 출력이 있다. 같은 관측 방법을 서버 프로그램에 적용하면 요청이 어디서 대기하는지 조사할 수 있다.

단계 할 일 도구
1 커널 경계에서 사건 기록 strace
2 주소와 접근 권한 확인 maps·pmap·gdb
3 worker와 I/O 대기 조사 thread 목록·runtime profiler
4 변경 전후 같은 부하 비교 지연·CPU·메모리 측정

확인 질문#

  • 같은 가상 주소의 데이터는? 프로세스의 mapping과 공유 관계로 결정된다.
  • syscall과 context switch의 관계는? syscall은 커널 진입이다. 다른 task 실행 여부는 스케줄링 결과로 확인한다.
  • fault 뒤 같은 명령어를 재실행하는 이유는? 원인을 해결한 뒤 해당 메모리 접근 등을 다시 수행하기 위해서다.
  • Go에서 P와 CPU 코어는? P는 runtime 자원이다. 실제 코어 실행 배치는 OS scheduler와 실행 환경에 따른다.

심층 사례: strace 관측 실패 디버깅#

[사실][OBSERVED] 프로그램은 출력했지만 syscall trace가 비어 있었다. Orb의 기존 Ubuntu amd64에서 재현한 뒤, x86-64 커널을 부팅한 QEMU에서 전체 LAB을 두 번 통과했다.

프로그램 실행과 syscall 관측은 각각 검사해야 한다. 출력이 정상이어도 tracing 도구가 필요한 정보를 보여주는지는 별도로 확인한다.

표기: [관측]은 실제 실행, [공식]은 1차 자료, [가설]은 추론, [결정]은 조치다. [판정]·[해석]은 관측에 대한 작성자 설명이다. 예상은 EXPECTED로 표시한다.

1. 먼저 무엇을 검증하려 했는가#

LAB의 목표#

Linux 프로그램의 커널 호출, 프로세스 흐름, 주소 mapping을 세 예제로 관측한다.

예제 프로그램의 동작 확인할 근거
hello.c stdout에 문자열 출력 write의 fd·길이·반환값, exit_group
flows.c fork로 부모·자식 생성 각 PID가 step 0→1→2 출력
maps.c private anonymous mapping 생성·쓰기 주소를 포함하는 rw-p 구간, pagemap

소스와 실행 스크립트

hello.c는 write로 표준 출력에 문자열을 보낸다.

C
static const char message[] = "hello, world\n";
ssize_t written = write(STDOUT_FILENO, message, sizeof message - 1);

문자열은 newline을 포함해 13바이트다. 반환값을 검사하면 이 호출이 요청한 바이트를 모두 썼는지 알 수 있다. 공식: write(2)

성공 조건을 구체적인 문장으로 쓰기#
검사 성공 조건
컴파일 컴파일러 종료 코드 0
실행 프로그램이 예상 문자열을 출력
관측 strace에 write와 exit_group이 나타남
해석 fd=1·요청 13바이트·반환 13을 읽을 수 있음

./hello의 출력으로 실행을 확인하고, trace의 syscall 행으로 커널 호출을 확인한다.

2. strace는 어떤 경계를 관측하는가#

[공식] ptrace는 한 프로세스가 다른 프로세스의 실행과 레지스터 등을 관측·제어할 수 있게 하는 Linux 인터페이스다. syscall tracing과 debugger 구현에 사용한다. ptrace(2)

[공식] strace는 프로그램의 syscall과 signal을 추적한다. 프로젝트 문서와 strace(1)

ELI10: 달리는 차와 계기판#

자동차가 움직이는지 확인하는 일과 계기판이 속도를 정확하게 표시하는지 확인하는 일은 서로 다른 검사다. 프로그램의 출력은 차가 움직였다는 관측에, strace 출력은 계기판의 관측에 대응한다. strace가 보여주는 정보는 커널 tracing 인터페이스와 아키텍처별 상태에 의존한다.

프로그램의 출력 확인과 kernel tracing을 통한 관측 확인의 두 경로
프로그램의 출력 확인과 kernel tracing을 통한 관측 확인의 두 경로

3. 첫 증상: 출력은 있고 trace는 없다#

기대한 결과#

[EXPECTED] syscall 이름·인자·반환값을 확인한다. 공백 정렬은 strace 버전에 따라 달라질 수 있다.

실행 출력
write(1, "hello, world\n", 13) = 13
exit_group(0)                  = ?
실제 관측#

[관측] OrbStack Docker의 Linux/amd64 환경에서 hello 프로그램은 실행됐다. 필터링된 trace에는 필요한 syscall 행이 나타나지 않아 LAB이 멈췄다.

당시 로그:

실행 출력
Linux 7.2.12-orbstack-00394-gbecdd6130068 x86_64 GNU/Linux
gcc (Debian 12.2.0-14+deb12u1) 12.2.0
strace -- version 6.1
hello, world

[관측] ARM64 native 컨테이너에서는 같은 예제·검사 스크립트가 통과했다. ARM64 전체 로그

[가설] 실행 환경에 따라 tracing 결과가 달라질 수 있다. 같은 예제와 검사 스크립트가 ARM64에서 통과했으므로, x86-64 실행 경로와 tracing 도구의 조합을 조사한다.

4. orb CLI로 기존 Ubuntu를 직접 확인했다#

환경을 먼저 목록화한다#

머신 목록과 아키텍처를 확인하고, 컴파일러·관측 도구의 설치 여부를 조회한다.

Shell
command -v orb orbctl
orbctl list
orbctl info ubuntu
orb -m ubuntu uname -a
orb -m ubuntu sh -c 'command -v gcc; command -v strace; command -v python3'

[관측] 이미 실행 중인 ubuntu 머신이 있었다. orbctl info는 Ubuntu 25.04, architecture amd64를 표시했다. 처음에는 Python만 있었고 GCC와 strace가 없었다.

첫 실패는 도구 부재였다#
실행 출력
verify.sh: gcc: not found
verify.sh: strace: not found

[판정] 컴파일러와 strace가 없어 검사 시작 단계에서 멈췄다. 도구를 설치한 뒤 tracing을 검사한다.

[결정] 해당 Ubuntu에 GCC·libc 개발 파일·strace를 설치하고 같은 LAB을 다시 실행했다.

Shell
orb -m ubuntu -u root sh -c \
  'apt-get update && apt-get install -y gcc libc6-dev strace'
orb -m ubuntu sh examples/0002-exceptional-control-flow/verify.sh
도구 설치 뒤의 실패를 따로 기록한다#

[관측] GCC 14.2.0·strace 6.14가 준비된 뒤에도 LAB은 exit 1이었다. Orb amd64 로그:

실행 출력
Linux 7.2.12-orbstack-00394-gbecdd6130068 x86_64 GNU/Linux
gcc (Ubuntu 14.2.0-19ubuntu2) 14.2.0
strace -- version 6.14
hello, world
예제를 더 작게 줄였다#
Shell
orb -m ubuntu strace -e trace=write,exit_group /bin/echo hello

[관측] 아래 mode 메시지가 반복됐고 마지막에 hello와 종료 코드 0이 나타났다. 필요한 write·exit_group trace는 나타나지 않았다. 출력 발췌이며, 반복 메시지는 최소 재현 trace에 있다.

실행 출력
[ Process PID=2682 runs in x32 mode. ]
[ Process PID=2682 runs in 64 bit mode. ]
...
hello
+++ exited with 0 +++

[추론] C 예제를 /bin/echo로 바꿔도 trace 누락이 재현됐다. 두 프로그램에 공통인 tracing 도구와 실행 환경을 우선 조사한다.

[미확인] mode 메시지가 실제 프로그램의 ABI 전환을 정확히 반영하는지는 검증하지 않았다. 이를 확인하려면 strace가 읽은 상태와 대상 프로세스의 상태를 대조해야 한다.

5. 왜 uname의 x86_64만으로 충분하지 않았는가#

[관측] 다음 설정과 등록 상태를 조회했다.

Shell
orbctl config get rosetta
orb -m ubuntu cat /proc/sys/fs/binfmt_misc/rosetta

설정 값은 true, 등록된 interpreter 표기는 [rosetta]였다.

[공식] OrbStack은 Apple Silicon에서 x86/amd64 Linux 머신의 Intel 바이너리를 Rosetta로 실행한다고 설명한다. Linux machines의 Intel emulation

[공식] rosetta 설정은 Intel 코드 실행에 Rosetta를 사용할지 제어한다. OrbStack settings

[해석] 이 환경의 amd64 바이너리는 Rosetta를 거쳐 실행된다. uname의 x86_64 표기는 실행 환경을 식별하는 단서다. tracing 호환성은 실제 syscall 행이 수집되는지로 검사한다.

amd64 userspace 번역 실행과 x86-64 kernel을 포함한 full-system 실행의 비교
amd64 userspace 번역 실행과 x86-64 kernel을 포함한 full-system 실행의 비교

6. 가설을 어떻게 좁혔는가#

가설 확인 방법 실제 결과 그때의 판단
필요한 도구가 없다 command -v와 버전 조회 Orb에 GCC·strace가 없었음 설치 후 재검사
C 예제 구현 문제다 /bin/echo로 축소 같은 trace 누락 재현 환경·도구 조합 조사 우선
단순한 출력 공백 차이다 실제 trace 내용 확인 syscall 행 자체가 없음 trace 생성 경로 조사
strace 6.1에만 생긴다 Orb의 6.14로 실행 같은 증상 두 버전에서 재현됨
tracing과 번역 실행의 경계에 호환성 문제가 있다 설정 확인·환경 비교 Rosetta 사용 상태, ARM64 통과 full-system 대조 실험 선택
x86-64 커널에서 LAB을 관측할 수 있다 x86-64 kernel 부팅 후 실행 전체 LAB 두 번 PASS 재현 환경으로 사용

초기 Docker 실험에서는 검사 스크립트의 공백 비교도 실패했다. 스크립트가 write(...) = 13의 공백 수를 고정해 비교하고 있었다. ARM64 trace에는 필요한 syscall 행이 있었으므로, 공백 정렬을 허용하는 정규식으로 검사할 수 있었다. Orb amd64에서는 이 변경 뒤에도 syscall 행이 누락됐다.

Shell
# 공백 정렬을 허용하면서 필요한 의미를 검사한다.
grep -E 'write\(1, "hello, world\\n", 13\)[[:space:]]+= 13' trace.txt

[판정] 공백 비교 실패는 trace를 검사하는 단계의 문제이고, 행 누락은 관측 결과를 얻는 단계의 문제다. 실패한 단계를 나누면 각각의 검사 방법을 정할 수 있다.

7. 왜 QEMU full-system을 선택했는가#

[목표] x86-64 Linux 커널에서 syscall·프로세스·mapping을 관측할 실행 환경을 확보한다.

선택지 얻을 수 있는 것 비용·영향 선택
ARM64 통과 결과 사용 ARM64 동작 근거 x86-64 관측 공백이 남음 보조 근거 유지
Orb amd64 실행 기존 환경을 활용한 직접 재현 이번 조합에서 trace 불완전 실행하고 실패 기록
전역 Rosetta 설정 변경 번역 방식의 다른 조건 비교 다른 머신·컨테이너에 영향 가능 이번 실험에서 유지
별도 QEMU x86-64 system x86-64 kernel 안에서 ptrace·syscall 관측 boot·rootfs 구성과 실행 시간 채택
실제 x86-64 컴퓨터 실제 하드웨어의 관측 별도 접근 환경 필요 이번 기록의 실행 범위 밖

[공식] QEMU system emulation은 CPU·메모리·장치 모델을 제공해 guest OS를 실행한다. TCG는 여러 CPU 아키텍처를 에뮬레이션할 수 있는 실행 엔진이다. QEMU Introduction

[결정 근거] host에 설치된 qemu-system-x86_64와 기존 Debian LAB image를 사용할 수 있었다. QEMU guest에서 x86-64 커널을 부팅하면 그 커널의 ptrace 경로를 관측할 수 있다. Orb의 전역 설정도 유지할 수 있어 별도 guest를 선택했다.

8. QEMU 환경을 어떻게 만들었는가#

용어 세 개#
용어 QEMU guest에서의 역할
rootfs guest 안의 /bin, /lib, /usr, LAB 소스 등을 담은 파일 트리
initramfs 커널이 초기 파일시스템으로 사용할 cpio archive
init 커널이 처음 실행할 사용자 프로그램. 이 guest는 작은 전용 init 사용
구성 순서와 이유#
  1. 기존 Dockerfile로 amd64 LAB image를 준비한다. GCC·strace·Python·예제의 사용자 공간 구성을 재사용한다.
  2. image의 파일 트리를 export한다. Docker를 rootfs를 만드는 도구로 사용한다.
  3. x86-64 Linux kernel을 준비한다. Alpine의 공식 v3.23 netboot kernel을 사용했고 관측 버전은 6.18.58-0-lts다.
  4. 작은 init을 정적 링크로 컴파일한다. early boot에서 필요한 mount와 LAB 실행을 담당한다.
  5. rootfs를 cpio+gzip으로 묶는다. 이를 initramfs로 커널에 전달한다.
  6. QEMU를 TCG·q35·CPU max로 실행한다. guest CPU 2개·RAM 1536 MiB를 설정하고 guest network는 끈다.
  7. guest serial log와 LAB 종료 코드를 확인한다. 출력 문구와 프로세스 종료 상태를 함께 평가한다.

init 소스는 /dev, /proc, /sys를 mount한 다음 LAB을 실행한다. /proc는 mapping·pagemap 검사에 필요하다. 이 init은 mount 호출의 개별 반환값을 검사하지 않는다. LAB에서 /proc 읽기에 성공한 결과가 최종 접근 가능 여부의 근거다.

정적 링크 init은 필요한 라이브러리를 실행 파일에 포함한다. 초기 부팅에서 동적 loader 경로에 대한 의존을 줄일 수 있다.

QEMU 실행 옵션:

Shell
qemu-system-x86_64 \
  -accel tcg -machine q35 -cpu max -m 1536 -smp 2 \
  -nographic -no-reboot -nic none \
  -kernel /path/to/vmlinuz \
  -initrd /path/to/root.cpio.gz \
  -append 'console=ttyS0 rdinit=/init loglevel=3'

console=ttyS0는 serial console로 로그를 받고, rdinit=/init은 initramfs의 전용 init을 실행하도록 지정한다. Kernel parameters -nic none은 guest의 가상 network 장치를 만들지 않는다. QEMU Invocation kernel·패키지 준비 단계에는 host의 인터넷 연결을 사용한다.

[재현] 다음 명령은 rootfs 구성부터 QEMU 실행·LAB 검사까지 수행한다.

Shell
# 레포 root에서. Docker와 QEMU가 준비되어 있어야 한다.
python3 tools/ch8/qemu-lab.py /tmp/csapp-x86-result.txt

재현 도구는 guest process와 임시 rootfs를 정리한다. 완료 시 init은 결과 marker를 출력하고 대기하며, host의 실행 도구가 QEMU process를 종료한다.

9. 실제로 무엇을 확인했는가#

[관측] 첫 실행 로그 발췌. 주소·PID·PFN은 해당 실행에서 얻은 값이다.

실행 출력
Linux 6.18.58-0-lts x86_64 GNU/Linux
gcc (Debian 12.2.0-14+deb12u1) 12.2.0
strace -- version 6.1
write(1, "hello, world\n", 13)          = 13
exit_group(0)                           = ?
...
maps: 7fc809dee000-7fc809df1000 rw-p 00000000 00:00 0
pagemap: present=1 PFN=255844
PASS: write/exit_group, parent-child flows, private writable mapping
CSAPP_QEMU_EXIT=0

...로 생략한 부모·자식 출력 등은 첫 실행 전체 로그에 있다.

관측 해석 남는 조건
write의 fd=1 표준 출력 fd 사용 실제 연결 대상은 실행 환경에 의존
요청 13·반환 13 해당 호출에서 13바이트 출력 성공 partial write·EINTR은 별도 처리 필요
exit_group(0) thread group 종료 경계 관측 인자 0은 종료 상태, 호출은 복귀하지 않음
부모·자식 각 3줄 두 프로세스 실행과 각각의 순서 확인 병렬 실행 확인에는 코어별 실행 시점 필요
rw-p 읽기·쓰기 가능한 private mapping copy-on-write에서 물리 페이지 공유 가능
present=1·PFN 표시 해당 guest 권한으로 페이지 정보 확인 PFN은 guest 물리 페이지 기준
LAB exit 0 syscall·프로세스 흐름·mapping 검사 통과 runtime·하드웨어 성능 비교는 미실행

[관측] 재실행에서도 같은 검사들을 통과했다. 두 번째 PFN은 329732였다. 검사는 실행마다 달라지는 주소·PFN 대신 mapping 권한·상태를 기준으로 한다. 재실행 로그

kernel URL·SHA-256·image ID·QEMU 버전은 환경 기록에 있다. kernel URL이 제공하는 파일과 Debian package는 업데이트될 수 있다. 동일한 바이너리로 재현하려면 환경 기록의 hash·image ID를 대조한다.

10. 해결한 범위와 남은 질문#

확인된 결론#

Orb의 기존 Ubuntu amd64에서 trace 누락이 재현됐다. QEMU full-system에서는 x86-64 커널의 syscall·프로세스 흐름·mapping을 두 번 관측했고, 모두 LAB 성공 조건을 충족했다.

근본 원인에 대한 현재 판단#

[가설] Orb amd64의 번역 실행과 syscall tracing 사이에 호환성 문제가 있을 수 있다. /bin/echo에서도 증상이 유지됐고, strace 6.1·6.14에서 재현됐으며, Rosetta 사용 설정을 확인했다. ARM64와 QEMU full-system에서 같은 LAB이 통과한 결과도 이 조사 방향을 뒷받침한다.

[미확인] 결함이 있는 구현과 정확한 원인은 확인되지 않았다. QEMU 비교에서는 커널·부팅 방식 등 여러 조건이 함께 바뀌었다. 원인을 좁히려면 Rosetta 설정의 A/B 실험, ptrace 요청·반환값·레지스터 구조 비교, OrbStack·Rosetta·strace의 최소 재현 코드와 버전별 대조가 필요하다.

[판정] x86-64 LAB의 재현 환경은 확보됐다. Orb의 원인 조사에는 조건을 하나씩 바꾸는 추가 실험이 필요하다.

11. 다른 문제에 적용하는 디버깅 순서#

  1. 성공 조건을 쪼갠다. 실행·관측·해석의 각 결과가 무엇이어야 하는지 적는다.
  2. 도구와 환경을 목록화한다. 실행 파일 경로, 버전, ISA, 커널, 권한, 번역 계층을 확인한다.
  3. 실패한 지점을 좁힌다. 명령 부재·권한 오류·빈 결과·포맷 불일치를 구분한다.
  4. 입력을 최소화한다. 사용자 프로그램에서 표준 명령으로 바꿔 증상이 유지되는지 본다.
  5. 가설마다 검사와 판정을 연결한다. 예상과 실제 결과를 기록한다.
  6. 목표에 맞는 대조 환경을 선택한다. 바꾼 조건과 그 때문에 남는 한계도 적는다.
  7. 재실행 가능한 근거를 남긴다. 명령·환경·원본 로그·종료 코드·성공 조건을 보관한다.
  8. 결론을 근거에 맞춘다. LAB 성공 여부와 도구의 원인 규명 여부를 각각 기록한다.
디버깅 사례 확인 질문#
  • 프로그램이 출력하고 종료 코드 0이면 LAB도 성공인가? LAB이 요구한 trace와 상태 검사까지 확인한다.
  • uname이 x86_64면 ptrace 호환성도 확인된 것인가? 실제 tracing 결과와 커널·번역 실행 경로를 함께 확인한다.
  • QEMU에서 통과하면 Rosetta 결함을 확정할 수 있는가? 여러 환경 조건이 함께 바뀌었으므로 구체적인 결함 위치는 추가 실험으로 좁힌다.
  • 왜 동일 PID·주소·PFN을 기대하지 않는가? 실행마다 변하는 값이며 검사의 목적은 관계·권한·동작을 확인하는 데 있다.

적용 조건과 미확인#

항목 상태 읽는 조건
교재 절·인쇄 쪽수 사실 북미판 공식 목차와 스터디 Global Edition. 판본별 인쇄 쪽수 표시
Exception 분류 조건부 일치 CSAPP의 넓은 exception 용법과 Intel의 interrupt/exception 구분
Syscall 레지스터 사실 Linux LP64 x86-64·AArch64 표. 다른 ABI는 별도 확인
주소 공간 그림 해설 개념 mapping. 실제 주소·kernel mapping 설정은 환경 의존
Runtime 설명 사실·해설 Go go1.26.0·OTP 29.1.2·Rust 문서. 세 runtime의 성능 비교 미실행
Divide fault·전원 제어 미실행 자료 검증. 실제 fault 재현과 host 전원 제어 실행 없음
강의 세부 발언 미확인 강의 메모 기준. 원 녹취 대조 없음

출처#

확인 기준일은 2026-10-25 KST다. 버전이 명시된 설명은 해당 버전을 기준으로 하며, 동적 문서는 이후 변경될 수 있다.