일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- #Best of the Best #OS #MINT64 #Sqix
- vim
- KASAN
- Find
- ftz
- 오버워치
- vi 외부 명령어
- #IntelManual #segment Descriptor #세그먼트 디스크립터 #MINT64 #Sqix
- #MINT64 #Sqix
- vi
- 오버워치 세이버메트릭스
- >
- command
- BEST of the BEST
- #Qt Creator
- #IntelManual
- Sqix
- C++11
- libpcap
- Overwatch League SaberMetrics
- Network
- linux
- #
- FTZ 레벨2
- libtins
- 인터럽트
- Today
- Total
Sqix
64bit 멀티코어 OS 제작하기 [4] - 2 : OS 이미지 로딩을 위한 부트로더 기능 추가 (2) 본문
※ 저번 주 공군 면접과 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 이미지를 제작하여 테스트를 해 보도록 하겠습니다.
'MINT64 OS ' 카테고리의 다른 글
Qt Creator 환경에서 진행하기 (0) | 2018.05.29 |
---|---|
64bit 멀티코어 OS 제작하기 [4] - 3 : 부트 로더 테스트용 가상 이미지 제작 (0) | 2018.05.28 |
64bit 멀티코어 OS 제작하기 [4] - 1 : OS 이미지 로딩을 위한 부트로더 기능 추가 (1) (0) | 2018.05.16 |
64bit 멀티코어 OS 제작하기 [3] - 3 : 부트로더 화면 제어 (0) | 2018.05.14 |
64bit 멀티코어 OS 제작하기 [3] - 2 : 가장 기초적인 부트 로더 제작 (0) | 2018.05.14 |