본문 바로가기

MINT64 OS 개발

8장. A20 게이트를 활성화하여 1MB이상 영역에 접근

Ⅰ. 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로 수정한다.

  • 정상적으로 진행한 결과