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 영역이 커널 주소 공간에 매핑되었는지, 기 권한이 있는지 확인한다.