Ⅰ. IA-32e 모드 커널과 메모리 맵
- IA-32e 모드 커널을 실행하기 위한 준비 작업으로, PC에 설치된 메모리가 64MB이상인지 검사하고, IA-32e 커널이 위치할 영역을 모두 0으로 초기화하는 작업을 수행한다.
- 부팅 과정을 완료하고 나서, 1MB 이상의 메모리에 정상적으로 접근이 되는지 확인하고 이를 위해 어떤 작업이 필요한지 살펴본다.
1. 1MB 이상의 메모리에 접근해야 하는 이유
- 부트 로더에 의해 커널 이미지가 메모리에 로딩되는 어드레스는 0x10000이다.
- 만약, 1MB 이하의 어드레스 중에서 비디오 메모리가 위치한 0xA0000 이하를 커널 공간을 사용한다고 가정하면, 보호 모드 커널과 IA-32e 모드 커널의 최대 크기는 (0xA0000 - 0x10000)로 576KB가 된다.
- 커널 이미지(보호모드 커널과 IA-32e 모드 커널 포함)에는 초기화되지 않은 영역(.bss 섹션)이 포함되지 않으므로 커널 이미지의 크기로 환산하면 이보다 더 작은 크기여야 한다.
- 커널이 별다른 기능을 하지 않는다면 문제가 되지 않지만, 이후 멀티 태스킹과 파일 시스템과 같은 기능이 추가되어 커널이 커진다면 576KB의 공간이 부족할 수 있다.
- MINT64 OS는 이런 문제를 해결하기 위해, 커널 이미지를 모두 0x10000 어드레스에 복사하되, 덩치가 큰 IA-32e 모드 커널은 2MB의 어드레스로 복사하여, 2MB ~ 6MB의 영역을 별도로 할당했다.
- IA-32e 모드의 커널 영역은 모든 섹션을 포함하여, 총 4MB 크기가 된다.
2. IA-32e 모드 커널이 위치할 영역을 0으로 초기화 하는 이유
- 1MB 이하 공간에서 IA-32e 모드 커널 이미지를 2MB의 어드레스로 복사한다면, 초기화한 부분이 커널 이미지로 덮어써진다.
- 그럼에도 미리 초기화하는 이유는 IA-32e 커널 이미지가 초기화되지 않은 영역을 포함하고 있지 않기 때문이다.
- 커널 이미지를 옮길 때도 이 영역은 해당되지 않으며, 이미지를 옮길 영역을 미리 0으로 초기화 하지 않는다면 어떤 임의의 값이 들어 있을 것이다.
- 이러한 상황에서 IA-32e 모드 커널이 실행되면 0이 아닌 값으로 설정되어 잘못된 조건문이 실행되는 등의 문제가 발생할 수 있다.
Ⅱ. IA-32e 모드 커널을 위한 메모리 초기화
- IA-32e 모드 커널이 위치할 영역을 초기화하는 방법을 알아본다.
- 커널 영역을 초기화하는 코드를 추가하여 빌드한 뒤, OS 실행했을 때 발생하는 문제와 원인에 대해 알아본다.
1. 메모리 초기화 기능 추가
- 1MB ~ 6MB 영역까지 0으로 초기화하는 기능을 C 코드로 구현한다.
- 1MB ~ 6MB 영역은 IA-32e 모드 커널이 위치할 곳은 아니지만, IA-32e 모드 커널을 위한 자료구조가 위치할 영역이니 같이 초기화를 진행한다.
- (1.Kernel32/Source) 위치의 Main.c 파일에 초기화를 수행하는 kInitializeKernel64Address( )함수를 추가해준다.
- kInitializeKernel64Address( )함수는 루프를 수행하며, 0x100000(1MB) ~ 0x600000(6MB) 영역을 모두 0으로 채우는 단순한 코드이다.
#include "Types.h" void kPrintString (int iX, int iY, const char* pcString); BOOL kInitializeKernel64Area( void ); //Main 함수 void Main(void){ DWORD i; kPrintString( 0, 3, "C Language Kernel Started~!!!"); // IA-32e 모드의 커널 영역을 초기화 kInitializeKernel64Area(); kPrintString(0, 4, "IA-32e Kernel Area Initialization Complete"); while(1); } ``` 생략 ``` // IA-32e 모드용 커널 영역을 0으로 초기화 BOOL kInitializeKernel64Area( void ){ DWORD* pdwCurrentAddress; // 초기화를 시작할 어드레스인 0x100000(1MB)을 설정 pdwCurrentAddress = (DWORD*) 0x100000; // 마지막 어드레스인 0x600000(6MB)까지 루프를 돌면서 4byte씩 0으로 채움 while((DWORD) pdwCurrentAddress < 0x600000){ *pdwCurrentAddress = 0x00; // 0으로 저장한 후 다시 읽었을 때, 0이 나오지 않으면 해당 어드레스를 사용하는데 // 문제가 생긴 것이므로 더이상 진행하지 않고 종료 if( *pdwCurrentAddress != 0 ){ return FALSE; } // 다음 어드레스로 이동 pdwCurrentAddress++; } return TRUE; }
2. 빌드와 실행
- make로 OS 이미지를 생성하고 QEMU를 실행한다.
- QEMU에서는 정상적으로 실행되지만, 실제 PC에서는 "IA-32e Kernel Area Initialization Complete" 문구가 출력되지 않는다.
- 문제의 원인은 PC가 하위 기종에 대한 호환성을 유지하기 위해, 어드레스 라인을 비활성화했기 때문이다.
- 이것은 QEMU의 한계로 볼 수 있으므로, 수시로 실제 PC를 사용하여 테스트해야 정확하게 확인할 수 있다.
Ⅲ. 1MB 어드레스와 A20 게이트
- A20은 어드레스 라인의 번호를 의미하지만, 실제로 이름 이상의 중요한 내용이 담겨있다.
- A20 게이트를 간단히 알아본 뒤, 시스템 포트와 BIOS 서비스를 이용하여 A20 게이트를 활성화시킨다.
1. A20 게이트의 의미와 용도
- 초창기 XT PC는 어드레스를 최대 1MB 어드레스까지 접근할 수 있었다.
- But, 리얼모드에서 세그먼트와 오프셋으로 접근할 수 있는 최대 어드레스는 0xFFFF:0xFFFF로 1MB가 넘는 0x10FFEF까지 접근할 수 있었다.
- 하드웨어의 한계로 1MB가 넘는 어드레스로 접근하는 경우, 하위 어드레스만 남아 실제 0xFFEF로 인식되었다.
- 16MB 어드레스까지 접근가능한 AT PC가 탄생하면서, 기존의 XT PC용 프로그램을 AT PC에서 실행하는데 문제가 발생하였다.
- 기존 XT PC 프로그램 중에서 XT PC의 특수한 어드레스 계산법 (1MB 이상의 어드레스를 1MB이하의 어드레스에 매핑)을 이용하는 프로그램 때문이었다.
- 이러한 호환성 문제를 해결하려고 ‘A20 게이트’가 도입되었다.
- A20 게이트에서 “A20”의 의미는 어드레스의 20번째 비트를 의미하며, A20의 역할은 어드레스 20번째 비트를 활성화하거나 비활성화하여 XT PC의 어드레스 계산 방식과 호환성을 유지시킨다.
- A20 게이트가 비활성화되면, 어드레스 라인의 20번째 (1M위치)가 항상 0으로 고정되므로 선형 주소가 0x10FFEF가 되더라도 0xFFEF로 처리할 수 있다.
- AT PC는 부팅과정을 완료하고 A20게이트를 무조건 0으로 설정하여 XT PC와 호환성을 유지했으며, A20 게이트를 활성화했을 때만 20번째 어드레스 비트가 정상적으로 동작하게 했다.
- A20 게이트가 비활성화 상태의 경우, 어드레스 라인 20번째 비트는 항상 0으로 설정되므로 홀수 MB에는 접근할 수 없다.
- 최초 부팅된 이후, A20 게이트가 비활성화 된 상태이므로 1~4MB까지의 어드레스를 초기화하면 홀수 MB영역을 제외한 0~1MB와 2~3MB 영역을 초기화하게 된다.
- 0 ~ 1MB 영역은 BIOS와 보호 모드 커널 영역으로 사용하고 있으므로 현재 커널이 수행중인 부분이 초기화됨으로써 문제가 발생한다.
- 이러한 문제 없이 정상 실행되는 경우는 BIOS에서 A20게이트를 사용하지 않거나 기본 값을 1로 설정하였기 때문이다.
2. A20 게이트 활성화 방법
- 활성화 방법은 크게 3가지가 존재한다. ① 키보드 컨트롤러로 활성화하는 방법 ② 시스템 컨트롤 포트로 활성화하는 방법 ③ BIOS 서비스로 활성화하는 방법
- 키보드 컨트롤러 활성화하는 방법
- AT PC의 초창기 시절부터 사용되는 방법으로, 과거 AT PC는 키보드 컨트롤러에 A20 게이트만 연결하여 키보드 컨트롤러를 통해 제어하도록 했다.
- 해당 방법은 속도가 느리고 소스 코드가 복잡하지만, PS/2 방식의 키보드/ 마우스를 지원하는 PC라면 어디서나 사용 가능하다는 장점이 있다.
- 시스템 컨트롤 포트로 활성화하는 방법
- 키보드 컨트롤러의 대안으로 나왔으며, 시스템 제어에 관련된 I/O 포트를 통해 A20게이트를 활성화하는 방법이다.
- 키보드 컨트롤러를 통하는 것보다 속도가 빠르고 소스코드가 간략하다는 장점이 있다.
- BIOS 서비스로 활성화하는 방법
- 486 프로세서를 지원하는 BIOS에서 처음 도입되었으며, BIOS 서비스 중에 시스템 관련된 서비스를 통해 A20 게이트를 활성화하는 방법이다.
- PC의 전반적인 정보를 관리하는 BIOS의 서비스를 사용하므로 3가지 방법 중에 가장 확실한 방법이다.
- 해당 실습에서는 “시스템 컨트롤 포트”와 “BIOS 서비스”를 이용하여 A20게이트를 활성화한다.
(1) 시스템 컨트롤 포트로 A20 게이트 활성화하기
- 시스템 컨트롤 포트는 I/O 포트 어드레스의 0x92에 위치하며, A20 게이트부터 하드 디스크 LED에 이르기까지 여러 가지 시스템 옵션을 담당한다.
- A20 게이트를 활성화하는 것이 목적이므로, 시스템 컨트롤 포트의 비트 1만 1로 설정하면 된다.
- 시스템 컨트롤 포트는 I/O 포트에 있으므로, 이에 접근하려면 별도 명령어를 사용해야 한다.
- x86 프로세서는 I/O 포트에 접근하는 in/out 명령어를 제공한다.
in al, 0x92 ; 시스템 컨트롤 포트(0x92)에서 1byte를 읽어 AL 레지스터에 저장
or al, 0x02 ; 읽은 값에 A20 게이트 비트(비트 1)를 1로 설정
and al, 0xFE ; 시스템 리셋 방지를 위해 0xFE와 AND연산하여 비트 0을 0으로 설정
out 0x92, al ; 시스템 컨트롤 포트(0x92)에 변경된 값을 1byte 설정
(2) BIOS 서비스로 A20 게이트 활성화 방법
- A20 게이트 관련 설정 값을 AX 레지스터에 넣고, A20 게이트를 활성화하는 BIOS의 시스템 서비스(인터럽트 벡터 0x15)를 호출하면 된다.
- BIOS 서비스 중, A20 게이트 관련 기능은 BIOS의 시스템 서비스에 포함되어 있으며, 시스템 서비스는 인터럽트 벡터 테이블의 0x15에 존재한다.
mov ax, 0x2401 ; A20 게이트 활성화 서비스 설정
int 0x15 ; BIOS 인터럽트 서비스 호출
jc .A20GATERROR ; A20 게이트 활성화가 성공했는지 확인
; A20 게이트 활성화가 실패되면, EFLAGS 레지스터의 CF 비트가 1로 설정되므로 이를 검사하여 에러 처리 코드로 이동
jmp .A20GATESUCCESS
.A20GATERROR:
``` 에러 처리 ```
.A20GATESUCCESS:
``` 성공 처리 ```
Ⅳ. A20 게이트 적용과 메모리 크기 검사
- 메모리 부족으로 발생할 수 있는 문제를 피하기 위해, PC에 설치된 메인 메모리가 최소 크기를 만족하는지 검사하는 기능을 추가한다.
- 이전에 작성한 코드를 적용시켜, 정상적으로 동작하는지 확인한다.
1. A20 게이트 활성화 코드 적용
- A20 게이트를 활성화 하기 위해, 시스템 컨트롤 포트와 BIOS 서비스를 사용하여 진행하였다. (원래 하나만 사용해도 되지만, BIOS 마다 구현 차이가 있으므로, 두가지 모두를 사용한다.)
- 순서는 BIOS 서비스를 실행하고, 실패시 시스템 컨트롤 포트가 사용되는 순으로 진행된다.
- BIOS 서비스 경우, 리얼 모드여야 하므로 A20 게이트 활성화 코드는 부트 로더 or 보호 모드 커널 엔트리 포인트에 추가해야 한다.
- (본 실습에서는 보호 모드 엔트리 포인트에 추가한다.)
- (1.Kernel32/Soure/EntryPoint.s) 일부를 수정한다.
- 세그먼트 레지스터의 초기화 이후, BIOS 서비스를 실행하여 A20 게이트를 활성화하고 실패할 경우 시스템 컨트롤 포트를 통해 다시 시도하는 것을 확인할 수 있다.
``` 생략 ``` ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; 코드 영역 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; START: mov ax, 0x1000 ; 보호 모드 엔트리 포인트의 시작 어드레스를 세그먼트 레지스터 값으로 변환 mov ds, ax ; DS 세그먼트 레지스터에 설정 mov es, ax ; ES 세그먼트 레지스터에 설정 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; A20 게이트를 활성화 ; BIOS를 이용한 전환이 실패했을 때, 시스템 컨트롤 포트로 전환 시도 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; BIOS 서비스를 사용해서, A20 게이트를 활성화 mov ax, 0x2401 ; A20 게이트 활성화 서비스 설정 int 0x15 ; BIOS 인터럽트 서비스 호출 jc .A20GATERROR ; A20 게이트 활성화가 성공했는지 확인 jmp .A20GATESUCCESS .A20GATERROR: ; 에러 발생 시, 시스템 컨트롤 포트로 전환 시도 in al, 0x92 ; 시스템 컨트롤 포트(0x92)에서 1byte를 읽어 AL 레지스터에 저장 or al, 0x02 ; 읽은 값에 A20 게이트 비트(비트 1)를 1로 설정 and al, 0xFE ; 시스템 리셋 방지를 위해 0xFE와 AND연산하여 비트 0을 0으로 설정 out 0x92, al ; 시스템 컨트롤 포트(0x92)에 변경된 값을 1byte 설정 .A20GATESUCCESS: cli ; 인터럽트가 발생하지 못하도록 설정 lgdt [ GDTR ] ; GDTR 자료구조를 프로세서에 설정하여, GDT 테이블을 로드 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; 보호 모드로 진입 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; mov eax, 0x4000003B ;PG=0, CD=1, NW=0, AM=0, WP=0, NE=1, ET=1, TS=1, EM=0, MP=1, PE=1 mov cr0, eax ;CR0 컨트롤 레지스터에 위에서 저장한 플래그를 설정하여 보호모드로 전환 ``` 생략 ```
2. 메모리 크기 검사 기능 추가
- 사용 가능한 메모리를 검사하는 가장 확실한 방법은 메모리에 특정 값을 쓰고 다시 읽어서 같은 값이 나오는지 확인하는 것이다.
- 메모리 크기를 BIOS 데이터 영역에서 읽어올 수 있지만, BIOS마다 구현에 차이가 있을 수 있으므로 직접 검사하여 측정하였다.
- 검사 방법은 1MB 단위로 어드레스를 증가시키면서 각 MB의 첫번째 4바이트에 0x12345678를 쓰고 읽어 보는 것으로 진행한다.
- (1.Kernel32/Source/Main.c) 파일을 수정한다.
- IA-32e 모드의 커널이 위치할 공간을 0으로 초기화하는 함수 전에 미리 메모리의 크기인 64MB 이상인지 검사하여, 이하면 에러 메시지를 출력하고 정지하도록 하였다.
- kInitializeKernel64Area( ) 함수도 초기화한 값을 검증하고 성공/실패를 반환하게 수정하였다.
#include "Types.h" void kPrintString (int iX, int iY, const char* pcString); BOOL kInitializeKernel64Area( void ); BOOL kIsMemoryEnough( void ); //Main 함수 void Main(void){ DWORD i; kPrintString( 0, 3, "C Language Kernel Start.............[Pass]"); // 최소 메모리 크기를 만족하는지 검사 kPrintString(0, 4, "Minimum Memory Size Chek.............[ ]"); if(kIsMemoryEnough() == FALSE){ kPrintString(45, 4, "Fail"); kPrintString(0, 5, "Not Enough Memory~!! MINT64 OS Requires Over " "64Mbyte Memory~!!"); while(1); }else{ kPrintString(45, 4, "Pass") } // IA-32e 모드의 커널 영역을 초기화 kPrintString(0, 5, "IA-32e Kernel Area Initialization....[ ]"); if(kInitializeKernel64Area() == FALSE){ kPrintString(45, 5, "Fail"); kPrintString(0, 6, "Kernel Area Initialization Fail~!!"); while(1); }; kPrintString(45, 5 "Pass") while(1); } //문자열 출력 함수 void kPrintString(int iX, int iY, const char* pcString){ CHARACTER* pstScreen = (CHARACTER*) 0xB8000; int i; pstScreen += (iY * 80) + iX; for(i = 0; pcString[i] != 0; i++){ pstScreen[i].bCharactor = pcString [i]; } } // IA-32e 모드용 커널 영역을 0으로 초기화 BOOL kInitializeKernel64Area( void ) { DWORD* pdwCurrentAddress; // 초기화를 시작할 어드레스인 0x100000(1MB)을 설정 pdwCurrentAddress = ( DWORD* ) 0x100000; // 마지막 어드레스인 0x600000(6MB)까지 루프를 돌면서 4바이트씩 0으로 채움 while( ( DWORD ) pdwCurrentAddress < 0x600000 ) { *pdwCurrentAddress = 0x00; // 0으로 저장한 후 다시 읽었을 때 0이 나오지 않으면 해당 어드레스를 // 사용하는데 문제가 생긴 것이므로 더이상 진행하지 않고 종료 if( *pdwCurrentAddress != 0 ) { return FALSE; } // 다음 어드레스로 이동 pdwCurrentAddress++; } return TRUE; } //MINT64 OS를 실행하기에 충분한 메모리를 가지고 있는지 체크 BOOL kIsMemoryEnough( void ) { DWORD* pdwCurrentAddress; // 0x100000(1MB)부터 검사 시작 pdwCurrentAddress = ( DWORD* ) 0x100000; // 0x4000000(64MB)까지 루프를 돌면서 확인 while( ( DWORD ) pdwCurrentAddress < 0x4000000 ) { *pdwCurrentAddress = 0x12345678; // 0x12345678로 저장한 후 다시 읽었을 때 0x12345678이 나오지 않으면 // 해당 어드레스를 사용하는데 문제가 생긴 것이므로 더이상 진행하지 않고 종료 if( *pdwCurrentAddress != 0x12345678 ) { return FALSE; } // 1MB씩 이동하면서 확인 pdwCurrentAddress += ( 0x100000 / 4 ); } return TRUE; }
- 결과 확인을 위해, QEMU의 메모리 크기를 최소 크기인 64MB 미만으로 설정하여 확인해본다.
- QEMU의 메모리 크기를 32MB로 수정한다.
- 정상적으로 진행한 결과
'MINT64 OS 개발' 카테고리의 다른 글
10장. 64비트 모드로 전환 (0) | 2023.07.20 |
---|---|
9장. 페이징 기능을 활성화하여, 64비트 전환 준비 (0) | 2023.07.20 |
7장. C언어로 커널 작성 (0) | 2023.07.20 |
6. 32비트 보호 모드로 전환 (0) | 2023.05.06 |
5. OS이미지 메모리에 복사 (0) | 2023.05.06 |