アセンブリでHello World on BIOS
はじめに
最近RustでOSを作ると意気込んでいるのですが、その第一歩としてブートローダーの理解を進めるためOS無しのHello Worldをやってみました。
環境
実機の用意は面倒なのでQEMUを使います。
またアセンブラとしてnasmを用意します。
特にホスト側へ依存する処理はないのでOSはWindowsでもMacでも何でも大丈夫ですが、MacOSだと上記2つをHomebrewからサクッとインストールできるので手軽だと思います。
brew install nasm
brew install qemu
コード
Makefileの実行等があるので以下のリポジトリをcloneして下さい。
アセンブリ本体
; 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 ブロック
ここでは諸々の初期化処理を行います。
最初の ORG
は 0x7c00
のメモリアドレスを基準として動作するという命令です。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ビットずつ順に参照していきます。
print ラベル
このラベルの中をループしながら画面に文字を描画していきます。
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
コード解説(差分のみ)
; 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モードへ切り替えます。
MOV BX, 0x000a
は文字のカラーコード緑色を意味します。これをセットした状態で描写すると緑色の文字が出てきます。
Discussion