Sqix

64bit 멀티코어 OS 제작하기 [4] - 2 : OS 이미지 로딩을 위한 부트로더 기능 추가 (2) 본문

MINT64 OS

64bit 멀티코어 OS 제작하기 [4] - 2 : OS 이미지 로딩을 위한 부트로더 기능 추가 (2)

Sqix_ow 2018. 5. 28. 15:51

※ 저번 주 공군 면접과 RCTF, 제 1회 KYSIS 해커톤 참여를 하게 된 관계로 포스팅이 늦어졌습니다. 죄송합니다.


이 글은 x86 architecture의 함수 호출 규약에 대해 다룹니다.


함수 호출 규약은 서브루틴이 caller에게서 변수를 받거나 반환하는지에 대해서 규약을 지정한 것입니다. 각 아키텍쳐마다 사용하는 호출 규약이 다릅니다. 우리는 x86 아키텍쳐에서 OS를 개발하고 있기 때문에, 이에 맞는 호출 규약에 대해서 다루고자 합니다. 


우리가 호출 규약에 대해서 알기 위해서는 먼저 호출자(Caller)와 피호출자(Callee)에 대해서 알아야 합니다. 간단합니다. 프로그램 실행 흐름에서, 호출하는 함수를 Caller라고 부르고 호출당하는 함수를 Callee라고 부릅니다. 예를 들어 보겠습니다.


int Add(int iA, int iB, int iC)

{

return iA + iB + iC;

}


void main(void)

{

int iReturn;

iReturn = Add(1, 2, 3);

}   


다음 코드는 어떻게 실행이 될까요? 의사 코드로 대략적으로 작성했습니다만, main함수의 코드가 실행이 되다 iReturn의 값을 가져올 때 Add 함수를 호출합니다. 즉, main()함수는 Add()를 호출합니다. 여기서 Add()를 호출하는 main()함수는 호출자(Caller)가 되고, 호출당하는 Add()는 피호출자(Callee)가 되는 것입니다. 


호출자와 피호출자에 대해 알아야 하는 이유는, 호출 규약이 Caller가 스택을 정리하는가? 혹은 Callee가 스택을 정리하는가?에 따라 나뉘기 떄문입니다. 대표적으로 X86 아키텍쳐에서 사용하는 호출 규약은 stdcall, cdecl, fastcall이 있습니다. stdcall과 fastcall은 Callee가 스택을 정리하고, cdecl은 Caller가 스택을 정리합니다.


(※이 외에도 optlink, pascal, safecall, thiscall 등이 있으니 궁금하신 분은 찾아보시기 바랍니다.)


위 Add함수를 예시로 들어 세 호출 규약에 대해 자세히 알아보겠습니다.


CDECL


cdecl은 C Declaration의 약자로, C언어에서 나온 호출 규약입니다. 매우 많은 C 컴파일러들이 해당 호출 규약을 이용하여 컴파일을 하고 있습니다. cdecl 호출 규약은 파라미터의 오른쪽에서 왼쪽 방향으로, 위 함수에서는 iC -> iB -> iA의 순서로 스택에 집어넣습니다. 즉, main에서 파라미터를 넣을 때 push 3 -> push 2 -> push 1의 순서로 스택에 값을 넣는다는 것이죠. 또한, 함수에서 값을 리턴할 때 AX 레지스터에 값을 넣어 반환합니다.


cdecl 규약은 Caller가 스택을 정리하는 규약이기 때문에, 스택을 정리하는 add esp, 12(사용한 파라미터가 3개이기 때문에 32비트 레지스터 크기인 4 * 3 == 12) 코드는 함수가 호출되고, 반환값을 저장한 후 main에서 등장하게 됩니다.


전체 어셈블리 코드는 다음과 같습니다.


Add:

push    ebp

mov    ebp,    esp


mov    eax,    dword [ ebp + 8 ]

add    eax,    dword [ ebp + 12 ]

add    eax,    dword [ ebp + 16 ]


pop    ebp

ret


Main:

push    ebp

mov    ebp,    esp


sub    esp,    8

push    3

push    2

push    1

call    Add

mov    dword [ ebp - 4 ],    eax

add    esp, 12


ret


STDCALL


stdcall 방식은 Standard Call의 약자로, cdecl과 같이 오른쪽에서 왼쪽으로 파라미터를 스택에 넣습니다. 역시 반환값은 AX에 담으며, 단지 호출된 함수에서(Callee) 스택을 정리합니다. 


Add:

push    ebp

mov    ebp,    esp


mov    eax,    dword [ ebp + 8 ]

add    eax,    dword [ ebp + 12 ]

add    eax,    dword [ ebp + 16 ]


pop    ebp

ret    12


Main:

push    ebp

mov    ebp,    esp


sub    esp,    8

push    3

push    2

push    1

call    Add

mov    dword [ ebp - 4 ],    eax


ret


Add 함수의 ret 12 명령어는 ret / add esp, 12와 같은 역할을 합니다.


FASTCALL


fastcall은 따로 표준이 있는 규약은 아니며, 컴파일러를 제조하는 업체마다 조금씩 다른 규약을 가지게 됩니다. 여기서는 MS의 컴파일러를 기준으로 설명하였는데, VC 컴파일러를 기준으로 설명되어 있습니다. VC 컴파일러의 fastcall은 왼쪽에서 두 개의 파라미터를 스택에 넣지 않고 cx, dx에 저장한다는 점이 stdcall과 다른 점입니다.


Add:

push    ebp

mov    ebp,    esp


mov    eax,    ecx

add    eax,    edx

add    eax,    dword [ ebp + 8 ]


pop    ebp

ret    4


Main:

push    ebp

mov    ebp,    esp


sub    esp,    8

push    3

mov    edx, 2

mov    ecx, 1

call    Add

mov    dword [ ebp - 4 ],    eax


ret


함수 호출 규약에 대해서 알아보았습니다. 부트 로더를 작성할 때에는 이 셋 중 cdecl(Caller가 스택 정리) 호출 규약을 이용하여 작성해 보도록 하겠습니다. 


[ORG 0x00] ; Code start address : 0x00

[BITS 16] ; 16-bit environment


SECTION.text ; text section(Segment)


jmp 0x07C0:START ; copy 0x07C0 to cs, and goto START


TOTALSECTORCOUNT: dw 1024


START:

mov ax, 0x07C0 ; convert start address to 0x07C0

mov ds, ax ; set ds register

mov ax, 0xB800 ; base video address

mov es, ax ; set es register(video address)


;STACK Generate code (0x0000:0000~ 0x0000:FFFF, 64KB)

mov ax, 0x0000

mov ss, ax ;

mov sp, 0xFFFE ;

mov bp, 0xFFFE ;


;SI Reg(string index) initialize

mov si, 0


.SCREENCLEARLOOP:

mov byte [ es: si ], 0 ; delete character at si index

mov byte [ es: si + 1 ], 0x0A ; copy 0x0A(black / green)

add si, 2 ; go to next location

cmp si, 80 * 25 * 2 ; compare si and screen size

jl .SCREENCLEARLOOP ; end loop if si == screen size


push MESSAGE1 ; push message's address

push 0 ; Y val

push 0 ; X val

call PRINTMESSAGE ; call function

add sp, 6 ; cdecl convention

push IMAGELOADINGMESSAGE ; push image loading message's address

push 1 ; Y val(1)

push 0 ; X val(0)

call PRINTMESSAGE ; call function

add sp, 6 ; cdecl convention


RESETDISK:

;BIOS Reset Function : service number 0, drive number 0 (Floppy)

mov ax, 0

mov dl, 0

int 0x13

jc HANDLEDISKERROR


;Read Sector from disk

mov si, 0x1000 ;set si : 0x1000, bx : 0x0000 -> 0x1000:0000

mov es, si

mov bx, 0x0000


mov di, word [ TOTALSECTORCOUNT ]


READDATA:

cmp di, 0 ; check remaining sectors

je READEND ; jump if di == 0 (ZF is set)

sub di, 0x1 ; di-- (if sectors remain)

; Call BIOS Read Function

mov ah, 0x02 ; Service Number : 0x02(Read Sector)

mov al, 0x1 ; Read 1 Sector 

mov ch, byte [ TRACKNUMBER ] ; set Track Number

mov cl, byte [ SECTORNUMBER ] ; set Sector Number

mov dh, byte [ HEADNUMBER ] ; set Head Number

mov dl, 0x00 ; set Drive Number(Floppy)

int 0x13 ; interrupt

jc HANDLEDISKERROR ; Error Handling (if CF == 1)


; Calculate copy, track, head, sector address

add si, 0x0020 ; convert 512 byte to seg reg

mov es, si ; increase address amount of 1 sector

mov al, byte [ SECTORNUMBER ] ; SECTORNUMBER++

add al, 0x01

mov byte [ SECTORNUMBER ], al

cmp al, 19 ; if (SECTORNUMBER <= 18)

jl READDATA ; Sign flag is opposite with Overflow Flag


xor byte [ HEADNUMBER ], 0x01 ; toggle head number

mov byte [ SECTORNUMBER ], 0x01 ; set Sector number to 0x01

cmp byte [ HEADNUMBER ], 0x00 ; cmp 0x00 & head number

jne READDATA ; head num != 0 -> loop


add byte [ TRACKNUMBER ], 0x01 ; increase track number 1

jmp READDATA ; loop


READEND:

push LOADINGCOMPLETEMESSAGE ; loading complete!

push 1 ; Y location

push 20 ; X location

call PRINTMESSAGE

add sp, 6 ; cdecl convention

; execute os image

jmp 0x1000:0000


HANDLEDISKERROR:

push DISKERRORMESSAGE

push 1

push 20

call PRINTMESSAGE

jmp $


PRINTMESSAGE:

push bp

mov bp, sp ; stack frame

push es

push si

push di

push ax

push cx

push dx

mov ax, 0xB800 ; video memory start addr

mov es, ax ; set es 

; get line addr

mov ax, word [ bp + 6 ] ; set ax with y location

mov si, 160 ; set si with the size of the line (160b)

mul si ; y location * line size (save on ax)

mov di, ax ; set y address on di


; get video memory address

mov ax, word [ bp + 4 ]

mov si, 2

mul si

add di, ax ; y location + x location

mov si, word [ bp + 8 ] ; string address (param 03)

.MESSAGELOOP:

mov cl, byte [ si ] ; copy character which is on the adress MESSAGE1's addr + SI register's value

cmp cl, 0 ; compare the character and 0 

je .MESSAGEEND ; if value is 0 -> string index is out of bound -> finish the routine


mov byte [ es : di ], cl ; if value is not 0 -> print the character on 0xB800 + di

add si, 1 ; go to next index

add di, 2 ; go to next video address

jmp .MESSAGELOOP ; loop code


.MESSAGEEND:

pop dx

pop cx

pop ax

pop di

pop si

pop es

pop bp

ret


MESSAGE1: db 'MINT64 OS Boot Loader Start~!!', 0 ;define the string that I want to print


DISKERRORMESSAGE: db 'DISK Error~!!', 0

IMAGELOADINGMESSAGE: db 'OS Image Loading...', 0

LOADINGCOMPLETEMESSAGE: db 'Complete~!!', 0


SECTORNUMBER: db 0x02

HEADNUMBER: db 0x00

TRACKNUMBER: db 0x00


times 510 - ($ - $$) db 0x00 ; $ : current line's addr, $$ : current section's base address -> $ - $$ : Offset!

; 510 - ( $ - $$ ) : offset to addr 510, db - 0x00 : declare 1byte and init to 0x00

; time : loop -> fill 0x00 from current address to 510


db 0x55 ; declare 1 byte and init to 0x55

db 0xAA ; declare 1 byte and init to 0xAA

; Address 511 : 0x55, 512 : 0xAA -> declare that this sector is boot sector





# qemu-system-x86_64 -m 64 -fda ./Disk.img -localtime -M pc


위 명령어를 통해 실행을 하여 보아도, 당연히 검은 화면만 나오게 됩니다.


어찌 보면 당연한 일입니다. OS 이미지가 없기 때문이죠.


부트로더를 작성하였으니, 이제 OS 이미지를 제작하여 테스트를 해 보도록 하겠습니다.



Comments