유저모드 Task Switching(유저모드와 콜 게이트)

아래 3개의 파일을 이용한다.

init.inc

SysCodeSelector equ 0x08

SysDataSelector equ 0x10

VideoSelector equ 0x18

TSSSelector equ 0x20

UserCodeSelector equ 0x28+3        ; 유저모드의 코드 세그먼트 디스크립터는 GDT의 첫번째에서 0x28만큼                                         떨어져 있음. 그리고 셀렉터의 PRL 필드에 2진수 11(10진수 3)을                                           추가하여 이 세그먼트가 유저 영역이라는 표시를 해야한다. 이를 위                                                 해 3을 더하였으며 더하기를 하는 것은 OR 연산과 같은 의미이다. 아래                                                   는 세그먼트 디스크립터의 모양이다.

0x28 -> 40바이트 -> 2진수로 101000, +3을 통해 RPL 값 지정
NULL 디스크립터를 포함하여 6번째 위치
TI가 0 이므로 이 세그먼트 디스크립
터는 GDT에 존재


UserDataSelector equ 0x30+3        유저모드의 데이터 세그먼트 디스크립터는 GDT의 맨 첫번째번지에서
                                        0x30만큼 떨어져 있다. 여기에도 마찬가지로 유저모드 라는 표시를 하                                         기 위해 +3을 해준다.




 boot.asm

%include "init.inc"

[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 ; ES:BX = 1000:1000
mov es, ax
mov bx, 0
mov ah, 2 ; 디스크에 있는 데이터를 es:bx의 주소로
mov al, 2 ; 2섹터를 읽을 것이다.
mov ch, 0 ; 0번째 Cylinder
mov cl, 2 ; 2번째 섹터부터 읽기 시작
mov dh, 0 ; Head = 0
mov dl, 0 ; Drive=0, A: 드라이브
int 13h ; read
jc read ; 에러가 나면 다시함
mov dx, 0x3F2 ; 플로피디스크 드라이브의
xor al, al ; 모터를 끈다.
out dx, al

cli
mov al, 0x11 ; PIC의 초기화
out 0x20, al ; 마스터 PIC
dw 0x00eb, 0x00eb ; jmp $+2, jmp $+2
out 0xA0, al ; 슬레이브 PIC
dw 0x00eb, 0x00eb
mov al, 0x20 ; 마스터 PIC 인터럽트 시작점
out 0x21, al
dw 0x00eb, 0x00eb
mov al, 0x28 ; 슬레이브 PIC 인터럽트 시작점
out 0xA1, al
dw 0x00eb, 0x00eb
mov al, 0x04 ; 마스터 PIC에 IRQ 2번에
out 0x21, al ; 스렐이브 PIC이 연결되어 있다.
dw 0x00eb, 0x00eb
mov al, 0x02 ; 스렐이브 PIC이 마스터 PIC의
out 0xA1, al ; IRQ 2번에 연결되어 있다.
dw 0x00eb, 0x00eb
mov al, 0x01 ; 8086 모드를 사용한다.
out 0x21, al
dw 0x00eb, 0x00eb
out 0xA1, al
dw 0x00eb, 0x00eb
mov al, 0xFF
out 0xA1, al
dw 0x00eb, 0x00eb
mov al, 0xFB
out 0x21, al
jmp 0x1000:0000
msgBack db '.', 0x67

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

kernel.asm

%include "init.inc"


[org 0x10000]

[bits 16]


start:

cld

mov ax, cs

mov ds, ax

xor ax, ax

mov ss, ax

xor eax, eax

lea eax, [tss] ; EAX에 tss의 물리 주소를 넣는다.

add eax, 0x10000

mov [descriptor4+2], ax

shr eax, 16

mov [descriptor4+4], al

mov [descriptor4+7], ah

; 커널 영역에 들어와 실행할 커널 함수의 오프셋을 콜게이트 디스크립터에 적재하는 일을 한다.
 
이 부분은 Protected Mode로 들어오기 전 16비트 코드로 구성되어 있으므로 org 0x10000의
 origin 선언이 적용되지 않으므로 printf 함수의 오프셋에 0x10000을 더한다. 그 후 콜게이트
 디스크립터의 알맞은 필드에 넣는다.

xor eax, eax

lea eax, [printf] ; EAX에 printf의 물리 주소를 넣는다.

add eax, 0x10000

mov [descriptor7], ax

shr eax, 16

mov [descriptor7+6], al

mov [descriptor7+7], ah

cli

lgdt[gdtr]

mov eax, cr0

or eax, 0x00000001

mov cr0, eax

jmp $+2

nop

nop

jmp dword SysCodeSelector:PM_Start

[bits 32]


PM_Start:

mov bx, SysDataSelector

mov ds, bx

mov es, bx

mov fs, bx

mov gs, bx

mov ss, bx

; Protected Mode로 들어온 후 스택을 임의의 값으로 지정한다.

lea esp, [PM_Start]    ; PM_Start 번지를 ESP 레지스터에 넣어 커널이 사용하는 스택으로 사용
                             스택에 저장할 때 마이너스 방향으로 진행되므로 PM_Start 보다 작은 쪽 사용
                            PM_Start 번지보다 작은 쪽은 프로그램이 실행되고 더 이상 사용되지 않                                  는 루틴이 존재하므로 스택 영역으로 사용해도 무방하다.


; GDT에 등록된 TSSSelector를 TR레지스터에 적재하는 것을 통해 TSS영역을 사용하도록 한다.
       TSS영역은 한개만 사용한다.(여기서만.. 다음 장에서는 각 태스크마다 TSS처럼 모든 레지스터를 저장해
       두는 메모리 영역을 두어 수동으로 관리하는 방법이 있음)

mov ax, TSSSelector

ltr ax

; ESP에 있는 값을 다시 TSS영역에 있는 tss_esp0의 주소에 넣는다. 이렇게 넣어두면 유저 모드 태스크에서 콜게이트를 거치며 특권 레벨이 변할 때 tss_esp0에 있던 값이 esp 레지스터로 들어가면서 커널 레벨에서 이 값으로 스택을 사용하게 된다.

mov [tss_esp0], esp ; 특권 레벨 0의 스택을 TSS에 지정해 둔다.

lea eax, [PM_Start-256]          ; PM_Start-256의 값을 tss_esp에 넣음. 이 값은 유저모드 태스크에서 사
                                 용하는 스택의 주소
이다.

mov [tss_esp], eax ; 특권 레벨 3의 스택을 TSS에 지정해 둔다.

; 유저모드로의 태스크 스위칭 루틴
 Protected Mode로 들어와 TR 레지스터와 TSS 영역 등을 세팅한 후 유저 모드인 것처럼 가장하여 IRET 명령을 통해 유저모드의 태스크를 실행시킨다.

mov ax, UserDataSelector ; 데이터 세그먼트를 유저 모드로 지정해 둔다.

mov ds, ax

mov es, ax

mov fs, ax

mov gs, ax

; ESP 레지스터에 PM_Start-256 값을 넣어 유저모드에서 사용할 스택 주소를 넣어둔다.

lea esp, [PM_Start-256]

; 현재 ESP에는 커널모드에서 사용하는 스택 주소가 들어있으며, 이곳에 올 때 까지 PUSH 명령을 사용하지 않았으므로 ESP에 있는 값은 PM_Start 번지 일 것이다. 이 스택에 아래 PUSH 명령을 통해 유저모드의 SS, ESP, EFLAGS, CS, EIP를 차례로 저장하고 IRET 한다.

push dword UserDataSelector ; SS

push esp ; ESP

push dword 0x200 ; EFLAGS

push dword UserCodeSelector ; CS

lea eax, [user_process]               ; EIP에 들어갈 값으로 user_process 함수의 오프셋을 지정

push eax ; EIP

iretd ; 유저 모드 태스크로 점프

printf:

mov ebp, esp                ; esp의 값을 ebp에 복사

push es

push eax

mov ax, VideoSelector       ; VideoSelector를 es에 넣는다.

mov es, ax

mov esi, [ebp+8]

mov edi, [ebp+12]

printf_loop:

mov al, byte [esi]

mov byte [es:edi], al

inc edi

mov byte [es:edi], 0x06

inc esi

inc edi

or al, al

jz printf_end

jmp printf_loop

printf_end:

pop eax

pop es

ret        ; ret 시 jmp $ 명령문이 있는 곳을 가리키게 된다. 그리고 그 자리에서 무한루프를 돈다.

; 유저모드의 태스크가 실행되면 이곳 user_process부터 진행된다.

user_process:

mov edi, 80*2*7

push edi ; 인수를 유저 모드 스택에 저장한다.

lea eax, [msg_user_parameter1]

push eax

call 0x38:0     ; 콜게이트를 통하여 커널 루틴을 호출, 커널모드의 함수 printf로 들어가게 된다.

jmp $        ; 무한루프 부분


msg_user_parameter1 db "This is User Parameter1", 0



gdtr:

dw gdt_end-gdt-1

dd gdt

gdt:

dd 0, 0

dd 0x0000FFFF, 0x00CF9A00

dd 0x0000FFFF, 0x00CF9200

dd 0x8000FFFF, 0x0040920B

descriptor4:

dw 104

dw 0

db 0

db 0x89

db 0

db 0

; 유저모드용 세그먼트 영역을 지정하는 디스크립터 추가(이 디스크립터의 셀렉터 번호는 init 파일에 존재)

dd 0x0000FFFF, 0x00FCFA00 ; 유저 코드 세그먼트

dd 0x0000FFFF, 0x00FCF200 ; 유저 데이터 세그먼트

; GDT에 콜게이트 디스크립터를 만들어 둔다.

descriptor7:

dw 0                    ; 오프셋 0, 이 값은 Protected Mode로 들어오기 전에 설정한다.

dw SysCodeSelector

db 0x02                ; 인수는 2개

db 0xEC

db 0

db 0

gdt_end:


tss:

dw 0, 0 ; 이전 태스크로의 back link

tss_esp0:

dd 0 ; ESP0

dw SysDataSelector, 0 ; SS0, 사용 안함

dd 0 ; ESP1

dw 0, 0 ; SS1, 사용 안함

dd 0 ; ESP2

dw 0, 0 ; SS2, 사용 안함

dd 0


tss_eip:

dd 0, 0 ; EIP, EFLAGS

dd 0, 0, 0, 0

tss_esp:

dd 0, 0, 0, 0 ; ESP, EBP, ESI, EDI

dw 0, 0 ; ES, 사용안함

dw 0, 0 ; CS, 사용안함

dw UserDataSelector, 0 ; SS, 사용안함

dw 0, 0 ; DS, 사용안함

dw 0, 0 ; FS, 사용안함

dw 0, 0 ; GS, 사용안함

dw 0, 0 ; LDT, 사용안함

dw 0, 0 ; 디버그용 T 비트, IO 허가 비트맵

times 1024-($-$$) db 0


컴파일 후 플로피디스크에 삽입 시 아래 그림과 같은 화면이 출력된다.



출력 성공!


보호

CPU는 일부 커널과 함께 모든 유저레벨의 프로그램이 잘 동작하고, 원하는 결과를 얻을 수 있도록 해준다.
Protected Mode로 넘어온 후에는 위에서 이야기한 바대로 여러 유저 프로그램이 같이 동작하기 때문에 CPU는 각 명령어마다 여러 가지 체크를 하고 합당한 명령만 실행시키고, 합당하지 않으면 폴트를 발생시켜 커널이나 유저 프로그램 자신이 이 에러를 다시 조정하도록 한다.
이러한 체크에는 4가지가 존재한다.
    1. Limit 체크
    2. Type 체크
    3. 특권 레벨 체크
    4. 명령 세트 체크


1. CPU의 체크 포인트

1.1 Limit 체크

세그먼트 디스크립터 내 G비트가 0일 때에는 Limit를 0xFFFFF(1MB)까지 선택할 수 있으며, 세그먼트 영역의 크기를 0으로 지정해도 되고, 0xFFFFF로 지정해도 된다는 의미이다.
세그먼트 디스크립터 내 G비트가 1일 때에는 Limit를 0xFFFFFFFF(4GB)까지 선택할 수 있으며, 세그먼트 영역의 크기를 0xFFF로 지정해도 되고, 0xFFFFFFFF로 지정해도 된다는 의미이다.
세그먼트 영역에서 유효 Limit는 전체 세그먼트 영역의 크기에서 1바이트를 뺀것과 같다. 아래의 경우 CPU는 일반 보호 예외(#GP)를 발생시킨다.
    1. 오프셋이 유효 Limit보다 큰 바이트
    2. 오프셋이 유효 Limit-1보다 큰 워드
    3. 오프셋이 유효 Limit-3보다 큰 더블워드
    4. 오프셋이 유효 Limit-7보다 큰 쿼드워드

1.2 Type 체크

세그먼트 디스크립터에는 S비트와 Type 필드가 있다. S비트가 0이면 Type 필드가 시스템 Type이라는 것이고, 1이면 코드나 데이터 세그먼트의 Type이라는 뜻이다.


예를 들어 P비트가 1, DPL이 00, S가 0이고, 32비트 인터럽트 게이트일때 디스크립터의 한 부분에 0x8E가 들어가고, 32비트 TSS였다면 0x89, 32비트 TSS의 B비트가 1로 세트되었을 때 0x8B가 되는 것을 기억하면 된다.

CPU는 세그먼트 셀렉터와 세그먼트 디스크립터를 조작할 때 여러가지 시점에서 Type 정보를 조사한다. 예를 들어 아래와 같은 경우가 Type 체크되는 대표적인 예이다.

1. 세그먼트 셀렉터가 세그먼트 레지스터에 로드될 때 CS에는 코드 세그먼트의 셀렉터만이 로드될 수 있다.
    MOV AX, 0x08
    MOV CS, AX

2. 디스크립터가 세그먼트 레지스터에 이미 로드되어 있는 세그먼트에 명령이 액세스 할 때
    -> 코드 세그먼트 영역에는 데이터의 쓰기가 금지된다.
    -> 읽기 전용 데이터 세그먼트에 데이터의 쓰기가 금지된다.
    -> 코드 세그먼트 영역에 읽기 가능한 플래그가 설정되지 않은 상태에서는 이 영역을 읽어 들일 수 없다.

3. CALL JMP 명령의 오퍼랜드에 셀렉터가 있을 때 그 셀렉터에 대한 디스크립터의 Type 필드를 조사한다.

TSS 셀렉터를 오퍼랜드로 한 CALL JMP 명령에서는 그 셀렉터에 대한 디스크립터가 TSS용인지를 자동 체크한다. 또는 IRET 명령이 내려졌을 때 현재 실행중인 태스크의 NT비트가 1로 세트되어 있으면 이에 대한 TSS영역에서 "이전 태스크로의 백링크"가 TSS용인지를 체크한다.

GDT의 맨 처음 디스크립터인 NULL 디스크립터를 CS나 SS에 로드하려고 할 때 일반보호 예외 발생

DS, ES, FS, GS에 로드는 가능하나 로드된 셀렉터를 사용하여 세그먼트 영역에 접근하려고 해도 일반 보호 예외(#GP)가 발생


2. 특권레벨

CPU의 특권레벨은 0~3으로 4개가 있으며, 0과 3의 2개 레벨만을 사용한다. 낮은 레벨의 태스크가 높은 레벨의 데이터를 참조하려고 하면 일반 보호 에러(#GP)가 발생한다.

특권레벨을 이해하기 위해서는 CPL, DPL, RPL 세 개의 레벨을 알아야 한다.

CPL(Current Privilege Level)
CPL은 현재 실행되고 있는 태스크의 특권 레벨로, CS, SS 셀렉터 레지스터의 0, 1번째 비트에 있는 수이다. 프로그램이 다른 특권 레벨의 코드 세그먼트로 제어 이행되면 CPU는 CPL을 변경한다.

DPL(Description Privilege Level)
DPL은 디스크립터에 기재된 DPL 값이고 2비트로 표현된다. 디스크립터를 통한 세그먼트 영역으로의 모든 접근에서 항상 CPL과 DPL의 관계가 체크된다. 예를 들어 현재 낮은 레벨의 CPL을 가지고 있는데, 높은 레벨의 데이터 세그먼트 영역에 접근을 시도한다면 CS의 0, 1비트(CPL)와 세그먼트 디스크립터에 있는 DPL 값을 비교할 것이다. 이러한 상황에서는 체크 후 물론 불가능하다는 판단이 내려진다.

RPL(Requested Privilege Level)
특권레벨 3인 프로세스가 콜게이트(낮은 특권 레벨에서 높은 특권 레벨의 루틴을 사용할 수 있게 하는 관문)를 통해 특권레벨 0에 있는 루틴을 실행 할 때가 있다. 이때 일시적으로 특권 레벨 0으로 들어가기 때문에 특권레벨 0의 데이터 영역에 접근 가능하다. 이러한 현상을 교모히 이용하는 프로그램도 존재할 가능성이 있다. RPL 값은 이러한 상황을 막기 위해 OS 커널이 사용하는 값이다. 예를 들어 콜게이트에 연결되어 있는 루틴에 이 루틴이 어디에서 불려졌는지를 기록하는 방법이 있다. 특권레벨 3인 프로세스가 콜게이트를 통해 이 루틴을 불렀다면 데이터 세그먼트 셀렉터에 특권레벨 3에서 불려졌다는 것을 표시한다. RPL은 누가 요구 했는가를 나타내는 값이다.


콜게이트

낮은 특권 레벨의 프로그램이 실행 도중 높은 특권 레벨로 변경되는 수단은 대표적으로 인터럽트, 예외, 콜게이트가 있다. 이 중 하드웨어 인터럽트와 예외에서는 낮은 특권 레벨 프로그램이 자신의 의지와는 상관없이 특권 레벨의 변경이 이루어지고, 소프트웨어 인터럽트와 콜게이트는 낮은 특권 레벨의 프로그램이 자신의 의지에 의해 높은 특권 레벨의 루틴을 잠깐 사용하는 것이다.
콜게이트도 다른 세그먼트 디스크립터와 함께 GDT 테이블에 디스크립터로서 포함되며, 또 하나의 세그먼트 정의라고 생각하면 된다. 낮은 특권 레벨의 프로그램이 높은 특권 레벨의 프로그램의 일부분을 사용하기 위한 창구 세그먼트이다. 디스크립터의 생김새는 아래와 같으며 "인수의 개수" 칸과 Type 이외의 부분은 다른 디스크립터와 비슷하다.

위 디스크립터를 GDT에 지정해 놓고, 유저 프로그램은 코드 셀렉터의 사용법과 마찬가지로 이 디스크립터를 셀렉터로 선택하여 JMP나 CALL 명령을 내린다. 오프셋은 디스크립터에 지정되어 있으므로 아무 숫자나 사용하여도 디스크립터에 지정된 오프셋의 루틴이 수행된다.


코드와 데이터의 특권레벨 관계

특권 레벨 간 JMP 명령은 불가(굳이 사용해야 한다면 콜게이트(Call Gate)를 사용하여 이루어 져야 한다.
CALL 명령은 특권 레벨 간의 이동 가능, 항상 낮은 특권 레벨에서 높은 특권 레벨에 대하여 이루어져야 하고, RET 명령은 높은 특권 레벨에서 낮은 특권 레벨에 대하여 이루어져야 한다. 특권 레벨 0의 코드가 특권 레벨 3의 코드를 불러낼 이유가 없기 때문이다.

특권레벨 0에서 동작하는 동안 CS(CPL) 셀렉터의 0, 1비트에는 00이 들어가 있다. 특권 레벨 3에서 동작하는 동안의 CS 셀렉터에는 0, 1비트에 11(10진수 3)이 들어가 있게 된다.

특권레벨 3에서 CALL 명령으로 특권레벨 0의 루틴을 불러내면 CS 셀렉터의 0, 1비트에 00, CPL은 0이며
특권레벨 0에서 RET 명령으로 특권레벨 3의 루틴으로 돌아갈 시 CS 셀렉터의 0, 1비트에 11(3), CPL은 3이다. (같음)
한가지 예외가 있다면 Conforming 세그먼트 일 경우, 특권레벨 0의 루틴을 불러내도 CPL의 값이 그대로 3인 채로 실행 되었다가 RET 명령으로 돌아온다. Conforming 세그먼트는 콜게이트와 관련있다.


유저 테스크가 콜 게이트 등을 사용하여 커널모드의 루틴을 불러내는 경우

1. CPU는 이 태스크의 TSS 영역에 있는 SS0, ESP0의 값을 참조하여 커널모드의 스택에 현재 유저 태스크가 사용하고 있는 SS, ESP, 그리고 유저 태스크가 현재 진행 중인 루틴의 주소인 CS, EIP를 차례로 PUSH한다. 그리고 CPU의 SS, ESP 레지스터에는 SS0, ESP0의 값을 각각 넣는다.
2. 콜게이트에 지정된 커널 모드의 루틴 주소로 점프 후 실행한다.
3. RET 명령이 내려질 시 현재 커널 모드의 스택에 저장된 SS, ESP, CS, EIP를 POP하여 CPU의 각각 레지스터에 복원하여 유저모드 태스크로 돌아간다. 이때 먼저 CS를 확인한 후 현재 커널 모드보다 낮은 특권 레벨이면 SS와 ESP를 POP한다.
호출 구문에 인수가 2개 있을 시 인수의 개수에 2를 기입해야 한다. GDT에 있는 콜게이트의 디스크립터의 "인수의 개수" 칸에 2가 들어있다면 유저 레벨의 스택에서 2개를 커널 모드의 스택에 복사한다.


인터럽트 예외가 발생하였을 때의 스택

EFLAGS가 추가된 것만 빼면 콜게이트의 경우와 비슷하다.
먼저 커널모드인 특권레벨 0에서 인터럽트 발생 시 스택 변경이 일어나지 않는다. 특권 레벨 0에서는 굳이 시스템을 보호할 필요가 없다. 그러므로 특권 레벨 0에서 사용하고 있던 SS, ESP 그대로의 값을 사용하여 스택에 EFLAGS, CS, EIP를 PUSH 한다.

유저레벨 태스크의 동작 중에 인터럽트나 예외가 발생하면 유저레벨 태스크의 TSS 영역에서 SS0, ESP0을 CPU의 SS, ESP 레지스터에 복사한다. 이로써 커널 모드의 스택을 사용하게 되는데, 커널모드 스택에 콜게이트의 경우와 같이 유저모드의 태스크가 사용하던 SS, ESP, EFLAGS, CS, EIP 등의 레지스터 값을 PUSH 하여 저장한다. 그 후 커널모드에 있는 인터럽트 핸들러 루틴으로 점프하고, 인터럽트 처리가 끝나면 IRET을 사용하여 인터럽트가 걸린 유저모드로 돌아오는데, 이때 스택에 있는 값들을 POP하여 CPU의 각각의 레지스터에 다시 복원한다.

TSS 영역에 SS0, ESP0, SS1, ESP1 등 레벨 별로 존재(SS3, ESP3은 존재하지 않음)하며, 커널모드 스택, 유저모드 스택 조정이 가능하다.(5장 Task Switcing 참조)


+ Recent posts