GeekOS 소스 분석(2) - bootsect.asm
GeekOS 소스 분석(2) - bootsect.asm
작성자 : 강대명
작성일 : 2003-07-05
bootsect.asm 에서는 이제 실제로 BIOS가 MBR을 읽어서 메모리에 올려주고, 거기서
부터 흐름이 시작된다. 즉 실제로 부팅이 되는 부분이라고 보면 된다.
; Boot sector for GeekOS
; Copyright (c) 2001, David H. Hovemeyer <daveho at cs.umd.edu>
; $Revision: 1.6 $
; This is free software. You are permitted to use,
; redistribute, and modify it as specified in the file "COPYING".
; Loads setup code and a program image from sectors 1..n of a floppy
; and executes the setup code (which will in turn execute
; the program image).
; Some of this code is adapted from Kernel Toolkit 0.2
; and Linux version 2.2.x, so the following copyrights apply:
; Copyright (C) 1991, 1992 Linus Torvalds
; modified by Drew Eckhardt
; modified by Bruce Evans (bde)
; adapted for Kernel Toolkit by Luigi Sgro
%include "defs.asm"
실제로 GeekOS가 부팅시에 어떻게 동작하는 지에 대해서 알게 해준다. 커널의 경우
BOOTSECTOR(bootsect.asm) + SETUPSECTOR(setup.asm) + 커널 이미지로 구성
되어 있다.
KERN_START_SEC equ (NUM_SETUP_SECTORS + 1)
; ----------------------------------------------------------------------
; The actual code
; ----------------------------------------------------------------------
16bit 코드를 생성하고 그 코드는 0 바이트 부터 시작한다라는 뜻이다.
[BITS 16]
[ORG 0x0]
BeginText: ; needed to calculate padding bytes to fill the sector
; Copy the boot sector into INITSEG.
먼저 BOOTSEG 주소의 데이터를 INITSEG 만큼으로 복사한다. 이것은 처음
부팅시 위치가 07C0:0000 즉 7c00 번지에 있기 때문이다. 이 앞 부분은
BIOS가 알아서 하드의 MBR을 읽어서 7c00 으로 복사를 한다. 한 섹터
이므로 256을 cx에 넣는다. 즉 한번에 WORD( 2byte ) 단위로 복사한다.
mov ax, BOOTSEG
mov ds, ax ; source segment for string copy
xor si, si ; source index for string copy
mov ax, INITSEG
mov es, ax ; destination segment for string copy
xor di, di ; destination index for string copy
cld ; clear direction flag
mov cx, 256 ; number of words to copy
rep movsw ; copy 512 bytes
그리고 INITSEG 위치의 after_move 로 점프한다. 그냥 INITSEG로
점프하면 무슨일이 벌어질까? 앞 부분의 복사 부분만 쭉쭉, 쭉쭉
계속 일어날 것이다. 즉 무한루프에 빠지게 된다.
jmp INITSEG:after_move
after_move:
; Now we're executing in INITSEG
; We want the data segment to refer to INITSEG
; (since we've defined variables in the same place as the code)
주석을 읽어보면 알겠지만, ax는 앞에서 INITSEG 로 지정되어 있다.
ds(data segment) 역시 INITSEG로 지정한다.
mov ds, ax ; ax still contains INITSEG
; Put the stack in the place where we were originally loaded.
; By definition, there is nothing important there now.
스택 세그먼트를 0으로 지정한다. 그리고 스택 포인터는
07c0 << 4 + 512 - 2 즉 7DFE 가 된다.(음냐 계산이 정확한지는 모름 -_-)
스택은 그냥 안쓰는 부분을 지정하면 된다고 생각한다. 어디서 봤는지 기억이
나지는 않는데, 스택을 잡을 때, 스택 프레임에 걸치게 하면 안된다고 했던것
같다.(이 부분은 확실해 지면, 수정을 ^^) 대충 512에 맞추게 패딩을 한다고
생각하면 된다.
mov ax, 0
mov ss, ax
mov sp, (BOOTSEG << 4) + 512 - 2
load_setup:
; Load the setup code.
sec_count 의 주소에 1을 넣는다. 여기서 잘못보면 sec_count 의 값이
0 이므로 절대 주소 0에 데이터를 집어넣는다고 생각하기 쉬운데, DS는
우리가 앞에 INITSEG 으로 지정했다. 즉 INITSEG:0 에 값을 넣었다고
생각하자.
mov word [sec_count], 1
.again:
이 부분은 ReadSector를 call 하기 위해서 파라매터를 설정하는 부분이다.
C에서 파라매터 넘기는 것을 생각하면 된다. 먼저 sec_count를 넘겨주고
SETUPSEG 를 넘겨준다. 그리고 3번째는 ax에 512를 곱해서 넘겨준다.
섹터를 바이트로 계산하기 위해서이다. ReadSector 의 코드 앞에 주석으로
각각이 뭘 의미하는지 나온다.
mov ax, [sec_count]
push ax ; 1st param to ReadSector (log sec num)
push word SETUPSEG ; 2nd param to ReadSector (seg base)
dec ax ; convert to 0-indexed
shl ax, 9 ; multiply by 512
push ax ; ...to get 3rd param (byte offset)
; read the sector from the floppy
이제 ReadSector를 호출한다.
call ReadSector
위에서 3개의 파라매터를 넘겨줬으므로 스택에서 제거한다.
이런 호출 방식은 C에서 기본적으로 사용하는 _cdecl 방식이다.
add sp, 6 ; clear 3 word params
; on to next sector
sec_count의 값을 1 증가시킨다.
inc word [sec_count]
; are we done?
필요한 섹터를 다 읽었으면, load_kernel로 이동한다.
즉 sec_count에 저장된 값과 NUM_SETUP_SECTORS 가 동일하면 된다.
cmp word [sec_count], NUM_SETUP_SECTORS
jle .again
load_kernel:
; Load the kernel image from sectors KERN_START_SEC..n of the
; floppy into memory at KERNSEG. Note that there are 128 sectors
; per 64K segment. So, when figuring out which segment to
; load the sector into, we shift right by 7 bits (which is
; equivalent to dividing by 128).
sec_count 에 KERN_START_SEC 값을 저장한다.
mov word [sec_count], KERN_START_SEC
.again:
앞에 setup image를 메모리에 올린것 처럼, 이제 KERNEL 이미지를 읽어서
KERN_START_SEC 에 올려둔다.
mov ax, [sec_count] ; logical sector on the floppy
push ax ; 1st param to ReadSector (log sec num)
sub ax, KERN_START_SEC ; convert to 0-indexed
mov cx, ax ; save in cx
shr ax, 7 ; divide by 128
shl ax, 12 ; ...and multiply by 0x1000
add ax, KERNSEG ; ...to get base relative to KERNSEG
push ax ; 2nd param to ReadSector (seg base)
and cx, 0x7f ; mod sector by 128
shl cx, 9 ; ...and multiply by 512
push cx ; to get offset in segment (3rd parm)
; read the sector from the floppy
call ReadSector
add sp, 6 ; clear 3 word params
; on to next sector
inc word [sec_count]
; have we loaded all of the sectors?
cmp word [sec_count], KERN_START_SEC+NUM_KERN_SECTORS
jl .again
; Now we've loaded the setup code and the kernel image.
; Jump to setup code.
그리고 아까 저장한 SETUPSEG 먼트로 이동한다. 여기서 부터가 setup.asm
의 시작이라고 보면된다.
jmp SETUPSEG:0
; Read a sector from the floppy drive.
; This code (and the rest of this boot sector) will have to
; be re-written at some point so it reads more than one
; sector at a time.
;
; Parameters:
; - "logical" sector number [bp+8]
; - destination segment [bp+6]
; - destination offset [bp+4]
ReadSector:
ReadSector 부분은 쉽기 때문에(설명하기 귀찮아서 그냥 넘어간다.
핵심만 이해하자 섹터를 읽어서 원하는 세그먼트의(2번째 파라매터 ),
오프셋(3번째 파라매터)에 저장한다. 현재의 스택 프레임을 저장하고,
sp를 bp에 넣는다.
push bp ; set up stack frame
mov bp, sp ; "
pusha 는 모든 레지스터를 저장한다.
pusha ; save all registers
%if 0
; debug params
mov dx, [bp+8]
call PrintHex
call PrintNL
mov dx, [bp+6]
call PrintHex
call PrintNL
mov dx, [bp+4]
call PrintHex
call PrintNL
%endif
아까 스택에 넣어둔 파라매터들을 꺼낸다. bp + 8의 이유는 3개의
파라매터가 3 * 2 로 6을 차지하고 거기에 call이 끝난뒤의 리턴 어드레스가
들어가기 때문이다.
; Sector = log_sec % SECTORS_PER_TRACK
; Head = (log_sec / SECTORS_PER_TRACK) % HEADS
mov ax, [bp+8] ; get logical sector number from stack
xor dx, dx ; dx is high part of dividend (== 0)
mov bx, SECTORS_PER_TRACK ; divisor
div bx ; do the division
mov [sec], dx ; sector is the remainder
and ax, 1 ; same as mod by HEADS==2 (slight hack)
mov [head], ax
; Track = log_sec / (SECTORS_PER_TRACK*HEADS)
mov ax, [bp+8] ; get logical sector number again
xor dx, dx ; dx is high part of dividend
mov bx, SECTORS_PER_TRACK*2 ; divisor
div bx ; do the division
mov [track], ax ; track is quotient
%if 0
; debugging code
mov dx, [sec]
call PrintHex
call PrintNL
mov dx, [head]
call PrintHex
call PrintNL
mov dx, [track]
call PrintHex
call PrintNL
%endif
; Now, try to actually read the sector from the floppy,
; retrying up to 3 times.
mov [num_retries], byte 0
.again:
mov ax, [bp+6] ; dest segment...
mov es, ax ; goes in es
mov ax, (0x02 << 8) | 1 ; function = 02h in ah,
; # secs = 1 in al
mov bx, [track] ; track number...
mov ch, bl ; goes in ch
mov bx, [sec] ; sector number...
mov cl, bl ; goes in cl...
inc cl ; but it must be 1-based, not 0-based
mov bx, [head] ; head number...
mov dh, bl ; goes in dh
xor dl, dl ; hard code drive=0
mov bx, [bp+4] ; offset goes in bx
; (es:bx points to buffer)
; Call the BIOS Read Diskette Sectors service
BIOS interrupt 13 번을 이용해서 섹터를 읽는다.
int 0x13
; If the carry flag is NOT set, then there was no error
; and we're done.
jnc .done
; Error - code stored in ah
mov dx, ax
call PrintHex
inc byte [num_retries]
cmp byte [num_retries], 3
jne .again
; If we got here, we failed thrice, so we give up
mov dx, 0xdead
call PrintHex
.here: jmp .here
.done:
리턴되기 전에 사용한 스택만큼 비워준다. 이제 스택에는 리턴 어드레스가
남는다. 보통 이 리턴 어드레스를 바꿔서 오버플로우라는 해킹 기법이
많이 발생한다.
popa ; restore all regisiters
pop bp ; leave stack frame
ret
; Include utility routines
%include "util.asm"
; ----------------------------------------------------------------------
; Variables
; ----------------------------------------------------------------------
; These are used by ReadSector
head: dw 0
track: dw 0
sec: dw 0
num_retries: db 0
; Used for loops reading sectors from floppy
sec_count: dw 0
EndText:
이제 부트섹터를 512로 맞춰줄 필요가 있다. 부트 섹터라는 표시로 0xAA55 가 들어간다.
사실 매직넘버인 0xAA55 의 경우 512 단위로 맞추는 것이 아니라, 섹터 단위로 맞추어진
다. 일반적으로 우리가 사용하는 섹터의 크기가 512일 뿐이다. 더 크다면, 그 끝이 항상
0xAA55로 설정되야 한다.
FillSector times (510 - (EndText - BeginText)) db 0
Signature dw 0xAA55 ; BIOS controls this to ensure this is a boot sector
댓글 달기