「手探りでCUI OS作成に挑む」連載
この記事は「手探りでCUI OS作成に挑む」連載の一部です。
全体の目次は「手探りでCUI OS作成に挑む」連載目次を御覧下さい。
目的
現在CUIのOSを開発しています。
動きをできるだけMS-DOSに準拠させるためにMBRをDUMP及び逆アセンブルして処理を追います。
前回QEMUを用いて仮想HDDにMS-DOSをインストールしました。
この仮想HDDのMBR部分(先頭512バイト)の中身を解析していきます。
https://qiita.com/earthen94/items/7491ac22a98e4ae823c9
(因みに私は横文字が嫌いなのですが、この分野はどう頑張っても避けて通れずいろんな横文字を多用してしまいました。)
MBRとは
DUMP及び逆アセンブルの生データ
逆アセンブル結果は全てアセンブリ形式で出力されていますが、実際には文字列データやパーティションの情報等も全て無理やりアセンブリとして変換されるので全てがコードという訳ではありません。
test@test-ThinkPad-X280:~$ sudo xxd -g1 -l512 msdos_hdd.img
00000000: fa 33 c0 8e d0 bc 00 7c 8b f4 50 07 50 1f fb fc .3.....|..P.P...
00000010: bf 00 06 b9 00 01 f2 a5 ea 1d 06 00 00 be be 07 ................
00000020: b3 04 80 3c 80 74 0e 80 3c 00 75 1c 83 c6 10 fe ...<.t..<.u.....
00000030: cb 75 ef cd 18 8b 14 8b 4c 02 8b ee 83 c6 10 fe .u......L.......
00000040: cb 74 1a 80 3c 00 74 f4 be 8b 06 ac 3c 00 74 0b .t..<.t.....<.t.
00000050: 56 bb 07 00 b4 0e cd 10 5e eb f0 eb fe bf 05 00 V.......^.......
00000060: bb 00 7c b8 01 02 57 cd 13 5f 73 0c 33 c0 cd 13 ..|...W.._s.3...
00000070: 4f 75 ed be a3 06 eb d3 be c2 06 bf fe 7d 81 3d Ou...........}.=
00000080: 55 aa 75 c7 8b f5 ea 00 7c 00 00 49 6e 76 61 6c U.u.....|..Inval
00000090: 69 64 20 70 61 72 74 69 74 69 6f 6e 20 74 61 62 id partition tab
000000a0: 6c 65 00 45 72 72 6f 72 20 6c 6f 61 64 69 6e 67 le.Error loading
000000b0: 20 6f 70 65 72 61 74 69 6e 67 20 73 79 73 74 65 operating syste
000000c0: 6d 00 4d 69 73 73 69 6e 67 20 6f 70 65 72 61 74 m.Missing operat
000000d0: 69 6e 67 20 73 79 73 74 65 6d 00 00 00 00 00 00 ing system......
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 80 01 ................
000001c0: 01 00 06 0f 3f dd 3f 00 00 00 e1 69 03 00 00 00 ....?.?....i....
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.
test@test-ThinkPad-X280:~$ sudo dd if=msdos_hdd.img of=mbr_dump.bin bs=512 count=1
记录了1+0 的读入
记录了1+0 的写出
512字节已复制,0.000234651 s,2.2 MB/s
test@test-ThinkPad-X280:~$ objdump -b binary -m i8086 -M intel -D mbr_dump.bin
mbr_dump.bin: 文件格式 binary
Disassembly of section .data:
00000000 <.data>:
0: fa cli
1: 33 c0 xor ax,ax
3: 8e d0 mov ss,ax
5: bc 00 7c mov sp,0x7c00
8: 8b f4 mov si,sp
a: 50 push ax
b: 07 pop es
c: 50 push ax
d: 1f pop ds
e: fb sti
f: fc cld
10: bf 00 06 mov di,0x600
13: b9 00 01 mov cx,0x100
16: f2 a5 repnz movs WORD PTR es:[di],WORD PTR ds:[si]
18: ea 1d 06 00 00 jmp 0x0:0x61d
1d: be be 07 mov si,0x7be
20: b3 04 mov bl,0x4
22: 80 3c 80 cmp BYTE PTR [si],0x80
25: 74 0e je 0x35
27: 80 3c 00 cmp BYTE PTR [si],0x0
2a: 75 1c jne 0x48
2c: 83 c6 10 add si,0x10
2f: fe cb dec bl
31: 75 ef jne 0x22
33: cd 18 int 0x18
35: 8b 14 mov dx,WORD PTR [si]
37: 8b 4c 02 mov cx,WORD PTR [si+0x2]
3a: 8b ee mov bp,si
3c: 83 c6 10 add si,0x10
3f: fe cb dec bl
41: 74 1a je 0x5d
43: 80 3c 00 cmp BYTE PTR [si],0x0
46: 74 f4 je 0x3c
48: be 8b 06 mov si,0x68b
4b: ac lods al,BYTE PTR ds:[si]
4c: 3c 00 cmp al,0x0
4e: 74 0b je 0x5b
50: 56 push si
51: bb 07 00 mov bx,0x7
54: b4 0e mov ah,0xe
56: cd 10 int 0x10
58: 5e pop si
59: eb f0 jmp 0x4b
5b: eb fe jmp 0x5b
5d: bf 05 00 mov di,0x5
60: bb 00 7c mov bx,0x7c00
63: b8 01 02 mov ax,0x201
66: 57 push di
67: cd 13 int 0x13
69: 5f pop di
6a: 73 0c jae 0x78
6c: 33 c0 xor ax,ax
6e: cd 13 int 0x13
70: 4f dec di
71: 75 ed jne 0x60
73: be a3 06 mov si,0x6a3
76: eb d3 jmp 0x4b
78: be c2 06 mov si,0x6c2
7b: bf fe 7d mov di,0x7dfe
7e: 81 3d 55 aa cmp WORD PTR [di],0xaa55
82: 75 c7 jne 0x4b
84: 8b f5 mov si,bp
86: ea 00 7c 00 00 jmp 0x0:0x7c00
8b: 49 dec cx
8c: 6e outs dx,BYTE PTR ds:[si]
8d: 76 61 jbe 0xf0
8f: 6c ins BYTE PTR es:[di],dx
90: 69 64 20 70 61 imul sp,WORD PTR [si+0x20],0x6170
95: 72 74 jb 0x10b
97: 69 74 69 6f 6e imul si,WORD PTR [si+0x69],0x6e6f
9c: 20 74 61 and BYTE PTR [si+0x61],dh
9f: 62 6c 65 bound bp,DWORD PTR [si+0x65]
a2: 00 45 72 add BYTE PTR [di+0x72],al
a5: 72 6f jb 0x116
a7: 72 20 jb 0xc9
a9: 6c ins BYTE PTR es:[di],dx
aa: 6f outs dx,WORD PTR ds:[si]
ab: 61 popa
ac: 64 69 6e 67 20 6f imul bp,WORD PTR fs:[bp+0x67],0x6f20
b2: 70 65 jo 0x119
b4: 72 61 jb 0x117
b6: 74 69 je 0x121
b8: 6e outs dx,BYTE PTR ds:[si]
b9: 67 20 73 79 and BYTE PTR [ebx+0x79],dh
bd: 73 74 jae 0x133
bf: 65 6d gs ins WORD PTR es:[di],dx
c1: 00 4d 69 add BYTE PTR [di+0x69],cl
c4: 73 73 jae 0x139
c6: 69 6e 67 20 6f imul bp,WORD PTR [bp+0x67],0x6f20
cb: 70 65 jo 0x132
cd: 72 61 jb 0x130
cf: 74 69 je 0x13a
d1: 6e outs dx,BYTE PTR ds:[si]
d2: 67 20 73 79 and BYTE PTR [ebx+0x79],dh
d6: 73 74 jae 0x14c
d8: 65 6d gs ins WORD PTR es:[di],dx
...
1be: 80 01 01 add BYTE PTR [bx+di],0x1
1c1: 00 06 0f 3f add BYTE PTR ds:0x3f0f,al
1c5: dd 3f fnstsw WORD PTR [bx]
1c7: 00 00 add BYTE PTR [bx+si],al
1c9: 00 e1 add cl,ah
1cb: 69 03 00 00 imul ax,WORD PTR [bp+di],0x0
...
1fb: 00 00 add BYTE PTR [bx+si],al
1fd: 00 55 aa add BYTE PTR [di-0x56],dl
分析
さすがに自力で分析することは面倒なためChatGPTを使って分析します。
元々精度はなかなか良いですし、ChatGPTが出力した逆アセンブル結果は自分で逆アセンブルしたものと照らし合わせて間違えていないか確認しながら分析するので心配はありません。
初期化処理(0x0000~0x000F)
割り込みを禁止し、スタックとセグメントレジスタを初期化したあとに割り込みを許可する典型的な書き方ですね。
0x0000: cli ; 割り込み禁止
0x0001: xor ax, ax ; AX = 0
0x0003: mov ss, ax ; SS = 0(スタックセグメントを0に設定)
0x0005: mov sp, 0x7C00 ; SP = 0x7C00(スタックを初期化)
0x0008: mov si, sp ; SI = SP
0x000A: push ax; pop es ; ES = 0
0x000C: push ax; pop ds ; DS = 0
0x000E: sti ; 割り込み許可
0x000F: cld ; 方向フラグクリア(文字列操作用処理)
自己複製(Relocation)(0x0010~0x0018)
MBRコードを 0x600 に複製し、そちらへ移動して続きのコードを実行します。
MBRはBIOSによって0x7C00へ読み込まれ、0x7C00から実行が始まります。
MBRの処理が終わり使命を終えるとVGR(OSを起動するブートローダ)を0x7C00へ読み込むのですが、そうすると自身が上書きされてしまうために退避をしています。(多分)
0x0010: mov di, 0x0600 ; 書き込み先アドレス(偏移0x600)
0x0013: mov cx, 0x0100 ; ワード数 = 256(512バイト)
0x0016: rep movsw ; DS:SI から ES:DI に 512 バイトをコピー
0x0018: jmp 0x0000:0x061D ; コピー先コードへジャンプ
パーティション探索(0x001D~0x0031)
MBRには最大4つのパーティションが登録できます。
(Windows等で一つのHDDをCドライブ、Dドライブ、Eドライブと複数に分割できるあの機能です。)
先頭のデータが0x80(OS起動可能)となっているパーティションを探し、あれば0x0035の処理へ移ります。
最大4つまで登録できるため最大4回走査します。
001D: BE BE 07 ; mov si, 0x07BE (パーティションテーブルの先頭)
0020: B3 04 ; mov bl, 0x04 (最大4つの項目を走査)
0022: 80 3C 80 ; cmp byte [si], 0x80 (アクティブフラグ確認)
0025: 74 0E ; je 0x0035 (見つかったらジャンプ)
0027: 80 3C 00 ; cmp byte [si], 0x00 (非アクティブならOK)
002A: 75 1C ; jne 0x0048(それ以外は不正 → エラー処理)
002C: 83 C6 10 ; add si, 0x10 (次のエントリへ)
002F: FE CB ; dec bl (カウンタ減)
0031: 75 EF ; jne 0x0022 (ループ続行)
パーティションテーブルの項目は以下の用になっています。(16バイト/項目×4項目)
このようなデータが最大4つ登録できます。この処理では0x00しか参照していません。
オフセット | 大きさ | 説明 | 値の例(0x01BE-0x01CD ) |
補足 |
---|---|---|---|---|
0x00 | 1バイト | ブートフラグ | 0x80 |
0x80 =アクティブ, 0x00 =非アクティブ |
0x01 | 1バイト | 開始ヘッド番号 | 0x01 |
CHS(Cylinder-Head-Sector)のHead |
0x02 | 2バイト | 開始シリンダ/セクタ | 0x0001 |
下位6ビットがセクタ番号、上位10ビットがシリンダ番号 |
0x04 | 1バイト | パーティション種類 | 0x06 |
0x06 =FAT16, 0x0B =FAT32など |
0x05 | 1バイト | 終了ヘッド番号 | 0x0F |
|
0x06 | 2バイト | 終了シリンダ/セクタ | 0x3FDD |
|
0x08 | 4バイト | 開始LBAアドレス | 0x0000003F |
論理セクタ番号(リトルエンディアン) |
0x0C | 4バイト | セクタ数 | 0x000369E1 |
パーティションの大きさ(セクタ単位) |
0x0048 - エラー文言出力(Invalid partition)
エラーの文言を出力しそのあと停止します。
CPUを停止させたいときによく使うjmp $ですね。
現在実行中のアドレスへjmpし続けること次の命令を実行させず、停止させることができます。
BIOS呼び出し0x10を使用して文字を一文字づつ出力しています。
mov si, 0x068BのところははMBR内の固定オフセット(0x68B = 0x600 + 0x8B)を指す
0048: BE 8B 06 ; mov si, 0x068B;
004B: AC ; lodsb ; 1文字読み込み
004C: 3C 00 ; cmp al, 0 ; 終端文字(0)かどうか判別
004E: 74 0B ; je 0x005B
0050: 56 ; push si
0051: BB 07 00 ; mov bx, 0x0007
0054: B4 0E ; mov ah, 0x0E
0056: CD 10 ; int 0x10 ; BIOS機能 1文字表示
0058: 5E ; pop si
0059: EB F0 ; jmp 0x004B
005B: EB FE ; jmp 0x005B(停止)
この処理は関数として3ヶ所から呼び出されている。
以下にその呼び出し部分を挙げる。
パーティションテーブル不正
"Invalid partition table"と表示する
mov si, 0x068Bで文字列を指す
0x68B = 0x600 + 0x8B
002A: 75 1C ; jne 0x0048(それ以外は不正 → エラー処理)
OS読み込みエラー(パーティションセクタの読み込みに失敗した場合)
"Error loading operating system"と表示する
0x06A3 = 0x600 + 00A3
※この場合mov si, 0x068Bは実行されない
0073: BE A3 06 ; mov si, 0x06A3
0076: EB D3 ; jmp 0x004B(エラー表示へ)
Boot sectorの署名が無効
"Missing operating system"と表示
0x06C2 = 0x600 + 0xC2
※この場合mov si, 0x068Bは実行されない
0078: BE C2 06 ; mov si, 0x06C2(Missing OS)
007B: BF FE 7D ; mov di, 0x7DFE(署名チェック位置)
007E: 81 3D 55 AA ; cmp word [di], 0xAA55
0082: 75 C5 ; jne 0x004B(署名不正 → Missing OS)
0x005D - パーティションセクタの読み込み開始
5回まで読み込み、最終的に失敗したら "Error loading operating system" を表示
005D: BF 05 00 ; mov di, 0x0005(再試行回数)
0060: BB 00 7C ; mov bx, 0x7C00(読み込み先アドレス)
0063: B8 01 02 ; mov ax, 0x0201(セクタ1個読み込み)
0066: 57 ; push di
0067: CD 13 ; int 0x13(BIOS機能)
0069: 5F ; pop di
006A: 73 0C ; jae 0x0078(成功)
006C: 31 C0 ; xor ax, ax
006E: CD 13 ; int 0x13(ディスクリセット)
0070: 4F ; dec di
0071: 75 ED ; jne 0x0060(再試行)
0073: BE A3 06 ; mov si, 0x06A3
0076: EB D3 ; jmp 0x004B(エラー表示へ)
0x0078 - Boot sectorの署名確認と実行
読み込んだブートセクタの末尾が0xAA55であるかを確認し、
違う場合は有効なブートセクタではないと判断してMissing operating systemと表示する。
0078: BE C2 06 ; mov si, 0x06C2(Missing OS)
007B: BF FE 7D ; mov di, 0x7DFE(署名チェック位置)
007E: 81 3D 55 AA ; cmp word [di], 0xAA55
0082: 75 C5 ; jne 0x004B(署名不正 → Missing OS)
0084: 89 EE ; mov si, bp(保存したパーティション情報)
0086: EA 00 7C 00 00 ; jmp 0x0000:0x7C00(Boot Sectorへジャンプ)
0x008B〜:エラー文言(データ)
1つめのパーティション項目(0x01B0〜0x01BF)
008B: "Invalid partition table" + 00
00A3: "Error loading operating system" + 00
00C2: "Missing operating system" + 00
パーティションテーブル(0x01B0〜0x01FF)
オフセット | サイズ | 説明 | 値(16進) | 解析値 |
---|---|---|---|---|
0x01BE | 1バイト | ブートフラグ | 80 | アクティブ(起動可能) |
0x01BF〜0x01C1 | 3バイト | 開始CHSアドレス | 01 01 00 | ヘッド:1、セクタ:1、シリンダ:0(詳細計算要) |
0x01C2 | 1バイト | パーティションタイプ | 06 | FAT16 |
0x01C3〜0x01C5 | 3バイト | 終了CHSアドレス | 0F 3F DD | ヘッド:15、セクタ:63、シリンダ:221(詳細計算要) |
0x01C6〜0x01C9 | 4バイト | 開始LBA | 3F 00 00 00 | 63(0x3F) |
0x01CA〜0x01CD | 4バイト | セクタ数 | E1 69 03 00 | 224,737セクタ(約109.7MB) |
今後
次回は、LBA 63 に存在すると推定されるボリューム・ブート・レコード(VBR)の解析を行います。
最終的には、MS-DOS と同様の手順で自作 OS を起動することを目標としています。
一般的な OS 自作書籍では、MBR(マスターブートレコード)と VBR(ボリュームブートレコード)の役割を明確に分けず、一体化した単純な構成で解説されている例が多く見られます。MBR から直接カーネルを読み込んで起動する方法も確かに有効ですが、FAT16 などのファイルシステムを取り入れる場合、MS-DOS 方式の「MBR → VBR → IO.SYS → MSDOS.SYS」的な2段階構成の方が実装しやすく、将来的な拡張にも対応しやすいと考えています。
私のこれまでの記事でも、MBR(先頭の 512 バイト)から直接カーネルコードを呼び出す例を扱いましたが、ここからは実際の OS に近い構成を意識し、MBR と VBR を分けて扱っていきます。