-
[운영체제 만들기] Exception프로젝트/운영체제 만들기 2023. 5. 25. 19:36
글의 참고
- https://wiki.osdev.org/Exceptions#Error_code
글의 전제
- 내가 글을 쓰다가 궁금한 점은 파란색 볼드체로 표현했다. 나도 모르기 때문에 나중에 알아봐야 할 내용이라는 뜻이다.
- 밑줄로 작성된 글은 좀 더 긴 설명이 필요해서 친 것이다. 그러므로, 밑 줄 처친 글이 이해가 안간다면 링크를 따라서 관련 내용을 공부하자.
- `글의 참조`에서 빨간색 볼드체로 체크된 링크는 이 글을 작성하면 가장 많이 참조한 링크다.
- `운영체제 만들기` 파트에서 퍼온 모든 참조 글들과 그림은 반드시 `이 글과 그림을 소스 코드로 어떻게 구현을 해야할까` 라는 생각으로 정말 심도있게 잠시 멈춰서 생각해봐야 실력이 발전한다.
글의 내용
- x86 익셉션(Exception)
: `x86`에서 인터럽트는 `발생`했다고 하지만, 익셉션은 `감지`했다고 표현한다. x86의 익셉션을 유발하는 소스는 3가지 종류가 있다. 앞에 2개는 소프트웨어 관련 에러고, `Machine-check`는 하드웨어 익셉션이 된다. 이 글에서는 `Processor-detected program-error`과 `Software-generated`에 대해 알아본다.
The processor receives exceptions from three sources:
• Processor-detected program-error exceptions.
• Software-generated exceptions.
• Machine-check exceptions.
- 참고 : Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A [ 6.4 SOURCES OF EXCEPTIONS ]: 유저 애플리케이션, 운영 체제 등이 실행되다가 에러를 일으키면, 프로세서는 이걸 감지해서 `익셉션`을 발생시킨다. x86은 편의성 이런 에러들을 `Porcessor-detected program-error`라고 정의했다.
: 그리고, `INT n` 명령어를 통해서 인터럽트 뿐만 아니라, `익셉션`도 발생시킬 수 있다. 문서에서는 `INT 3`이 익셉션을 발생시킨다고 한다. 그런데, 당연히 에러 코드 같은 것들은 `푸쉬`하지 않는다고 한다. 혹시나, `INT 3` 명령어를 하고 나서 `3`에 대한 익셉션 핸들러를 만들고 거기서 절대 에러 코드를 `POP`하면 안된다. 그러면, EIP(복귀 주소)를 POP하게 되서, 익셉션 핸들러를 리턴할 때, 이상한 주소로 복귀하게 될 거다.
The processor generates one or more exceptions when it detects program errors during the execution in an application program or the operating system or executive. Intel 64 and IA-32 architectures define a vector number for each processor-detectable exception. Exceptions are classified as `faults`, `traps`, and `aborts` (see Section 6.5, “Exception Classifications”).
- 참고 : Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A [ 6.4.1 Program-Error Exceptions ]
The `INTO`, `INT 3`, and `BOUND` instructions permit exceptions to be generated in software. These instructions allow checks for exception conditions to be performed at points in the instruction stream. For example, INT 3 causes a breakpoint exception to be generated.
The INT n instruction can be used to emulate exceptions in software; but there is a limitation. If INT n provides a vector for one of the architecturally-defined exceptions, the processor generates an interrupt to the correct vector (to access the exception handler) but does not push an error code on the stack. This is true even if the associated hardware-generated exception normally produces an error code. The exception handler will still attempt to pop an error code from the stack while handling the exception. Because no error code was pushed, the handler will pop off and discard the EIP instead (in place of the missing error code). This sends the return to the wrong location.
- 참고 : Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A [ 6.4.2 Software-Generated Exceptions ]: `x86`에서 익셉션은 3 가지 종류로 나뉜다. 나누는 기준은 `익셉션 처리 후 어디로 복귀할 것인가`이다.
The processor generates one or more exceptions when it detects program errors during the execution in an application program or the operating system or executive. Intel 64 and IA-32 architectures define a vector number for each processor-detectable exception. Exceptions are classified as `faults`, `traps`, and `aborts` (see Section 6.5, “Exception Classifications”).
- 참고 : Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A [ 6.4.1 Program-Error Exceptions ]
Exceptions are classified as `faults`, `traps`, or `aborts` depending on the way they are reported and whether the instruction that caused the exception can be restarted without loss of program or task continuity.
• Faults — A fault is an exception that can generally be corrected and that, once corrected, allows the program to be restarted with no loss of continuity. When a fault is reported, the processor restores the machine state to the state prior to the beginning of execution of the faulting instruction. The return address (saved contents of the CS and EIP registers) for the fault handler points to the faulting instruction, rather than to the instruction following the faulting instruction.
• Traps — A trap is an exception that is reported immediately following the execution of the trapping instruction. Traps allow execution of a program or task to be continued without loss of program continuity. The return address for the trap handler points to the instruction to be executed after the trapping instruction.
• Aborts — An abort is an exception that does not always report the precise location of the instruction causing the exception and does not allow a restart of the program or task that caused the exception. Aborts are used to report severe errors, such as hardware errors and inconsistent or illegal values in system tables.
NOTE
One exception subset normally reported as a fault is not restartable. Such exceptions result in loss of some processor state. For example, executing a POPAD instruction where the stack frame crosses over the end of the stack segment causes a fault to be reported. In this situation, the exception handler sees that the instruction pointer (CS:EIP) has been restored as if the POPAD instruction had not been executed. However, internal processor state (the general-purpose registers) will have been modified. Such cases are considered programming errors. An application causing this class of exceptions should be terminated by the operating system.
- 참고 : Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A [ 6.5 EXCEPTION CLASSIFICATIONS ]: `Faults`는 수정이 가능한 익셉션이라고도 한다. `Faults` 익셉션이 발생하면, 폴트 핸들러가 호출된다. 그리고 폴트를 처리하고 나서 주소를 복귀할 때, 폴트가 발생했던 지점으로 다시 돌아간다. `Traps`은 `Faults`와 거의 같지만, 복귀 주소가 트랩이 발생했던 지점이 아닌, 트랩이 발생했던 바로 다음 주소를 가리킨다는 점이 다르다. `Aborts`는 즉각적인 종료가 요구되는 익셉션이다. 복귀 따위는 존재하지 않는다.
- 예약된 익셉션
: x86 아키텍처에서는 0-31번까지의 인터럽트는 예약되어 있다.
- 예외 처리 반환
: 예외 처리의 주의 사항은 마지막 처리 과정에 있다. RET이 아닌, IRET을 사용해야 한다. 그 이유는 RET은 EIP만 반환하지만, IRET은 CS, EFLAGS, EIP를 반환하기 때문이다. 만약, 권한 레벨의 변경까지 있을 경우, SS와 ESP도 추가된다.
- 예외 처리 절차
: 아래의 내용은 인텔 문서 `6.12.1 Exception- or Interrupt-Handler Procedures`를 참고하자. 인터럽트 및 익셉션 핸들러는 현재 실행중인 태스크의 컨텍스트에서 실행된다. 아래에서 보다시피, 기본적으로는 인터럽트 및 익셉션 핸들러는 실행되는 코드의 위치만 바뀔 뿐이지 데이터는 이전에 실행되고 있던 태스크의 데이터를 그대로 사용한다.
: 여기서 `스택 스위치` 라는 개념이 나온다. 즉, 익셉션 및 인터럽트의 절차는 2가지 경우로 나뉜다.
1" 인터럽트 당한 태스크 < 인터럽트 및 익셉션 핸들러의 권한
" 인터럽트 및 익셉션 핸들러를 위한 새로운 스택(컨텍스트)을 생성한다(TSS의 SS0, ESP0을 이용해서 새로운 스택 생성).
" 인터럽트 당한 태스크의 SS, ESP, ELFAGS, CS, EIP를 새로운 스택에 저장한다.
2" 인터럽트 당한 태스크 권한 >=인터럽트 및 익셉션 핸들러의 권한
" 인터럽트 및 익셉션 핸들러는 위한 새로운 스택을 생성하지 않는다. 인터럽트 당한 태스크의 스택을 이어서 사용한다.
" 인터럽트 당한 태스크의 ELFAGS, CS, EIP를 저장한다.: 당연한 얘기겠지만, 스택을 새로 생성해서 실행하니 이전 태스크의 컨택스트로 복귀할 때, 이전 태스크가 참조했던 스택으로 원상 복구해줘야 하므로 SS, ESP가 추가적으로 저장되는 것이다. 그리고 권한이 높을 경우, 스택을 새로 생성하는 이유는 `보안` 문제 때문이다. 유저 레벨 코드에서 시스템 콜을 호출하면 결국 커널 코드가 실행된다. 그런데, 스택을 별도로 생성해주지 않으면 유저 레벨 코드와 커널 레벨 코드가 공유되기 때문에 이런 부분은 당연히 막아야 한다.
: 익셉션 및 인터럽트 핸들러 절차를 종료하고 원래 위치로 복귀하고 싶다면, 반드시 `IRET` 명령을 사용해야 한다. `IRET`은 `RET` 명령과 유사하지만, 차이가 있다면 `EFLAGS`를 원복한다는 것이다. 함수를 호출할 때는 스택에 EFLAGS를 저장하지 않는다. 그러나 익셉션 및 인터럽트가 호출되면 기본적으로 EFLAGS가 스택에 저장된다.
- 에러 코드
: 예외가 디스크립터(GDT, LDT, IDT)와 관련이 있는 경우, 프로세서는 익셉션 핸들러의 스택에 `에러 코드`를 푸쉬한다. 에러 코드의 포맷은 아래와 같다.
: 인텔 공식문서에 있는 내용을 그대로 가져왔다. 참고로, 위 에러 코드 포맷에서 세그먼트 셀렉터는 앞에 3비트의 내용을 토대로 나온 디스크립터(GDT, LDT, IDT)의 인덱스를 의미한다. 더 정확한 정보는 인텔 공식문서 `6.1.3. ERROR CODE`를 참고하자.
EXT External event (bit 0) — When set, indicates that the exception occurred during delivery of an event external to the program, such as an interrupt or an earlier exception.
IDT Descriptor location (bit 1) — When set, indicates that the index portion of the error code refers to a gate descriptor in the IDT; when clear, indicates that the index refers to a descriptor in the GDT or the current LDT. TI GDT/LDT (bit 2) — Only used when the IDT flag is clear. When set, the TI flag indicates that the index portion of the error code refers to a segment or gate descriptor in the LDT; when clear, it indicates that the index refers to a descriptor in the current GDT.
...
...
To keep the stack aligned for doubleword pushes, the upper half of the error code is reserved.: 정렬을 유지하기 위해 더블 워드(32비트)의 상위 절반(16비트)은 예약 영역으로 남겨놓는다.
: 익셉션 및 인터럽트 핸들러 작성시 주의 사항이 있다. 에러 코드는 `IRET` 명령으로 POP 되지 않으므로, 핸들러는 반드시 반환하기 전에 에러 코드를 POP 해야 한다.
...
The error code is pushed on the stack as a doubleword or word (depending on the default interrupt, trap, or task gate size). To keep the stack aligned for doubleword pushes, the upper half of the error code is reserved. Note that the error code is not popped when the IRET instruction is executed to return from an exception handler, so the handler must remove the error code before executing a return.
...
- 참고 : - 참고 : Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A [ 6.13 ERROR CODE ]- 익셉션 로그
: 폴트가 발생했을 때, 핸들러를 통해 레지스터들의 정보를 출력해주면 디버깅에 좀 더 수월하다. QEMU나 BOCHS의 디버깅 기능을 이용하는 것도 좋지만, 실제 하드웨어 디버깅시에는 위의 툴들의 도움을 받을 수 없다. 그럴 때, 현재 커널의 상태를 로그를 통해서만 알 수 있다. 페이지 폴트 핸들러를 통해서 익셉션 핸들러에서 어떻게 프로세서 정보들을 출력하는지 알아 보자.
isr_page_fault: cli cld push 14 ; push irq pushad ; push GPR`s push gs ; push Segment Registers push fs push es push ds call isr_page_fault_handler pop ds pop es pop fs pop gs popad ; You should be careful the location of this code. this code will be executed after GPR`s and Segments registers was poped. add esp, 8 ; pop error code, interrupt vector sti ; re-enable interrupt iret
: 위에서 작성되어 있는 `예외 처리 절차`에서 볼 수 있다시피, 익셉션이 발생하면 프로세서는 일단 `EFLAGS , CS, EIP` 순으로 스택이 푸쉬한다. 만약, 유저 레벨의 시스템 콜이면 `SS, ESP, EFLAGS, CS, EIP` 순으로 스택에 푸쉬한다. 스택은 푸쉬하면 아래로 내려가므로, 메모리 주소가 감소하게 된다. 즉, 처음 들어온 `SS`의 주소가 가장 높고, `ESP`가 다음으로 높고, 이런식이다. 그리고나서 프로세서는 `에러 코드`를 푸쉬한다. 기억해야 하는 건 방금 언급한 6개는 프로세서가 자동으로 스택에 푸쉬해준다는 것이다. 아래 구조체를 보자.
struct exception_info { uint32_t ds, es, fs, gs; uint32_t edi, esi, ebp, esp, ebx, edx, ecx, eax; uint32_t irq ,err; uint32_t eip, cs, eflags, uesp, uss; // prefix `u` is related to user-level };
: 구조체 뒤쪽에 선언될 수록 메모리 주소 뒤쪽에 할당된다. 즉, 높은 곳에 할당된다. `struct exception_info` 구조체의 메모리 주소가 가장 작은 변수는 `ds`이고, 가장 높은 주소는 `uss`다. 그 다음 푸쉬하는 순서를 보면 `irq , GPR`s , 세그먼트 레지스터`순으로 스택에 푸쉬한다. 프로세서가 자동으로 넣어준 데이터들 바로 다음에 `irq`를 푸쉬하는 이유는 스택 정리를 편하게 하기 위함이다. 만약, 에러 코드 다음에 irq를 바로 넣지 않고, 다른 레지스터들부터 넣으면 `add esp, 4`를 별도로 2번 사용해야 될 지도 모른다.
- #DF(0x08)
: 대기
- #GF(0x0D)
: 대기
- #PF(0x0E)
: 페이지 폴트가 발생하면 프로세서는 스택에 아래의 내용을 푸쉬한다. `4.6 Access Rights`와 `4.7 Page-Fault Exceptions`에 아래 에러 코드에 관하여 자세히 설명되어 있다.
: #PF가 발생하면, 제일 먼저 확인해야 하는 내용은 `CR2 레지스터`다. `CR2 레지스터`에는 #PF가 발생한 지점에 대한 가상 주소가 들어가 있다. 그래서 페이지 폴트 핸들러는 CR2의 내용을 토대로 어떤 페이지 디렉토리 및 테이블 엔트리가 유효하지 않은 것인지 찾아낼 수 가 있다. 그런데, 주의점이 있다. 페이지 폴트를 처리하는 과정에서 페이지 폴트가 또 발생할 수 가 있다. 그래서 페이지 폴트 핸들러는 CR2의 내용을 저장해놓고, 작업을 하는게 좋다. 왜냐면, 페이지 폴트가 발생할 때 마다, 프로세서는 페이지 폴트가 발생했던 가상 주소를 CR2에 계속 리로드하기 때문이다. 그런데, 2번째 페이지 폴트는 페이지 폴트 핸들러를 호출하지 않는다. 어쨋든 폴트도 익셉션이기 때문에, 2번째 페이지 폴트는 더블 폴트(#DF) 핸들러가 호출되게 된다.
The contents of the CR2 register. The processor loads the CR2 register with the 32-bit linear address that generated the exception. The page-fault handler can use this address to locate the corresponding page directory and page-table entries. Another page fault can potentially occur during execution of the page-fault handler; the handler should save the contents of the CR2 register before a second page fault can occur. If a page fault is caused by a page-level protection violation, the access flag in the page-directory entry is set when the fault occurs. The behavior of IA-32 processors regarding the access flag in the corresponding page-table entry is model specific and not architecturally defined.
Processors update CR2 whenever a page fault is detected. If a second page fault occurs while an earlier page fault is being delivered, the faulting linear address of the second fault will overwrite the contents of CR2 (replacing the previous address). These updates to CR2 occur even if the page fault results in a double fault or occurs during the delivery of a double fault.
- 참고 : Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A [ Interrupt 14—Page-Fault Exception (#PF ]'프로젝트 > 운영체제 만들기' 카테고리의 다른 글
페이징 (0) 2023.05.30 PIT [작성중] (0) 2023.05.29 [운영체제 만들기] 에러 사항 (0) 2023.05.24 [운영체제 만들기] 문자열 - 라이브러리 (0) 2023.05.18 [운영체제 만들기] 코딩 컨벤션 (0) 2023.05.16