페이지 테이블: 사랑 이야기 (그게 아니야)

발행: (2026년 1월 10일 오후 01:42 GMT+9)
11 min read
원문: Dev.to

Source: Dev.to

Diary Entry

Dear diary,

오늘 나는 UEFI라는 편안한 포옹을 떠나는 것이 40살에 부모님 집을 떠나는 것과 같다는 것을 깨달았다. 이전에 마법처럼 동작하던 모든 것이 이제는 세상이 어떻게 돌아가는지 실제로 이해해야만 작동한다.

아침 9시에 커피를 들고 앉아, UEFI에서 베어 메탈로 전환하는 것이 간단할 것이라고 자신했다. 결국 나는 AHCI 스토리지와 키‑밸류 스토어를 성공적으로 구현했으니, 글로벌 디스크립터 테이블(GDT)을 설정하고 내 커널을 실행하는 것이 얼마나 어려울 수 있겠는가 라는 생각이 들었다. 그 오만함은 뚜렷했다.

계획은 다음과 같이 합리적으로 보였다:

  1. ExitBootServices 호출.
  2. 64‑비트 롱 모드를 위한 적절한 GDT 설정.
  3. 키보드 입력을 폴링 방식으로 구현.
  4. 커널 셸 실행.

나는 SSD에 직접 로그를 쓰는 로깅 시스템까지 구축했으니 재부팅 사이에도 디버깅할 수 있었다. 뭐가 잘못될 수 있겠는가?

모든 것이. 모든 것이 잘못될 수 있다.

First attempt

첫 번째 시도는 유망했다. ExitBootServices가 성공했고, GDT가 불만 없이 로드되었으며, 나는 커널 모드에서 실행 중이었다. 커널 셸 프롬프트도 보였다. 승리는 확정된 듯했지만, 자신감 넘치는 sti 명령으로 인터럽트를 활성화하려고 했을 때였다.

머신은 트리플‑폴트를 즉시 일으켰다.

트리플 폴트는 x86 프로세서가 “포기한다, 이제 네가 알아서 해라”라고 말한 뒤, 디지털 세계에서 테이블을 뒤집고 나가는 것과 같은 행동을 수행하는 방식이다. 가장 도움이 되면서도 가장 도움이 되지 않는 오류 상황이다—뭔가 치명적인 문제가 있다는 것은 알지만, CPU가 그 원인을 알려주는 건 너무 큰 노력이라고 판단한 것이다.

나는 다음 두 시간을 **“인터럽트 거부 단계”**라 부르는 시간에 보냈다.

  • 분명히 인터럽트 자체가 문제는 아니다.
  • GDT가 잘못됐을 수도 → 세 번이나 다시 작성했으며, 각 반복마다 점점 더 복잡해졌다.
  • 스택이 손상됐을 수도 → 스택 카나리와 검증 코드를 추가했다.
  • UEFI가 남긴 어떤 상태가 방해하고 있을 수도 → 생각할 수 있는 모든 레지스터를 클리어해 보았다.

머신은 같은 기계적인 정확도로 트리플‑폴트를 계속 일으켰고, 나는 계속해서 커피를 만들었다.

Switching to polling

정오가 되자 나는 인터럽트가 문제라는 것을 받아들이고 포기하기로 했다. 키보드 입력을 폴링하는 것이 우아하지는 않지만 동작할 수 있었다. 간단한 PS/2 컨트롤러 폴링 루프를 구현해 기본 키보드 입력을 얻었고, 커널 셸도 정상 작동했으며 로그를 SSD에 저장할 수도 있었다.

Milestone 5는 기술적으로 완료됐지만, 마치 차를 직접 밀어 결승선을 통과시킨 듯한 느낌이었다.

Back to interrupts – Milestone 6

오후는 IDT 광산에서 보냈다. 중세 필경사가 원고를 복사하듯 조심스럽게 인터럽트 서비스 루틴을 만들었다. 완벽한 스택 프레임을 생성하는 매크로 시스템을 작성하고, 어떤 인터럽트 상황도 우아하게 처리할 수 있는 정교한 핸들러를 만들었으며—모든 것을 완전히 부숴버렸다.

인터럽트를 활성화한 첫 테스트에서는 sti 직후 **Debug Exception (Vector 1)**이 즉시 발생했다. 이것은 실제로 진전이었다—트리플 폴트 대신 구체적인 예외가 발생한 것이다. CPU가 최소한 무언가가 잘못됐다는 신호를 보내고 있었지만, 그 신호가 의미하는 바는 전혀 이해되지 않았다.

디버그 예외는 디버그 레지스터 브레이크포인트에 걸리거나 트랩 플래그가 설정돼 싱글‑스텝 모드가 될 때 발생한다. 나는 디버거를 사용하고 있지 않았고, 트랩 플래그를 의도적으로 설정한 적도 없었다. 하지만 x86 프로세서는 30년 전의 사소한 일까지 기억하는 친척과 같다—가장 불편한 곳에 상태를 남겨두는 법이다.

한 시간 정도 더 고민한 끝에 UEFI가 디버깅 상태를 남겨두었을 가능성을 깨달았다. 모든 디버그 레지스터(DR0부터 DR7까지)와 RFLAGS의 트랩 플래그를 클리어하는 코드를 추가했다. 디버그 예외는 사라졌지만, 이제 새로운 문제가 생겼다: 타이머 인터럽트가 발생하지 않는다.

“Silent treatment” phase

PIC을 설정했고, IDT도 설정했다.

Source:

up, interrupts were enabled, but my timer tick counter remained stubbornly at zero. The system wasn’t crashing, which was somehow more frustrating than when it was exploding spectacularly.

  • Verified the PIC configuration seventeen times.
  • Read Intel manuals until my eyes bled.
  • Checked and re‑checked the IDT entries.

Everything looked correct on paper, but the hardware seemed to be politely ignoring my carefully crafted interrupt handlers.

The breakthrough

At 6 pm I was explaining the problem to my rubber duck (a literal rubber duck I keep on my desk for debugging purposes—don’t judge). As I described my elegant ISR macro system, I realized the problem: I was being too clever.

My macros were generating complex stack‑frame management code that was somehow corrupting the interrupt return address. When I looked at the actual assembly output, it was a nightmare of stack manipulation that would make a spaghetti factory jealous.

So I threw it all away and wrote the simplest possible interrupt handlers using naked functions with inline assembly. No fancy macros, no elegant abstractions—just the bare minimum code to handle an interrupt and return cleanly:

__attribute__((naked)) void isr_timer(void) {
    asm volatile (
        "push %rax\n"
        "incq g_timer_ticks\n"
        "movb $0x20, %al\n"
        "outb %al, $0x20\n"   // Send EOI
        "pop %rax\n"
        "iretq"
    );
}

It was inelegant. It was primitive. It worked perfectly.

When I enabled interrupts with the new handlers, the timer immediately started ticking at exactly 100 Hz. The keyboard interrupt began capturing input flawlessly. After eight hours of fighting with sophisticated abstractions, the solution was to write interrupt handlers like it was 1985.

Takeaway

There’s something profoundly humbling about spending an entire day implementing “modern” kernel architecture only to discover that the most primitive approach is the most reliable. It’s a reminder that simplicity often trumps elegance, especially when you’re talking directly to hardware.

End of entry.

ke spending hours crafting a gourmet meal and then realizing that a peanut butter sandwich would have been both more satisfying and less likely to poison you.

By evening, I had a fully functional interrupt‑driven kernel. The timer was ticking, the keyboard was responsive, and the kernel shell worked flawlessly. I could watch the timer ticks increment in real‑time, each one a small victory over the chaos of bare‑metal programming. I saved the kernel log to review later:

[KERNEL] Enabling interrupts (STI)...
[KERNEL] Interrupts ENABLED.
[KERNEL] Timer ticks after delay: 199
[KERNEL] Kernel mode active (interrupt mode)

Those simple log messages represent eight hours of debugging, three complete rewrites of the interrupt system, and more coffee than any human should consume in a single day. But they also represent something more: a functioning kernel that has successfully transitioned from UEFI’s protective embrace to the harsh reality of bare‑metal operation.

Looking back, the lessons are clear.

  1. x86 processors remember everything and forgive nothing – always clear the debug registers when transitioning from UEFI.
  2. The PIC hasn’t changed significantly since the 1980s – trying to abstract away its quirks usually makes things worse.
  3. When sophisticated solutions fail, sometimes the answer is to write code like it’s three decades ago.

Most importantly, I learned that there’s a particular satisfaction in building something from first principles, even when those principles seem designed to maximize human suffering. Every successful interrupt is a small victory over the entropy of the universe. Every timer tick is proof that somewhere in the chaos of transistors and electrons, my code is executing exactly as intended.

Tomorrow I’ll tackle content‑addressed storage and time‑travel debuggin

Source:

g. 겉보기에 아직 충분히 고통을 겪지 않은 것 같고, 취미 OS 개발의 매력은 언제나 겸손해지게 할 또 다른 복잡성 레이어가 기다리고 있다는 점이다.

하지만 오늘 밤 나는 여기 앉아 타이머 틱 카운터가 한 번에 하나씩 인터럽트가 발생할 때마다 증가하는 모습을 지켜보며, 운영 체제를 만드는 것이 여가 시간을 보내는 합리적인 방법이라고 가장하려고 한다.

Back to Blog

관련 글

더 보기 »

Interrupt Handlers의 관리와 양육

인터럽트 핸들러의 관리와 양육 친애하는 일기여, 오늘 나는 내 초기 단계의 운영 체제에 목소리를 부여하기로 결심했다. 문자 그대로는 아니지만—그건 무섭겠지만—.

Rhiza의 커널 연대기: 커널 개발

Kernel Development Focus 이 이야기는 커널 개발에 초점을 맞춘 스토리 중심의 여정입니다. 세션 개요 - 세션 ID: rhiza-blog-1767663121960 - 시간 범위: 2...