Bootloader 의 Stage 1 에서 사용된다. ; ; Floppy diskette 의 첫 번째 섹터 512 bytes 에 기록되는 코드로써 ; BIOS 에 의해 0x7c00 번지에 로딩되어 실행된다. ; FAT12 를 위해 BPB 를 포함하고 있다. ; (BPB: Boot Parameter Block) ; ;.
더 자세히 ...
Bootloader 의 Stage 1 에서 사용된다. ; ; Floppy diskette 의 첫 번째 섹터 512 bytes 에 기록되는 코드로써 ; BIOS 에 의해 0x7c00 번지에 로딩되어 실행된다. ; FAT12 를 위해 BPB 를 포함하고 있다. ; (BPB: Boot Parameter Block) ; ;.
2. Stage 1 Boot Loader
OS 를 개발하기 위해서는 가장 먼저 전원이 인가되는 시점부터, 어떻게 H/W 가 초기화 되면서 동작을 시작하는 지를 알아야 한다. 그래야 어떤 시점에, 어떤 코드를 어떻게 해줘야 하는지를 알 수 있기 때문이다.
이 단계는 S/W Engineering 관점에서 보면, 요구사항 분석 단계 정도로 볼 수 있다. 우리의 고객이 무엇을 원하는지 (물론, 여기서는 고객이 H/W ^^; 라고 했을 때,) 를 알아내는 단계이다.
x86 CPU 를 탑재하고 있는 IBM PC 는 다음과 같은 과정으로 운영체제를 구동시킨다.
| 순서 | 하는 일 |
| 1 | POST |
| 2 | Searching bootable disk |
| 3 | Load boot image(sector) to specified address of memory(0x07c00) |
| 4 | Set ‘PC(IP)’ to 0x07c00 to execute boot code |
위 Table 에서 2번째 단계를 보면, BIOS 가 Bootable Disk 를 검색한다고 되어 있다. Floppy Disk 로부터 부팅하는 OS 인 경우, Floppy disk 의 첫번째 섹터(512 Bytes) 마지막 2 Bytes 에 Bootable signature 가 기록된다. 이 Signature 값은 0xaa55 으로, 이 값이 기록된 경우 부팅 가능 이미지로 판단, 해당 섹터를 메모리로 적재하고 있다.
그러므로, 앞으로 구현 할 부트로더도 이러한 이유에서 마지막 2 Bytes 를 0xaa55 를 쓰기 위해 할애한다. 부팅을 위해 0x7c00 으로 올라가는 것은 디스크 첫 섹터 512 Bytes 이며, 가장 처음부터 코드가 실행된다. 그러므로 반드시 첫 섹터의 처음부터 코드를 기록해야 한다. 어셈블러를 이용해 코딩하는 것이 대부분인데, 이 경우, 첫 부분에 반드시 코드가 나와야 한다.
이를 Sequence Diagram 으로 살펴 보면 다음과 같다. 다음 Sequence Diagram 의 가장 왼쪽에 BIOS actor 가 위 Table 의 일을 수행하고 있다.
Sequence of booting
이제 실제 구현을 위한 고민을 해보자. Disk 의 첫 섹터를 읽어 들인다는 것과 해당 섹터에 부트로더를 쓰면 된다는 것을 알았다. 그렇다면, 어떻게 그 이미지를 만들고, 어떻게 그 이미지를 디스크에 쓸 수 있을까? 이미지를 디스크에 쓰기 위해서는 잘 만들어진 도구를 사용하거나 직접 구현할 수 있다.
필자는 간단히 다음과 같은 코드를 구현했다. (Linux 환경에서 사용 가능하다.)
#include <sys/stat.h>
int main(
int argc,
char *argv[])
{
int fd;
char buffer[512];
int ret;
if(argc != 3) {
}
if (fd < 0) {
return ret;
}
ret =
read(fd, buffer, 512);
if (ret != 512) {
}
return ret;
}
return ret;
}
if (fd < 0) {
return ret;
}
if (ret < 0) {
}
return ret;
}
ret =
write(fd, buffer, 512);
if (ret != 512) {
}
return ret;
}
}
return 0;
}
Disk 에 Sector 단위로 뭔가 쓸 수 있는 방법을 준비 했으니, 실제 Disk 에 의미 있는 데이터를 기록하기로 하자.
물론, 처음 512B(Boot Sector) 에는 Stage 1 Boot Loader 를 저장할 것이다. 이 Stage 1 Boot Loader 에서는 보다 큰 Code & Data 를 우리가 원하는 메모리 위치로 적재할 것이다.
그렇다면, 어디에 있는 것을 어디에 로딩해야 하는 것일까?
저 작은 Boot Loader 에서 Disk 에 저장 되어 있는 File system 을 이해해서 저장된 파일을 찾아 로딩한다는 것은 어려운 일이다. (물론 불가능 한 것은 아니다.)
그래서 필자는 Disk Layout(FAT12) 을 살펴보기 시작 했다.
- 주의
- 독자도 일단은 FAT12 에 대해 매우 간략히 여기서 살펴보고(꼭 필요한 부분만, 자세한 설명 없이) 지나갈 것이다. 그러므로, 그냥 그런것이 있구나 라고만 이해를 하고 넘어가길 바란다. 좀 더 자세한 내용은 뒷 장에서 원 없이 설명 될 것이다. FAT12
어딘가 실행코드를 꾸겨 넣을 만한 곳이 없을까?
FAT12 Disk Layout
이 Layout 을 보면, Reserved Sectors 라는 부분을 쉽게 찾을 수 있다. (강조 해 놓았으니까..) 일반적으로 이 reserved sector 는 사용되지 않는다. 즉 reserved sector 없이 Boot Sector 다음에 바로 FAT 가 나오게 배치하는 것이다.
하지만 필자는 이 Reserved Sector 를 이용하기로 했다.
이 Reserved Sector 에 보다 큰 Code chunk 를 저장해 놓고, 512B Code 에서 해당 영역을 읽어, 메모리로 올린 후 그 위치로 Jump 하는 것이다. 그럼 이렇게 저장한 코드를 어디로 올려야 하는 것일까?
올릴 수 있는 메모리 주소를 찾기 위해서 간단히 Memory Map 을 찾아보자.
| 시작 주소 | 마지막 주소 | 영역 |
| 0x0000:0x0500 | 0x0000:0x7BFF | Free Usable Memory |
| 0x0000:0x7c00 | 0x0000:0x7DFF | Boot program |
| 0x0000:0x7E00 | 0x9000:0xFFFF | Free usable memory |
- 주의
- 앞서 FAT12 Disk Layout 과 같은 맥락으로, 이 보다 자세한 내용은 다음 장에서 살펴보기로 하고, 여기서는 간단히 짚고 넘어 가기만 한다.
Memory Map 을 보면, Free Usable memory 가 0x0500 에서 부터 시작 한다는 것을 알 수 있다. 어중간하게 0x7c00 에 로딩된 부트로더를 0x0500 으로 옮겨서 다만 수십 바이트라도 연속적으로 할당된 공간을 확보할 것이다.
아래 Assembly 코드는 0x7C00 에 있는 data 를 0x0500 으로 복사하는 일을 한다.
mov si, BOOT
mov di, MOVE
mov cx, 0x0100
rep movsw
| rep movsw - Copy word(2bytes) from DS:SI to ES:DI, CX times |
Step 1. Move The Boot Loader
복사가 끝나면 새로운 위치로 점프를 해서 코드를 계속 수행 한다. 이 때 주의 할 것은, Jump 할 목적지이다.
이미 실행된 코드가 있는 곳으로 다시 Jump 를 하면 무한 Loop 가 되므로, Jump 할 위치를 잘 찾아야 한다.
; Jump to the move'd "after" label
mov si, after
sub si, BOOT
add si, MOVE
jmp si
after:
si 에 after label 의 주소를 넣고, BOOT (0x7c00) 을 빼서 Offset 을 계산한다. 이 코드가 수행되는 시점에 Base Address 는 0x7c00 이므로, after 에서 BOOT(0x7c00) 을 빼면 offset 을 얻을 수 있는 것이다. 그 다음, 이동 시킬 대상 위치, BOOT(0x0500) 에 이 Offset 을 더해서 그 위치로 Jump 를 하는 것이다. 물론 거기엔 after: 라는 label 이 후의 코드들이 있을 것이다.
새로운 메모리 영역으로 실행 위치를 옮긴 후에, Reserved Sectors 에 있는 나머지 Boot Loader 를 메모리로 로딩 한다.
; Update the relative offset
; Check the loading sector
mov si, sec
sub si, BOOT
add si, [RELT]
mov cx, [si] ; Number of sectors
dec cx ; Except the first sector
cmp cx, 0
jz load_done ; If there is no sectors to load, go out!
여기서도 어김없이, 메모리 아끼기 신공을 발휘한다. RELT 는 코드가 저장되어 있는 영역으로 0x0540 이다. (0x0 ~ 0x40 까지는 BPB 가 기록되어 있다.)
위 assembly code 를 pseudo code 로 바꿔보면 다음과 같다.
short int *addr = 0x0540;
addr = 0x0500;
si = sec;
si = si - 0x7C00;
si = si + 0x0500;
cx = *si;
sec 라고 정의된 것은, BPB 에 정의되어 있는 메모리 주소를 가리킨다. 이 주소는 0x7c00 에 로딩될 때의 주소가 된다. 이 위치는 메모리를 옮겨오기 전의 위치이므로, 여기서 한번 메모리 주소를 다시 계산해주고, 그 값을 읽어오는 것이다.
간단하게 우리는 이미 저 위치의 주소를 알고 있으므로, Hard coding 해도 상관 없다.
sec 는 다음장의 BPB(Boot Parameter Block) 설명 부분에 선언되어 있다.
이렇게 어렵게, Reserved Sector 개수를 얻어 왔으니, 실제로 해당 Sector 를 Memory 에 읽어 로딩 시키는 작업을 해보자.
; Reset a disk drive
xor ah, ah
xor dl, dl
int 0x13
디스크 드라이브를 reset 시키고, (BIOS 함수 사용)
0x0070:0x0000 위치에 Reserved Sector 에 저장된 두번째 Loader 를 로딩한다.
; Second stage loader will be loaded on 0x0070:0x0000
mov bx, LOADER_SEGMENT ; Loading to segment 0x0070
mov ax, 0x01 ; Sectors starts with 1
load_second_stage:
push bx ; Segment
push 0 ; Offset
push ax ; Sector
call read_sector
add sp, 6
add bx, 0x20 ; Next address (SEG{0x20} == 0x0200 == 512)
inc ax ; Next sector
mov si, msg_dot
sub si, BOOT
add si, [RELT]
push si
call print
add sp, 2
loop load_second_stage
load_done:
Disk Drive 의 Motor 를 끄고,
; Turn disk drive motor off
mov dx, 0x03F2
xor al, al
out dx, al
두번째 부트로더가 저장된 곳으로 IP 를 이동 한다.
jmp LOADER_SEGMENT:0x0000
이 장에서는 512B Boot Loader 에서 좀 더 많은 일을 할 수 있는 Boot Loader 를 임의의 메모리 위치로 올리고, 실행하는 방법을 알아 보았다. 문제는 이렇게 만든 코드를 어떻게 컴파일 해야 하는가 이다.
일반적으로 컴파일러는 실행 가능한 형태의 Executable image file 을 output 으로 생성한다. 즉 Object file –> Linking 과정을 거친 결과물이 생성되는데, 이 때 이 결과물은 일정한 형식을 가지게 된다.
Linux 에서는 ELF, Windows 에서는 PE/COFF 라고 불리는 것이 바로 그것이다.
Executable & Linkable File Format
Portable Executable File Format
이런 Format 들은 Header 가 있고, 메모리 적재 방식등의 부가 정보들을 담고 있다.
하지만 우리가 만드는 Boot Loader 는 딱 BIOS 가 512 Bytes 만큼을 지정된 메모리 영역으로 옮긴 후 해당 위치에서 부터 바로 실행을 시작하는 코드여야 한다.
즉 ELF 나 PE 같은 Format 이 있어서는 안 된다. CPU 가 이해할 수 없는 Machine code 가 나와서는 안된다는 것이다.
이를 위해 우리는 LD 즉 Linker Descriptor Utility 와 GCC 옵션에 대한 이해가 필요하다.
Linker Descriptor
GNU Compiler Collection
Net-wide Assembler(Intel Syntax)
다음 장 부터는 메모리맵을 시작으로 어떤 일들을 더 해줘야 하는지 알아보도록 한다.
; ; ; ;
- 작성자
- Sung-jae Park nices.nosp@m.j@ni.nosp@m.cesj..nosp@m.com ;
- 날짜
- 2011-7-21 ; ;
%define STACK_SEGMENT 0x9000
%define STACK_SP 0xFFFF
%define DATA_SEGMENT 0x0000 ; Our code's origin is 0x7c00;0x0000
%define TWO_TRACKS 36
%define BOOT 0x7C00
%define MOVE 0x0500
%define RELT 0x0540
[BITS 16]
[ORG 0x7c00] ; x86 BIOS firmware will load this boot code to here.
; Main routine
; //! [Stage 1 BPB]
; BPB (Boot parameter block ; For FAT12)
db 0xeb, 0x3c ; 0x00 - JMP 0x40 (offset +
db 0x90 ; 0x02 - "NOP Instruction"
db 'mkdosfs ' ; 0x03 - B bytes, OEM Name
sector_size dw 512 ; 0x0B - Size of sector
db 1 ; 0x0D - Sector per cluster
sec dw RESERVED_SECTOR ; 0x0E - Reserved sector count (boot sector)
db 2 ; 0x10 - Number of FATs
root_entry_count dw 0xe0 ; 0x11 - 224
dw 2880 ; 0x13 - Total sector 16 bits
db 0xF0 ; 0x15 - Media type (F0: Removable)
dw 9 ; 0x16 - FAT size = 9 sectors (16)
sector_per_track dw 18 ; 0x18 -
header_no dw 2 ; 0x1A - Number of heads
dd 0 ; 0x1C - Hidden sectors
dd 0 ; 0x20 - Total sector (32)
bootdrive db 0 ; 0x24 - Driver number (floppy)
db 0 ; 0x25 - Reserved
db 0x29 ; 0x26 - Boot signature
dd 0x20050718 ; 0x27 - Volume ID
volume_label db 'NCOS ' ; 0x2B - Volume label
fs_type db 'FAT12 ' ; 0x36 - File system type
; //! [Stage 1 BPB]
jmp entry
relt dw BOOT
; \fn entry
; \brief BPB 바로 다음에 나타나는 코드로 진입지점이다.
entry: ; 0x7C3E
; //! [Initialize registers]
cli
mov ax, STACK_SEGMENT
mov ss, ax
mov sp, STACK_SP
mov bp, sp
sti
mov ax, DATA_SEGMENT
mov ds, ax
mov es, ax
; //! [Initialize registers]
; //! [Stage 1 Copy Code]
mov si, BOOT
mov di, MOVE
mov cx, 0x0100
rep movsw
; //! [Stage 1 Copy Code]
; //! [Stage 1 Relocate Code]
; Jump to the move'd "after" label
mov si, after
sub si, BOOT
add si, MOVE
after:
;
;
; Update the relative offset
mov WORD [RELT], MOVE
; Check the loading sector
mov si, sec
sub si, BOOT
add si, [RELT]
mov cx, [si] ; Number of sectors
dec cx ; Except the first sector
cmp cx, 0
jz load_done ; If there is no sectors to load, go out!
;
;
; Reset a disk drive
xor ah, ah
xor dl, dl
int 0x13
;
;
; Second stage loader will be loaded on 0x0070:0x0000
mov bx, LOADER_SEGMENT ; Loading to segment 0x0070
mov ax, 0x01 ; Sectors starts with 1
load_second_stage:
push bx ; Segment
push 0 ; Offset
push ax ; Sector
call read_sector
add sp, 6
add bx, 0x20 ; Next address (SEG{0x20} == 0x0200 == 512)
inc ax ; Next sector
mov si, msg_dot
sub si, BOOT
add si, [RELT]
push si
call print
add sp, 2
loop load_second_stage
load_done:
;
mov si, msg_ncloader
sub si, BOOT
add si, [RELT]
push si
call print
add sp, 2
;
; Turn disk drive motor off
mov dx, 0x03F2
xor al, al
out dx, al
;
;
jmp LOADER_SEGMENT:0x0000
;
; \fn print(str)
; \brief 주어진 문자열을 화면에 출력한다.
; \param[in] str 출력할 문자열
; \return void 없음
;
print:
push bp
mov bp, sp
pusha
mov si, [bp + 4] ; si: address of a string
cld ; Direction forward
print_loop:
lodsb
cmp al, 0
je print_done
mov ah, 0x0E
mov bx, 0x0007
int 0x10
print_done:
popa
pop bp
ret
;
; \fn read_sector(segment, offset, sector)
; \brief 지정된 섹터를 메모리로 읽어 들인다.
; \param[in] segment 적재할 대상 메모리 세그먼트
; \param[in] offset 세그먼트 내에서의 오프셋
; \param[in] sector 읽어들일 섹터 번호
; \return void 없음
;
read_sector:
push bp
mov bp, sp
pusha
mov ax, [bp+4]
mov bl, TWO_TRACKS
div bl ; qutient(al) == track
mov ch, al ; # of tracks
shr ax, 8 ; To divide remainder of the previous division
sub si, BOOT
add si, [RELT]
mov bl, [si] ; Dividen
div bl ; quotient(al) == head
mov dh, al ; # of head
mov cl, ah ; sector starting number
inc cl
mov si, bootdrive
sub si, BOOT
add si, [RELT]
mov dl, [si]
mov ax, 0x0201 ;
read a(1) sector
mov es, [bp+8]
mov bx, [bp+6]
int 0x13
popa
pop bp
ret
;
msg_ncloader db 'N'
msg_disk_err db 'CLoader', 13, 10, 0
msg_dot db '.', 0
times 510 - ($ - $$) db 0
dw 0xAA55
;
;