관리 메뉴

맨땅에 코딩

인터럽트와 예외(QEMU·플러그인 관점) 본문

카테고리 없음

인터럽트와 예외(QEMU·플러그인 관점)

나는 푸딩 2025. 10. 26. 20:23

1. 기본 개념

인터럽트(Interrupt)는 CPU에게 “지금 당장 처리해야 할 일이 생겼다!” 라고 알리는 신호이고,
예외(Exception)는 프로그램 실행 중 CPU가 오류를 감지했을 때 발생하는 이벤트다.

 

공통점: CPU가 현재 실행 중인 명령어를 "일시 중단"하고, 정해진 "핸들러 함수"로 점프한다.

 

2. 인터럽트의 종류

구분 이름 예시 발생 주체
하드웨어 인터럽트 Hardware Interrupt 키보드 입력, 네트워크 패킷 도착 외부 장치
소프트웨어 인터럽트 Software Interrupt int 0x80, syscall (시스템콜) 프로그램
예외(Exception) Fault / Trap / Abort 0으로 나누기, 페이지 폴트 CPU 내부

 

  • 하드웨어: 외부 장치가 "CPU 나 좀 봐달라."
  • 소프트웨어: 프로그램이 "OS, 이거 대신 처리해달라."
  • 예외: CPU가 '야, 이건 오류다."

3. 인터럽트 발생 시 흐름

CPU는 인터럽트를 받으면 다음 순서로 동작한다.

① 현재 실행 중인 명령어 일시 중단  
② 현재 RIP(명령어 위치)와 FLAGS를 스택에 저장  
③ IDT(Interrupt Descriptor Table)에서 해당 번호의 핸들러 주소 찾음  
④ RIP을 핸들러 주소로 교체 (점프)  
⑤ 핸들러 실행 (OS 코드)  
⑥ 완료 후 iret 명령으로 원래 위치로 복귀

 

그래서 인터럽트는 "함수 호출"처럼 보이지만, OS 입장에서는 "예상치 못한 강제 분기"이다.

 

4. 인터럽트 벡터 테이블 (IDT)

IDT는 각 인터럽트 번호(0~255)에 대한 "핸들러 주소"를 저장하는 테이블이다.
번호 이름 설명
0 Divide Error 0으로 나눔
6 Invalid Opcode 존재하지 않는 명령어
13 General Protection Fault 권한 오류
14 Page Fault 페이지 매핑 없음
32~47 하드웨어 IRQ 타이머, 키보드, 마우스 등
128 System Call (int 0x80) 유저모드 → 커널모드 진입

 

QEMU 플러그인은 이 번호를 그대로 받을 수 있다.

[IRQ] #14 PAGE_FAULT @0xFFFFF80200001000

 

5. 하드웨어 인터럽트(IRQ)

외부 장치(디스크, 키보드 등)가 CPU에게 "작업 완료" 신호를 보내는 것이다.

 

하드웨어 인터럽트는 “PIC(Programmable Interrupt Controller)”나 “APIC(Advanced PIC)”를 통해 전달된다.

타이머 칩 → APIC → CPU(IRQ#0)
네트워크 카드 → APIC → CPU(IRQ#11)

 

CPU는 해당 IRQ를 받으면 IDT의 0x20~0x2F 영역을 찾아간다.

 

플러그인으로 이 이벤트를 잡으면

  • CPU가 언제 외부 이벤트로 깨어났는지
  • IRQ 발생 빈도, 지연 시간, 핸들러 처리 시간을 분석할 수 있다.

6. 소프트웨어 인터럽트(System Call)

프로그램이 운영체제 커널 기능을 요청할 때 CPU 명령어 int 0x80 또는 syscall을 실행한다.
User Mode 프로그램
   ↓
int 0x80 / syscall
   ↓
Ring 0 커널 코드로 진입
   ↓
시스템 콜 처리 후 복귀

 

이 때, 권한이 Ring 3 → Ring 0 으로 바뀐다.

유저 모드 → 커널 모드 전환 시점 추적 가능하다.

플러그인은 이것을 IRQ #128 (혹은 Syscall 이벤트)으로 감지할 수 있다.

 

7. 예외(Exception)

CPU가 "이건 명령 실행 중 오류다"라고 판단했을 때 발생한다.
예외 번호 이름 원인
0 Divide Error 0으로 나누기
6 Invalid Opcode 잘못된 명령어 실행
13 General Protection 접근권한 위반
14 Page Fault 메모리 매핑 안 됨

 

디버거가 가장 중요하게 보는 이벤트로, 실행 중 크래시나 접근 위반을 탐지할 수 있다.

 

플러그인은 이것을 로그로 남길 수 있다.

[EXC] Page Fault @ 0xfffff80212345678 | Reason: Write to non-present page

 

8. 인터럽트 처리 후 복귀

핸들러가 끝나면 iretq 명령으로 복귀:

pop RIP
pop FLAGS
resume previous instruction

 

이것이 OS가 "원래 하던 일로 돌아가는" 과정이다.

  QEMU 플러그인에서 이 순간을 감지하면 "스케줄링 사이클"을 재구성할 수 있다.

 

9. QEMU에서 인터럽트 시뮬레이션 구조

QEMU는 하드웨어 인터럽트도 가상화해서 처리함:

[가상 디바이스] ─→ qemu_irq_pulse()
                 └→ CPU 내부 irq_pending flag set
→ QEMU main loop가 이 플래그를 확인
→ guest CPU에서 handle_interrupt() 실행

 

즉, 실제 하드웨어 없이도 "IRQ 이벤트"가 일어나는 것처럼 흉내낸다.

 

우리가 해야할 일은 플러그인에서 이때 호출되는 "IRQ 핸들러 진입 시점"을 hook  → 로그 남기기 이다.

 

10. QEMU 플러그인 예시(IRQ 추적)

static void irq_cb(qemu_plugin_id_t id, unsigned int vcpu_index, int irq_num)
{
    printf("[IRQ] vCPU=%u | IRQ=%d | Timestamp=%ld\n",
           vcpu_index, irq_num, clock());
}

void qemu_plugin_install(qemu_plugin_id_t id, const qemu_info_t *info, int argc, char **argv)
{
    qemu_plugin_register_vcpu_interrupt_cb(id, irq_cb);
}

 

위와 같이 콜백을 등록하면, 게스트 OS에서 발생하는 모든 인터럽트 이벤트가 터미널에 찍힌다.

 

11. 인터럽트 타임라인 시각화 예시

Time →
│
├── IRQ#0 (Timer Tick) → 스케줄러 실행
├── IRQ#14 (Page Fault) → MMU 핸들러 진입
├── IRQ#11 (Network RX) → 패킷 처리
├── IRQ#0 (Timer Tick) → 다음 스레드로 전환
│
└── ...

 

→ 이런 식으로 CPU가 외부 신호로 얼마나 자주 context switch 하는지 분석이 가능하다.