🐈‍⬛

アセンブリでHello World on BIOS

2024/12/14に公開

はじめに

最近RustでOSを作ると意気込んでいるのですが、その第一歩としてブートローダーの理解を進めるためOS無しのHello Worldをやってみました。

環境

実機の用意は面倒なのでQEMUを使います。
またアセンブラとしてnasmを用意します。

特にホスト側へ依存する処理はないのでOSはWindowsでもMacでも何でも大丈夫ですが、MacOSだと上記2つをHomebrewからサクッとインストールできるので手軽だと思います。

brew install nasm
brew install qemu

コード

Makefileの実行等があるので以下のリポジトリをcloneして下さい。

https://github.com/ryo-yamaoka/hello-bootloader

アセンブリ本体

hello_minimal.asm
; initialize
ORG 0x7c00
MOV AX, 0
MOV CS, AX
MOV DS, AX
MOV ES, AX
MOV SS, AX
MOV SI, msg

print:
    MOV AL, [SI]
    CMP AL, 0
    JE fin
    MOV AH, 0x0e ; set print char
    INT 0x10 ; call BIOS video function
    ADD SI, 1
    JMP print

fin:
    HLT
    JMP fin

msg:
    DB 0x0a, 0x0a
    DB "hello, world"
    DB 0x0a
    DB 0 ; end sign
    TIMES 510 - ($ - $$) DB 0
    DW 0xaa55

解説

;initialize ブロック

ここでは諸々の初期化処理を行います。

最初の ORG0x7c00 のメモリアドレスを基準として動作するという命令です。BIOSはブート領域512バイトをこのメモリアドレスに読み込みます。これはどうもコンピューター黎明期にIBMの中の人が決めた固定値のようなのですがいずれにせよ「そういうもの」なので受け入れるしかありません。興味があれば参考文献を参照してみて下さい。

MOV AX, 0 からの5行はセグメントレジスターの初期化をやっています。ここに意図しない値が入っているとメモリアドレスの計算がおかしくなるので必須です。セグメントレジスターに0を直接投入すると

src/hello_minimal.asm:3: error: invalid combination of opcode and operands

のようなエラーが発生するのでAXから入れます。なお今回は試しませんがORGを0セットしてその分セグメントレジスターを0x7c00前提に初期化するという方法でも動きます。

MOV SI, msg でSIレジスターに msg: ラベル部分のメモリアドレスを入れます。本当はメッセージ全体をレジスターに読み込めれば楽なんですが、レジスターの容量は非常に小さいのでそれは不可能です。代わりに後のステップでこのアドレスから8ビットずつ順に参照していきます。

このラベルの中をループしながら画面に文字を描画していきます。

MOV AL, [SI] はAL(アキュムレーターレジスターの下位8ビット)にSIレジスターに指定されたメモリアドレスの内容を読み込む、という命令です。ループ1回目なので msg: ラベル最初の8ビット(=1バイト)の 0x0a つまりASCIIコードの改行です。

CMP AL, 0 は一つ前のステップで読み出した内容が 0 であるか否かを判定しています。これは msg: ラベル4行目の DB 0 ; end sign で表示したいメッセージの終わりを識別するためのものです。真であれば翌行の JE fin にジャンプします。

CMPの結果が偽、つまりまだ終わっていない場合 MOV AH, 0x0e でAH(アキュムレーターレジスターの上位8ビット)に「文字描写する」という命令をセットします。但しこれだけでは描写は実行されません。

INT 0x10 でソフトウェア割り込みを発生させます。0x10はBIOSビデオファンクションを意味し、これで実際に画面に文字が描写されます。

ADD SI, 1 はSIレジスターの値( msg: のメモリアドレス)を1バイト進め次の文字を読み取れる準備をします。

JMP print で先頭に戻るのでADDやCMPと合わさりforループのような振る舞いとなります。

fin ラベル

HLT でCPUに何もしないという指示を送りその後 JMP fin で先頭に戻ります。つまり実質的にこのHello Worldプログラムの終了となります。

msg ラベル

先頭4行は先述の通り画面に描写する文字列そのものを定義しています。

5行目の TIMES 510 - ($ - $$) DB 0 はこれまで書いてきた内容を差し引いた上で510バイト目までを0で埋めるという内容です。単なるパディングなので必ずしも 0 でなくとも大丈夫ですが他の文字にする理由も無いでしょう。

最後の DW 0xaa55 で一つ前の命令と合わせることでブートセクター末尾に aa55 となるようにします。これはブートシグネチャーを表しておりBIOSが起動可能か否かを判別する非常に重要な意味を持っています。

実行

make build によりコードをアセンブルします。実体としては単にnasmコマンドを実行してアセンブリコードをバイナリーに吐いているだけです。

その後 run-minimal でQEMUに先のバイナリーをドライブとしてマウントし起動します。以下のようになれば成功です。

おまけ

BIOSビデオファンクションをもうちょっと使って文字に色を付けたりしてみました。

make build
make run

コード解説(差分のみ)

hello.asm
; initialize
ORG 0x7c00
MOV AX, 0
MOV CS, AX
MOV DS, AX
MOV ES, AX
MOV SS, AX
MOV SI, msg

; set VGA mode
MOV AX, 0x0012
INT 0x10

print:
    MOV AL, [SI]
    CMP AL, 0
    JE fin
    MOV AH, 0x0e
    MOV BX, 0x000a ; set color code green
    INT 0x10
    ADD SI, 1
    JMP print

fin:
    HLT
    JMP fin

msg:
    DB 0x0a, 0x0a
    DB "hello, world"
    DB 0x0a
    DB 0
    TIMES 510 - ($ - $$) DB 0
    DW 0xaa55

; set VGA mode

文字色を使って描画するために MOV AX, 0x0012 をセットしてBIOSビデオファクションを呼びVGAモードへ切り替えます。

print

MOV BX, 0x000a は文字のカラーコード緑色を意味します。これをセットした状態で描写すると緑色の文字が出てきます。

参考文献

Discussion