IPLでHello,world!
UEFI 以前のマシンでのみ有効
UEFI 対応マシンでは、Compatibility Support Module (CSM) を有効にする必要がある。
UEFI ではなくレガシーブートの第一歩として、IPLで Hello, world! を表示させる。 なぜレガシー?
UEFIの方が簡単だが、UEFIを持たない古いマシンで動かすには必須。
これでできること
開発環境として 8086 アセンブラが使えることの確認
IPL の起動シーケンスの理解
IPLをファイルとして作成できることの確認
Hyper-V ではファイルをCDやフロッピーディスクやハードディスクとして認識させることができる。
IPLをFDイメージに書き込む方法
IPLを起動できることの確認
アセンブラの選定
まずは、HDDエミュレーションとFDDエミュレーションが考えられる。
(CD、PXEはまた別に行う。)
Hello, world! を表示させるために、BIOS の文字列出力機能を使用する。
とりあえず、Debian をビルド用OSとして使用する。
(将来的にバッチ処理でビルドさせようと考えたら FreeDOS の選択肢はない。
しかし、8086のコードをステップ実行させたい場合は FreeDOS の debug を使った方がよい。
以後、アセンブラは分かっていることが前提。
VSCode や GitHub codespaces で使えるように .devcontainer を設定。
.devcontainer が扱える環境を事前に用意する必要がある。
管理しやすくするため、docker-compose を使用している。
(Debian を使うだけなら、Dockerfile 1つで十分だが)
ディレクトリ構成
code:console
$ tree .devcontainer
.devcontainer
├── devcontainer.json
├── docker-compose.yml
└── workspace
└── docker
└── Dockerfile
code:.devcontainer/workspace/docker/Dockerfile
FROM mcr.microsoft.com/devcontainers/base:debian
code:.devcontainer/docker-compose.yml
version: "3"
services:
workspace:
build:
context: ./workspace/docker
dockerfile: Dockerfile
volumes:
- type: bind
source: ..
target: /workspace
tty: true
stdin_open: true
code:.devcontainer/devcontainer.json
{
"name": "create-ipl",
"dockerComposeFile": [
"docker-compose.yml"
],
"service": "workspace",
"workspaceFolder": "/workspace",
"customizations": {
"vscode": {
"extensions": [
"EditorConfig.EditorConfig",
"shardulm94.trailing-spaces"
]
}
}
}
VSCode の場合、以上のファイルを用意して、Ctrl + P で "Dev Container: Rebuild and Reopen in Container" を実行すると、Debian のコンテナが動作する。
code:console
$ cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
VERSION_CODENAME=bookworm
ID=debian
とりあえず JWasm を採用。
$ unzip JWasm211bl.zip
$ chmod +x ./jwasm
サンプルの中の Lin64_1.asm (Linux 64bit 用)をアセンブル、実行して動作確認をする。
$ cd Samples
$ ../jwasm -elf64 -Fo=Lin64_1.o Lin64_1.asm
$ gcc -nostartfiles Lin64_1.o -o Lin64_1
code:console
$ ./Lin64_1
Hello, world!
Lin64_1.asm に書かれているコマンドをそのまま実行すると以下のようなエラーが出た。
code:console
$ gcc Lin64_1.o -o Lin64_1
/usr/bin/ld: Lin64_1.o: in function `_start':
Lin64_1.asm:(.text+0x0): multiple definition of `_start'; /usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/Scrt1.o:(.text+0x0): first defined here
/usr/bin/ld: warning: Lin64_1.o: missing .note.GNU-stack section implies executable stack
/usr/bin/ld: NOTE: This behaviour is deprecated and will be removed in a future version of the linker
/usr/bin/ld: Lin64_1.o: warning: relocation in read-only section `.text'
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/12/../../../x86_64-linux-gnu/Scrt1.o: in function `_start':
(.text+0x17): undefined reference to `main'
/usr/bin/ld: warning: creating DT_TEXTREL in a PIE
collect2: error: ld returned 1 exit status
_start が二重に定義されている(C言語用のものがリンクされている)ように見える。
Stackoverflow にて、-nostartfiles をオプションに付ければ回避できることが分かった。
BIOS コールと DOS コールは混同されやすいので注意。
INT 21h は DOS コールなので IPL では使えない。
まずは100%動くはずのものを作る。
(なぜか最初に作った Hello, world! が動かなかったので、まずは BIOS int 10h AH=0eh の動作確認のため)
code:a.asm
code segment use16
assume cs:code
org 7C00h
start:
mov al, 41h ; 'A'
mov ah, 0eh ; tty print character
int 10h
jmp start
db 510-($-start) dup(0) ; padding
dw 0aa55h ; boot sector signature
code ends
end start
アセンブルする。
code:console
$ ./jwasm -bin -nologo -Zp1 a.asm
a.asm: 15 lines, 2 passes, 0 ms, 0 warnings, 0 errors
コードが期待通りにできているか確認。
code:console
$ od -A x -t x1z a.BIN
000000 b0 41 b4 0e cd 10 eb f8 00 00 00 00 00 00 00 00 >.A..............<
000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 >................<
*
0001f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa >..............U.<
000200
1.44MB フロッピーディスクのイメージの作成
すべて0で埋める
$ dd if=/dev/zero of=fd.img bs=512 count=2880
1.44MB フロッピーディスクの1セクタは512バイトでセクタ数は 2880。
IPLの起動ではファイルシステムの構成は問われないので、全部0で埋めてよい。
(本来は FAT にする。)
IPL をフロッピーディスクのイメージの先頭に上書きする。
$ dd if=a.BIN of=fd.img bs=512 count=1 conv=notrunc
fd.img をダウンロード
(ローカルのファイルシステムに接続されているなら、そのまま変更できるはず。)
fd.vfd にリネーム
以下を実施
実行結果
https://gyazo.com/631b53f0c5e160d3f5056e1fcd91f334
少なくともここまでできれば、フロッピーディスクイメージにアセンブルしたIPLを書き込めて実行できることが確認できる。
QEMU で実行する場合
code:bat
qemu-system-x86_64 -boot a -fda fd.img -m 512
-m はメモリサイズ(MB)
-boot a で fda からのブート
-fda でフロッピーディスクAドライブにイメージファイルを設定
サブルーチン化して動作するか確認
code:b.asm
code segment use16
assume cs:code
org 7C00h
start:
mov al, 42h ; 'B'
call put_char
jmp start
put_char:
mov ah, 0eh ; tty print character
int 10h
ret
db 510-($-start) dup(0) ; padding
dw 0aa55h ; boot sector signature
code ends
end start
問題なければBが延々と表示される。
実行結果
https://gyazo.com/797597b6a6f5a032812e6817d7506888
IP(戻り先)
AX, BX, CX, DX
SI, DI, BP, SP
CS, DS, ES, SS
CS, SS は 0000 だが、DS, ES は違うところを指してしまっている。(不定?)
このため、DS, ES が影響する場合にずれる。
SP は 0000:7c00 より手前にあるので、ラップアラウンドしない限り、スタックがIPLを破壊することはない。(Hyper-V の BIOS の場合)
DL は本来起動デバイスを示すはず。
このレジスタテストプログラムはどこでも使えるはずなので、BIOSの特性(あるいは不具合)を見るのに使えるはず。
Hello, world! を表示する IPL
code:hello.asm
code segment use16
assume cs:code
org 7C00h ; ipl origin address
start:
xor ax, ax ; ds := 0000h
mov ds, ax
mov si, offset hello_string ; set string address
call print_string
jmp $ ; stop
print_string:
lodsb ; load al from ds:si and increment si test al, al ; if (al == 0) then return
jnz print_string_put
ret
print_string_put:
call put_char
jmp print_string
put_char:
mov ah, 0Eh
int 10h
ret
hello_string db "Hello, world!", 0
db 510-($-start) dup(0) ; padding
dw 0aa55h ; boot sector signature
code ends
end start
出力結果
https://gyazo.com/47808e76e5387f2a7a3e5b9a3b2ff6c9
色々試して分かったこと (Hyper-V, JWasm の仕様も含む)
db 疑似命令の前にラベルを付けられるが、ラベルにコロンが付いていない場合は、db の先頭の値とみなされる。
このため mov si, hello_string とするとError A2048: Operands must be the same size: 2 - 1 とエラーになる。
mov si, offset hello_string とするとアドレスと認識されて通るようになる。
hello_string: とコロンを付ければ offset の指定は要らない。
8086 のシフトは、shr al, 1 か shr al, cl しかできない。shr al, 4 はできない。
Error A2030: Instruction or register not accepted in current CPU mode となる。
mov [mem], ax は通らない。mov word ptr [mem], axとする必要がある。
Error A2049: Invalid instruction operands となる。
mov ax, [mem] は ax に mem のアドレスが渡される。メモリから読み出すなら mov ax, word ptr [mem]とする必要がある。
hlt 命令では止まらない。次の命令が実行されてしまう。
jmp $ として無限ループにするのが常套手段
cli で割り込み禁止にすると hlt で止まりっぱなしになる。
一部の CPU では hlt は実装されなかった。
最初、なぜか Hello, world! が表示されなかったのは、使った lodsb が [DS:SI] を見ていて、DS が 0000h でなかったため。
参考