3.1. bootasm.S
bootasm.SではGDTを作り、プロテクトモードへ切り替え、bootmain関数を呼び出す。
A20ラインの有効化
最初にBIOSが立ち上がり、xv6.imgの最初の510~511バイト目の0x55AAを見てブートセクタだと判断して物理アドレス0x7C00にディスクから1セクタ(512バイト)分読み込む。
つまりxv6.imgのbootblockの部分が0x7C00に読み込まれる。
bootblockの頭の方はbootasm.Sなので、startから実行が開始される。
cli命令で割り込みを無効化する。
axレジスタを使い、各セグメントレジスタ(ds, es, ss)の値を0に初期化する。
次に、キーボードコントローラを使ってアドレスバスのA20ライン以上を使えるように設定を行う。
このA20ラインや、セグメント機構、ページング、割り込み等については「はじめて読む486」(書籍2)に詳しく記載されている。
まだアドレスバスが0~19までしか無く、20bitで1MBのアドレスを使用していた時代、0xFFFFFの次0x100000へのアクセスで0x0にアクセスすることができたらしく、その性質を利用したプログラムも書かれていたらしい。その互換性を維持するために起動時はアドレスバスがA19までしか使用できないようになっているらしいので、この上限を開放する必要がある。
A20ラインの有効化にはいくつかの方法があり、その内のひとつとしてキーボードコントローラを使用した方法がある。
キーボードコントローラの仕様については「Keyboard scancodes」(リンク7)の「The AT keyboard controller」に載っている。
上記リンクによると、ポート0x64からの読み込みはステータスレジスタの内容となり、書き込みはコマンドとして解釈される。
また、ポート0x60への書き込みはコマンドのデータとして解釈される。
ラベルsata20.1では、ポート0x64からステータス0x2以外が読み出せるまでループしている。 ステータスは0bitがアウトプットバッファを示し、1bitがインプットバッファを示していて、0が空で1がフル。 バッファが空だった場合、ポート0x64にコマンド0xd1を書き込む。0xd1はアウトプットポートにデータを書き込むコマンド。 ラベルsata20.2のループでバッファが空であることを確認できるまで待ち、ポート0x60にデータ0xdfを書き込む。データ0xdfをアウトプットポートに書き込むと、A20ラインが有効になる。
bootasm.S
.code16 # Assemble for 16-bit mode
.globl start
start:
cli # BIOS enabled interrupts; disable
# Zero data segment registers DS, ES, and SS.
xorw %ax,%ax # Set %ax to zero
movw %ax,%ds # -> Data Segment
movw %ax,%es # -> Extra Segment
movw %ax,%ss # -> Stack Segment
# Physical address line A20 is tied to zero so that the first PCs
# with 2 MB would run software that assumed 1 MB. Undo that.
seta20.1:
inb $0x64,%al # Wait for not busy
testb $0x2,%al
jnz seta20.1
movb $0xd1,%al # 0xd1 -> port 0x64
outb %al,$0x64
seta20.2:
inb $0x64,%al # Wait for not busy
testb $0x2,%al
jnz seta20.2
movb $0xdf,%al # 0xdf -> port 0x60
outb %al,$0x60
GDTの作成とロード
プロテクトモードに切り替える準備をする。
プロテクトモードのセグメント機構ではセグメントディスクリプタを使うので、事前にGDTを作ってlgdt命令で設定する必要がある。GDTに関しても「はじめて読む486」(書籍2)に詳しく記載されている。
lgdt命令ではgdtrにGDTのサイズとアドレスをセットする。
サイズは2バイトで、ラベル間の差を取って(gtddesc - gdt - 1)求める。
アドレスにはgdtラベルのアドレスをセットする。
gdtラベルからGDTのエントリが書かれてる。全部で3エントリ。セグメントディスクリプタを作成するSEG_ASMマクロはasm.hに定義されている。
p2align 2で2 * 2 = 4バイトでアラインメントする。
セグメントディスクリプタの構造はプロセッサのマニュアル「Intel 64 and IA-32 architectures software developer's manual combined volumes: 1, 2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D, and 4」(リンク8)の「vol.3A 3.4.5 Segment Descriptors」に記載されてる。
作成されるセグメントディスクリプタの内容は、ビルドで作成されたbootblockバイナリを見て、マニュアルの図と照らし合わせるとわかりやすい。
SEG_ASMマクロと引数の一部を電卓で計算すると、コードセグメントディスクリプタの方に値0x9Aが入っているのが分かるので、bootblockをxxdで開いてlessにパイプして9aでサーチすると位置0x60にGDTを見つけることができる。
GDTに作成される3つのエントリは以下のようになっている。
エントリ1: NULL。8バイト全て0。
エントリ2: コードセグメントディスクリプタ。値は16進数で FF FF 00 00 00 9A CF 00。
エントリ3: データセグメントディスクリプタ。値は16進数で FF FF 00 00 00 92 CF 00。
コードセグメントディスクリプタの値はリミット値が0xFFFFF、セグメントベースが0x00000000、属性が0x9AC。属性は以下のようになってる。
type 1100
S 0
DPL 01
P 1
AVL 1
O 0
D 0
G 1
リミット値は0xFFFFFだけど、Gフラグが1だからページ単位になって4K倍されるので0xFFFFF * 4096 = 4GB。
データセグメントディスクリプタの値もコードセグメントディスクリプタとだいたい同じになっている。属性は0x92C。
bootasm.S
# Bootstrap GDT
.p2align 2 # force 4 byte alignment
gdt:
SEG_NULLASM # null seg
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg
SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg
gdtdesc:
.word (gdtdesc - gdt - 1) # sizeof(gdt) - 1
.long gdt # address gdt
asm.h
#define SEG_NULLASM \
.word 0, 0; \
.byte 0, 0, 0, 0
// The 0xC0 means the limit is in 4096-byte units
// and (for executable segments) 32-bit mode.
#define SEG_ASM(type,base,lim) \
.word (((lim) >> 12) & 0xffff), ((base) & 0xffff); \
.byte (((base) >> 16) & 0xff), (0x90 | (type)), \
(0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)
#define STA_X 0x8 // Executable segment
#define STA_W 0x2 // Writeable (non-executable segments)
#define STA_R 0x2 // Readable (executable segments)
プロテクトモードへの切り替え
cr0のプロテクトモード有効フラグ(0bit)を立ててプロテクトモードに切り替える。
GDTのコードセグメントディスクリプタを使ってプロテクトモードでの実行を開始するために、ファージャンプをする。csレジスタの値はファージャンプでなければ変更できないため。
セグメントディスクリプタは1エントリ8バイトなのでljmp $8, $start32
で、NULLの次のコードセグメントディスクリプタを指定する。
bootasm.S
# Switch from real to protected mode. Use a bootstrap GDT that makes
# virtual addresses map directly to physical addresses so that the
# effective memory map doesn't change during the transition.
lgdt gdtdesc
movl %cr0, %eax
orl $CR0_PE, %eax
movl %eax, %cr0
//PAGEBREAK!
# Complete the transition to 32-bit protected mode by using a long jmp
# to reload %cs and %eip. The segment descriptors are set up with no
# translation, so that the mapping is still the identity mapping.
ljmp $(SEG_KCODE<<3), $start32
mmu.h
#define SEG_KCODE 1 // kernel code
#define SEG_KDATA 2 // kernel data+stack
start32ラベルから32bit命令で実行が始まる。
ds, es, ssにデータセグメントディスクリプタを設定する。
fs, gsに0を設定する。
espにstartのアドレスを設定する。startは0x7C00なのでそこから下に向かってスタックが伸びていくことになる。
bootmain関数を呼び出す。bootmain関数はbootmain.cに定義されいる。
bootasm.S
.code32 # Tell assembler to generate 32-bit code now.
start32:
# Set up the protected-mode data segment registers
movw $(SEG_KDATA<<3), %ax # Our data segment selector
movw %ax, %ds # -> DS: Data Segment
movw %ax, %es # -> ES: Extra Segment
movw %ax, %ss # -> SS: Stack Segment
movw $0, %ax # Zero segments not ready for use
movw %ax, %fs # -> FS
movw %ax, %gs # -> GS
# Set up the stack pointer and call into C.
movl $start, %esp
call bootmain
もしもbootmainがリターンした場合、ポート0x8a00にデータ0x8ae0を送る。
Bochsのマニュアル「Bochs Developers Guide」(リンク9)の「Chapter 3. Advanced debugger usage」から、ポート0x8A00はコマンドレジスタのサーバになっていて、コマンド0x8ae0はデバッガプロンプトに戻ることが分かる。そしてデバッガプロンプトに戻るということはCtrl-Cと同義であると書いてある。
デバッガプロンプトに戻った後、無限ループする。
bootasm.S
# If bootmain returns (it shouldn't), trigger a Bochs
# breakpoint if running under Bochs, then loop.
movw $0x8a00, %ax # 0x8a00 -> port 0x8a00
movw %ax, %dx
outw %ax, %dx
movw $0x8ae0, %ax # 0x8ae0 -> port 0x8a00
outw %ax, %dx
spin:
jmp spin