본문 바로가기
OS/만들면서 배우는 OS 커널의 구조와 원리

3. Protected Mode로 변환

by 내용증명장인 2026. 4. 8.

해당 챕터에서는 Protected Mode로 변환하기 위한 설정이 무엇이 있는지 알 수 있다.

boot.asm

[org 0]
    jmp 07c0h:start

start:
    mov ax, cs
    mov ds, ax
    mov es, ax

    mov ax, 0xb800
    mov es, ax
    mov di, 0
    mov ax, word [msgBack]
    mov cx, 0x7ff

paint:
    mov word [es:di], ax
    add di, 2
    dec cx
    jnz paint

read:
    mov ax, 0x1000
    mov es, ax
    mov bx, 0

    mov ah, 2
    mov al, 1
    mov ch, 0
    mov cl, 2
    mov dh, 0
    mov dl, 0
    int 0x13

    jc read

    jmp 0x1000:0000

msgBack db '.', 0x67

times 510-($-$$) db 0
dw 0AA55h

 

kernel.asm

[org 0]
[bits 16]

start:
    mov ax, cs
    mov ds, ax
    xor ax, ax
    mov ss, ax

    cli

    lgdt [gdtr]

    mov eax, cr0
    or eax, 0x00000001
    mov cr0, eax

    jmp $+2
    nop
    nop

    db 0x66
    db 0x67
    db 0xEA
    dd PM_Start
    dw SysCodeSelector


[bits 32]

PM_Start:
    mov bx, SysDataSelector
    mov ds, bx
    mov es, bx
    mov fs, bx
    mov gs, bx
    mov ss, bx

    xor eax, eax
    mov ax, VideoSelector
    mov es, ax
    mov edi, 80*2*10+2*10
    lea esi, [ds:msgPMode]
    call printf
    
    jmp $

printf:
    push eax

printf_loop:
    or al, al
    jz printf_end
    mov al, byte [esi]
    mov byte [es:edi], al
    inc edi
    mov byte [es:edi], 0x06
    inc esi
    inc edi
    jmp printf_loop

printf_end:
    pop eax
    ret

msgPMode db "We are in Protected Mode", 0

gdtr:
    dw gdt_end - gdt - 1
    dd gdt+0x10000

gdt:
    dw 0
    dw 0
    db 0
    db 0
    db 0
    db 0

SysCodeSelector equ 0x08
    dw 0xFFFF
    dw 0x0000
    db 0x01
    db 0x9A
    db 0xCF
    db 0x00

SysDataSelector equ 0x10
    dw 0xFFFF
    dw 0x0000
    db 0x01
    db 0x92
    db 0xCF
    db 0x00

VideoSelector equ 0x18
    dw 0xFFFF
    dw 0x8000
    db 0x0B
    db 0x92
    db 0x40
    db 0x00
gdt_end:

두 개의 어셈블리 코드를 작성하였다.

해당 코드를 컴파일 후 실행하면 다음과 같은 화면이 출력된다.

16bit에서는 세그먼트:오프셋 구조로 인해 최대로 지정 가능한 메모리 주소가 0xFFFF:0xFFFF 까지 밖에 지정을 못했다.
즉, 0xFFFF * 0x10 + 0xFFFF = 0x10FFEF까지 지정 가능했다.
이 중 일부는 비디오, BIOS 영역으로 인해 사용 가능한 구역이 더 제한됐다.

그리고 물리주소를 계산할 때 세그먼트 * 0x10(0x10 단위로 곱해지므로) 이므로, 0x10005 같은 물리주소는 안되었다.

이를 32bit Protected Mode로 보완하게 되었다.


Protected Mode

16bit에선 물리 주소를 계산할 때 세그먼트 * 0x10 + 오프셋으로 계산하기 때문에 간단했다.
이 공식으로 cpu가 프로그램의 메모리 주소를 찾을 수 있었다.

32bit 보호모드에서는 권한 및 범위, 용도 확인이 추가되었다.
이를 GDT라는 테이블을 이용하여 수행한다.

GDT

16bit에서 세그먼트:오프셋 구조를 통해 물리주소로 이동하였다.
32bit에서는 GDT에 주소 값을 저장 후 세그먼트 레지스터에 저장하여 물리주소로 이동한다.

GDT는 여러 디스크립터가 모인 테이블을 뜻한다.
32bit Protected Mode에서 GDT를 통해 메모리를 큰 덩어리(세그먼트)로 나누고, 영역과 권한을 설정하여 허용된 범위에서 함부로 벗어나거나 침범하지 못하게 한다.
디스크립터에는 Base Address와 Limit, 속성 정보가 들어있다.

Base Address는 세그먼트 시작 주소를 의미하며,
Limit는 세그먼트의 크기를 의미하고 속성은 출입 권한 등을 의미한다.

GDT 내 디스크립터는 다음과 같은 구조를 가지고 있다.

디스크립터 구조

limit 15~0 부분에서 가장 왼쪽이 15번 비트, 오른쪽이 0비트이며 base address 31~24bit의 왼쪽 부분이 63번 비트, limit 19~16bit의 오른쪽 부분이 48번 비트이다.

Base Address는 32비트로 구성되고, 상/하위 비트가 나뉘어있다.
Limit는 20비트로 구성되어 있다.

속성

G
0일 경우 : limit 값이 세그먼트의 크기를 나타낸다. limit는 20bit이므로 최대 0xFFFFF가 된다.
1일 경우 : limit 값에 0xFFF 를 곱하여 최대 0xFFFFFFFF가 된다. (0xFFFFF * 0xFFF)

limit가 1일 때, G비트의 값이
0이면 limit의 크기는 1바이트, 1이면 0xFFF바이트가 된다.

P
세그먼트가 메모리상에 존재하는지 나타냄. 웬만하면 1이다.

DPL
2비트 값이며, 세그먼트가 커널 레벨인지 유저 레벨인지 나타냄.
0일 경우 : 커널 레벨
1, 2일 경우 : 기기 드라이버
3일 경우 : 사용자 레벨

S
세그먼트가 시스템 세그먼트인지, 코드 or 데이터 세그먼트인지 나타냄.
0일 경우 : 시스템 세그먼트
1일 경우 : 코드 or 데이터 세그먼트

Type
4비트로 구성되어 있다.

첫 번째 비트가 0일 경우 데이터 세그먼트이다.
이 때, 두 번째 비트가 0이면 EXPAND UP(Base부터 Limit까지 주소가 증가하는 방향으로 데이터 저장)이며
1이면 EXPAND DOWN(가장 높은 주소 값에서 아래로 데이터 저장) 형태이다.
세 번째 비트가 0일 경우 세그먼트 영역 읽기만 가능이고, 1일 경우 읽기쓰기 모두 가능하다.

첫 번째 비트가 1일 경우 코드 세그먼트라는 의미이다.
두 번째 비트가 0일 경우 Conforming 을 지원한다. (Conforming을 통해 RPL이 3인 경우 DPL이 1인 프로그램을 실행 가능하게 해줌)
세 번째 비트가 0이면 실행 가능, 1이면 읽기 가능이다. (코드 세그먼트이기 때문에 쓰기 지원을 하지 않는다)

D
0일 경우 16비트, 1일 경우 32비트이다.

AVL
개발자를 위한 비트 공간, CPU는 안건드는 영역

GDTR

CPU에는 48비트짜리 GDTR이라는 레지스터가 있다.

GDTR의 구조는 위와 같으며, lgdt 명령어를 통해 gdtr에 GDT 위치를 저장한다.
그 후 CPU가 GDT를 사용할 때 gdtr을 통해 메모리 상에서 GDT의 위치를 찾아낸다.

Protected Mode의 주소 지정 방법

GDT의 위치를 알았다면, GDT의 몇 번째 디스크립터를 사용해야할지 알아야한다.
이를 위해 셀렉터 레지스터라는 것이 존재한다.

셀렉터 레지스터 구조

위는 셀렉터 레지스터의 구조이며 상위 13비트에 디스크립터를 찾기 위한 인덱스 정보가 들어있다. 0, 1번째 비트에 RPL, 2번째 비트에 TI 값이 들어간다.

TI는 GDT를 사용한다면 0, LDT를 사용한다면 1이 설정된다.

RPL은 요청자의 권한을 의미한다.
RPL이 3(사용자 레벨)인데 DPL이 1(커널 레벨)이면 사용 불가하다.


GDT가 설정되어있는 상태에서 CPU는 GDTR을 통해 GDT의 시작 주소를 알아내고, 세그먼트 셀렉터로부터 인덱스 정보를 받아 GDT에서 몇 번째 디스크립터인지 찾는다.
이렇게 찾아낸 디스크립터에는 해당 메모리 영역의 베이스 주소, Limit(크기), 접근 권한이 들어있어 해당 세그먼트 영역에 정보를 가지고 접근할 수 있게 된다.

1. gdtr을 통해 GDT의 Base Address를 가져온다.
2. 세그먼트 셀럭터를 통해 몇 번째 디스크립터를 사용할지 찾는다.
3. RPL과 DPL 값을 비교하고 서로 일치하면 내용을 DS에 복사한다.


소스 코드 해석

kernel.asm은 네 가지의 디스크립터로 구성되어 있다.
NULL 디스크립터, SysCodeSelector, SysDataSelector, VideoSelector 네 개가 있다.

NULL 디스크립터

dw 0
dw 0
db 0
db 0
db 0
db 0

NULL 디스크립터는 형식상 항상 기재해야한다.

두 번째 디스크립터인 SysCodeSelector를 살펴보면 다음과 같다.

dw 0xFFFF
dw 0x0000
db 0x01
db 0x9A
db 0xCF
db 0x00

이 값을 디스크립터 구조에 대입하면 다음과 같다.
Limit 15~0비트 : 0xFFFF
Base Address 15~0비트 : 0x0000
Base Address 23~16비트 : 0x01
0x9A는 1001 1010 이므로, P: 1, DPL: 00, S: 1, Type: 1010
0xCF는 1100 1111 이므로, G: 1, D: 1, Limit 19~16비트 : 0xF
Base Address 31~24비트 : 0x00
이렇게 볼 수 있다.

Limit의 크기는 0xF FFFF가 된다.
Base Address는 0x00 01 0000, 즉, 0x00010000이 된다.
해당 세그먼트는 P가 1비트이므로 메모리 상에 존재하며, DPL이 0이므로 커널 레벨, S는 1이므로 코드 or 데이터 세그먼트,
Type의 경우 1010이므로 첫 번째 비트 1 코드 세그먼트, 두 번째 비트 0 non-Conforming, 읽기 가능
G는 1비트이므로 Limit의 크기는 0xFFFFF * 0xFFF = 0xFFFFFFFF, D는 1이므로 32비트를 의미하게 된다.

다음은 GDTR 코드이다.

gdtr:
    dw gdt_end - gdt - 1
    dd gdt+0x10000

gdtr의 주소가 다음과 같이 저장되어 있다.
gdt_end - gdt - 1은 GDT의 limit(크기)를 의미하고, gdt+0x10000은 gdt의 시작 주소를 의미한다.

start:
    mov ax, cs
    mov ds, ax
    xor ax, ax
    mov ss, ax

    cli

    lgdt [gdtr]

    mov eax, cr0
    or eax, 0x00000001
    mov cr0, eax

lgdt [gdtr] 코드는 gdtr 레지스터에 저장된 gdt의 시작 주소를 lgdt 명령어를 통해 찾는 코드이다.

SysCodeSelector equ 0x08
SysDataSelector equ 0x10
VideoSelector 	equ 0x18

위 코드는 세그먼트 셀렉터이다. NULL 디스크립터는 따로 명시가 되어있지 않다. GDT의 시작 주소가 NULL 디스크립터이기 때문이다. 세그먼트 셀렉터의 수를 통해서 GDT의 인덱스 파악이 가능하다. (TI, RPL 비트는 + n 형태로 붙는다.(0x08 + 7))
10진수로 나타내면 각 디스크립터는 8, 16, 24가 되는데, 디스크립터가 8바이트이기 때문에 인덱스 번호가 이렇게 붙는다.
8로 나누면 1, 2, 3과 같은 인덱스 번호가 된다. 그렇게 GDT에 총 8192개의 디스크립터를 표현 가능하다.

이렇게 GDT 내 디스크립터를 찾았으니 데이터가 들어가야한다.
nasm -f bin -o kernel.bin kernel.asm -l list.txt 명령어를 통해 컴파일하여 기계어와 소스코드를 동시 출력하였다.

63000000 이므로 리틀 엔디언으로 인해 0x63이다.

0x63에는 문자열이 존재한다.
현재 물리주소는 0x10000 + 0x63이므로 문자열은 0x10063에 존재하며, limit는 0xFFFFFFFF인데 그보단 작은 것을 확인할 수 있다.

16비트 Real Mode에서 32비트 Protected Mode 전환

kernel.asm에서 16비트였다가 32비트로 전환된다.

start:
    mov ax, cs
    mov ds, ax
    xor ax, ax
    mov ss, ax

    cli

    lgdt [gdtr]

    mov eax, cr0
    or eax, 0x00000001
    mov cr0, eax

    jmp $+2
    nop
    nop

    db 0x66
    db 0x67
    db 0xEA
    dd PM_Start
    dw SysCodeSelector
mov eax, cr0
or eax, 0x00000001
mov cr0, eax

해당 코드에서 32비트 protected mode로 전환한다.
cr0은 32bit 레지스터이며 0번째 비트(PE)가 1일 때 보호모드이다.
그러므로 위 코드를 통해 0번째 비트를 1로 바꾸어 protected mode로 전환한다.

jmp $+2
nop
nop

해당 코드를 통해 파이프 라인 과정에서 남은 16비트 Real Mode 명령어를 비우게 된다.
jmp 명령어 수행 시 읽어둔 명령어 큐가 강제로 비워지게 되며, nop을 통해 비워지는 시간을 번다.

db 0x66
db 0x67
db 0xEA
dd PM_Start
dw SysCodeSelector

위 코드에서 db 0xEA는 jmp 명령어를 기계어로 바꾸어 놓은 것이며, 0x66(eax) 0x67(esi)는 16비트에서 32비트 오퍼랜드 사용 시 해당 기계어로 컴파일 된다.

32bit Protected Mode 전환 후

[bits 32]

PM_Start:
    mov bx, SysDataSelector
    mov ds, bx
    mov es, bx
    mov fs, bx
    mov gs, bx
    mov ss, bx

전환 후 위와 같은 코드를 수행하게 되며 bx에 SysDataSelector 인덱스 값인 0x00000010(32비트)값으로 16비트 값이 들어있는 세그먼트들을 초기화 시킨다.

'OS > 만들면서 배우는 OS 커널의 구조와 원리' 카테고리의 다른 글

6. 보호  (0) 2026.04.13
5. 태스크 스위칭, 문맥 교환  (0) 2026.04.13
4. 인터럽트와 예외  (0) 2026.04.10
2. 커널 로드  (0) 2026.04.06
1. 부트스트랩  (0) 2026.04.03