본문 바로가기

개발 공부/컴퓨터시스템 (CSAPP)

8장 예외적인 제어흐름 (Exceptional Control Flow)

💡 ECF란?

💥 Exceptional Control Flow (예외적인 제어 흐름)
평소 흐름대로 진행되던 프로그램이 갑자기 다른 곳으로 “점프”해서 실행되는 현상

 

ECF 종류 설명 예시
인터럽트 외부 장치가 CPU를 호출함 키보드 입력, 타이머
예외 (Exception) CPU가 실행 중 에러 감지 0으로 나누기, 페이지 폴트
시스템 콜 (Trap) 프로그램이 OS에 서비스 요청 read(), write() 등
시그널 OS나 다른 프로세스가 프로그램에 알림 Ctrl+C, SIGCHLD 등
비지역 점프 함수 호출 스택 무시하고 점프 setjmp, longjmp, throw/catch

🧠 왜 중요하냐?

✔ 운영체제가 동작하는 기본 원리

  • 모든 입출력, 메모리 관리, 프로세스 전환은 결국 ECF 덕분에 가능함.
  • 시스템 콜도 trap이라는 ECF의 일종.

✔ 프로세스/스레드 전환

  • 다른 프로세스로 제어권을 넘기는 문맥 전환(context switch) 자체도 ECF!

✔ 네트워크, 디스크, 타이머 등 외부 이벤트 처리

  • 예외 흐름이 없으면 비동기 처리 자체가 불가능

8.1 예외상황

💥 예외(Exception):

💡 예외는 CPU가 어떤 이벤트(문제 or 신호)를 감지했을 때 실행 흐름을 갑자기 튀게 만드는 것
*_→ *_정상 흐름 중 발생한 중요한 사건에 반응하는 방법

🧠 예외의 정체: 그냥 갑자기 “점프”

  • CPU가 명령어를 실행하다가, 뭔가 문제가 생기거나 외부 신호가 들어오면운영체제에 알려줘야 함!
  • → “이건 평범한 흐름이 아니다!”

그래서 CPU는 예외 벡터(예외 테이블)를 참고해서

해당 예외에 대응되는 OS 함수(예외 처리 핸들러)로 점프!

📦 예외가 발생하는 두 가지 상황

상황 예시
🧠 내부 이벤트 (명령어 자체 문제) - 0으로 나누기- 페이지 폴트- 산술 오버플로우
⚙️ 외부 이벤트 (명령어와 직접 관련 X) - 타이머 인터럽트- 디스크 I/O 완료- 네트워크 패킷 수신

✅ 예외 처리 후 흐름의 3가지 경우

  1. 현재 명령어로 돌아가기
    • 예: 페이지 폴트 → 메모리 로드하고 → 그 명령 다시 실행
  2. 다음 명령어로 넘어가기
    • 예: 디버깅용 트랩 → 감시 끝났으니 그냥 다음으로
  3. 프로그램 종료
    • 예: 0으로 나누기, 권한 오류 등 → 복구 불가 상황

8.1.1 예외처리

예외 처리 과정

단계 설명
1. 예외 발생 CPU가 어떤 이벤트(에러, 인터럽트 등)를 감지함
2. 예외번호 결정 각 예외마다 고유 번호가 있음 (예: 0 → divide by zero)
3. 예외 테이블 조회 예외번호를 인덱스로 사용해서 핸들러 주소 확인
4. 예외 핸들러 호출 운영체제의 커널 코드로 점프
5. 핸들러 처리 문제 처리 후 흐름 복귀 또는 종료

 

🎯 예외번호는 누가 정하나?

종류 예시 정하는 주체
하드웨어 예외 divide by zero, 페이지 폴트 등 CPU 설계자
소프트웨어 예외 시스템 콜, I/O 인터럽트 등 OS 설계자

 

💡 예외 테이블과 베이스 레지스터

  • 예외 테이블: 예외번호(k)에 따라 핸들러 주소를 저장해둔 점프 테이블
  • 베이스 레지스터: 예외 테이블이 메모리 어디 있는지 저장된 특수 CPU 레지스터
  • 예외 발생 → 주소 = 베이스 + (k * 항목 크기)로 핸들러 주소 계산

 

⚠️ 예외 처리와 함수 호출의 차이점

항목 함수 호출 예외 처리
리턴주소 저장 무조건 다음 인스트럭션 예외 종류에 따라 현재 or 다음
저장 위치 사용자 스택 커널 스택
모드 전환 없음 사용자 모드 → 커널 모드
저장 상태 리턴주소만 리턴주소 + CPU 상태 (EFLAGS 등)

 

📦 예외 처리 시 스택에 저장되는 것들

  • 리턴 주소
  • EFLAGS 레지스터 (조건 코드 포함)
  • 기타 CPU 레지스터 상태
  • → 전부 커널 스택에 저장됨 (사용자 스택 아님!)

 

🔙 복귀 시 사용되는 명령

  • 예외 핸들러가 끝나면, iretq (x86-64 기준) 같은 명령으로
    • 커널 스택에서 상태 복원
    • 사용자 모드로 전환
    • 중단되었던 프로그램으로 복귀

 


8.1.2 예외의 종류

종류 설명 예시 리턴
인터럽트 CPU 외부에서 발생, 비동기적, 하드웨어 신호에 반응 타이머, 디스크 I/O, 네트워크 패킷 수신 등 다음 인스트럭션
트랩 프로그램이 의도적으로 발생시킨 예외, 동기적 syscall, breakpoint, 디버그용 trap 다음 인스트럭션
오류(fault) 프로그램 실행 중 복구 가능한 오류, 동기적 페이지 폴트, 보호 에러 등 현재 인스트럭션 (재실행)
중단(abort) 복구 불가능한 치명적 오류, 프로그램 강제 종료 메모리 패리티 에러, 하드웨어 오류 등 리턴하지 않음

 

 

🔍 각 예외의 핵심 정리

인터럽트 (Interrupt)

  • 비동기적: CPU가 지금 어떤 명령을 실행하든 관계없이 외부에서 날아옴
  • 발생 시점: 현재 명령이 끝난 후
  • 처리 방식: 인터럽트 핸들러 실행 후 → 다음 인스트럭션으로 이동
  • 예시: 네트워크 패킷 도착, 타이머 만료, 디스크 데이터 준비 완료

 

트랩 (Trap)

  • 동기적: 현재 실행 중인 명령어가 명시적으로 트랩을 발생시킴
  • 대표 용도: 시스템 콜
  • 처리 방식: 커널 루틴 실행 후 → 다음 인스트럭션으로 이동
  • 예시: read(), write(), fork(), exit() 같은 시스템 콜

 

오류 (Fault)

  • 동기적 오류지만, 복구 가능할 수도 있음
  • 처리 방식:
    • 복구 가능: 현재 인스트럭션으로 돌아가서 재실행
    • 불가능: abort 루틴으로 → 프로그램 종료
  • 대표 예시: 페이지 폴트
    • 해당 메모리 페이지를 디스크에서 불러오고 다시 시도

 

중단 (Abort)

  • 복구 불가능한 치명적 오류
  • 처리 방식: 예외 핸들러가 즉시 응용 프로그램 종료
  • 예시: 메모리 하드웨어 오류, 버스 에러

 

📌 기억하기 쉽게

  • 💥 동기적 오류: “내가 잘못한 거야” → 지금 이 명령이 문제
  • 🔔 비동기적 오류: “밖에서 누가 부른 거야” → 외부에서 이벤트가 온 거야

 


 

8.1.3 리눅스/x86—64 시스템에서의 예외상황

예외 번호 예외 이름 설명
0 나누기 오류 0으로 나누거나 결과가 너무 클 때 → 프로그램 강제 종료 (복구 안 됨)
13 일반 보호 오류 잘못된 메모리 접근 (예: 읽기 전용 공간에 쓰기 시도) → 세그폴트
14 페이지 폴트 접근한 메모리 페이지가 아직 로드되지 않음 → 디스크에서 불러와 복구
18 머신 체크 치명적 하드웨어 오류 (DRAM, CPU 내부 등) → 복구 불가, 즉시 종료

 

🧠 포인트 1: 예외 번호

  • x86-64 시스템에서는 예외를 0 ~ 255까지 구분
    • 0~31: CPU(하드웨어)에서 정의한 예외
    • 32~255: 운영체제(OS)에서 정의한 예외 (시스템 콜, 인터럽트 등)

 

🧠 포인트 2: 시스템 콜은 “트랩” 예외

  • 리눅스에서 시스템 콜은 syscall이라는 특별한 명령어로 발생
  • 이건 사용자 모드 → 커널 모드로 진입하는 방법
레지스터 역할
%rax 시스템 콜 번호 (예: write = 1)
%rdi 첫 번째 인자
%rsi 두 번째 인자
%rdx 세 번째 인자
%r10 네 번째 인자
%r8 다섯 번째 인자
%r9 여섯 번째 인자
syscall 리턴 %rax (성공 시 값, 실패 시 음수 errno)

 

int main() {
    write(1, "hello, world\n", 13); // 시스템 콜 호출
    _exit(0);                        // 프로그램 종료 syscall
}

 

  • write()는 파일 디스크립터 1(STDOUT)에 문자열을 출력
  • _exit()은 리턴 없이 프로그램을 바로 종료
mov $1, %rax       # syscall 번호: write
mov $1, %rdi       # fd: stdout
mov $msg, %rsi     # 메시지 주소
mov $13, %rdx      # 바이트 수
syscall            # 시스템 콜 실행

mov $60, %rax      # syscall 번호: exit
xor %rdi, %rdi     # 종료 코드 0
syscall            # 시스템 콜 실행

 

 


 

 

📌 이걸 개발자로서 안다는 건…

 

✅ 1. 프로그램이 어떻게 운영체제와 대화하는지를 이해

  • 파일을 읽고, 쓰고, 종료하고, 자식 프로세스를 만들고…
  • 이 모든 건 예외적 흐름(=시스템 콜, 트랩)을 통해 커널과 직접 대화하는 것

👉 개발자는 단순히 함수 쓰는 게 아니라, OS에 명령을 내리는 행위를 하는 것

 

 

✅ 2. 버그와 오류에 대한 깊은 이해

  • 프로그램이 0으로 나눴다 → CPU가 예외 감지 → 커널이 프로세스를 죽인다 → Segmentation fault
  • 페이지 폴트가 난다 → 커널이 메모리 불러오고 → 현재 명령어 재실행

👉 이 흐름을 이해하면 디버깅이 훨씬 깊어진다.

“아, 이건 커널에서 나를 죽인 거구나”

“이건 핸들러에서 복구되었구나”

 

✅ 3. 보안, 안정성, 리소스 보호 원리를 이해

  • 사용자 모드에서는 제한된 명령만 사용 가능
  • 예외 발생 시만 커널 모드 진입 → 커널 스택 사용 → 자원 통제

👉 왜 우리가 OS 없이 root 권한도 막고, 프로세스끼리 메모리 접근도 못 하게 되는지 이해하게 됨

권한 분리와 보안 모델의 기반

 

 

✅ 4. 시스템 프로그래밍 능력 향상

  • 직접 시스템 콜 호출 (syscall)
  • 예외 핸들러 작성 (커널 모듈, low-level code)
  • 시그널 처리 (SIGSEGV, SIGCHLD 등)

👉 이건 고급 프로그래머, 특히 백엔드, 시스템, 인프라 개발자라면 반드시 알아야 할 수준

 

 

✅ 5. 동시성과 인터럽트 기반 설계 능력

  • 타이머 인터럽트 → 문맥 전환
  • 인터럽트 기반 이벤트 루프 → 서버/디바이스 드라이버 설계
  • select, epoll, signal, alarm 같은 메커니즘의 본질 이해

 

👉 네트워크 프로그래밍, 이벤트 기반 서버를 구현할 때까지 연결되는 기반