OS自作入門 1, 2日目 機械語のブートセクタを解読する


きっかけ

 「30日でできる!OS自作入門」のゼミがインターン先で始まることになって、その予習を始めました。本では、1日目にブートセクタをバイナリエディタで入力して実行したあと、2, 3日目でアセンブリ言語、C言語を導入していて、機械語とは1日でおさらばっぽいです。個人的には機械語⇔アセンブリの対応にも興味があるので、ブートセクタの部分だけでも機械語・アセンブリ対訳で読んでみたいのです。

目標

  • 1日目で入力したブートセクタ(helloos.imgの冒頭512バイト)を1バイトずつ追って、2日目に出てくるアセンブリ(helloos.nas)との対応を確認する。
  • そのために:Intel 8086の機械語をデコードする方法を調べる。起動直後、CPUがリアルモードで動いているときは8086の上位互換なので、8086について知っていればしばらくは読めそう。

 2つ目もやると長くなるので、この記事では1つ目だけ書きます。2つ目は別の機会にまとめるかもしれないです。2つ目に該当する参考文献だけを次項に書くことにします。

仮定と参考資料

 機械語をアセンブリに翻訳して、やっていることを理解することが目標なので、レジスタなどのCPUのことについて改めてまとめたりはしません。そのほか、本の1, 2日目に書いてあることも仮定します。

 参考にするドキュメントはこちらです。160ページ以降(Machine Instruction Encodeing and Decoding)に出てくるTable 4-8, 4-10, 4-13をめちゃくちゃ使います。45ページからの2.7 Instruction Setに各命令の詳細があるので、必要に応じて参照するようにします。

おことわり

 機械語を調べ始めたのが昨日なので、ドキュメント全部読めていませんし、憶測で読み進めている箇所が多々あります。したがって、その規則はどこに書いてあるのか?と聞かれても確かなことは答えられないかもしれません。誤字・脱字・表記・間違いの指摘や、参考になる資料を教えてくださるのは大歓迎です。
 説明中の=(イコール)は必ずしも数学的な等号ではありません。左右が意味的に同じようなものだと考えたときには、説明の短縮のために=を使っています。

ソースコード(機械語)

 読むプログラムは、以下にコピペしたブートセクタです。512バイトです。これがBIOSによってメモリの0x7C00から0x7DFFに読み込まれ、先頭から実行されるんだそうです。なのでEB 4E ...から読み始めればいいわけです。

00000000   EB 4E 90 48  45 4C 4C 4F  49 50 4C 00  02 01 01 00  .N.HELLOIPL.....
00000010   02 E0 00 40  0B F0 09 00  12 00 02 00  00 00 00 00  ...@............
00000020   40 0B 00 00  00 00 29 FF  FF FF FF 48  45 4C 4C 4F  @.....)....HELLO
00000030   2D 4F 53 20  20 20 46 41  54 31 32 20  20 20 00 00  -OS   FAT12   ..
00000040   00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
00000050   B8 00 00 8E  D0 BC 00 7C  8E D8 8E C0  BE 74 7C 8A  .......|.....t|.
00000060   04 83 C6 01  3C 00 74 09  B4 0E BB 0F  00 CD 10 EB  ....<.t.........
00000070   EE F4 EB FD  0A 0A 68 65  6C 6C 6F 2C  20 77 6F 72  ......hello, wor
00000080   6C 64 0A 00  00 00 00 00  00 00 00 00  00 00 00 00  ld..............
00000090   00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
000000A0   00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
000000B0   00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
000000C0   00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
000000D0   00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
000000E0   00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
000000F0   00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
00000100   00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
00000110   00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
00000120   00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
00000130   00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
00000140   00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
00000150   00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
00000160   00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
00000170   00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
00000180   00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
00000190   00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
000001A0   00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
000001B0   00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
000001C0   00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
000001D0   00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
000001E0   00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
000001F0   00 00 00 00  00 00 00 00  00 00 00 00  00 00 55 AA  ..............U.

読んでいく

0x 0000 0000

00000000   EB 4E 90 48  45 4C 4C 4F  49 50 4C 00  02 01 01 00  .N.HELLOIPL.....

EB 4E

 まずどの命令なのかを決定したいです。Table 4-13を見ます。どうやら8086の命令は、最初の1バイトでだいたい何をする命令なのかわかるようです。左側、1ST BYTEのカラムでEBを探します。2ND BYTEカラムにIP-INC8、ASM-86 INSTRUCTION FORMATのカラムにJMP SHORT-LABELとありました。
 命令を解釈します。JMPなので無条件ジャンプです。SHORT-LABELは「後続の8bitを符号付き2進数と思え」という意味っぽいです(2.7 Instruction Setを読みました)。したがって、EB 4Eでひとかたまりの命令だということになります。
 ジャンプ先を計算します。0x4Eを2進数に直すと0100 1110ですが、最高位が0なので符号付き2進数としてもそのまま正の数0x4Eということになります。ジャンプ先は、直後の命令の番地(この場合はEB 4Eの次ですから0x0002)に先ほど計算した0x4Eを加算することで0x0050とわかりました。なお、このジャンプは相対的なジャンプなので、今の計算ではブートセクタが0x7C00以降にコピーされる事実を無視しています。絶対番地が必要なら、0x0050 + 0x7C00 = 0x7C50とすればよいでしょう。
 helloos.nasには、JMP entryという行がありました。おそらく、このentryがアセンブル時に0x0050に変換され、さらに番地を逆算してEB 4Eになったんじゃないでしょうか。アセンブラのソースコード読んでここらへんの仕組みも知りたいです。
 これは後でわかることですが、0x0050に飛んだあと、プログラムの実行は0x004F以前に戻ってくることはありません。なので0x0002から0x004Fまでは無視して0x0050以降を読んでいくことにします。本によればここにはフロッピーディスクとかFAT12ファイルシステムとかに必要なデータが書いてあるらしいです。

0x 0000 0050

00000050   B8 00 00 8E  D0 BC 00 7C  8E D8 8E C0  BE 74 7C 8A  .......|.....t|.

B8 00 00

 どんどん読んでいきます
 Table 4-13いわく、B8MOV AX IMMED16だそうです。AXレジスタに2バイトの直値を代入するということです。B8に加えて、2バイトの直値00 00でひとセットです。MOV AX, 0がhelloos.nasでの該当箇所だと思います。

8E D0

 8EMOV SEGREG, REG16/MEM16です。これは面倒くさいやつです。まず、直後のバイトを2進数と見ます(D0 -> 1101 0000)。そしてこれを2bit, 3bit, 3bitに分割し(1101 0000 -> 11 010 000)、それぞれをMOD, REG, R/Mと呼びます(MOD = 11, REG = 010, R/M = 000)。
 これらの使い方を説明します。MOV SEGREG, REG16/MEM16SEGREGを決定するためには、REG(= 010)を使用します。本にはセグメントレジスタとして、

000 ES
001 CS
010 SS
011 DS
100 FS
101 GS

が挙げられていました。左の番号は自分でつけたものです。このセグメントレジスタの中で、番号がREG(= 010)のもの、すなわちSSSEGREGとなります。
 次に、REG16/MEM16の決定方法ですが、これは2段階にわけられます。1段階目は、レジスターから読み出すのかメモリーから読み出すのかの決定です。これにはMOD(= 11)を使用します。Table 4-8を見ると、MOD = 11Register Modeだということがわかります。2段階目は、どのレジスターから読み出すのか、ということになりますが、これも本に出てきた16ビットのレジスターとR/M(= 000)を見比べればわかります。

000 AX
001 CX
010 DX
011 BX
100 SP
101 BP
110 SI
111 DI

の中で番号がR/M(= 000)のものはAXです。これで8E D0MOV SS, AXに翻訳できました。

BC 00 7C

 BCMOV SP, IMMED16ですね。00 7Cが直値ということになります。ここで注意しないといけないのは、SPに代入されるのは0x007Cではなくて、0x7C00だということです。2バイト以上の直値が出てきたら、バイト単位に区切って逆順に並び替えればいいんだと自分は理解しています。このようなエンコード方式(?)をリトルエンディアンと言うらしいです。helloos.nasにもちゃんとMOV SP, 0x7c00という行があります。

8E D8

 8E = MOV SEGREG, REG16/MEM16,
 D8 = 1101 1000 = 11 011 000
したがって、
 SEGREG = DS,
 REG16/MEM16 = AX
ゆえに
 8E D8 = MOV DS, AX

8E C0

 C0 = 1100 0000 = 11 000 000
したがって
 SEGREG = ES,
 REG16/MEM16 = AX
ゆえに
 8E C0 = MOV ES, AX

BE 74 7C

BE = MOV SI, IMMED16
 すなわち、SIレジスタに直値0x7C74が代入されます。helloos.nasにMOV SI, msgなる行がありますが、msgラベルの場所がブートセクタの中で0x0074と計算され、実際のメモリ上では0x7C74の場所にあると計算されているんだと思われます。
 8Aというバイトが残っていますが、putloopの前で区切りがいいので節を変えます。

0x 0000 0060

00000050   B8 00 00 8E  D0 BC 00 7C  8E D8 8E C0  BE 74 7C 8A  .......|.....t|.
00000060   04 83 C6 01  3C 00 74 09  B4 0E BB 0F  00 CD 10 EB  ....<.t.........
00000070   EE F4 EB FD  0A 0A 68 65  6C 6C 6F 2C  20 77 6F 72  ......hello, wor

8A 04

 8A = MOV REG8, REG8/MEM8,
 04 = 0000 0100 = 00 000 100
8ビットレジスタのREG(= 000)番目ですから、REG8 = ALです。

000 AL
001 CL
010 DL
011 BL
100 AH
101 CH
110 DH
111 BH

 Table 4-8を見ると、MOD = 00だからMemory Modeだということがわかります。また、no displacement followsなので、後続のバイトはもうありません。じゃあメモリーのどこを読みに行くんだというのは、Table 4-10を見ればわかります。R/M = 100, MOD = 00なので、"(SI)"を読み込めということになります。"(SI)"はhelloos.nasでは[SI]と書かれていますが、レジスタSIが指し示すメモリー上の番地です。初回のループではmsg(= 0x7c74)番地の1バイトがALに代入されます。

83 C6 01

 C6 = 1100 0110 = 11 000 110ですから、1バイト目が83, 2バイト目がMOD 000 R/Mの行が該当する命令です。ADD REG16/MEM16, IMMED8ですね。MOD = 11, R/M = 110なので、R/M = 110番目の16ビットレジスターSIがdestです。srcはIMMED8, すなわち1バイトの直値であり、それは0x01です。

3C 00

 3C = CMP AL, IMMED8
ALと1バイトの直値0x00を比較してフラグを立てます。

74 09

 74 = JE/JZ SHORT-LABEL
前項の比較結果がEqual/Zeroのときにジャンプします。09 = 0000 1001を符号付き2進数とみなしても09です。直後の命令の番地が0x0068なので、0x0068 + 0x0009 = 0x0071が行き先です(面倒くさいのでブートセクタの中での相対値です。)。ラベルで言うとfin, 命令で言うとF4ですね。

B4 0E

 B4 = MOV AH, IMMED8
0Eは直値です。

BB 0F 00

 BB = MOV BX, IMMED16
0F 00 = 0x000Fは直値です。リトルエンディアンに注意。

CD 10

 CD = INT IMMED8
10が直値です。

EB EE

 きりが良いので0x 0000 0070に食い込みます。EBJMP SHORT-LABELです。今まで符号付き2進数といいながら全部正の数だったのですが、初めて負の数が登場します。計算方法を説明しましょう。EEを2進数に直します(= 1110 1110)。最高位とそれ以外に分けます(= 1 1101110)。最高位だけマイナスをつけることに注意して10進数に直すと、- 128 + 64 + 32 + 8 + 4 + 2 = - 18となります。直後の命令の番地0x0071から18 = 0x0012を引いた番地に飛びます。0x0071 - 0x0012 = 0x005Fで、ラベルで言うとputloopです。

0x 0000 0070

00000070   EE F4 EB FD  0A 0A 68 65  6C 6C 6F 2C  20 77 6F 72  ......hello, wor

F4

 F4 = HLTです。

EB FD

 EBJMP SHORT-LABELでした。ジャンプ先を計算しましょう。FD = 1111 1101 = 1 1111101 = - 128 + 64 + 32 + 16 + 8 + 4 + 1 = - 3です。直後の命令(というかデータ)の番地は0x0074ですから0x0074 - 0x0003 = 0x0071にジャンプすることになります。命令で言うとF4, ラベルで言うとfinですね。

データ部分

00000070   EE F4 EB FD  0A 0A 68 65  6C 6C 6F 2C  20 77 6F 72  ......hello, wor
00000080   6C 64 0A 00  00 00 00 00  00 00 00 00  00 00 00 00  ld..............

0A 0Aから始まる16バイトは文字のASCIIコードです。そのあとは0x01FD番地までは00が続きます。このブートセクタに限って言えば、この00だらけの部分のコードが実行されることはないし、データとして読み込まれることもありません。

一応言及しておく

000001F0   00 00 00 00  00 00 00 00  00 00 00 00  00 00 55 AA  ..............U.

 0x01FE, 0x01FF55 AA(0xAA55)はブートセクタの末尾に必ずつけなければならないシグネチャだそうです。
 このあとには、2セクタ目と11セクタ目の冒頭に、FAT12ファイルシステム的に必要らしいF0 FF FF(0xFFFFF0?)がありますが、0x01FFまでのブートセクタが続きを読み込まずに無限ループに入るので、今回は使われません。以上で、1日目に入力したブートセクタの挙動を最初から最後まで追うことができました。