5장에서 했던 태스크 스위칭은 CPU를 통한 태스크 스위칭이었다.
이번 장에서는 수동으로 태스크 스위칭을 구현한다.
CPU를 통한 태스크 스위칭를 통해서 JMP나 CALL 명령어 수행 시 어셈블리 코드를 통해 스택에 레지스터 값을 저장해주지 않아도 CPU가 자동으로 수행하지만, 수동 태스크 스위칭의 경우 레지스터 값을 수동으로 스택에 PUSH하고 POP 해야 한다.
수동으로 태스크 스위칭을 수행하게 되면 TSS의 활용이 달라지는데, 유저모드에서 커널모드로 전환 시 ESP0, SS0 필드만을 참고하게 된다.
즉, 기존 자동 태스크 스위칭은 TSS에서 모든 레지스터 값을 저장하고 로드하지만, 수동 태스크 스위칭은 대부분의 레지스터를 스택에서 로드 및 저장하고 TSS에서 커널 스택이 어디있는지 찾기 위해 ESP0, SS0만을 참고하게 된다.
유저 모드와 콜게이트
소스 코드
init.inc
SysCodeSelector equ 0x08
SysDataSelector equ 0x10
VideoSelector equ 0x18
TSSSelector equ 0x20
UserCodeSelector equ 0x28+3
UserDataSelector equ 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
mov es, ax
mov bx, 0
mov ah, 2
mov al, 2
mov ch, 0
mov cl, 2
mov dh, 0
mov dl, 0
int 13h
jc read
mov dx, 0x3F2
xor al, al
out dx, al
cli
mov al, 0x11
out 0x20, al
dw 0x00eb, 0x00eb
out 0xA0, al
dw 0x00eb, 0x00eb
mov al, 0x20
out 0x21, al
dw 0x00eb, 0x00eb
mov al, 0x28
out 0xA1, al
dw 0x00eb, 0x00eb
mov al, 0x04
out 0x21, al
dw 0x00eb, 0x00eb
mov al, 0x02
out 0xA1, al
dw 0x00eb, 0x00eb
mov al, 0x01
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]
add eax, 0x10000
mov [descriptor4+2], ax
shr eax, 16
mov [descriptor4+4], al
mov [descriptor4+7], ah
xor eax, eax
lea 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
lea esp, [PM_Start]
mov ax, TSSSelector
ltr ax
mov [tss_esp0], esp
lea eax, [PM_Start-256]
mov [tss_esp], eax
mov ax, UserDataSelector
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
lea esp, [PM_Start-256]
push dword UserDataSelector
push esp
push dword 0x200
push dword UserCodeSelector
lea eax, [user_process]
push eax
iretd
;-------------------SubRoutines--------------------
printf:
mov ebp, esp
push es
push eax
mov ax, VideoSelector
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
user_process:
mov edi, 80*2*7
push edi
lea eax, [msg_user_parameter1]
push eax
call 0x38:0
jmp $
msg_user_parameter1 db "This is User Parameter1", 0
;-----------------Data Area-------------------
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
dd 0x0000FFFF, 0x00FCFA00
dd 0x0000FFFF, 0x00FCF200
descriptor7:
dw 0
dw SysCodeSelector
db 0x02
db 0xEC
db 0
db 0
gdt_end:
tss:
dw 0, 0
tss_esp0:
dd 0
dw SysDataSelector, 0
dd 0
dw 0, 0
dd 0
dw 0, 0
dd 0
tss_eip:
dd 0, 0
dd 0, 0, 0, 0
tss_esp:
dd 0, 0, 0, 0
dw 0, 0
dw 0, 0
dw UserDataSelector, 0
dw 0, 0
dw 0, 0
dw 0, 0
dw 0, 0
dw 0, 0
times 1024-($-$$) db 0
소스코드 분석
SysCodeSelector equ 0x08
SysDataSelector equ 0x10
VideoSelector equ 0x18
TSSSelector equ 0x20
UserCodeSelector equ 0x28+3
UserDataSelector equ 0x30+3
유저 모드 세그먼트 설정
init.inc이며 UserCodeSelector와 UserDataSelector에 +3이 되어있는 것을 확인할 수 있다.
+3은 해당 세그먼트가 유저 영역이라는 것을 의미한다. (어차피 GDT 디스크립터는 8바이트 씩 늘어나므로 3번째 비트까지는 항상 0이다.)
콜게이트 설정
descriptor7:
dw 0
dw SysCodeSelector
db 0x02
db 0xEC
db 0
db 0
콜게이트 디스크립터이다.
db 0x02를 통해 인수의 개수가 2개인 것을 알 수 있고, P:1(실행 중), DPL:3(유저)인 것을 알 수 있다.
xor eax, eax
lea eax, [printf]
add eax, 0x10000
mov [descriptor7], ax
shr eax, 16
mov [descriptor7+6], al
mov [descriptor7+7], ah
xor eax, eax를 통해 eax를 0으로 만들고, 0이 된 eax에 printf 시작 주소 값이 들어간다.
eax에 0x10000이 더해져 printf의 물리 주소가 들어가게 된다.
eax의 하위 16비트인 ax가 descriptor7의 루틴 오프셋 15~0 에 들어가게 된다.
eax를 16비트만큼 shr(shift right)하여 eax에 상위 16비트에 있는 값이 하위 16비트로 이동하게 된다.
descriptor 6번째 바이트에 ax 하위 8비트 al이, descriptor 7번째 바이트에 ax 상위 8비트 ah가 들어가게 된다.
printf의 물리주소가 들어가게 되어 printf 콜게이트가 완성된다.
lea esp, [PM_Start]
mov ax, TSSSelector
ltr ax
mov [tss_esp0], esp
lea eax, [PM_Start-256]
mov [tss_esp], eax
PM_Start의 시작 주소를 esp에 넣고, esp를 tss의 커널 스택 시작 주소로 설정한다.
그리고 유저 스택의 시작 주소를 PM_Start-256번지로 설정한다. (256바이트는 특별한 기준이 있는 것이 아닌 임의의 숫자이다.)
mov ax, TSSSelector와 ltr ax 코드를 통해 PM_Start에서 태스크 스위칭 발생 시 TSSSelector 세그먼트에 있는 tss를 통해서 이루어질 수 있도록 ltr명령어를 통해 tr 레지스터에 tss 디스크립터 인덱스 값을 삽입한다.
유저 모드로의 태스크 스위칭
mov ax, UserDataSelector
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
lea esp, [PM_Start-256]
push dword UserDataSelector
push esp
push dword 0x200
push dword UserCodeSelector
lea eax, [user_process]
push eax
iretd
데이터 세그먼트에 UserDataSelector를 이용하여 유저 모드로 지정한다.
lea esp, [PM_Start-256] 코드를 통해 esp에 유저 스택 시작 주소를 넣어둔다.
그리고 SS, ESP, EFLAGS, CS, EIP 값들을 유저 스택에 push하고 IRET한다.
즉, CPU의 자동 스위칭 기능을 사용하지 않고, 스택에 유저 모드 데이터를 쌓아놓은 후 인터럽트의 iret명령을 통해 유저 모드 태스크를 실행하는 코드이다.
인터럽트가 발생한 것처럼 꾸며서 태스크 스위칭 효과를 내는 것이다.
콜게이트를 사용한 시스템 콜
user_process:
mov edi, 80*2*7
push edi
lea eax, [msg_user_parameter1]
push eax
call 0x38:0
jmp $
msg_user_parameter1 db "This is User Parameter1", 0
유저 모드 태스크 코드이다.
call 0x38:0 코드를 통해 printf를 호출한다.
0x38은 GDT 인덱스를 의미하는데 GDT 0x38(7*0x8)은 init.inc에 정의되어있지 않다.
하지만 descriptor7의 설정 값은 7번째에 위치하고 있으므로 0x38 gdt는 descriptor7, 즉, 콜게이트 호출이라는 것을 알 수 있다.
80*2*7 값과 msg_user_parameter1의 주소 값을 인수로 받는다. 그리고 콜게이트 디스크립터를 call하게 되면 커널 모드 루틴인 printf:를 수행하게 된다.
해당 코드에서는 printf를 호출하여 This is User Parameter1 문자열을 출력한다.
printf:
mov ebp, esp
push es
push eax
mov ax, VideoSelector
mov es, ax
mov esi, [ebp+8]
mov edi, [ebp+12]
printf: 루틴이다.
현재 user_process에서 0x38:0을 통해 콜게이트를 호출하면서 커널 스택 영역에 인수 2개가 들어와있는 상태이고, 그 상태에서 printf 호출을 통해 ebp가 설정된다.
printf 호출 전에 인수 2개가 넘어왔기 때문에 돌아갈 주소와 세그먼트 8바이트 때문에 ebp+8~ebp+12에 인수 2개가 위치하게 된다.
그리고 코드가 계속해서 실행되며 printf_end의 ret을 통해 user_process로 돌아가게 되고, jmp $ 명령어에 의해 무한 루프를 돌게 된다.
여러 개의 유저 모드 태스크 실행
소스 코드
init.inc
SysCodeSelector equ 0x08
SysDataSelector equ 0x10
VideoSelector equ 0x18
TSSSelector equ 0x20
UserCodeSelector equ 0x28+3
UserDataSelector equ 0x30+3
boot_5_User_Tasks.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
mov es, ax
mov bx, 0
mov ah, 2
mov al, 9
mov ch, 0
mov cl, 2
mov dh, 0
mov dl, 0
int 13h
jc read
mov dx, 0x3F2
xor al, al
out dx, al
cli
mov al, 0x11
out 0x20, al
dw 0x00eb, 0x00eb
out 0xA0, al
dw 0x00eb, 0x00eb
mov al, 0x20
out 0x21, al
dw 0x00eb, 0x00eb
mov al, 0x28
out 0xA1, al
dw 0x00eb, 0x00eb
mov al, 0x04
out 0x21, al
dw 0x00eb, 0x00eb
mov al, 0x02
out 0xA1, al
dw 0x00eb, 0x00eb
mov al, 0x01
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_5_User_Tasks.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]
add eax, 0x10000
mov [descriptor4+2], ax
shr eax, 16
mov [descriptor4+4], al
mov [descriptor4+7], ah
xor eax, eax
lea 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]
times 80 dd 0
PM_Start:
mov bx, SysDataSelector
mov ds, bx
mov es, bx
mov fs, bx
mov gs, bx
mov ss, bx
lea esp, [PM_Start]
cld
mov ax, SysDataSelector
mov es, ax
xor eax, eax
xor ecx, ecx
mov ax, 256
mov edi, 0
loop_idt:
lea esi, [idt_ignore]
mov cx, 8
rep movsb
dec ax
jnz loop_idt
mov edi, 8*0x20
lea esi, [idt_timer]
mov cx, 8
rep movsb
mov edi, 8*0x21
lea esi, [idt_keyboard]
mov cx, 8
rep movsb
mov edi, 8*0x80
lea esi, [idt_soft_int]
mov cx, 8
rep movsb
lidt [idtr]
mov al, 0xFC
out 0x21, al
sti
mov ax, TSSSelector
ltr ax
mov eax, [CurrentTask]
add eax, TaskList
lea edx, [User1regs]
mov [eax], edx
add eax, 4
lea edx, [User2regs]
mov [eax], edx
add eax, 4
lea edx, [User3regs]
mov [eax], edx
add eax, 4
lea edx, [User4regs]
mov [eax], edx
add eax, 4
lea edx, [User5regs]
mov [eax], edx
mov eax, [CurrentTask]
add eax, TaskList
mov ebx, [eax]
jmp sched
scheduler:
lea esi, [esp]
xor eax, eax
mov eax, [CurrentTask]
add eax, TaskList
mov edi, [eax]
mov ecx, 17
rep movsd
add esp, 68
add word [CurrentTask], 4
mov eax, [NumTask]
mov ebx, [CurrentTask]
cmp eax, ebx
jne yet
mov byte [CurrentTask], 0
yet:
xor eax, eax
mov eax, [CurrentTask]
add eax, TaskList
mov ebx, [eax]
sched:
mov [tss_esp0], esp
lea esp, [ebx]
popad
pop ds
pop es
pop fs
pop gs
iretd
CurrentTask dd 0
NumTask dd 20
TaskList: times 5 dd 0
;------------------SubRoutines--------------------
printf:
push eax
push es
mov ax, VideoSelector
mov es, ax
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 es
pop eax
ret
;------------------------User Process Routine ------------------------
user_process1:
mov eax, 80*2*2+2*5
lea ebx, [msg_user_process1_1]
int 0x80
mov eax, 80*2*3+2*5
lea ebx, [msg_user_process1_2]
int 0x80
inc byte [msg_user_process1_2]
jmp user_process1
msg_user_process1_1 db "User process1", 0
msg_user_process1_2 db ".I'm running now.", 0
user_process2:
mov eax, 80*2*2+2*35
lea ebx, [msg_user_process2_1]
int 0x80
mov eax, 80*2*3+2*35
lea ebx, [msg_user_process2_2]
int 0x80
inc byte [msg_user_process2_2]
jmp user_process2
msg_user_process2_1 db "User process2", 0
msg_user_process2_2 db ".I'm running now.", 0
user_process3:
mov eax, 80*2*5+2*5
lea ebx, [msg_user_process3_1]
int 0x80
mov eax, 80*2*6+2*5
lea ebx, [msg_user_process3_2]
int 0x80
inc byte [msg_user_process3_2]
jmp user_process3
msg_user_process3_1 db "User process3", 0
msg_user_process3_2 db ".I'm running now.", 0
user_process4:
mov eax, 80*2*5+2*35
lea ebx, [msg_user_process4_1]
int 0x80
mov eax, 80*2*6+2*35
lea ebx, [msg_user_process4_2]
int 0x80
inc byte [msg_user_process4_2]
jmp user_process4
msg_user_process4_1 db "User process4", 0
msg_user_process4_2 db ".I'm running now.", 0
user_process5:
mov eax, 80*2*9+2*35
lea ebx, [msg_user_process5_1]
int 0x80
mov eax, 80*2*10+2*35
lea ebx, [msg_user_process5_2]
int 0x80
inc byte [msg_user_process5_2]
jmp user_process5
msg_user_process5_1 db "User process5", 0
msg_user_process5_2 db ".I'm running now.", 0
; ---------------- Data Area ------------------
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
dd 0x0000FFFF, 0x00FCFA00
dd 0x0000FFFF, 0x00FCF200
descriptor7:
dw 0
dw SysCodeSelector
db 0x02
db 0xEC
db 0
db 0
gdt_end:
tss:
dw 0, 0
tss_esp0:
dd 0
dw SysDataSelector, 0
dd 0
dw 0, 0
dd 0
dw 0, 0
dd 0
tss_eip:
dd 0, 0
dd 0, 0, 0, 0
tss_esp:
dd 0, 0, 0, 0
dw 0, 0
dw 0, 0
dw 0, 0
dw 0, 0
dw 0, 0
dw 0, 0
dw 0, 0
dw 0, 0
;--------------- User1 Task_Structure -----------------
times 63 dd 0
User1Stack:
User1regs:
dd 0, 0, 0, 0, 0, 0, 0, 0
dw UserDataSelector, 0
dw UserDataSelector, 0
dw UserDataSelector, 0
dw UserDataSelector, 0
dd user_process1
dw UserCodeSelector, 0
dd 0x200
dd User1Stack
dw UserDataSelector, 0
;--------------- User2 Task_Structure -----------------
times 63 dd 0
User2Stack:
User2regs:
dd 0, 0, 0, 0, 0, 0, 0, 0
dw UserDataSelector, 0
dw UserDataSelector, 0
dw UserDataSelector, 0
dw UserDataSelector, 0
dd user_process2
dw UserCodeSelector, 0
dd 0x200
dd User2Stack
dw UserDataSelector, 0
;--------------- User3 Task_Structure -----------------
times 63 dd 0
User3Stack:
User3regs:
dd 0, 0, 0, 0, 0, 0, 0, 0
dw UserDataSelector, 0
dw UserDataSelector, 0
dw UserDataSelector, 0
dw UserDataSelector, 0
dd user_process3
dw UserCodeSelector, 0
dd 0x200
dd User3Stack
dw UserDataSelector, 0
;--------------- User4 Task_Structure -----------------
times 63 dd 0
User4Stack:
User4regs:
dd 0, 0, 0, 0, 0, 0, 0, 0
dw UserDataSelector, 0
dw UserDataSelector, 0
dw UserDataSelector, 0
dw UserDataSelector, 0
dd user_process4
dw UserCodeSelector, 0
dd 0x200
dd User4Stack
dw UserDataSelector, 0
;--------------- User5 Task_Structure -----------------
times 63 dd 0
User5Stack:
User5regs:
dd 0, 0, 0, 0, 0, 0, 0, 0
dw UserDataSelector, 0
dw UserDataSelector, 0
dw UserDataSelector, 0
dw UserDataSelector, 0
dd user_process5
dw UserCodeSelector, 0
dd 0x200
dd User5Stack
dw UserDataSelector, 0
idtr:
dw 256*8-1
dd 0
; ----------------- Intterupt Service Routines ------------------
isr_ignore:
push gs
push fs
push es
push ds
pushad
mov ax, SysDataSelector
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov al, 0x20
out 0x20, al
mov edi, (80*2*0)
lea esi, [msg_isr_ignore]
call printf
inc byte [msg_isr_ignore]
jmp ret_from_int
isr_32_timer:
push gs
push fs
push es
push ds
pushad
mov ax, SysDataSelector
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov al, 0x20
out 0x20, al
mov edi, 80*2*0
lea esi, [msg_isr_32_timer]
call printf
inc byte [msg_isr_32_timer]
jmp ret_from_int
isr_33_keyboard:
push gs
push fs
push es
push ds
pushad
mov ax, SysDataSelector
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
in al, 0x60
mov al, 0x20
out 0x20, al
mov edi, (80*2*0)+(2*35)
lea esi, [msg_isr_33_keyboard]
call printf
inc byte [msg_isr_33_keyboard]
jmp ret_from_int
isr_128_soft_int:
push gs
push fs
push es
push ds
pushad
push eax
mov ax, SysDataSelector
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
pop eax
mov edi, eax
lea esi, [ebx]
call printf
jmp ret_from_int
ret_from_int:
xor eax, eax
mov eax, [esp+52]
and eax, 0x00000003
xor ebx, ebx
mov bx, cs
and ebx, 0x00000003
cmp eax, ebx
ja scheduler
popad
pop ds
pop es
pop fs
pop gs
iret
msg_isr_ignore db "This is and ignorable interrupt", 0
msg_isr_32_timer db ".This is the timer interrupt", 0
msg_isr_33_keyboard db ".This is the keyboard interrupt", 0
msg_isr_128_soft_int db ".This is the soft_int interrupt", 0
;------------------ IDT ------------------
idt_ignore:
dw isr_ignore
dw 0x08
db 0
db 0x8E
dw 0x0001
idt_timer:
dw isr_32_timer
dw 0x08
db 0
db 0x8E
dw 0x0001
idt_keyboard:
dw isr_33_keyboard
dw 0x08
db 0
db 0x8E
dw 0x0001
idt_soft_int:
dw isr_128_soft_int
dw 0x08
db 0
db 0xEF
dw 0x0001
times 4608-($-$$) db 0
위 코드를 컴파일하여 실행하면 다음과 같이 동작한다.

I'm running now 앞의 문자가 빠르게 바뀌게 된다.
소스코드 분석
소스코드 분석에 앞서 이 코드는 다음과 같이 작동한다.
먼저 32bit 전환 후 PM_Start 루틴이 실행된다.
- PM_Start의 jmp sched를 통해 user_process1 시작
- sched 루틴의 ebx는 User1regs를 가리키고 User1regs의 EIP는 user_process1 주소를 저장하고 있음
- pop, iret을 통해 User1regs의 17개 레지스터 모두 CPU에 로드
- ↓ 타이머 인터럽트 발생
- 커널 진입 및 레지스터 임시 저장
- isr_32_timer 인터럽트 핸들러 수행 → PUSH하여 유저모드 레지스터 커널 스택에 저장
- ret_from_int 수행하여 CS 레지스터 확인을 통해 인터럽트 전 태스크가 유저모드인지 확인, 유저모드면 scheduler: 루 틴 수행, 아니면 POP 수행
- ↓
- scheduler 루틴 수행
- 커널 스택에 있는 유저 레지스터 값을 User1regs에 저장
- 커널 스택은 인터럽트 전 위치로 esp 이동
- CurrentTask를 4 증가시켜 20인지 확인 후 20이 아니면 yet: 루틴으로 jmp
- ↓
- yet 루틴 수행
- eax에는 User2regs 시작 주소가 있음
- ↓
- sched 루틴 수행
- user_process2를 실행하게 됨
위 과정을 반복하여 user_process5까지 진행 후 CurrentTask가 0으로 초기화되며 무한 반복한다.
user_process1:
mov eax, 80*2*2+2*5
lea ebx, [msg_user_process1_1]
int 0x80
mov eax, 80*2*3+2*5
lea ebx, [msg_user_process1_2]
int 0x80
inc byte [msg_user_process1_2]
jmp user_process1
msg_user_process1_1 db "User process1", 0
msg_user_process1_2 db ".I'm running now.", 0
프로그램이 실행된 초기에 sched 루틴을 통해 User1regs에 저장된 데이터를 참조하여 user_process1이 실행된 상황이다.
이때 int 0x80을 통해 인터럽트가 실행된다. 해당 인터럽트는 트랩 게이트를 통한 소프트웨어 인터럽트인데, 인터럽트 도중 인터럽트가 들어올 수 있다.
IDT 디스크립터를 메모리에 복사하는 0x80번 인터럽트는 idt_soft_int라는 디스크립터이고 해당 디스크립터는 isr_128_soft_int라는 인터럽트 핸들러를 가리키고 있다.
isr_128_soft_int 인터럽트 핸들러는 call printf를 수행한다.
따라서 int 0x80은 유저 모드에서 커널 모드로 진입하기 위한 소프트웨어 인터럽트 방식의 시스템 콜 구현체다.
isr_32_timer:
push gs
push fs
push es
push ds
pushad
mov ax, SysDataSelector
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov al, 0x20
out 0x20, al
mov edi, 80*2*0
lea esi, [msg_isr_32_timer]
call printf
inc byte [msg_isr_32_timer]
jmp ret_from_int
user_process가 실행되는 도중 타이머 인터럽트로 인해 isr_32_timer가 동작하게 된다.
즉, 유저모드에서 커널모드로 전환되고 태스크 스위칭이 발생한다.
인터럽트 핸들러는 다음과 같이 동작한다.
세그먼트와 레지스터 값들을 스택에 저장하고 커널 데이터 세그먼트 값으로 변경한다.
그리고 PIC를 초기화하여 다음 인터럽트를 받을 수 있다는 신호를 보낸다.
그리고 문자열을 표시한 뒤 첫 번째 문자열의 ASCII 코드 값을 1 증가시킨다.
그리고 ret_from_int로 점프하게 된다.
ret_from_int:
xor eax, eax
mov eax, [esp+52]
and eax, 0x00000003
xor ebx, ebx
mov bx, cs
and ebx, 0x00000003
cmp eax, ebx
ja scheduler
popad
pop ds
pop es
pop fs
pop gs
iret
ret_from_int는 세그먼트 셀렉터 값의 RPL을 참조하여 인터럽트 전 권한이 유저 모드인지 커널 모드인지 확인한다.
유저 영역 루틴 수행 중에 발생한 인터럽트라면 태스크 스위칭을 해야하기 때문에 scheduler로 점프한다.
ret_from_int 루틴은 다음과 같이 동작한다.
xor eax, eax
eax를 0으로 초기화한다.
mov eax, [esp+52]
and eax, 0x00000003
현재 커널 스택의 esp+52 지점에는 인터럽트 전 태스크의 CS의 값이 저장되어있고, CS에는 세그먼트 셀렉터 값이 저장되어있다.
세그먼트 셀렉터의 값과 0x00000003을 AND 연산하게 되는데, 세그먼트 셀렉터의 하위 2비트는 RPL 값이므로 2진수 0011(0x00000003)과 AND 연산을 함으로써 세그먼트 셀렉터의 RPL이 유저모드 레벨인지 확인이 가능하게 된다.
ebx에는 현재 실행되고 있는 인터럽트의 CS 레지스터 값이 들어가게 된다.(커널 유저 레벨)
이 또한 xor ebx, ebx를 통해 ebx 또한 0으로 초기화 한 뒤, 현재 CS 레지스터 값(현재 세그먼트 셀렉트 값)의 RPL을 확인한다.
cmp eax, ebx
ja scheduler
cmp eax, ebx를 통해 eax의 값이 크면(유저모드면) scheduler 루틴으로 점프하게 된다. 물론 작으면 pop을 통해 유저 모드의 레지스터와 세그먼트들을 CPU에 로드한 후 iret를 통해 복귀한다.
scheduler:
lea esi, [esp]
xor eax, eax
mov eax, [CurrentTask]
add eax, TaskList
mov edi, [eax]
mov ecx, 17
rep movsd
add esp, 68
add word [CurrentTask], 4
mov eax, [NumTask]
mov ebx, [CurrentTask]
cmp eax, ebx
jne yet
mov byte [CurrentTask], 0
해당 루틴은 유저 모드 태스크의 레지스터 값을 저장하는 루틴이다
코드는 다음과 같다.
lea esi, [esp]
현재 스택 포인터를 출발지로 설정한다.
xor eax, eax
mov eax, [CurrentTask]
add eax, TaskList
mov edi, [eax]
eax에 변수 CurrentTask의 값을 넣고 TaskList의 주소 값을 더한다.
그리고 edi에 eax 값(User1regs 주소 = CurrentTask 값 + TaskList 주소 값)을 저장한다.
mov ecx, 17
rep movsd
add esp, 68
해당 코드는 레지스터, 세그먼트, EIP, CS, EFLAGS 등 17개의 레지스터를 지정된 esi에서 edi까지(User1regs) 4바이트씩 17번 복사한다.
그리고 커널 스택 영역을 68(17 * 4) 바이트 만큼 되돌린다.
add word [CurrentTask], 4
mov eax, [NumTask]
mov ebx, [CurrentTask]
cmp eax, ebx
jne yet
mov byte [CurrentTask], 0
해당 코드는 라운드 로빈 방식을 사용하며, CurrentTask에 4를 더하고 CurrentTask와 NumTask(20)의 값이 같지 않으면 yet루틴으로 점프하게 된다.
CurrentTask가 16일 때 user_process5이므로 20이 되면 0으로 초기화한다.
yet:
xor eax, eax
mov eax, [CurrentTask]
add eax, TaskList
mov ebx, [eax]
해당 코드를 통해 ebx에는 TaskList 시작 주소 + CurrentTask의 값이 저장된다.
sched:
mov [tss_esp0], esp
lea esp, [ebx]
popad
pop ds
pop es
pop fs
pop gs
iretd
mov [tss_esp0], esp
아직까지는 커널 모드이므로 tss의 esp0에 커널 스택 포인터를 저장한다.
lea esp, [ebx]
그리고 yet 루틴에서 저장했던 ebx(TaskList 시작 주소 + CurrentTask, 즉, User2regs 주소 값)를 esp에 삽입한다.
그리고 pop 명령어를 통해 esp에 저장된 User2regs의 시작 주소에서 레지스터와 세그먼트 값들을 CPU에 로드하고 iret을 통해 EIP, CS, EFLAGS등의 값들을 CPU에 로드하게 된다.
'OS > 만들면서 배우는 OS 커널의 구조와 원리' 카테고리의 다른 글
| 8-2. 페이징 - 페이징 구현 (0) | 2026.04.23 |
|---|---|
| 8-1. 페이징 - A20 게이트 (0) | 2026.04.21 |
| 6. 보호 (0) | 2026.04.13 |
| 5. 태스크 스위칭, 문맥 교환 (0) | 2026.04.13 |
| 4. 인터럽트와 예외 (0) | 2026.04.10 |