Top

第2章 ブートローダー

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には「セグメント」という単位でメモリを分割して管理するメモリ管理方法があり、そのための設定です。各セグメントの設定は「ディスクリプタ」というデータ構造で行います。「ディスクリプタ」の「テーブル」なので「ディスクリプタテーブル」です。

2.1 MBR部

boot/Makefile

リスト2.1: boot/Makefile

 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ディレクトリ

boot/boot.ld

リスト2.2: boot/boot.ld

 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"を配置するようにしています。

boot/boot.s

リスト2.3: boot/boot.s

 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設定(1~21行目)
  2. FDからRAMへロード(22~228行目)
  3. PIC(Programmable Interrupt Controller)初期化(230~252行目)
  4. CPU設定(254~284行目)
  5. カーネルへジャンプ(286行目)

「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を参照してください。


Top