project 3. virtual memory를 구현하면서. page fault 예외를 처리하기 위한 page_fault()
함수를 열심히 만들었다.
page fault는 결국 수 많은 예외 중 하나인데, 어떻게 pintos는 발생한 예외가 page fault인지 판단하고, 그 핸들러로 page_fault()
함수를 호출할 수 있는걸까?
먼저 IDT가 무엇일까?
인터럽트 벡터 테이블을 구현하기 위해 x86 아키텍처에서 사용되는 데이터 구조체이다. IDT는 프로세서가 인터럽트와 예외에 대한 정확한 반응을 결정하기 위해 사용된다.
쉽게 말하자면, 예외나 인터럽트가 발생했을때 해당 예외 또는 인터럽트를 어떻게 처리해야할지 결정하는데 도움을 주는 테이블이다. IDT를 통해 예외를 식별하고, 그에 맞는 핸들링 함수를 호출할 수 있게 된다.
아래는 kaist pintos의 IDT 초기화가 이루어지는 과정이다.
intr-stubs.S
/* All the stubs. */
STUB(00, zero) STUB(01, zero) STUB(02, zero) STUB(03, zero)
STUB(04, zero) STUB(05, zero) STUB(06, zero) STUB(07, zero)
STUB(08, REAL) STUB(09, zero) STUB(0a, REAL) STUB(0b, REAL)
STUB(0c, zero) STUB(0d, REAL) STUB(0e, REAL) STUB(0f, zero)
STUB(10, zero) STUB(11, REAL) STUB(12, zero) STUB(13, zero)
STUB(14, zero) STUB(15, zero) STUB(16, zero) STUB(17, zero)
STUB(18, REAL) STUB(19, zero) STUB(1a, REAL) STUB(1b, REAL)
STUB(1c, zero) STUB(1d, REAL) STUB(1e, REAL) STUB(1f, zero)
...
...
intr-stubs.S
파일에 작성되어있는 많은 STUB 함수들의 포인터가 어셈블 시점에 실행파일에 포함되며, 이때 intr_stubs[]
배열의 요소로 초기화 된다. 이렇게 초기화된 배열은 예외 또는 인터럽트가 발생했을때 참조하게 되는데,vec_no
에 해당하는 인덱스의 함수 포인터가 가리키는 STUB 함수를 호출하게 된다.
먼저 init.c
파일의 main()
함수 내에서 pintos에서 처리되는 예외들에 대한 초기화가exception_init()
함수를 통해 이루어진다.
init.c - main()
int
main (void) {
...
#ifdef USERPROG
exception_init();
syscall_init ();
#endif
...
}
exception.c - exception_init()
이 함수 내에는 page fault 외에도 여러가지 예외에 대한 핸들링 함수들이 초기화 된다. 그 중에서 우리는 page fault의 경우만 살펴보기로 하겠다.
void exception_init(void)
{
...
/* Most exceptions can be handled with interrupts turned on.
We need to disable interrupts for page faults because the
fault address is stored in CR2 and needs to be preserved. */
intr_register_int(14, 0, INTR_OFF, page_fault, "#PF Page-Fault Exception");
}
interrupt.c - intr_register_int()
void
intr_register_int (uint8_t vec_no, int dpl, enum intr_level level,
intr_handler_func *handler, const char *name)
{
...
register_handler (vec_no, dpl, level, handler, name);
}
interrupt.c - register_handler()
static void
register_handler (uint8_t vec_no, int dpl, enum intr_level level,
intr_handler_func *handler, const char *name) {
...
intr_handlers[vec_no] = handler;
intr_names[vec_no] = name;
}
여기 까지의 함수를 통해 각 예외의 핸들러는 intr_handlers
라는 이차원 배열에 vec_no
에 해당하는 인덱스와 함께 저장된다.
예외가 발생하면 CPU가 IDT를 참고하여, 앞서 초기화 했던 intr_stubs[]
배열에서 예외의 vec_no에 해당하는 함수 포인터를 호출하게 된다. page fault의 경우 intr_stubs[14]
가 될 것이고, 이는 STUB(14, zero)
가 호출됨을 의미한다.
intr-stubs.S - STUB(NUMBER, TYPE)
/* Emits a stub for interrupt vector NUMBER.
TYPE is `zero', for the case where we push a 0 error code,
or `REAL', if the CPU pushes an error code for us. */
#define STUB(NUMBER, TYPE) \
.section .text; \
.globl intr##NUMBER##_stub; \
.func intr##NUMBER##_stub; \
intr##NUMBER##_stub: \
TYPE; \
push $0x##NUMBER; \
jmp intr_entry; \
.endfunc; \
.section .data; \
.quad intr##NUMBER##_stub;
/* All the stubs. */
...
STUB(14, zero) ...
...
STUB 함수가 호출되면 스택에 예외에 해당하는 번호가 push되고(intr_frame->vec_no
초기화), intr_entry
함수로 이동한다.
intr-stubs.S - intr_entry
intr_entry:
/* Save caller's registers. */
subq $16,%rsp
movw %ds,8(%rsp)
movw %es,0(%rsp)
subq $120,%rsp
movq %rax,112(%rsp)
movq %rbx,104(%rsp)
movq %rcx,96(%rsp)
movq %rdx,88(%rsp)
movq %rbp,80(%rsp)
movq %rdi,72(%rsp)
movq %rsi,64(%rsp)
movq %r8,56(%rsp)
movq %r9,48(%rsp)
movq %r10,40(%rsp)
movq %r11,32(%rsp)
movq %r12,24(%rsp)
movq %r13,16(%rsp)
movq %r14,8(%rsp)
movq %r15,0(%rsp)
cld /* String instructions go upward. */
movq $SEL_KDSEG, %rax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss
movw %ax, %fs
movw %ax, %gs
movq %rsp,%rdi
call intr_handler
...
.endfunc
intr_entry
함수에서 intr_frame
에 포함되는 레지스터 값들을 스택에 rsp
가 감소하는 방향으로 push하여 초기화 하고, intr_handler
함수의 인자로 rsp
의 값을 넘겨 intr_frame
포인터 형태로 사용할 수 있도록 한다.
interrupt.c - intr_handler()
void
intr_handler (struct intr_frame *frame) {
...
/* Invoke the interrupt's handler. */
handler = intr_handlers[frame->vec_no];
if (handler != NULL)
handler (frame);
...
}
앞서 확인했던 초기화 과정에서 intr_handlers
이차원 배열에 vec_no을 인덱스로해서 각 예외의 핸들러가 저장되었던것을 확인했다.
그리고 예외가 발생하면 intr_handler()
가 호출되고. 해당 예외의 vec_no에 해당하는 핸들러가 실행되는것을 확인할 수 있다.
make_intr_gate()
, make_trap_gate()
는 IDT 엔트리를 초기화하는 매크로 함수. 인터럽트나 예외가 발생했을때 호출될 함수와 권한 수준을 지정한다.
즉, 어떤 예외나 인터럽트가 발생했을때 어떤 함수를 호출할지를 결정하는것이다. 어떤 함수가 호출될지는 intr_stubs[]
배열에 담겨 있는데, 이는 컴파일 및 어셈블 시점에 intr-stubs.S 파일에서 초기화 됨.
그리고 런타임에 해당 vec_no에 해당하는 예외 핸들링 함수를 intr_handlers[]
배열에 초기화 한다.
이렇게 예외 처리할 준비가 끝났고, page fault가 발생한다면 앞서 초기화한 IDT를 참고해 예외 또는 인터럽트에 해당하는 STUB 함수를 호출하고, 런타임때 초기화 된 그에 맞는 핸들링 함수가 호출되는것이다.