目的
IBM PC互換BIOSでは、ブートセクタを物理アドレス0x7C00に配置するのが一般的な仕様となっています。
(あくまでも互換性の為にそういう取り決めがあるだけで、ハード的な決まりではありません。)
SeaBIOS
ディスク読み込み時は07c0:0000指定で、その後JMPする時はu16 bootip = (bootseg & 0x0fff) << 4で調整して0000:7c00指定になっていますが、最終的な物理アドレスは同じです。(何故こんな指定の仕方なのかは知りません)
...省略...
/****************************************************************
* Boot code (int 18/19)
****************************************************************/
// Jump to a bootup entry point.
static void
call_boot_entry(struct segoff_s bootsegip, u8 bootdrv)
{
dprintf(1, "Booting from %04x:%04x\n", bootsegip.seg, bootsegip.offset);
struct bregs br;
memset(&br, 0, sizeof(br));
br.flags = F_IF;
br.code = bootsegip;
// Set the magic number in ax and the boot drive in dl.
br.dl = bootdrv;
br.ax = 0xaa55;
farcall16(&br);
}
...省略...
// Boot from a disk (either floppy or harddrive)
static void
boot_disk(u8 bootdrv, int checksig)
{
u16 bootseg = 0x07c0;
// Read sector
struct bregs br;
memset(&br, 0, sizeof(br));
br.flags = F_IF;
br.dl = bootdrv;
br.es = bootseg;
br.ah = 2;
br.al = 1;
br.cl = 1;
call16_int(0x13, &br);
if (br.flags & F_CF) {
printf("Boot failed: could not read the boot disk\n\n");
return;
}
if (checksig) {
struct mbr_s *mbr = (void*)0;
if (GET_FARVAR(bootseg, mbr->signature) != MBR_SIGNATURE) {
printf("Boot failed: not a bootable disk\n\n");
return;
}
}
tpm_add_bcv(bootdrv, MAKE_FLATPTR(bootseg, 0), 512);
/* Canonicalize bootseg:bootip */
u16 bootip = (bootseg & 0x0fff) << 4;
bootseg &= 0xf000;
call_boot_entry(SEGOFF(bootseg, bootip), bootdrv);
}
...省略...
switch (ie->type) {
case IPL_TYPE_FLOPPY:
printf("Booting from Floppy...\n");
boot_disk(0x00, CheckFloppySig);
break;
case IPL_TYPE_HARDDISK:
printf("Booting from Hard Disk...\n");
boot_disk(0x80, 1);
break;
case IPL_TYPE_CDROM:
boot_cdrom((void*)ie->vector);
break;
...省略...
昔のIBM-BIOS
ブートローダを0x7c00へ読み込んでjmpから起動しています。
...省略...
ORG 7C00H
BOOT_LOCN LABEL FAR
...省略...
;------ MUST LOAD SYSTEM FROM DISKETTE -- CX HAS RETRY COUNT
MOV CX,4 ; SET RETRY COUNT
H1: ; IPL_SYSTEM
PUSH CX ; SAVE RETRY COUNT
MOV AH,0 ; RESET THE DISKETTE SYSTEM
INT 13H ; DISKETTE_IO
JC H2 ; IF ERROR, TRY AGAIN
MOV AH,2 ; READ IN THE SINGLE SECTOR
MOV BX,0 ; TO THE BOOT LOCATION
MOV ES,BX
MOV BX,OFFSET BOOT_LOCN
MOV DX,0 ; DRIVE 0, HEAD 0
MOV CX,1 ; SECTOR 1, TRACK 0
MOV AL,1 ; READ ONE SECTOR
INT 13H ; DISKETTE_IO
H2: POP CX ; RECOVER RETRY COUNT
JNC H4 ; CF SET BY UNSUCCESSFUL READ
LOOP H1 ; DO IT FOR RETRY TIMES
;------ UNABLE TO IPL FROM THE DISKETTE
H3: ; CASSETTE_JUMP:
INT 18H ; USE INTERRUPT VECTOR TO GET TO BASIC
;------ IPL WAS SUCCESSFUL
H4:
JMP BOOT_LOCN
...省略...
NASMでブートローダを書く際に決まり文句の用にorg 0x7C00と書きますが、これは実際にはどのような作用があるのかを調べます。
今回はブートローダに絞って話を進めます。
絶対アドレスと相対アドレス
アセンブリを書いているとjmp,call,データ等、アドレスを指定する場面が出てきます。
アドレスで指定しますが、場面によって相対アドレスの場合と絶対アドレス指定に分かれます。
まずは試しにコードを書いて逆アセンブリして見てみます。
bits 16
org 0x7C00
start:
jmp next1
nop ; 相対アドレスは次のバイトからの命令数なのでNOPを入れないと0となって分かり辛い
next1:
jmp 0x0000:next2
next2:
call next3
cmp ax,0
jne next4
nop
next4:
mov si, test_msg
hang:
cli
hlt
jmp hang
next3:
ret
test_msg db "abcdefgtest", 0x0D, 0x0A, 0
times 510-($-$$) db 0
dw 0xAA55
NASMでコンパイルし、逆アセンブルして生成される機械語を確認します。
test@test-fujitsu:~/kaihatsu$ nasm -f bin test.asm -o test.bin
test@test-fujitsu:~/kaihatsu$ ndisasm -b 16 test.bin
00000000 EB01 jmp short 0x3
00000002 90 nop
00000003 EA087C0000 jmp 0x0:0x7c08
00000008 E80D00 call 0x18
0000000B 83F800 cmp ax,byte +0x0
0000000E 7501 jnz 0x11
00000010 90 nop
00000011 BE197C mov si,0x7c19
00000014 FA cli
00000015 F4 hlt
00000016 EBFC jmp short 0x14
00000018 C3 ret
結果は以下の様になりました。
jmp next1(IPのみ書き換え):相対アドレス
jmp 0x0000:next2(CS:IP両方書き換え):絶対アドレス
call 0x18:相対アドレス
jnz 0x11:相対アドレス
mov si,0x7c19(データの指定):絶対アドレス
普段NASMでコードを書いている時はラベルを使うので意識しませんが機械語単位で見ると、相対アドレス指定と絶対アドレス指定の場合があります。
絶対アドレスはORGで指定した0x7C00+オフセットとなっており、ORGはNASMが絶対アドレスの計算をする為の基準となっていることが分かります。
ORGは必須か?
org 0x7C00は絶対に必須なのでしょうか。
基本BIOSはブートローダをメモリ0x7C00へ書き込むという規則の元に作られているので問題はありませんが、仮に別の領域に書き込むBIOSがあったらどうでしょうか。
ブートローダ内で読み込まれたアドレスを特定し、その値を基準に絶対アドレスを計算することが出来ます。
call 0xXXXXはIPのアドレスをスタックに積んだ上で指定したアドレスへ飛びます。擬似命令で書くとpush ip,jmp 0xXXXXのようになります。(逆にretはpop,jmp)
よってcallを実行した後でpopをすることでメモリ上のどこに読み込まれていたとしても、アドレスを特定する事が出来ます。
詳しくは筆者の以下の記事を参照して下さい。
callがスタックに積んだ戻りアドレス→ 8086:jmp/call/intによるSPとスタックの変化
call,popで現在のアドレスを取り出す実験→ __x86.get_pc_thunk関数とは(OS開発用調査)
ブートローダ上からメモリのどこに展開されているか調べる実験→ DVD起動のブートセクタが何処に読み込まれるか検証
0x7C00の前提に依存しないブートローダ
次にORGを書かずどこに読み込まれても実行できるブートローダを書いてみます。(ブートローダと言っても実際に何かをロードする訳ではありません。)
絶対アドレスが必要な部分はその場で計算しています。
実行した様子
nasm -f bin test.asm -o test.bin
qemu-system-i386 -hda test.bin
bits 16
start:
cli
; スタックの設定
xor ax, ax
mov ss, ax
mov sp, 0x9000
; CSの値を取り出す
push cs
pop ds
; 文字表示の為VRAM(ES : DI)の設定
mov ax, 0xB800
mov es, ax
xor di, di
; ORGに依存しないJMP(CS:IP書き換え)をretfで実現
; retfはスタックから CS:IP を同時にpopして跳ぶ命令
call get_ip
get_ip:
pop ax
add ax, next2 - get_ip ; AX = next2 のオフセット
push cs
push ax ; IPへnext2を設定
retf ; 現在の CS:next2 へ far return
next2:
; 文字列表示(POPで取り出したIPから文字列の絶対アドレスを計算)
call get_msg
get_msg:
pop si
add si, msg - get_msg
call print_string
; CS表示
mov ax, cs
call print_hex16
mov al, ':'
call putc
; IPの値を取り出す
call here ; CALL=PUSH IP + JMP
here:
pop ax ; AX = 現在IP
call print_hex16
.hang:
cli
hlt
jmp .hang
; =========================================
; putc
; AL = 文字
; ※diはこの関数内でしか使用していないので保存しない
; 呼ばれる度に増えていく
; =========================================
putc:
mov ah, 0x0C ; 赤
mov [es:di], ax
add di, 2
ret
; =========================================
; print_hex16
; AXを16進4桁で表示
; =========================================
print_hex16:
push ax
push bx
push cx
push dx
mov bx, ax
mov cx, 4 ; 2バイトの16進数表記は4文字
.loop:
rol bx, 4
mov dl, bl
and dl, 0x0F
cmp dl, 9
jbe .num
add dl, 'A' - 10
jmp .out
.num:
add dl, '0'
.out:
mov al, dl
call putc
loop .loop
pop dx
pop cx
pop bx
pop ax
ret
; =========================================
; print_string
; DS:SI = 0終端文字列
; =========================================
print_string:
.loop:
lodsb ; AL = [DS:SI], SI++
test al, al
jz .done
call putc
jmp .loop
.done:
ret
; =========================================
; 表示する文字列
; =========================================
msg db "CS:IP=", 0
times 510-($-$$) db 0
dw 0xAA55
逆アセンブルすると0x7C00基準の絶対アドレスはありません。
test@test-fujitsu:~/kaihatsu$ ndisasm -b 16 test.bin
00000000 FA cli
00000001 31C0 xor ax,ax
00000003 8ED0 mov ss,ax
00000005 BC0090 mov sp,0x9000
00000008 0E push cs
00000009 1F pop ds
0000000A B800B8 mov ax,0xb800
0000000D 8EC0 mov es,ax
0000000F 31FF xor di,di
00000011 E80000 call 0x14
00000014 58 pop ax
00000015 83C007 add ax,byte +0x7
00000018 0E push cs
00000019 50 push ax
0000001A CB retf
0000001B E80000 call 0x1e
0000001E 5E pop si
0000001F 83C65A add si,byte +0x5a
00000022 E84800 call 0x6d
00000025 8CC8 mov ax,cs
00000027 E81900 call 0x43
0000002A B03A mov al,0x3a
0000002C E80B00 call 0x3a
0000002F E80000 call 0x32
00000032 58 pop ax
00000033 E80D00 call 0x43
00000036 FA cli
00000037 F4 hlt
00000038 EBFC jmp short 0x36
0000003A B40C mov ah,0xc
0000003C 268905 mov [es:di],ax
0000003F 83C702 add di,byte +0x2
00000042 C3 ret
00000043 50 push ax
00000044 53 push bx
00000045 51 push cx
00000046 52 push dx
00000047 89C3 mov bx,ax
00000049 B90400 mov cx,0x4
0000004C C1C304 rol bx,byte 0x4
0000004F 88DA mov dl,bl
00000051 80E20F and dl,0xf
00000054 80FA09 cmp dl,0x9
00000057 7605 jna 0x5e
00000059 80C237 add dl,0x37
0000005C EB03 jmp short 0x61
0000005E 80C230 add dl,0x30
00000061 88D0 mov al,dl
00000063 E8D4FF call 0x3a
00000066 E2E4 loop 0x4c
00000068 5A pop dx
00000069 59 pop cx
0000006A 5B pop bx
0000006B 58 pop ax
0000006C C3 ret
0000006D AC lodsb
0000006E 84C0 test al,al
00000070 7405 jz 0x77
00000072 E8C5FF call 0x3a
00000075 EBF6 jmp short 0x6d
00000077 C3 ret
00000078 43 inc bx
00000079 53 push bx
0000007A 3A4950 cmp cl,[bx+di+0x50]
0000007D 3D0000 cmp ax,0x0
BIOSを改造し、0x7C00以外にブートローダを展開
実際にブートローダを0x7c00以外に読み込むBIOSは見つからないのでSeaBIOSを改造して0x8000へ読み込む様に改造します。
SeaBIOSの改造方法は以下の記事を参照して下さい。
SeaBIOSをソースからビルドしQEMUで動作
make
qemu-system-i386 -bios out/bios.bin -drive file=test.bin,format=raw
// Boot from a disk (either floppy or harddrive)
static void
boot_disk(u8 bootdrv, int checksig)
{
//u16 bootseg = 0x07c0;
u16 bootseg = 0x0800; // <<<<<今回書き換えた箇所はここだけ
// Read sector
struct bregs br;
memset(&br, 0, sizeof(br));
br.flags = F_IF;
br.dl = bootdrv;
br.es = bootseg;
br.ah = 2;
br.al = 1;
br.cl = 1;
call16_int(0x13, &br);
if (br.flags & F_CF) {
printf("Boot failed: could not read the boot disk\n\n");
return;
}
if (checksig) {
struct mbr_s *mbr = (void*)0;
if (GET_FARVAR(bootseg, mbr->signature) != MBR_SIGNATURE) {
printf("Boot failed: not a bootable disk\n\n");
return;
}
}
tpm_add_bcv(bootdrv, MAKE_FLATPTR(bootseg, 0), 512);
/* Canonicalize bootseg:bootip */
u16 bootip = (bootseg & 0x0fff) << 4;
bootseg &= 0xf000;
call_boot_entry(SEGOFF(bootseg, bootip), bootdrv);
}
BIOSからブートローダを0x8000へ読み込んでも正しく動作しました。
8086互換CPUで確認(V30)
実機で上のブートローダを0x2000:0x0000へ読み込みます。
実機のBIOSはチップ上に焼き込まれていて変更できない為、ブートローダを2段階にし、
BIOS → 0x7C00ブートローダ → 前章で作成したブートローダのように起動します。
test.asm側から見ると、BIOSに呼ばれたのか、別のブートローダに呼ばれたのかは判別できません。
nasm -f bin boot.asm -o boot.bin
cat boot.bin test.bin > os.img
lsblk
sudo dd if=os.img of=/dev/sdb bs=512 count=2 conv=notrunc
以下のブートローダを間にかませることで、SeaBIOSを改造したように、ブートローダの読み込み先を変更します。
org 0x7C00
start:
; セグメント初期化
xor ax, ax
mov ds, ax
mov es, ax
; スタック設定
mov ss, ax
mov sp, 0x7C00
; ディスクから第2セクタを 0x2000:0x0000 に読み込む
mov ax, 0x2000 ; 読み込み先アドレス = ES:BX
mov es, ax
mov bx, 0x0000
mov ah, 0x02 ; INT 13h サービス 2: 読み込み
mov al, 1 ; 読み込むセクタ数 = 1
mov ch, 0 ; シリンダ = 0
mov cl, 2 ; セクタ番号 = 2(1始まり)
mov dh, 0 ; ヘッド = 0
mov dl, 0x80 ; ドライブ番号
int 0x13
jc disk_error ; エラー時に無限ループ
; 読み込んだコードへ跳ぶ(第2セクタ)
jmp 0x2000:0x0000
disk_error:
jmp $
times 510-($-$$) db 0
dw 0xAA55