ブートローダー(その8)
前回は、リアルモードにおける割り込みテーブルとタイマ割り込みの解説と実装を行い、実際に動作することを確認した。
今回はブートシーケンスの話に戻り、プロテクトモードにおけるセグメントと 32-bit プロテクトモードへの以降についての話を実際に書かれたソースを見ながら行う。
まずは、x86 のセグメント機構について軽くおさらいをする。
ここでは、機械語の命令のオペランドや命令のアドレス部で指定される値を論理アドレスと呼ぶ事にする。
このプログラムでは、(ページング機能が実装されていないので) セグメントのベースアドレスとオフセットによって物理アドレスが決定する。
セグメントの識別子を "セグメントセレクタ" と呼ぶ。16 bit で表されるこの値は、セグメントレジスタに保存される。x86 のセグメント機構では、セグメントレジスタを読んでセグメントを識別し、対象のセグメントのベースアドレスとオフセットによってアドレッシングを行っている。
又、グローバルディスクリプタテーブルのうち、一番目のセグメントセレクタ (セレクタ値 0) のセグメントは、無効なセグメントとして予約されており、ディスクリプタテーブルのオフセット 0x08 から始まる 64 bit 毎の値が有効なセグメントの情報として利用できる。
次に、グローバルディスクリプタテーブルについて話をする。
グローバルディスクリプタテーブルは、32-bit プロテクトモードのセグメント機構において、 64 bit で表される各セグメントのベースアドレス、セグメントのサイズ、そしてセグメントが持つ属性情報を持ち、48 bit の GDTR に保存される。GDTR はシステムレジスタの一種で、値を格納するためには lgdt という命令を実行する。
ここで、グローバルディスクリプタテーブルに登録されるディスクリプタについて話をする。
各ディスクリプタの 64 bit のビットフィールドについての説明は、下のページで書詳しくかれている。
http://www.linux-security.cn/ebooks/ulk3-html/0596005652/understandlk-CHP-2-SECT-2.html
ソースでは、224 行目から 240 行目でカーネルコードセグメントとカーネルデータセグメントの初期化を行っている。224 行目では、先頭の 8 バイトを "0" で埋めている。これは、先に話した無効なセグメントを示す予約領域にあたる場所なので "0" で埋めている (別に "0" で埋めることに意味は無い(はず)。"1" で埋めても動く)。
223 gdt:
224 .fill 0x01,8,1
225
226 .word 0xFFFF # 4Gb - (0x100000*0x1000 = 4Gb)
227 .word 0 # base address = 0
228 .word 0x9A00 # code read/exec
229 .word 0x00CF # granularity = 4096, 386
230 # (+5th nibble of limit)
231
232 .word 0xFFFF # 4Gb - (0x100000*0x1000 = 4Gb)
233 .word 0 # base address = 0
234 .word 0x9200 # data read/write
235 .word 0x00CF # granularity = 4096, 386
236 # (+5th nibble of limit) 237 gdt_end:
238 idt_48: 239 .word 0 # idt limit = 0
240 .long 0 # idt base = 0L 241
242 gdt_48: 243 .word gdt_end - gdt - 1 # gdt limit
244 .long 0 # gdt base (filled in later)
242 行目にある変数 gdt_48 は、GDTR に保存される値である。上位 16 bit でグローバルディスクリプタテーブルのサイズを表し、下位 32 bit でディスクリプタテーブルのアドレスを表す。
下位のディスクリプタテーブルのアドレス部分の初期化は、174 行目で行い、175 行目でグローバルディスクリプタテーブルの登録処理を行っている。
170 xorl %eax, %eax # Compute gdt_base
171 movw %ds, %ax # (Convert %ds:gdt to a linear ptr) 172 shll $4, %eax
173 addl $gdt, %eax 174 movl %eax, (gdt_48+2)
174 movl %eax, (gdt_48+2)
175 lgdt gdt_48 # load gdt with whatever is
次はいよいよ、プロテクトモードへの以降である。
CR0 レジスタの下位 1 bit を "1" にする事で、CPU の動作モードがプロテクトモードに変わる。248 行目では、これを行う為 lmsw 命令を実行する。この命令は、オペランドで指定された値の下位 4 bit を CR0 の下位 4 bit に指定する。
248 movw $0x1, %ax 249 lmsw %ax
250 jmp flush_pipeline 251
252 jmp end_loop ## EOP (end of Program) 253
254 ## protected mode ## 255 flush_pipeline:
256 .byte 0x66, 0xea 257 code32:
258 .long startup_32 + 0x90000 # will be set to %cs+startup_32
259 .word __BOOT_CS
ここでようやく CPU の動作モードがプロテクトモードに切り替わったわけだが、プロテクトモードにおけるプログラムを処理する為に、まだ儀式を行う必要がある。
lmsw 命令を実行した後、直下で指定されているラベルにジャンプしている。ここでは、CPU のパイプラインの内容をクリアしている(らしい)。
なぜ jmp 命令によってパイプラインの内容がクリアになるのかというと、(Intel アーキテクチャの仕組みを詳しく理解しているわけではないので、アーキテクチャの一般論になるのだが) パイプラインは次の命令のフェッチやオペランドの解読等を先んじて行っているのだが、動的な処理 (分岐など) が行われると、機械的な先読みによる効能がなくなる為、パイプラインの内容が意味を持たなくなる。そうなったとき、Intel アーキテクチャではパイプラインの内容をクリアしている(らしい)。
パイプラインをクリアする必要性は、オペランドにおけるアドレス指定等で、CPU の動作モードの差異がある為だ(と思う)。
jmp で飛んだ後、256 行目の処理が行われる。これは紛う方無きバイナリコードである。
バイナリコードの 0xea は、gas における ljmp のオペコードに対応する。
.file "foo.s"
.text
.global main
main:
ljmp $0xEE, $0xDDDDDDDD
上記プログラム (foo.s) を自分の環境でコンパイルして解析すると、次の機械語が吐き出される。
0000000 457f 464c 0101 0001 0000 0000 0000 0000
0000010 0001 0003 0001 0000 0000 0000 0000 0000
0000020 0068 0000 0000 0000 0034 0000 0000 0028
0000030 0007 0004 ddea dddd eedd 0000 2e00 7973
0000040 746d 6261 2e00 7473 7472 6261 2e00 6873
0000050 7473 7472 6261 2e00 6574 7478 2e00 6164
0000060 6174 2e00 7362 0073 0000 0000 0000 0000
0000070 0000 0000 0000 0000 0000 0000 0000 0000
0000080 0000 0000 0000 0000 0000 0000 0000 0000
0000090 001b 0000 0001 0000 0006 0000 0000 0000
00000a0 0034 0000 0007 0000 0000 0000 0000 0000
00000b0 0004 0000 0000 0000 0021 0000 0001 0000
00000c0 0003 0000 0000 0000 003c 0000 0000 0000
00000d0 0000 0000 0000 0000 0004 0000 0000 0000
00000e0 0027 0000 0008 0000 0003 0000 0000 0000
00000f0 003c 0000 0000 0000 0000 0000 0000 0000
0000100 0004 0000 0000 0000 0011 0000 0003 0000
0000110 0000 0000 0000 0000 003c 0000 002c 0000
0000120 0000 0000 0000 0000 0001 0000 0000 0000
0000130 0001 0000 0002 0000 0000 0000 0000 0000
0000140 0180 0000 0060 0000 0006 0000 0005 0000
0000150 0004 0000 0010 0000 0009 0000 0003 0000
0000160 0000 0000 0000 0000 01e0 0000 000c 0000
0000170 0000 0000 0000 0000 0001 0000 0000 0000
0000180 0000 0000 0000 0000 0000 0000 0000 0000
0000190 0001 0000 0000 0000 0000 0000 0004 fff1
00001a0 0000 0000 0000 0000 0000 0000 0003 0001
00001b0 0000 0000 0000 0000 0000 0000 0003 0002
00001c0 0000 0000 0000 0000 0000 0000 0003 0003
00001d0 0007 0000 0000 0000 0000 0000 0010 0001
00001e0 6600 6f6f 732e 6d00 6961 006e
00001ec
52 バイト目からの 6 バイトで ddea dddd eedd という数値が出現している。Intel のアーキテクチャの記憶・転送方式はリトルエンディアンなので、これを CPU が解読する順序に直すと、
ea dd dd dd dd ee
となる。先の foo.s の内容と比較すると ljmp 命令は 5 byte の機械語で表され、最初の 1 byte はオペコードで、次の 4 byte が移動先のオフセットで、最後の 1 byte がセグメントセレクタ値である事がわかる。
そもそも何故このように、アセンブラコードに機械語を埋め込むような事をするかというと、second.S の先頭で宣言した .code16gcc によってそれ以下のコードが 16-bit アセンブラプログラムであるとコンパイラが認識するため、ljmp 命令を実行する事ができないので、ljmp 命令のバイナリコードを直接書く事で処理を実現させている。
ここでブートローダーのソースの 256 行目を見てみる。この時点ではまだ 16-bit コードで動いている為、0x66 によって、次の 1 byte の命令を 32-bit コードで実行する。
以下のアドレスの Intel 80386 Programmer's Reference Manual の Mixing 16-Bit and 32 Bit Code にそれに関する記述が存在する。
http://www.logix.cz/michal/doc/i386/
さて。これで一連のブート処理は完了した。ダンプを見ると、CR0 の最下位ビットが "1" になっている事が確認できる。
ここで、32-bit プロテクトモードに入ってからの処理が完了する事を確認してみる。
270 print_vga:
271 pushw %es
272 movl $0x000b8000, %ebx
273 movw %ax, %es
274
275 movw $0x0000, %cx
276 movb $0x9f, %dl
277
278 movb $0x50, (%ebx)
279 inc %ebx
280 movb %dl, (%ebx)
281
282 popw %es
270 行目からの処理は、前々回話した VRAM による文字出力を行う(ちゃんと 32 bit アドレッシングを行っている)。次の画像は、実行時のスクリーンショットである。ちゃんと、背景色が紫の文字 "P" が画面の左上に出力される。
ここまでで、ブートローダの峠を越えた事は間違い無い。
ここから 32-bit プログラムがはじまる。
- Category(s)
- 学習経過
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/ohyama/30fc30c830ed30fc30fc-305d306e8/tbping