x86 보호 모드에서 Local APIC 타이머를 활용한 고정밀 타이머 구현

x86-64 보호 모드 환경에서 Local APIC(Advanced Programmable Interrupt Controller) 타이머는 PIT나 HPET 없이도 CPU 내부 타이머를 활용해 고정밀 주기적 인터럽트를 생성한다. 운영체제 커널에서 스케줄러 틱, 시간 측정, 프로파일링 등에 활용할 수 있으며, TSC(Time Stamp Counter)와 결합하면 더 정밀한 타이밍을 얻을 수 있다.

전체 구현 코드

아래 코드는 NASM 문법으로 작성한 64비트 보호 모드용 Local APIC 이머 예제다. IDT와 스택은 이미 설정되어 있다고 가정한다.

; apic_htimer.asm
; nasm -f elf64 apic_htimer.asm -o apic_htimer.o
; ld apic_htimer.o -o apic_htimer.elf

section .data
    ; Local APIC 레지스터 오프셋
    LAPIC_SVR     equ 0x0F0
    LAPIC_EOI     equ 0x0B0
    LAPIC_ID      equ 0x020
    LAPIC_TMR_DIV equ 0x3E0
    LAPIC_TMR_ICR equ 0x380
    LAPIC_TMR_CCR equ 0x390
    LAPIC_LVT_TMR equ 0x320

    ; APIC 관련 상수
    APIC_BASE_MSR equ 0x1B
    APIC_ENABLE   equ 0x800

    ; 타이머 모드 및 벡터
    TMR_VECTOR    equ 0x40
    TMR_PERIODIC  equ 0x20000
    DIVIDE_BY_1   equ 0xB

section .bss
    align 8
global_counter: resd 1
tsc_start_lo:   resd 1
tsc_start_hi:   resd 1

section .text
global _start

_start:
    call init_local_apic
    call measure_apic_frequency
    call start_periodic_timer

.main:
    hlt
    jmp .main

; --------------------------------------------------
; Local APIC 활성화
; --------------------------------------------------
init_local_apic:
    ; IA32_APIC_BASE MSR의 bit 11을 설정해 APIC 활성화
    mov ecx, APIC_BASE_MSR
    rdmsr
    or eax, APIC_ENABLE
    wrmsr

    ; Spurious Interrupt Vector Register 설정
    mov edi, 0xFEE00000
    mov dword [edi + LAPIC_SVR], 0x1FF
    ret

; --------------------------------------------------
; APIC 타이머 주파수 측정
; --------------------------------------------------
measure_apic_frequency:
    mov edi, 0xFEE00000

    ; 분주비 1로 설정
    mov dword [edi + LAPIC_TMR_DIV], DIVIDE_BY_1

    ; one-shot 모드로 최대 카운트 설정
    mov dword [edi + LAPIC_LVT_TMR], TMR_VECTOR
    mov dword [edi + LAPIC_TMR_ICR], 0xFFFFFFFF

    ; TSC를 이용해 10ms 대기
    rdtsc
    mov [tsc_start_lo], eax
    mov [tsc_start_hi], edx

.wait_10ms:
    rdtsc
    sub eax, [tsc_start_lo]
    sbb edx, [tsc_start_hi]
    cmp edx, 0
    jl .wait_10ms
    cmp eax, 34000000        ; 3.4GHz 기준 10ms
    jb .wait_10ms

    ; 10ms 동안 소한 카운트 계산
    mov ecx, [edi + LAPIC_TMR_CCR]
    mov eax, 0xFFFFFFFF
    sub eax, ecx

    ; 1ms 주기를 위한 카운트로 변환
    xor edx, edx
    mov ecx, 10
    div ecx
    ret

; --------------------------------------------------
; 주기적 타이머 시작
; --------------------------------------------------
start_periodic_timer:
    mov edi, 0xFEE00000
    mov dword [edi + LAPIC_LVT_TMR], TMR_PERIODIC | TMR_VECTOR
    mov dword [edi + LAPIC_TMR_DIV], DIVIDE_BY_1
    mov dword [edi + LAPIC_TMR_ICR], eax
    ret

; --------------------------------------------------
; APIC 타이머 인터럽트 핸들러
; --------------------------------------------------
apic_timer_handler:
    push rax
    push rdi

    inc dword [global_counter]

    ; End of Interrupt 신호 전송
    mov edi, 0xFEE00000
    mov dword [edi + LAPIC_EOI], 0

    pop rdi
    pop rax
    iretq

section .note.GNU-stack noalloc noexec nowrite progbits

심 동작 원리

1. Local APIC 활성화

IA32_APIC_BASE MSR(0x1B)의 bit 11을 설정하면 Local APIC이 활성화된다. 이 작업은 CPL=0(커널 모드)에서 수행해야 하며, 실제 APIC 레지스터는 물리 주소 0xFEE00000에 매핑된다.

mov ecx, 0x1B
rdmsr
or eax, 0x800
wrmsr

이후 SVR(Spurious Interrupt Vector Register)에 0x1FF를 면 bit 8(APIC Software Enable)이 설정되고, 가짜 인터럽트 벡터 0xFF가 할당된다.

2. 타이머 주파수 측정

APIC 타이머의 입력 주파수는 CPU마다 다르므로 실행 시점에 측정해야 한다. 타이머를 one-shot 모드로 최대값(0xFFFFFFFF)으로 설정하고, TSC를 기준으로 일정 시간만큼 대기한 뒤 감소한 카운트를 읽는다.

mov dword [edi + LAPIC_TMR_ICR], 0xFFFFFFFF
; ...
mov ecx, [edi + LAPIC_TMR_CCR]
mov eax, 0xFFFFFFFF
sub eax, ecx

위 예제는 10ms 동안 감소한 카운트를 측정한 뒤 10으로 나누어 1ms 주기에 필요한 카운트를 계산한다. 부팅 시 1초를 기다리는 방식보다 빠르게 보정할 수 있다.

3. 주기 모드 및 인터럽트 처리

LVT 타이머 레지스터(0x320)의 bit 17을 1로 설정하면 주기적(periodic) 모드가 된다. EOI 레지스터(0x0B0)에 0을 써야 다음 인터럽트가 발생한다.

mov dword [edi + LAPIC_LVT_TMR], 0x20040
mov dword [edi + LAPIC_EOI], 0

드 및 실행

다음 명어로 컴파일하고 QEMU에서 실행할 수 있다.

sudo apt-get install nasm qemu-system-x86
nasm -f elf64 apic_htimer.asm -o apic_htimer.o
ld apic_htimer.o -o apic_htimer.elf
qemu-system-x86_64 -kernel apic_htimer.elf -cpu qemu64,+apic

실제 운영체제 커널에 통합할 때는 ELF 파일을 부트로더가 적재하는 대신, 어블리 코드를 커널 이미지에 연결하고 IDT와 GDT를 먼저 초기화해야 한다.

커널 통합 시 고려사항

  • IDT 연결: 인터럽트 터 0x40에 대한 IDT 게이트가 반드시 설정되어 있어야 한다.
  • 스택 교환: ISR에서 실행 중이던 스택을 손상시키지 않도록 별도의 인터럽트 스택을 준비하거나 swapgs를 활용한다.
  • MMIO 매핑: APIC 기본 주소가 페이지 테이블에 매핑되어 있어야 하며, 쓰기 한이 있어야 한다.
  • 티코어: SMP 환경에서는 각 CPU 코어마다 독립적인 Local APIC를 가지므로 BSP와 AP 모두 이머를 개별 설정해야 한다.

문제 해결

  • 인터럽트가 발생하지 않음: IDT 항목, APIC enable 비트, LVT 타이머 벡터 번호, PIC 마스크 여부를 확인한다.
  • 주기가 부정확함: TSC 주파수가 변동하는 경우 P-state/C-state를 고정하거나 HPET로 보정한다.
  • 페이지 폴트: APIC MMIO 영역이 커널 주소 공간에 매핑되었는지, 기 권한이 있는지 확인한다.

태그: x86-64 Local-APIC APIC-Timer OS-Development Assembly

9월 22일 08:46에 게시됨