PCの電源を入れると、「BIOS」と呼ばれるマザーボードに元から書き込まれているソフトウェアが、起動ディスクの第1セクタ(MBRと呼ばれる)をRAMへロードし、CPUに実行させます。1セクタは512バイトです。ブートローダー・カーネル・ユーザーランドが512バイトに収まるはずもないので、この512バイトのプログラムで適宜RAMへロードする必要があります。なお、OS5のブートローダーは512バイトに収まっているので、OS5の場合は512バイトのプログラムだけでブートローダーは完結しています*1。
[*1] GRUB等の高機能なブートローダーは512バイトに収まらないので、ブートローダーを512バイトの初段と、そこからロードされる2段目という形で多段ブートしていたりします。
ブートローダーでは、カーネルとユーザーランドのRAMへのロードと、CPU設定を行っています。CPU設定は、割り込みや各種ディスクリプタテーブル*2の設定などを行っています。CPU設定で特に重要なのは、リアルモード(16ビットのモード)からプロテクトモード(32ビットのモード)への移行です。CPUのモードをプロテクトモードへ移行させた後、カーネルの先頭アドレスへジャンプします。
[*2] x86 CPUには「セグメント」という単位でメモリを分割して管理するメモリ管理方法があり、そのための設定です。各セグメントの設定は「ディスクリプタ」というデータ構造で行います。「ディスクリプタ」の「テーブル」なので「ディスクリプタテーブル」です。
1: .s.o: 2: as --32 -o $@ $< 3: 4: boot.bin: boot.o 5: ld -m elf_i386 -o $@ $< -T boot.ld -Map boot.map 6: 7: boot.o: boot.s 8: 9: clean: 10: rm -f *~ *.o *.bin *.dat *.img *.map 11: 12: .PHONY: clean
ブートローダーのMakefileです。現状、ブートローダーはすべてアセンブラ書いています(512バイトと小さく、Cで処理を書くほどのことをしていない為)。ソースファイルはboot.sだけです。Makefileとしても、この単一のアセンブラファイルをas(GNUアセンブラ)でアセンブルし、ld(GNUリンカ)でリンク、を行っています。
コンパイルを行う環境はx86_64の環境なので、x86_32のバイナリを生成するために、asコマンドでは"--32"オプションを、ldコマンドでは"-m elf_i386"のオプションをつけています。なお、恥ずかしながら、当初はこれらのオプションを知りませんでした。そのため、開発環境をx86_32からx86_64へ変えたときは、x86_32の仮想環境を用意していました。x86_32のバイナリを生成するこれらのオプションは、パッチを作成された方がいて、マージさせていただいたものです。(これがマージさせてもらった初めてで、今のところ唯一のパッチです。)
あと、Makefileの書き方ですが、.s.o:
という書き方は、boot.o: boot.s
のように個々のターゲットを記述しなければならないので、あんまり良くないなぁと思います。%.o: %.s
の書き方で汎用的なターゲット指定を書いておけば、boot.o: boot.s
の記述を消せますね。なお、本書で説明はしていないのですが、ドキュメントディレクトリ*3のMakefileでは'%'の書き方で汎用的なターゲット指定を行っています。(ブートローダー等の古いMakefileも直すべきで、自分のタスクリストには入っていたのですが、優先度:低で放置していました。言い訳ですが。。。)
[*3] OS5のソースディレクトリ直下のdocディレクトリ
1: OUTPUT_FORMAT("binary"); 2: 3: SECTIONS 4: { 5: .text : {*(.text)} 6: .rodata : { 7: *(.strings) 8: *(.rodata) 9: *(.rodata.*) 10: } 11: .data : {*(.data)} 12: .bss : {*(.bss)} 13: 14: . = 510; 15: .sign : {SHORT(0xaa55)} 16: }
ブートローダーのリンカスクリプトです。
MBRの512バイトの先頭へジャンプしてくるので、textセクションが先頭に来るように並べています。
また、BIOSが「起動可能なディスクか否か」の判別として、「MBRの末尾2バイトが"0xaa55"であるか」をチェックしているので、リンカスクリプトで先頭から510バイト目に"0xaa55"を配置するようにしています。
1: .code16 2: 3: .text 4: cli 5: 6: movw $0x07c0, %ax 7: movw %ax, %ds 8: movw $0x0000, %ax 9: movw %ax, %ss 10: movw $0x1000, %sp 11: 12: /* ビデオモード設定(画面クリア) */ 13: movw $0x0003, %ax 14: int $0x10 15: 16: movw $msg_welcome, %si 17: call print_msg 18: 19: movw $msg_now_loading, %si 20: call print_msg 21: 22: /* ディスクサービス セクタ読み込み 23: * int 0x13, AH=0x02 24: * 入力 25: * - AL: 読み込むセクタ数 26: * - CH: トラックの下位8ビット 27: * - CL(上位2ビット): トラックの上位2ビット 28: * - CL(下位6ビット): セクタを指定 29: * - DH: ヘッド番号を指定 30: * - DL: セクタを読み込むドライブ番号を指定 31: * - ES: 読み込み先のセグメント指定 32: * - BX: 読み込み先のオフセットアドレス指定 33: * 出力 34: * - EFLAGSのCFビット: 0=成功, 1=失敗 35: * - AH: エラーコード(0x00=成功) 36: * - AL: 読み込んだセクタ数 37: * 備考 38: * - トラック番号: 0始まり 39: * - ヘッド番号: 0始まり 40: * - セクタ番号: 1始まり 41: * - セクタ数/トラック: 2HDは18 42: * - セクタ18の次は、別トラック(裏面)へ 43: * - 64KB境界を超えて読みだすことはできない 44: * (その際は、2回に分ける) 45: */ 46: 47: /* トラック0, ヘッド0, セクタ2以降 48: * src: トラック0, ヘッド0のセクタ2以降 49: * (17セクタ = 8704バイト = 0x2200バイト) 50: * dst: 0x0000 7e00 〜 0x0000 bfff 51: */ 52: load_track0_head0: 53: movw $0x0000, %ax 54: movw %ax, %es 55: movw $0x7e00, %bx 56: movw $0x0000, %dx 57: movw $0x0002, %cx 58: movw $0x0211, %ax 59: int $0x13 60: jc load_track0_head0 61: 62: /* トラック0, ヘッド1, 全セクタ 63: * src: トラック0, ヘッド1の全セクタ 64: * (18セクタ = 9216バイト = 0x2400バイト) 65: * dst: 0x0000 a000 〜 0x0000 c3ff 66: */ 67: load_track0_head1: 68: movw $0x0000, %ax 69: movw %ax, %es 70: movw $0xa000, %bx 71: movw $0x0100, %dx 72: movw $0x0001, %cx 73: movw $0x0212, %ax 74: int $0x13 75: jc load_track0_head1 76: 77: /* トラック1, ヘッド0, 全セクタ 78: * src: トラック1, ヘッド0の全セクタ 79: * (18セクタ = 9216バイト = 0x2400バイト) 80: * dst: 0x0000 c400 〜 0x0000 e7ff 81: */ 82: load_track1_head0: 83: movw $0x0000, %ax 84: movw %ax, %es 85: movw $0xc400, %bx 86: movw $0x0000, %dx 87: movw $0x0101, %cx 88: movw $0x0212, %ax 89: int $0x13 90: jc load_track1_head0 91: 92: /* トラック1, ヘッド1, セクタ1 - 12 93: * src: トラック1, ヘッド1の12セクタ 94: * (12セクタ = 6144バイト = 0x1800バイト) 95: * dst: 0x0000 e800 〜 0x0000 ffff 96: */ 97: load_track1_head1_1: 98: movw $0x0000, %ax 99: movw %ax, %es 100: movw $0xe800, %bx 101: movw $0x0100, %dx 102: movw $0x0101, %cx 103: movw $0x020c, %ax 104: int $0x13 105: jc load_track1_head1_1 106: 107: /* トラック1, ヘッド1, セクタ13 - 18 108: * src: トラック1, ヘッド1の6セクタ 109: * (6セクタ = 3072バイト = 0xc00バイト) 110: * dst: 0x0001 0000 〜 0x0001 0bff 111: */ 112: load_track1_head1_2: 113: movw $0x1000, %ax 114: movw %ax, %es 115: movw $0x0000, %bx 116: movw $0x0100, %dx 117: movw $0x010d, %cx 118: movw $0x0206, %ax 119: int $0x13 120: jc load_track1_head1_2 121: 122: /* トラック2, ヘッド0, 全セクタ 123: * src: トラック2, ヘッド0の全セクタ 124: * (18セクタ = 9216バイト = 0x2400バイト) 125: * dst: 0x0001 0c00 〜 0x0001 2fff 126: */ 127: load_track2_head0: 128: movw $0x1000, %ax 129: movw %ax, %es 130: movw $0x0c00, %bx 131: movw $0x0000, %dx 132: movw $0x0201, %cx 133: movw $0x0212, %ax 134: int $0x13 135: jc load_track2_head0 136: 137: /* トラック2, ヘッド1, 全セクタ 138: * src: トラック2, ヘッド1の全セクタ 139: * (18セクタ = 9216バイト = 0x2400バイト) 140: * dst: 0x0001 3000 〜 0x0001 53ff 141: */ 142: load_track2_head1: 143: movw $0x1000, %ax 144: movw %ax, %es 145: movw $0x3000, %bx 146: movw $0x0100, %dx 147: movw $0x0201, %cx 148: movw $0x0212, %ax 149: int $0x13 150: jc load_track2_head1 151: 152: /* トラック3, ヘッド0, 全セクタ 153: * src: トラック3, ヘッド0の全セクタ 154: * (18セクタ = 9216バイト = 0x2400バイト) 155: * dst: 0x0001 5400 〜 0x0001 77ff 156: */ 157: load_track3_head0: 158: movw $0x1000, %ax 159: movw %ax, %es 160: movw $0x5400, %bx 161: movw $0x0000, %dx 162: movw $0x0301, %cx 163: movw $0x0212, %ax 164: int $0x13 165: jc load_track3_head0 166: 167: /* トラック3, ヘッド1, 全セクタ 168: * src: トラック3, ヘッド1の全セクタ 169: * (18セクタ = 9216バイト = 0x2400バイト) 170: * dst: 0x0001 7800 〜 0x0001 9bff 171: */ 172: load_track3_head1: 173: movw $0x1000, %ax 174: movw %ax, %es 175: movw $0x7800, %bx 176: movw $0x0100, %dx 177: movw $0x0301, %cx 178: movw $0x0212, %ax 179: int $0x13 180: jc load_track3_head1 181: 182: /* トラック4, ヘッド0, 全セクタ 183: * src: トラック4, ヘッド0の全セクタ 184: * (18セクタ = 9216バイト = 0x2400バイト) 185: * dst: 0x0001 9c00 〜 0x0001 bfff 186: */ 187: load_track4_head0: 188: movw $0x1000, %ax 189: movw %ax, %es 190: movw $0x9c00, %bx 191: movw $0x0000, %dx 192: movw $0x0401, %cx 193: movw $0x0212, %ax 194: int $0x13 195: jc load_track4_head0 196: 197: /* トラック4, ヘッド1, 全セクタ 198: * src: トラック4, ヘッド1の全セクタ 199: * (18セクタ = 9216バイト = 0x2400バイト) 200: * dst: 0x0001 c000 〜 0x0001 e3ff 201: */ 202: load_track4_head1: 203: movw $0x1000, %ax 204: movw %ax, %es 205: movw $0xc000, %bx 206: movw $0x0100, %dx 207: movw $0x0401, %cx 208: movw $0x0212, %ax 209: int $0x13 210: jc load_track4_head1 211: 212: /* トラック5, ヘッド0, セクタ1 - 14 213: * src: トラック5, ヘッド0のセクタ1〜14 214: * (14セクタ = 7168バイト = 0x1c00バイト) 215: * dst: 0x0001 e400 〜 0x0001 ffff 216: */ 217: load_track5_head0_1: 218: movw $0x1000, %ax 219: movw %ax, %es 220: movw $0xe400, %bx 221: movw $0x0000, %dx 222: movw $0x0501, %cx 223: movw $0x020e, %ax 224: int $0x13 225: jc load_track5_head0_1 226: 227: movw $msg_completed, %si 228: call print_msg 229: 230: /* マスタPICの初期化 */ 231: movb $0x10, %al 232: outb %al, $0x20 /* ICW1 */ 233: movb $0x00, %al 234: outb %al, $0x21 /* ICW2 */ 235: movb $0x04, %al 236: outb %al, $0x21 /* ICW3 */ 237: movb $0x01, %al 238: outb %al, $0x21 /* ICW4 */ 239: movb $0xff, %al 240: outb %al, $0x21 /* OCW1 */ 241: 242: /* スレーブPICの初期化 */ 243: movb $0x10, %al 244: outb %al, $0xa0 /* ICW1 */ 245: movb $0x00, %al 246: outb %al, $0xa1 /* ICW2 */ 247: movb $0x02, %al 248: outb %al, $0xa1 /* ICW3 */ 249: movb $0x01, %al 250: outb %al, $0xa1 /* ICW4 */ 251: movb $0xff, %al 252: outb %al, $0xa1 /* OCW1 */ 253: 254: call waitkbdout 255: movb $0xd1, %al 256: outb %al, $0x64 257: call waitkbdout 258: movb $0xdf, %al 259: outb %al, $0x60 260: call waitkbdout 261: 262: /* GDTを0x0009 0000から配置 */ 263: movw $0x07c0, %ax /* src */ 264: movw %ax, %ds 265: movw $gdt, %si 266: movw $0x9000, %ax /* dst */ 267: movw %ax, %es 268: subw %di, %di 269: movw $12, %cx /* words */ 270: rep movsw 271: 272: movw $0x07c0, %ax 273: movw %ax, %ds 274: lgdtw gdt_descr 275: 276: movw $0x0001, %ax 277: lmsw %ax 278: 279: movw $2*8, %ax 280: movw %ax, %ds 281: movw %ax, %es 282: movw %ax, %fs 283: movw %ax, %gs 284: movw %ax, %ss 285: 286: ljmp $8, $0x7e00 287: 288: print_msg: 289: lodsb 290: andb %al, %al 291: jz print_msg_ret 292: movb $0xe, %ah 293: movw $7, %bx 294: int $0x10 295: jmp print_msg 296: print_msg_ret: 297: ret 298: waitkbdout: 299: inb $0x60, %al 300: inb $0x64, %al 301: andb $0x02, %al 302: jnz waitkbdout 303: ret 304: 305: .data 306: gdt_descr: 307: .word 3*8-1 308: .word 0x0000, 0x09 309: /* .word gdt,0x07c0 310: * と設定しても、 311: * GDTRには、ベースアドレスが 312: * 0x00c0 [gdtの場所] 313: * と読み込まれてしまう 314: */ 315: gdt: 316: .quad 0x0000000000000000 /* NULL descriptor */ 317: .quad 0x00cf9a000000ffff /* 4GB(r-x:Code) */ 318: .quad 0x00cf92000000ffff /* 4GB(rw-:Data) */ 319: 320: msg_welcome: 321: .ascii "Welcome to OS5!\r\n" 322: .byte 0 323: msg_now_loading: 324: .ascii "Now Loading ... " 325: .byte 0 326: msg_completed: 327: .ascii "Completed!\r\n" 328: .byte 0
ブートローダー本体のソースコードです。アセンブラは、時間がたつと真っ先に読めなくなる箇所なので、少し詳しく説明します。
このソースコードで行っている処理の流れは以下の通りです。
「1. CPU設定」について、まず、".code16"が16bit命令のアセンブラであることを示しています。"cli"ではすべての割り込みを無効化し、ブートローダーの処理中の、まだハードウェアの設定を行っていない段階で割り込みを受け付けないようにしています。6~10行目の"movw"命令の辺りではセグメントの設定とスタックポインタの設定をしています。OS5ではブートローダーの段階ではスタック領域のベースを0x0000 1000に設定しています。そして、12~14行目ではBIOSの機能を使ってビデオモードの設定と画面クリアを行っています。BIOSの機能は「1. 汎用レジスタにパラメータをセット」、「2. ソフトウェア割り込み」の流れです。ここではテキストモードを示す"0x03"をAXレジスタの下位8ビット(ALレジスタ)にセットし、画面モードに関する機能を呼び出す0x10のソフトウェア割り込みを実行しています。
「2. FDからRAMへロード」について、ブートローダーの行数の大半がこの処理です。24~227行目までの203行あり、全328行の内の6割程あります。見ればわかる通りですが、47~60行目のようなコード片を繰り返し並べています。64KB境界をまたぐ場合を除き、1つのコードブロックで1つのトラックをロードします。1トラックずつなのはBIOSの機能でまとまってロードできるのが1トラックずつだったためです。ループ等を使わずにコード片を何度も書いているのは、アセンブラの領域はあまり凝ったことをすると保守できなくなる(2~3か月後とかに見たとき、処理の流れを追えなくなる)気がしたからです。FDからのロード処理は、ロードサイズを増やす等で後々に処理を修正する可能性があることが分かっていたので、自分にとって平易な書き方をしています。そのため、ここで使用しているディスクサービスのBIOS命令については、説明をコメントで書いています。
「PIC(Programmable Interrupt Controller)初期化」では、マスタとスレーブのPICの設定で、すべての割り込みを無効化しています。PICのIOレジスタの使い方はIntel 8259のデータシートを確認してください。(あるいは、自作OS系のサイトや書籍などでも紹介しています。)
「4. CPU設定」では、カーネルへジャンプする直前のCPU設定を行っています。254~260行目はキーボードコントローラ(KBC)の設定です。waitkbdoutでは、キーボードコントローラのステータスがビジーを抜けるまで待っています。262~270行目では、0x7c00 XXXXにあるGDT(グローバルディスクリプタテーブル)*4を0x0009 0000へコピーしています。なお、このGDTは、カーネルへロングジャンプするためにしか使いません(カーネル側では別途GDTを設定します)。わざわざ0x0009 0000へコピーしている理由は、lgdtw命令でGDTをロードする際に指定するGDTのセグメントセレクタに0x07c0を指定できなかったためです(何か間違えているのか、どうなのか、不明)。その後、lgdtw命令でGDTRへgst_descrの内容をロードします(272~274行目)。
[*4] x86 CPUで使用する色々なデータ構造の先頭アドレスと長さを管理するテーブルです。セグメントというメモリ管理方式のディスクリプタや、タスク管理のTSS(タスクステートストラクチャ)を登録します。
ここまででプロテクトモード(32ビットのモード)へ移行するための準備が完了です。276~277行目ではMSRの最下位ビットに1をセットし、プロテクトモードへ移行させています。その後、279~284行目でセグメントレジスタへセグメントセレクタを設定しています(ここでは、プロテクトモードでのセグメントセレクタを設定しています)。そして、カーネルの先頭アドレスへジャンプします(286行目)。OS5では、カーネルは0x0000 7e00から配置するように決めています。物理アドレス空間のメモリマップは、図1.2を参照してください。