-
Protected mode프로젝트/운영체제 만들기 2023. 8. 5. 15:10
글의 참고
- https://wiki.osdev.org/Protected_mode
- https://wiki.osdev.org/Segmentation#Protected_Mode
- http://www.rcollins.org/articles/pmbasics/tspec_a1_doc.html
글의 전제
- 내가 글을 쓰다가 궁금한 점은 파란색 볼드체로 표현했다. 나도 모르기 때문에 나중에 알아봐야 할 내용이라는 뜻이다.
- 밑줄로 작성된 글은 좀 더 긴 설명이 필요해서 친 것이다. 그러므로, 밑 줄 처친 글이 이해가 안간다면 링크를 따라서 관련 내용을 공부하자.
- `글의 참조`에서 빨간색 볼드체로 체크된 링크는 이 글을 작성하면 가장 많이 참조한 링크다.
- `운영체제 만들기` 파트에서 퍼온 모든 참조 글들과 그림은 반드시 `이 글과 그림을 소스 코드로 어떻게 구현을 해야할까` 라는 생각으로 정말 심도있게 잠시 멈춰서 생각해봐야 실력이 발전한다.
글의 내용
- 보호 모드 초기화
: 보호 모드 진입전에, 반드시 선행되야 하는 작업들이 있다. 예를 들어, 32비트 코드를 사용하므로, GDT와 32비트 코드 및 데이터 세그먼트 디스크립터가 필요하다. 그리고 A20이 활성화되어야 있어야 한다. 그 외에는 사실 필수는 아니다. 그런데, 아래는 IDT, TSS 까지 요구하고 있다. 그런데, `익셉션` 핸들러는 설정할 필요가 있다. 인터럽트와 다르게 `익셉션`은 프로세서 내부적으로 발생시키기 때문에, 피할 방법이 없다. 그래서 핸들러 설정을 해놓으면 나중에 에러가 발생했을 때, 디버깅하기가 좋다. 혹시나 64비트까지 진입할 생각이 있다면, 32비트에서 인터럽트 관련 IDT를 설정하지 않아도 될 것이다. 64비트 진입 후 IDT를 설정해도 늦지 않는다. 그런데, TSS는 선을 넘었다. 유저 레벨을 만들 생각이 없다면, TSS는 필수가 아니다.
: 권한 수준의 변경이 없다면, TSS는 필수가 아니다. 스펙에서는 멀티태스킹 메커니즘을 사용하면 TSS가 필수인 것 처럼 보이지만, 현대 운영체제들은 모두 소프트에어 멀티태스킹을 사용하기 때문에, TSS는 필수가 아니다.
- 보호 모드 진입
: 먼저 스펙을 해석해야 한다.
: 많아 보이지만, 위의 모든 절차를 해야 한다는 내용이 아니고, 부연 설명이 많은 것 뿐이다. 보호 모드 진입을 위해서는 3가지의 선행 조건이 만족되어야 한다.
1" 인터럽트 비활성화
2" A20
3" GDT 설정: 그런데 인터럽트 비활성화를 하는 이유가 뭘까? 보호 모드로 바뀐 직후에 문제가 있다. 리얼 모드에서 보호 모드로 바뀔 경우, 리얼 모드에서 사용되던 BIOS에 의해서 처리되던 인터럽트 및 IVT 처리들은 모두 사용할 수 없게 된다. 보호 모드 부터는 새로운 인터럽트 처리 방식인 IDT를 도입해야 한다. 그런데, 인터럽트를 비활성화 하지 않으면 보호 모드로 바뀐 직후 인터럽트를 핸들링 할 수 있는 IDT가 없으므로, 에러를 발생하게 된다. 그래서 보호 모드로 들어가기 전에 인터럽트를 비활성화하고 IDT의 설정이 모두 완료된 시점에 인터럽트를 다시 활성화해야 한다.
: CR0의 0번째 비트를 1로 바꾸면 CPU가 세그먼트 레지스터를 로딩하는 방식이 달라진다. 저 값이 0일 때는, 16비트 세그먼트 레지스터 로딩한다. 그러므로, CPU는 16비트 CS를 사용하게 된다. 저 값이 1이 되면 CPU는 32비트 세그먼트 레지스터를 로딩한다. 그 뿐만이 아니다. 저 비트를 바꿈으로써, 우리가 작성하는 x86 어셈블리 명령어들이 모두 32비트 방식으로 처리된다. 그러나, 한 가지 바뀌지 않는 것이 있다. 바로 CS 디스크립터에 대한 정보이다. 최초에 우리가 x86 CPU를 만지게 될 때는, 이미 16비트 세그먼트 레지스터가 CPU에 로딩된 상태이다. 그런데, GDT를 GDTR에 로딩했다고 해서 32비트 세그먼트 레지스터를 CPU에 로딩한게 아니다.
: 즉, CR0 레지스터의 0번째 비트를 세트한다고 해서 자동으로 32비트 세그먼트 레지스터가 CPU에 로딩되는게 아니다. 아직도 CPU의 CS는 여전히 16비트 세그먼트 레지스터다. 그래서 우리는 CR0 레지스터의 0번째 비트를 세트한 직후에, 곧 바로 FAR JUMP를 통해 32비트 CS 세그먼트 디스크립터를 CPU에 로딩해야 한다. 그래야 CPU는 32비트 명령어를 처리하게 된다. 참고로, FAR JUMP가 아닌 NEAR JUMP를 쓰면 계속 16비트 모드로 명령어들을 처리하게 된다. 왜냐면, NEAR JUMP는 세그먼트 레지스터를 교체하지 않기 때문이다. 16비트 세그먼트 레지스터에서 32비트 세그먼트 레지스터로 교체되려면 FAR JUMP를 사용 해야한다.
: 아래의 코드를 보자. 보호 모드 진입 들어 갈 때, 사용하는 일반적인 코드다. 아래의 FAR JUMP 코드는 `[BITS 16]` 지시어 안에 있다. 즉, 16비트를 작성된 코드다.
[BITS 16] _pmode: mov eax, cr0 or al, 1 mov cr0, eax jmp 0x08:_pmain ; 00000023 EA[2800]0800 [BITS 32] _pmain: mov ax, 0x10 mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax call main
[BITS 16] _pmode: mov eax, cr0 or al, 1 mov cr0, eax [BITS 32] jmp 0x08:_pmain ; 00000023 EA[2A000000]0800 _pmain: mov ax, 0x10 mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax call main
: 결론적으로, 위 코드는 제대로 동작하지 못한다. 그 이유가 뭘까(참고로, FAR JUMP는 CS와 동시에 [E|R]IP를 바꿀 수 있는 거의 유일한 명령어다)? 디스어셈블리어를 통해서 위의 2개의 코드에서 `jmp 0x08:_pmain`시 어디로 점프되는지를 보면 쉽게 알 수 있다. 16비트 코드이 점프는 `0x2800`으로 점프한다. 그러나 32비트 코드는 `0x2A000000`로 점프한다. 일단 2개의 주소가 서로 다르다는 것은 알겠다. 즉, 코드는 동일한데 점프하는 위치가 다르다. 어떤 코드가 맞을까?
: 먼저 알아야 하는건, 보호 모드에서도 여전히 세그먼트 주소 지정을 사용할 수 있다는 것이다. 즉, A:B 형태의 주소 지정이 가능하다는 것이다. 그런데, 보호 모드에서는 그 의미가 약간 다르다.
1" 리얼 모드 - (세그먼트 주소 << 4) + 오프셋
2" 보호 모드 - 세그먼트 셀렉터 + 오프셋: 즉, 보호모드에서 A는 `세그먼터 셀렉터`가 가리키는 GDT 엔트리 주소값이다. 그럼 이제 위의 코드를 해석해보면, 다음과 같다.
1" 리얼 모드 `jmp 0x08:_pmain` : 세그먼트 셀렉터가 가리키는 0x08(코드 세그먼트 디스크립터)를 로딩해서 _pmain 심볼로 점프(0x2800)한다.
2" 보호 모드 `jmp 0x08:_pmain` : 세그먼트 셀렉터가 가리키는 0x08(코드 세그먼트 디스크립터)를 로딩해서 _pmain 심볼(0x2A000000)로 점프한다.: 가장 큰 차이는 사실 `BITS` 지시어다. BITS 지시어는 프로세서가 아닌 어셈블리어에게 어셈블을 진행할 때, 해당 지시어 아래에 존재하는 코드 및 데이터를 BITS 지시어에 작성된 비트로 어셈블 하라고 명령한다. 그렇게 해서 나온 값이 위의 값들이다. 프로세서는 단지 저 출력된 값들을 실행할 뿐이다.
: 결론은 후자의 코드가 잘못된 것은 맞지만, 명백하게 이 코드가 잘못됬다고 설명하기가 어렵다.
- 보호 모드와 리얼 모드 차이
: 주소 지정
: 인터럽트 루틴
'프로젝트 > 운영체제 만들기' 카테고리의 다른 글
[메모리] Fixed-size blocks allocation (0) 2023.08.05 Real mode (0) 2023.08.05 [운영체제 만들기] FAT (0) 2023.08.05 GDT (1) 2023.08.05 부트 로더 (0) 2023.08.03