Skip to content

Latest commit

 

History

History
674 lines (539 loc) · 43.7 KB

Ch6_32비트 보호 모드로 전환하자.md

File metadata and controls

674 lines (539 loc) · 43.7 KB

32비트 보호 모드로 전환하자

리얼 모드에서 보호 모드로 전환하려면 크게 6단계를 거쳐야한다.

단계 모드 설명
1 16비트 리얼 모드 세그먼트 디스크립터 생성
보호 모드 코드와 데이터용 세그먼트 디스크립터 생성
2 16비트 리얼 모드 GDT 정보 생성
세그먼트 디스크립터의 시작 어드레스와 디스크립터의 전체 크기 저장
3 16비트 리얼 모드 프로세서에 GDT 정보 설정
GDTR 레지스터에 GDT의 시작 어드레스와 크기 설정
4 16비트 리얼 모드 CR0 컨트롤 레지스터 설정
CR0 컨트롤 레지스터의 PE 비트=1, PG 비트=0
5 16비트 리얼 모드 jmp 명령으로 CS 세그머늩 셀렉터 변경과 보호 모드로 전환
jmp 0x08: 보호모드 커널의 시작 어드레스
6 32비트 보호 모드 각종 세그먼트 셀렉터 및 스택 초기화
DS, ES, FS, GS, SS 세그먼트 셀렉터와 ESP, EBP 레지스터 초기화
7 32비트 보호 모드 보호 모드 커널 실행

세그먼트 디스크립터 생성

세그먼트 디스크립터(Segment Descriptor)는 세그멘테이션 기법(메모리 관리 기법)에서 세그먼트의 정보를 나타내는 자료구조이다. 세그먼트는 메모리 공간을 임의의 크기로 나눈 영역을 의미하며, 세그먼트를 복잡하게 구성할수록 세그먼트 디스크립터의 수도 증가한다. MINT64 OS에서는 보호 모드의 기본 기능만 사용하므로 4GB 저체 메모리 공간을 지정하는 커널 코드와 데이터 세그먼트만 사용한다.

세그먼트에 대한 정보를 나타내는 세그먼트 디스크립터는 크게 코드 세그먼트 디스크립터와 데이터 세그먼트 디스크립터로 나누어진다. 코드 세그먼트 디스크립터는 실행 가능한 코드가 포함된 세그먼트에 대한 정보를 나타내며, CS 세그먼트 셀렉터에 사용된다. 데이터 세그먼트 디스크립터는 데이터가 포함된 세그먼트에 대한 정보를 나타내며, CS 세그먼트 셀렉터를 제외한 나머지 셀렉터에 사용할 수 있다. 스택 영역 또한 데이터를 읽고 쓰는 데이터 영역의 한 종류이므로 데이터 세그먼트 디스크립터를 사용한다.

보호모드의 세그먼트 디스크립터는 8바이트로 아래와 같이 다양한 필드가 있다.

필드 설명
기준 주소 세그먼트 시작 어드레스
32비트 크기이며, 0바이트 ~ 4GB까지 설정 가능
세그먼트 크기 세그먼트의 크기
20비트 크기
G비트가 0으로 설정되면 0바이트 ~ 1MB까지 설정 가능
G비트가 1로 설정되어 있으면 0바이트~4GB까지 설정 가능
타입 세그먼트의 타입
코드 또는 데이터 세그먼트로 설정 가능
설정가능한 값은 뒤에 나오는 표 참고
S 디스크립터의 타입
1로 설정할 경우 세그먼트 디스크립터를 의미하고, 0으로 설정하면 시스템 디스크립터를 나타냄
DPL Descriptor Privilege Level의 약자로 해당디스크립터를 사용에 필요한 권한을 의미
0(Highest) ~ 3(Lowest)의 범위를 가짐
CPL(Current Privilege Level)나 RPL(Requested Privilege Level)과 조합되어 저근 권한을 제한하는데 사용
P Present 의 의미로 현재 디스크립터가 유효한 디스크립터인지 표시
1로 설정하면 유효한 디스크립터이며, 0으로 설정하면 IA32-e모드의 32비트 호환모드용 코드 세그먼트이다.
AVL Available의 약자로 OS가 임의의 용도로 사용할 수 있는 영역
L IA-32e모드에서 사용하는 필드로 64비트 코드 세그먼트나 32비트 코드 세그먼트를 의미
1로 설정하면 IA-32e모드의 64비트용 코드 세그먼트임을 나타내며, 0으로 설정하면 IA-32e 모드의 32비트 호환 모드용 코드 세그먼트임을 나타냄
D/B Default Operation Size를 의미
1로 설정하면 32비트용 세그먼트, 0으로 설정하면 16비트용 세그먼트
G Granularity의 약자로 디스크립터의 세그먼트 크기 필드에 곱해질 가중치를 의미
1로 설정하면 세그먼트 크기에 4KB를 곱하며, 0으로 설정하면 가중치를 곱하지 않음

MINT64 OS에 필요한 세그먼트는 다음과 같다.

  • 커널 코드와 데이터용 세그먼트 디스크립터 각 1개
  • 커널 코드와 데이터용 세그먼트는 0~4GB까지 모든 영역에 접근할 수 있어야한다.
  • 보호모드용 코드와 데이터에 사용할 기본 오퍼랜드 크기는 32비트여야한다.
  • 보호 기능으 ㄴ사용하지 않으며, 프로세서의 명령을 사용하는데 제약이 없어야 하므로 최상위 권한 0이어야 한다.

코드 세그먼트 디스크립터와 데이터 세그먼트 타입 설정

코드 세그먼트와 데이터 세그먼트를 설정하려면 S필드와 타입 필드를 조합해야한다. 우선 S필드부터 살펴보겠다. 코드 세그먼트와 데이터 세그먼트는 세그먼트 디스크립터이므로 간단하게 s필드의 값을 1로 설정한다. 세그먼트 타입은 4비트 크기의 타입 필드를 이요해서 설정하며, 각 비트 값은 아래와 표와 같은 의미를 나타낸다. MINT64 OS에서는 기본적인 세그먼트 타입만 사용하고, 코드 세그먼트는 실행/읽기 타입으로 설정하고 데이터 세그먼트는 읽기/쓰기 타입으로 설정한다. 아래 표에서 찾아보면 코드 세그먼트 타입은 0x0a(Execute/Read), 데이터 세그먼트 타입은 0x02(Read/Write)가 된다.

세그먼트 영역 설정

MINT64 OS의 커널 세그먼트 디스크립터는 4GB 전체 영역에 접근할 수 있어야한다. 그러므로 커널용 세그먼트 디스크립터의 기준 주소는 0으로 설정한다. 세그먼트의 기준 주소는 결정했으니 이제 세그먼트의 크기를 설정할 차례이다. 크기 필드는 총 20비트며 20비트로 표현할 수 있는 최댓값은 2^20(=1MB)이다. 크기 필드만으로는 4GB까지의 영역을 표현할 수 없으므로 20비트의 크기를 4GB로 확장할 무엇인가가 필요하다. 이때 사용하는 것이 G필드이며, G필드의 값을 1로 설정하면 크기 필드에 4KB를 곱한 것이 실제 세그먼트의 크기가 된다. 1MB에 4KB를 곱하면 4GB가 되므로 크기 필드와 G필드를 사용하면 메모리 전체 영역을 세그먼트의 영여긍로 설정할 수 있다.

기본 오퍼랜드 크기와 권한 설정

보호 모드는 32비트로 동작하므로 기본 오퍼랜드의 크기 역시 32비트로 설정한다. 여러 필드 중에 기본 오퍼랜드의 크기는 D/B 필드가 담당하며, 1로 설정하면 기본 오퍼랜드의 크기를 32비트로 설정할 수 있다. 기본 오퍼랜드의 크기와 관련된 필드가 D/B필드만 있는것은 아니다. IA-32e모드의 64비트 서브 모드 또는 32비트 호환 모드를 설정하는 L필드도 있다. 6장에서 생성하는 디스크립터는 보호 모드용이므로 L비트는 0으로 설정한다.

권한 필드는 보호 모드의 주요 특징 중 하나인 보호 기능에 핵심 역할을 한다. 프로세서는 디스크립터의 권한 필드에 설정된 값과 세그먼트 셀렉터의 권한을 비교하여 접근이 가능한지를 판단하며, x86 프로세서에서 동작하는 운영체제의 대부분도 이 기능를 사용하여 OS의 핵심 부분을 보호하고 있다, MINT 64 OS의 보호 모드는 권한을 따로 구분하지 않으므로 권한 필드를 모두 최상위 레벨 0으로 설정한다. 세그먼트 권한 필드와 세그먼트 셀렉터의 관계는 보호모드에서 IA-32e모드로 전환하는 단계에서 다시 살펴보겠다.

기타 필드 설정

이제 남은 두 필드를 마무리하겠다. 생성한 세그먼트 디스크립터는 보호 모드로 전환하는 과정에서 사용하므로 유효한 디스크립터라는 것을 알려야한다. 디스크립터가 유효함을 나타내는 필드는 P필드이며 1로 설정하면 해당 디스크립터를 사용할 수 있다. AVL필드는 임의로 사요할 수 있는 필드로 MINT64 OS에서는 별도의 값을 쓰지 않으므로 0으로 설정한다.

세그먼트 디스크립터 생성 코드

아래 코드는 코드 세그먼트 디스크립터와 데이터 세그먼트 디스크립터를 생성하는 소스코드이다.

CODESCRIPTOR:
    dw 0xFFFF       ; Limit [15:0]
    dw 0x0000       ; Base [15:0]
    db 0x00         ; Base [23:16]
    db 0x9A         ; P=1, DPL=0, Code Segment, Execute/Read
    db 0xCF         ; G=1, D=1, L=0, Limit[19:16]
    db 0x00         ; Base [31:24]
    
DATADESCRIPTOR:
    dw 0xFFFF       ; Limit [15:0]
    dw 0x0000       ; Base [15:0]
    db 0x00         ; Base [23:16]
    db 0x92         ; P=1, DPL=0, Data Segment, Read/Write
    db 0xCF         ; G=1, D=1, L=0, Limit[19:16]
    db 0x00         ; Base [31:24]

64비트 IA-32e 모드로 전환하려면 반드시 32비트 보호 모드를 거쳐야 한다. 보호 모드는 현대 OS가 제공하는 4GB의 주소 공간, 멀티태스킹, 페이징, 메모리 보호 등의 기능을 하드웨어적으로 지원한다. 목표가 32비트 OS라면 깊게 공부해야한지만, 64비트 OS로 전환하기 위한 임시모드로 쓰기때문에 깊게 공부할 필요가 없다. 보호 모드의 전체 기능에 대해서 살펴보기보다는 64비트 모드로 전환하는데 필요한 기능을 중심으로 알아보겠다.

GDT 정보 생성

GDT(Global Descriptor Table) 자체는 연속된 디스크립터의 집합이다. 즉 MINT 64 OS에서 사용하는 코드 세그먼트 디스크립터와 데이터 세그먼트 디스크립터르 ㄹ연속된 어셈블리어 코드로 나타내면 그 전체 영역이 GDT가 된다. 다만 한 가지 제약 조건이 있다면 널 디스크립터(NULL Descriptor)를 가장 앞부분에 추가해야한다는 것이다. 널 디스크립터는 프로세서에 의해 예약된 디스크립터로 모든 필드가 9으로 초기화된 디스크립터이며 일반적으로 참조되지 않는다.

GDT는 디스크립터의 집합이므로 프로세서에 GDT의 시작 어드레스와 크기 정보를 로딩해야한다. 따라서 이것을 저장하는 자료구조가 필요하며, 이 자료구조는 아래와 같은 구조를 하고 있다.

GDT 정보를 저장하는 자료구조의 기준 주소는 32비트의 크기이며, 데이터 세그먼트의 기준 주소와 관계없이 어드레스 0을 기준으로 하는 선형 주소이다. 따라서 GDT의 시작 어드레스를 실제 메모리상의 어드레스로 변환할 필요가 없다. GDT의 선형 주소는 현재 코드가 실행되고 있는 세그먼트의 기준 주소를 알고 있으므로, 현재 세그먼트의 시작을 기준으로 GDT의 오프셋을 구하고, 세그먼트 기준 주소를 더해주면 구할 수 있다. 현재 코드는 부트로더에 의해 0x10000에 로딩되어 실행되고 있으므로 자료구조를 생성할 때 GDT 오프셋에 아래와 같이 0x10000을 더해주면 선형 주소가 된다.

앞에서 세그먼트 디스크립터를 생성했으므로, 널 디스크립터와 GDT 정보를 저장하는 자료구조만 앞뒤로 추가하면 된다.

; GDTR 자료구조 정의
GDTR:
    dw GDTEND - GDT - 1         ; 아래 위치하는 GDT 테이블의 전체 크기
    dd ( GDT - $$ + 0x10000 )   ; 아래에 위치하는 GDT 테이블의 시작 어드레스
                                ; 실제 GDT가 있는 선형 주소 계산을 위해
                                ; 현재 섹션 내의 GDT 오프셋에 세그먼트 기준 주소인 0x10000을 더함

;GDT 테이블 정의
GDT:
    ; 널 디스크립터, 반드시 9으로 초기화 해야함
    NULLDescriptor:
        dw 0x0000
        dw 0x0000
        db 0x00
        db 0x00
        db 0x00
        db 0x00
    
    ; ~~ 생략 ~~
    
    ; 보호 모드 커널용 데이터 세그먼트 디스크립터
    DATADESCRIPTOR:
        dw 0xFFFF       ; Limit [15:0]
        dw 0x0000       ; Base [15:0]
        db 0x00         ; Base [23:16]
        db 0x92         ; P=1, DPL=0, Data Segment, Read/Write
        db 0xCF         ; G=1, D=1, L=0, Limit[19:16]
        db 0x00         ; Base [31:24]
GDTEND:

보호 모드로 전환

보호 모드로 전환하려면 GDTR 레지스터 설정, CR0 컨트롤 레지스터 설정, jmp 명령 수행 3단계만 수행하면 된다. 프로세서의 레지스터에 값을 설정하는 작업은 앞에서 살펴본 작업보다 훨씬 간단하다.

프로세서에 GDT 정보 설정

프로세서에 GDT 정보를 설정하려면 lgdt 명령어를 사용한다. lgdt 명령어를 2바이트 크기와 4바이트 기준 주소로 된 GDT 정보 자료 구조를 오퍼랜드로 받는다.

lgdt [ GDTR ]       ; GDTR 자료구조를 프로세서에 설정하여 GDT 테이블을 로드

CR0 컨트롤 레지스터 설정

CR0 컨트롤 레지스터에는 보호 모드 전환에 관련된 필드 외에 캐시(Cache), 페이징(Paging), 실수 연산 장치(FPU) 등과 관련된 필드가 포함되어 있다. CR0 컨트롤 레지스터는 아래 그림과 같은 형식으로 구성되며, 각 필드에 대한 내용은 아래 표와 같다.

필드 설명
PE Protection Enable의 약자로 보호 모드 진입 여부를 설정
1로 설정하면 보호 모드로 진입하며, 9으로 설정하면 리얼모드로 진입한다.
MP Monitor Coprocessor의 약자로 wait또는 fwait 명령 실행 시 TS 필드 참고 여부를 설정
1로 설정하면 wait 또는 fait 명령 실행 시 TS 필드가 1이면 Device-not-available 예외가 발생하며, 0으로 설정하면 TS 필드의 값을 무시함
EM Emulation의 약자로 프로세서에 FPU 내장되었는지 여부를 설정
1로 설정하면 FPU 관련 명령 실행 시 Device-not-available또는 Invalid-opcode 예외가 발생하며, 0으로 설정하면 정상적으로 실행
FPU가 없으면 실수 연산 명령을 소프트웨어적으로 처리할 목적으로 사용
TS Task Switched의 약자로 태스크가 전환되었음을 나타냄
1로 설정하면 FPU관련 명령 실행 시 Device-not-available 예외 발생하며, 0으로 설정하면 FPU 관련 명령을 정상적으로 실행
EM 필드와 MP 필드와 조합하여 FPU의 상태를 저장하고 복구하는 용도로 사용
ET Extension Type의 약자로 1로 예약됨
과거 386, 486 프로세서에서 FPU를 지원한다는 것을 표시하는 용도로 사용
NE Numeric Error의 약자로 FPU 에러 처리 여부를 내부 인터럽트 또는 외부 인터럽트 중 선택
1로 설정하면 FPU에러를 프로세서 내부의 예외로 연결하며, 0으로 설정하면 인터럽트로 연결함
WP Write Protect의 약자로서, 쓰기 금지 기능을 사용할지 여부를 설정
1로 설정하면 상위 권한(0~2)의 코드가 유저 권한(3)으로 설정된 읽기 전용 페이지에 쓸 수 없으며, 0으로 설정하면 페이지 속성에 관계없이 쓸 수 있다.
AM Alignment Mask의 약자로 어드레스 정렬 검사 기능을 사용할지 여부를 설정
1로 설정하면 데이터나 어드레스가 특정 값의 배수에서 시작하는지 체크하며, 0으로 설정하면 체크하지 않음
NW Not Write-throughdㅢ 약자로 캐시 정책 중 Write-through를 사용할지 여부를 설정
1로 설정하면 데이터나 어드레스가 특정 값의 배수에서 시작하는지 체크하며, 0으로 설정하면 체크하지 않음
CD Cache Disable의 약자로 프로세서의 캐시를 사용할지 여부를 설정
1로 설정하면 캐시를 사요하지 않으며, 0으로 설정하면 캐시를 사용함
PG Paging의 약자로서, 페이징 기능을 사용할지 여부를 설정
1로 설정하면 페이징 기능을 사용하며, 0으로 설정하면 페이징 기능을 사용하지 않음

MINT64 OS에서 보호모드는 거쳐가는 임시 모드에 불과하므로 세그먼테이션 기능외에는 사용하지 않는다. 따라서 페이징, 캐시, 메모리 정렬 검사, 쓰기 금지 기능을 모두 사용하지 않음으로 설정하면 된다. FPU 역시 쓰지 않으므로 임시 값으로 설정한다. FPU 에 관련된 필드를 제외한 나머지필드는 해당 필드를 설정하는 것만으로 관련 기능을 제어할 수 있다. 하지만, FPU에 관련된 필드(EM, ET, MP, TS, NE)는 서로 연관되어 있으므로 FPU 관련 필드를 설정하는 방법에 대해서 알아보겠다.

먼저, FPU 내장 여부에 관련된 필드부터 설정하겠다. x86 프로세서에는 FPU가 내장되어 있으므로 EM 필드를 0으로 설정해서 FPU 명령을 소프트웨어로 에뮬레이션하지 않게 하고, ET 필드를 1로 설정한다. 지금은 임시로 초기화를 수행한 것이므로 FPU를 사용하면 정상적으로 작동하지 않는다. 따라서 MP 필드와 TS 필드와 NE 필드를 1로 설정하여 FPU 명령이 실행되었을때 예외가 발생하게 설정한다. 보호 모드에서는 예외에 대해 처리르 ㄹ하지 않으므로 가능하면 실수 연산을 하지 않는것이 좋다.

위에 그림에 표시된 값은 표를 참고해서 설정한 CR0 컨트롤 레지스터의 실제 값이다. 이 값을 CR0 컨트롤 레지스터에 설정하는 것이 전부이며, 설정하는 코드는 아래와 같다. 코드에 특이한 부분이 있다면 CR0 컨트롤 레지스터가 32비트 크기이므로 32비트 크기의 범용 레지스터인 EAX를 사용했다는 점이다.

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 컨트롤 레지스터에 위에서 저장한 플래그를 설정하여 보호 모드로 전환

CR0 컨트롤 레지스터도 설정했으니, 이제 남은 작업은 jmp 명령으로 CS 세그먼트 레지스터를 갱신하고 32비트 코드로 변경하는것뿐이다. 이과정 역시 몇 줄의 어셈블리어 코드로 처리가 가능하다.

보호 모드로 전환과 세그먼트 셀렉터 초기화

보호 모드로 전환하기 위한 준비는 끝났다. 남은 것은 32비트 코드를 준비한 후, 한 줄의 어셈블리어 코드로 CS 세그먼트 셀렉터(=레지스터)의 값을 바꾸는 것이다. 어셈 블리어로 16비트나 32비트 코드를 생성하려면 BITS 명령을 사용한다. 부트로더에서 16비트(리얼모드) 코드를 생성하려고 사용한 [BITS 16] 코드를 기억할 것이다. 32비트(보호 모드)코드를 생성하려면 역시 마찬가지로 [BITS 32를 입력한다. BITS 16, BITS 32 이후에 위치하는 코드는 모두 16비트나 32비트 코드로 생성된다.

CS 세그먼트 셀렉터를 교체하려면 jmp 명령과 세그먼트 레지스터 접두사를 사용해야한다. 리얼 모드의 세그먼트 레지스터는 세그먼트의 시작 어드레스(기준주소)를 저장하는 레지스터이다. 보호 모드의 세그먼트는 리얼모드와 달리 다양한 정보를 포함하고 있으므로 세그먼트 정보는 디스크립터에 저장하고 세그먼트 셀렉터는 그 디스크립터를 지시하는 용도로 사용한다. 그렇다면 보호 모드에서 GDT 내의 디스크립터를 어떠한 방식으로 지시해야하는가? 보호 모드도 마찬가지로 세그먼트 셀렉터에 어드레스를 설정하면 된다. 다만, 세그먼트의 기준 주소 대신 GDT 내의 디스크립터의 어드레스를 사용하며, 이는 GDT의 시작 어드레스로부터 떨어진 거리(오프셋)를 의미한다. 예를 들어 NULL 디스크립터 다음에 있는 커널 코드 세그먼트 디스크립터를 사용하고 싶다면, 디스크립터의 크기가 8바이트이므로 0x08을 세그먼트 셀렉터에 넣으면 된다. 또는 GDT의 세 번째에 위치한 커널 데이터 디스크립터를 사용하고 싶다면 0x10(=16)와 같이 사용하면 원하는 디스크립터를 지시할 수 있다. 아래 코드는 세그먼트를 사용하여 보호 모드로 전환하고 나서 나머지 세그먼트 셀렉터를 커널 데이터 세그먼트 디스크립터로 초기화하는 코드이다.

; 커널 코드 세그먼트를 0x00을 기준으로 하는 것으로 교체하고 EIP의 값을 0x00을 기준으로 재설정
; CS 세그먼트 셀렉터 : EIP
jmp dword 0x08: ( PROTECTEDMODE - $$ + 0x10000 )  
; 커널 코드 세그먼트가 0x00을 기준으로 하는 반명 실제 코드는 0x10000을 기준으로 실행되고 있으므로
; 오프셋에 0x10000을 더해 세그먼트 교체 후에도 같은 선형 주소를 가리키게 함

[BITS 32]           ; 이하의 코드는 32비트 코드로 설정
PROTECTEDMODE:
    mov ax, 0x10    ; 보호 모드 커널용 데이터 세그먼트 디스크립터를 AX 레지스터에 저장
    mov ds, ax      ; DS 세그먼트 셀렉터에 설정
    mov es, ax      ; ES 세그먼트 셀렉터에 설정
    mov fs, ax      ; FS 세그먼트 셀렉터에 설정
    mov gs, ax      ; GS 세그먼트 셀렉터에 설정
    
    ; 스택을 0x00000000 ~ 0x0000FFFF 영역에 64KB 크기로 생성
    mov ss, ax      ; SS 세그먼트 셀렉터에 설정
    mov esp, 0xFFFE ; ESP 레지스터의 어드레스를 0xFFFE로 설정
    mov ebp, 0xFFFE ; EBP 레지스터의 어드레스를 0xFFFE로 설정

보호 모드용 PRINTSTRING 함수

리얼 모드용 함수를 보호 모드로 변환하는 거은 그리 어렵지 않다. 스택의 크기가 2바이트에서 4바이트로 증가하며, 범용 레지스터의 크기가 32비트로 커졌다는 것 정도만 알면 누구나 변경 가능하다. 아래의 보호모드용 PRINTSTRING 함수인 PRINTMESSAGE 함수와 5장의 리얼모드 PRINTSTRING함수를 비교해보자

기존 부트로더 PRINTMESSAGE 함수

; 메시지를 출력하는 함수
;   PARAM: x좌표, y좌표, 문자열
PRINTMESSAGE:
    push bp     ; 베이스 포인터 레지스터(BP)를 스택에 삽입
    mov bp, sp  ; 베이스 포인터 레지스터(BP)에 스택 포인터 레지스터(SP)의 값을 설정
                ; 베이스 포인터 레지스터(BP)를 이용해서 파라미터에 접근할 목적
                ; 호출된 직후의 SP 레지스터 값을 저장하여 BP 레지스터와 고정된 오프셋으로 파라미터에 접근하게함
    push es     ; ES 세그먼트 레지스터부터 DX 레지스터까지 스택에 삽입
    push si     ; 함수에서 임시로 사용하는 레지스터로 함수의 마지막 부분에서 스택에 삽입된 값을 꺼내 원래 값으로 복원
    push di
    push ax
    push cx
    push dx

    ; ES 세그먼트 레지스터에 비디오 모드 에드레스 설정
    mov ax, 0xB800          ; 비디오 메모리 시작 어드레스(0xB8000)를 세그먼트 레지스터 값으로 변환
    mov es, ax              ; ES 세그먼트 레지스터에 설정

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; X, Y의 좌표로 비디오 메모리의 어드레스를 계산함
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; Y 좌표를 이용해서 먼저 라인 어드레스를 구함
    mov ax, word [ bp + 6 ] ; 파라미터 2(Y좌표)를 AX 레지스터에 설정
    mov si, 160             ; 한 라인의 바이트 수(2 * 80 컬럼)를 SI 레지스터에 설정
    mul si                  ; AX 레지스터와 SI 레지스터를 곱하여 화면 Y어드레스 계산
    mov di, ax              ; 계산된 화면 Y 어드레스를 DI 레지스터에 설정

    ; X 좌표를 이용해서 2를 곱한 후 최종 어드레스를 구함
    mov ax, word [ bp + 4 ] ; 파라미터 1(X좌표)를 AX 레지스터에 설정
    mov si, 2               ; 한 문자를 나타내는 바이트수(2)를 SI 레지스터에 설정
    mul si                  ; AX 레지스터와 SI 레지스터를 곱하여 화면 X 어드레스를 계산
    add di, ax              ; 화면 Y 어드레스와 계산된 X 어드레스를 더해서 실제 비디오 메모리 어드레스를 계산

    ; 출력할 문자열의 어드레스
    mov si, word [ bp + 8 ] ; 파라미터 3(출력할 문자열의 어드레스

.MESSAGELOOP:               ; 메시지를 출력하는 루프
    mov cl, byte [ si ]     ; SI 레지스터가 가리키는 문자열 위치에서 한 문자를 CL 레지스터에 복사
                            ; CL 레지스터는 CX 레지스터의 하위 1바이트를 의미
                            ; 문자열은 1바이트면 충분하므로 CX 레지스터의 하위 1바이트만 사용

    cmp cl, 0               ; 복사된 문자와 0을 비교
    je  .MESSAGEEND         ; 복사한 문자의 값이 0이면 문자열이 종료되었음을 의미하므로 .MESSAGEEND로 이동하여 문자 출력 종료

    mov byte [ es: di ], cl ; 0이 아니라면 메모리 어드레스 0xB800:di에 문자를 출력

    add si, 1               ; SI 레지스터에 1을 더하여 다음 문자열로 이동
    add di, 2               ; DI 레지스터에 2를 더하여 비디오 메모리의 다음 문자 위치로 이동
                            ; 비디오 메모리는 (문자, 속성)의 쌍으로 구성되므로 문자만 출력하려면 2를 더해야함

    jmp .MESSAGELOOP        ; 메시지 출력 루프로 이동하여 다음 문자를 출력

.MESSAGEEND:
    pop dx      ; 함수에서 사용이 끝난 DX 레지스터부터 ES 레지스터까지를 스택에 삽입된 값을 이용해서 복원
    pop cx      ; 스택은 가장 마지막에 들어간 데이터가 먼저나오는 자료구조이므로 삽입의 역순으로 제거해야한다
    pop ax
    pop di
    pop si
    pop es
    pop bp      ; 베이트 포인터 레지스터(BP) 복원
    ret         ; 함수를 호출한 다음 코드의 위치로 복귀

보호 모드용 PRINTSTRING 함수인 PRINTMESSAGE 함수

; 메시지를 출력하는 함수
;   PARAM: x좌표, y좌표, 문자열
PRINTMESSAGE:
    push ebp    ; 베이스 포인터 레지스터(EBP)를 스택에 삽입
    mov ebp,esp ; 베이스 포인터 레지스터(EBP)에 스택 포인터 레지스터(ESP)의 값을 설정
                ; 베이스 포인터 레지스터(EBP)를 이용해서 파라미터에 접근할 목적
                ; 호출된 직후의 ESP 레지스터 값을 저장하여 BP 레지스터와 고정된 오프셋으로 파라미터에 접근하게함
    push esi    ; ES 세그먼트 레지스터부터 DX 레지스터까지 스택에 삽입
    push edi    ; 함수에서 임시로 사용하는 레지스터로 함수의 마지막 부분에서 스택에 삽입된 값을 꺼내 원래 값으로 복원
    push eax
    push ecx
    push edx

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; X, Y의 좌표로 비디오 메모리의 어드레스를 계산함
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; Y 좌표를 이용해서 먼저 라인 어드레스를 구함
    mov eax, dword [ ebp + 12 ] ; 파라미터 2(Y좌표)를 EAX 레지스터에 설정
    mov esi, 160                ; 한 라인의 바이트 수(2 * 80 컬럼)를 ESI 레지스터에 설정
    mul esi                     ; EAX 레지스터와 ESI 레지스터를 곱하여 화면 Y어드레스 계산
    mov edi, eax                ; 계산된 화면 Y 어드레스를 EDI 레지스터에 설정

    ; X 좌표를 이용해서 2를 곱한 후 최종 어드레스를 구함
    mov eax, dword [ ebp + 8 ]  ; 파라미터 1(X좌표)를 EAX 레지스터에 설정
    mov esi, 2                  ; 한 문자를 나타내는 바이트수(2)를 ESI 레지스터에 설정
    mul esi                     ; EAX 레지스터와 ESI 레지스터를 곱하여 화면 X 어드레스를 계산
    add edi, eax                ; 화면 Y 어드레스와 계산된 X 어드레스를 더해서 실제 비디오 메모리 어드레스를 계산

    ; 출력할 문자열의 어드레스
    mov esi, dword [ ebp + 16 ] ; 파라미터 3(출력할 문자열의 어드레스)

.MESSAGELOOP:               ; 메시지를 출력하는 루프
    mov cl, byte [ esi ]    ; ESI 레지스터가 가리키는 문자열 위치에서 한 문자를 CL 레지스터에 복사
                            ; CL 레지스터는 ECX 레지스터의 하위 1바이트를 의미
                            ; 문자열은 1바이트면 충분하므로 ECX 레지스터의 하위 1바이트만 사용

    cmp cl, 0               ; 복사된 문자와 0을 비교
    je  .MESSAGEEND         ; 복사한 문자의 값이 0이면 문자열이 종료되었음을 의미하므로 .MESSAGEEND로 이동하여 문자 출력 종료

    mov byte [ edi + 0x0B8000 ], cl  ; 0이 아니라면 메모리 어드레스 0xB8000:EDI에 문자를 출력

    add esi, 1              ; ESI 레지스터에 1을 더하여 다음 문자열로 이동
    add edi, 2              ; EDI 레지스터에 2를 더하여 비디오 메모리의 다음 문자 위치로 이동
                            ; 비디오 메모리는 (문자, 속성)의 쌍으로 구성되므로 문자만 출력하려면 2를 더해야함

    jmp .MESSAGELOOP        ; 메시지 출력 루프로 이동하여 다음 문자를 출력

.MESSAGEEND:
    pop edx     ; 함수에서 사용이 끝난 EDX 레지스터부터 EBP 레지스터까지를 스택에 삽입된 값을 이용해서 복원
    pop ecx     ; 스택은 가장 마지막에 들어간 데이터가 먼저나오는 자료구조이므로 삽입의 역순으로 제거해야한다
    pop eax
    pop edi
    pop esi
    pop ebp     ; 베이트 포인터 레지스터(EBP) 복원
    ret         ; 함수를 호출한 다음 코드의 위치로 복귀

변경된 부분들

  • 연산에 사용되는 범용 레지스터가 대부분 32비트 범용 레지스터로 수정되었다
  • 스택의 크기가 4바이트로 변경되었기에 파라미터를 오프셋이 4의 배수로 바뀌었다.
  • 리얼 모드에서 비디오 어드레스 지정으 ㄹ위해 사용하던 ES세그먼트 레지스터가 사려졌다. 리얼모드에서는 레지스터의 한계로 64KB 범위의 어드레스만 접근이 가능했으므로 화면 포시를 위해 별도의 세그머트가 필요했으나, 보호모드에서는 32비트 오프셋을 사용할 수 있으므로 4GB 전 영역에 접근 가능하다. 따라서 리얼 모드에서 사용하던 ES 세그먼트 레지스터를 제거하고, 직접 비디오 메모리에 접근해서 데이터를 쓰도록함.

보호 모드용 커널 이미지 빌드와 가상 OS 이미지 교체

지금 까지 보호 모드로 전환하는 방법에 대해 알아보았다. 이제 코드를 조합해서 하나의 파일로 만들고 가상 OS 이미지와 교체하여 정상저긍로 실행되는지 확인하겠다.

커널 엔트리 포인트 파일 생성

01.Kernel32 디렉터리의 Source 디렉터리 밑에 EntryPoint.s 파일을 추가하고, 아래와 같이 입력한다. EntryPoint.s 파일은 보호 모드 커널의 가장 앞부분에 위치하는 코드로 보호 모드 전환과 초기화를 수행하여 이후에 위치하는 코드를 위한 환경을 제공한다.

[ORG 0x00]          ; 코드의 시작 어드레스를 0x00으로 설정
[BITS 16]           ; 아래 코드를 16비트 코드로 설정

SECTION .text       ; text 섹션(세그먼트) 정의

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 코드 영역
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
START:
    mov ax, 0x1000  ; 보호 모드 엔트리 포인트의 시작 어드레스 0x1000를 세그먼트 레지스터 값으로 변환
    mov ds, ax      ; DS 세그먼트 레지스터에 설정
    mov es, ax      ; ES 세그먼트 레지스터에 설정

    cli             ; 인터럽트가 발생하지 못하도록 설정
    lgdt [ GDTR ]       ; GDTR 자료구조를 프로세서에 설정하여 GDT 테이블을 로드

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; 보호 모드로 진입
    ; Disable Paging, Disable Cache, Internal FPU, Disable Align Check, Enable ProtectedMode
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    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 컨트롤 레지스터에 위에서 저장한 플래그를 설정하여 보호 모드로 전환

    ; 커널 코드 세그먼트를 0x00을 기준으로 하는 것으로 교체하고 EIP의 값을 0x00을 기준으로 재설정
    ; CS 세그먼트 셀렉터 : EIP
    jmp dword 0x08: ( PROTECTEDMODE - $$ + 0x10000 )
    ; 커널 코드 세그먼트가 0x00을 기준으로 하는 반명 실제 코드는 0x10000을 기준으로 실행되고 있으므로
    ; 오프셋에 0x10000을 더해 세그먼트 교체 후에도 같은 선형 주소를 가리키게 함


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 보호 모드로 진입
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[BITS 32]           ; 이하의 코드는 32비트 코드로 설정
PROTECTEDMODE:
    mov ax, 0x10    ; 보호 모드 커널용 데이터 세그먼트 디스크립터를 AX 레지스터에 저장
    mov ds, ax      ; DS 세그먼트 셀렉터에 설정
    mov es, ax      ; ES 세그먼트 셀렉터에 설정
    mov fs, ax      ; FS 세그먼트 셀렉터에 설정
    mov gs, ax      ; GS 세그먼트 셀렉터에 설정

    ; 스택을 0x00000000 ~ 0x0000FFFF 영역에 64KB 크기로 생성
    mov ss, ax      ; SS 세그먼트 셀렉터에 설정
    mov esp, 0xFFFE ; ESP 레지스터의 어드레스를 0xFFFE로 설정
    mov ebp, 0xFFFE ; EBP 레지스터의 어드레스를 0xFFFE로 설정

    ; 화면에 보호 모드로 전환되었다는 메세지 보여주기
    push ( SWITCHSUCCESSMESSAGE - $$ + 0x10000 )    ; 출력할 메시지의 어드레스를 스택에 삽입
    push 2              ; 화면 Y 좌표 2를 스택에 삽입
    push 0              ; 화면 X 좌표 0를 스택에 삽입
    call PRINTMESSAGE   ; PRINTMESSAGE 함수 호출
    add esp, 12         ; 삽입한 파라미터 제거

    jmp $   ; 현재위치에서 무한 루프 수행

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 함수 코드 영역
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 메시지를 출력하는 함수
;   PARAM: x좌표, y좌표, 문자열
PRINTMESSAGE:
    push ebp    ; 베이스 포인터 레지스터(EBP)를 스택에 삽입
    mov ebp,esp ; 베이스 포인터 레지스터(EBP)에 스택 포인터 레지스터(ESP)의 값을 설정
                ; 베이스 포인터 레지스터(EBP)를 이용해서 파라미터에 접근할 목적
                ; 호출된 직후의 ESP 레지스터 값을 저장하여 BP 레지스터와 고정된 오프셋으로 파라미터에 접근하게함
    push esi    ; ES 세그먼트 레지스터부터 DX 레지스터까지 스택에 삽입
    push edi    ; 함수에서 임시로 사용하는 레지스터로 함수의 마지막 부분에서 스택에 삽입된 값을 꺼내 원래 값으로 복원
    push eax
    push ecx
    push edx

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; X, Y의 좌표로 비디오 메모리의 어드레스를 계산함
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    ; Y 좌표를 이용해서 먼저 라인 어드레스를 구함
    mov eax, dword [ ebp + 12 ] ; 파라미터 2(Y좌표)를 EAX 레지스터에 설정
    mov esi, 160                ; 한 라인의 바이트 수(2 * 80 컬럼)를 ESI 레지스터에 설정
    mul esi                     ; EAX 레지스터와 ESI 레지스터를 곱하여 화면 Y어드레스 계산
    mov edi, eax                ; 계산된 화면 Y 어드레스를 EDI 레지스터에 설정

    ; X 좌표를 이용해서 2를 곱한 후 최종 어드레스를 구함
    mov eax, dword [ ebp + 8 ]  ; 파라미터 1(X좌표)를 EAX 레지스터에 설정
    mov esi, 2                  ; 한 문자를 나타내는 바이트수(2)를 ESI 레지스터에 설정
    mul esi                     ; EAX 레지스터와 ESI 레지스터를 곱하여 화면 X 어드레스를 계산
    add edi, eax                ; 화면 Y 어드레스와 계산된 X 어드레스를 더해서 실제 비디오 메모리 어드레스를 계산

    ; 출력할 문자열의 어드레스
    mov esi, dword [ ebp + 16 ] ; 파라미터 3(출력할 문자열의 어드레스)

.MESSAGELOOP:               ; 메시지를 출력하는 루프
    mov cl, byte [ esi ]    ; ESI 레지스터가 가리키는 문자열 위치에서 한 문자를 CL 레지스터에 복사
                            ; CL 레지스터는 ECX 레지스터의 하위 1바이트를 의미
                            ; 문자열은 1바이트면 충분하므로 ECX 레지스터의 하위 1바이트만 사용

    cmp cl, 0               ; 복사된 문자와 0을 비교
    je  .MESSAGEEND         ; 복사한 문자의 값이 0이면 문자열이 종료되었음을 의미하므로 .MESSAGEEND로 이동하여 문자 출력 종료

    mov byte [ edi + 0x0B8000 ], cl  ; 0이 아니라면 메모리 어드레스 0xB8000:EDI에 문자를 출력

    add esi, 1              ; ESI 레지스터에 1을 더하여 다음 문자열로 이동
    add edi, 2              ; EDI 레지스터에 2를 더하여 비디오 메모리의 다음 문자 위치로 이동
                            ; 비디오 메모리는 (문자, 속성)의 쌍으로 구성되므로 문자만 출력하려면 2를 더해야함

    jmp .MESSAGELOOP        ; 메시지 출력 루프로 이동하여 다음 문자를 출력

.MESSAGEEND:
    pop edx     ; 함수에서 사용이 끝난 EDX 레지스터부터 EBP 레지스터까지를 스택에 삽입된 값을 이용해서 복원
    pop ecx     ; 스택은 가장 마지막에 들어간 데이터가 먼저나오는 자료구조이므로 삽입의 역순으로 제거해야한다
    pop eax
    pop edi
    pop esi
    pop ebp     ; 베이트 포인터 레지스터(EBP) 복원
    ret         ; 함수를 호출한 다음 코드의 위치로 복귀


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 데이터 영역
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 아래의 데이터들을 8바이트에 맞춰 정렬하기 위해 추가
align 8, db 0

; GDTR의 끝을 8byte로 정렬하기 위해 추가
dw 0x0000

; GDTR 자료구조 정의
GDTR:
    dw GDTEND - GDT - 1         ; 아래 위치하는 GDT 테이블의 전체 크기
    dd ( GDT - $$ + 0x10000 )   ; 아래에 위치하는 GDT 테이블의 시작 어드레스
                                ; 실제 GDT가 있는 선형 주소 계산을 위해
                                ; 현재 섹션 내의 GDT 오프셋에 세그먼트 기준 주소인 0x10000을 더함

;GDT 테이블 정의
GDT:
    ; 널 디스크립터, 반드시 9으로 초기화 해야함
    NULLDescriptor:
        dw 0x0000
        dw 0x0000
        db 0x00
        db 0x00
        db 0x00
        db 0x00

    ; 보호 모드 커널용 코드 세그먼트 디스크립터
    CODESCRIPTOR:
        dw 0xFFFF       ; Limit [15:0]
        dw 0x0000       ; Base [15:0]
        db 0x00         ; Base [23:16]
        db 0x9A         ; P=1, DPL=0, Code Segment, Execute/Read
        db 0xCF         ; G=1, D=1, L=0, Limit[19:16]
        db 0x00         ; Base [31:24]

    ; 보호 모드 커널용 데이터 세그먼트 디스크립터
    DATADESCRIPTOR:
        dw 0xFFFF       ; Limit [15:0]
        dw 0x0000       ; Base [15:0]
        db 0x00         ; Base [23:16]
        db 0x92         ; P=1, DPL=0, Data Segment, Read/Write
        db 0xCF         ; G=1, D=1, L=0, Limit[19:16]
        db 0x00         ; Base [31:24]
GDTEND:


; 보호 모드로 전환되었다는 메시지
SWITCHSUCCESSMESSAGE:   db 'Switch To Protected Mode Success~!!', 0

times 512 - ( $ - $$ )  db 0x00 ; 512로 맞추기 위해 남은 부분을 0으로 채움

makefile 수정과 가상 OS 이미지 파일 교체

새로운 파일이 추가되었으므로 추가된 파일을 빌드할 수 있도록 makefile을 수정해야한다. 기존의 makefile은 VirtualOS.asm 파일을 빌드하여 가상 OS 이미지를 생성했다. 가상 OS이미지 대신 실제 커널 이미지를 생성해야하므로 엔트리 포인트 파일을 빌드할 수 있게 01.Kernel32/makefile을 아래와 같이 수정한다.

all: Kernel32.bin

Kernel32.bin: Source/EntryPoint.s
	nasm -o Kernel32.bin $<

clean:
	rm -f Kernel32.bin

$<라는 기호는 처음 보았을 것이다. 이 매크로는 Dependency의 첫 번째 파일을 의미하는 매크로이다. 따라서 'Source/EntryPoint.s'로 치환되며, 이 엔트리 파일은 빌드되어 Kernel32.bin 파일로 생성된다.

최상위 디렉토리의 makefile도 수정해야한다. 아래처럼 수정한다.

Disk.img: 00.BootLoader/BootLoader.bin 01.Kernel32/Kernel32.bin
	@echo
	@echo =============== Disk Image Build Start ===============
	@echo

	cat $^ > Disk.img

	@echo
	@echo =============== All Build Complete ===============
	@echo

여기서 보면 $^라는 새로운 기호를 사용했다. 이는 앞에서 설명한 $< 역활과 비슷하지만, Dependency에 나열된 전체 파일을 의미한다. 따라서 00.BootLoader/BootLoader.bin 01.Kernel32/Kernel32.bin로 치환된다.

makefile 전체 내용이다.

all: BootLoader Kernel32 Disk.img

BootLoader:
	@echo
	@echo =============== Build Boot Loader ===============
	@echo

	make -C 00.BootLoader

	@echo
	@echo =============== Build Complete ===============
	@echo

Kernel32:
	@echo
	@echo =============== Build 32bit Kernel ===============
	@echo

	make -C 01.Kernel32

	@echo
	@echo =============== Build Complete ===============
	@echo

Disk.img: 00.BootLoader/BootLoader.bin 01.Kernel32/Kernel32.bin
	@echo
	@echo =============== Disk Image Build Start ===============
	@echo

	cat $^ > Disk.img

	@echo
	@echo =============== All Build Complete ===============
	@echo

clean:
	make -C 00.BootLoader clean
	make -C 01.Kernel32 clean
	rm -f Disk.img

OS 이미지 통합과 QEMU 실행

이처럼 OS이미지가 로딩되었다는 말만 나오고 보호모드로 진입하지 못할 수 있다.

부트로더에 OS 이미지의 크기가 1024로 설정되어있기 때문이다. 빌드한 보호 모드 커널 이미지의 크기는 512바이트(1섹터) 이므로 부트로더가 한 섹터를 로딩한 후 나머지 1023 섹터를 읽으려다가 정지한것이다. 다라서 정상적으로 실행되려면 BootLoader.asm파일의 TOTALSECTORCOUNT값을 아래와 같이 변경해야한다.

TOTALSECTORCOUNT:   dw  1       ; 부트로더를 제외한 MINT64 OS이미지의 크기

정상적으로 실행된 모습이다.

다음 장에서는 C언어로 커널 개발하는 방법을 알아볼것이다.