コメントでメモ。
#include "asm.h"
#include "memlayout.h"
#include "mmu.h"
# => 該当ヘッダファイルにはいくつかのマクロが定義されていたが、アセンブリから.hをincludeできるのかよくわかってない。とりあえず飛ばす。
# Start the first CPU: switch to 32-bit protected mode, jump into C.
# The BIOS loads this code from the first sector of the hard disk into
# memory at physical address 0x7c00 and starts executing in real mode
# with %cs=0 %ip=7c00.
# 最初のCPUを開始、32-bitプロテクトモードに移行してCのコードにジャンプする。
# BIOSはこのコード(bootasm.S)をハードディスクの最初のセクタからメモリの
# 0x700に読みこんで%cs=0, %ip=0x7c00にしてリアルモードで実行される。
PC-AT互換機の仕様では、CPUはまず8086互換環境を提供する「リアルモード」で起動され、16bitプログラムを読み込み、自らを32bitモード(プロテクトモード)に切り替えることになっている、らしい。実際、次の行からのコードの.code16
は、16ビットの実行コードを生成する指示であるらしい。
次に%cs, %ipについて。
-
%cs
この本ではレジスタの最初に%をつけて表記しているので、実質は「csレジスタ」という概念。略表記を外すと、"Code Segment"レジスタというらしくCPUが読み込むプログラムの場所を指し示すために使われる。 -
%ip
実行中のプログラムの位置を指す実行ポインタを表すレジスタ。
.code16 # 16bit用の実行コードを吐くように設定
.globl start
start:
cli # 割り込みフラグを0にクリア。つまり割り込みされなくする
# DS,ES,SSに0をセット。ちなみに命令に"w"がついてるのはオペランド(非演算子)のサイズをしてするため。wはword=16bitの意。
xorw %ax,%ax # 汎用レジスタaxに0をセット。
movw %ax,%ds # DS-Data Segment(CPUが読み書きするデータが存在するセグメントを指す)に0をセット。
movw %ax,%es # ES-Extra Segment DSのサポート的な役割らしい。
movw %ax,%ss # SS-StackSegment SPレジスタと合わせてスタックの先頭アドレスを指すらしい。
# 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.
# http://caspar.hazymoon.jp/OpenBSD/annex/gate_a20.html を参照のこと。
# 本質的ではないため、飛ばしても良いと思う。
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
# 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.
リアルモードからプロテクトモードへスイッチ。仮想アドレスを物理アドレスにマッピングするGDTというものを使う。gdtというのは"Grobal Descriptor Table"と呼ばれる。こはOSや種々のプロセスから共通に参照されるセグメントを定義するためのディスクリプタテーブル。
といってもセグメントってなんやねん、ディスクリプタってなんやねんって感じなので調べていく。
セグメントというのはメモリを分割する一緒の方式。
そもそもメモリには1種類のアドレスしかなく、アドレスバス-さっき出てきたA1~A20-を使用してアクセスする。
CPUは直接物理アドレスを指定するのではなく、セグメントアドレスとオフセットアドレスの組み合わせによってメモリの番地を指定する。何セグメントの先頭から何番目か、的な。
このセグメントとオフセットの組み合わせをアドレス変換回路というものがリニアアドレスという、これはほとんど物理アドレスのようなものだと思っていいが、そういうものに変換してくれるということらしい。そんで、まあさっきの*sレジスタのセグメントに色々データを取りに行くと。
セグメントディスクリプタというのは各セグメントについての情報を詰めたもので、アドレス変換回路はここから情報を読み取ることによって、セグメントアドレス、とその先頭値-セグメントベースという-を求める。そして、オフセットアドレスを足してリニアアドレスを求める。
セグメントディスクリプタは、
- セグメントベース(実際の物理アドレス上でそのセグメントがどこから始まるか)
- リミット値(大きさはどれほどか)
- セグメントの属性(後述。たとえば「データ」とか「プログラム」とか)
を記述する。セグメントディスクリプタテーブルにはセレクタ値-セグメントアドレス-の順番にディスクリプタが収められているので、アドレス変換回路はそこから情報を拾ってくる。
なお、先頭の8バイトはセグメントレジスタを無効にするために使われる(<- よくわからん)らしいので、なにやら使えないらしい。
さてここで先にラベルで呼ばれてるgdtのところを見ておく。
# Bootstrap GDT
.p2align 2 # 2^2 => 4bytes区切りで取り扱うよ〜って宣言
gdt:
SEG_NULLASM # null seg
# => #define SEG_NULLASM@asm.h
.word 0, 0; 2word => 32bitを0クリア
.byte 0, 0, 0, 0 4bite => 32bitを0クリア
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg STA_X => 0x8, STA_R => 0x2
# => #define SEG_ASM(type,base,lim)@asm.h
.word (((lim) >> 12) & 0xffff), ((base) & 0xffff)
.byte (((base) >> 16) & 0xff), (0x90 | (type)),
(0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)
SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg STA_W => 0x2
gdtdesc:
.word (gdtdesc - gdt - 1) # sizeof(gdt) - 1
.long gdt # address gdt
セグメントディスクリプタのフォーマットについてはこのページが詳しい。ただし、イメージ図と実際にメモリに書き込んでいく順番が逆転してるのには注意すること。
今回はコードセグメントとデータセグメントを両方0始まりの4GB確保している。別にセグメントは範囲が重複しててもいいらしい。用途的にあんまりそうは思えないんだけど、なんでだっけな。
=> 本を読むと、「現在のLinuxではメモリ保護機能はセグメントじゃなくて、ページングによって実現しているため、セグメントはしかたなく作ってるだけ」都の表記が。なるほど。。。
lgdt gdtdesc #gdtdescの値をgdtに読み込む。
movl %cr0, %eax # このcr0レジスタのPEビットに1を入れることによって
orl $CR0_PE, %eax # プロテクトモード(すなわち32bitモード)を有効にするらしい
movl %eax, %cr0 # でもまだ移行完了ではないよう。
//PAGEBREAK!
# Complete transition to 32-bit protected mode by using 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.
# %csとeipレジスタの値をreloadするためにlongjmp命令を行うと、32bit プロテクトモードへの移行は完了する。
ljmp $(SEG_KCODE<<3), $start32
jjmp命令というのがなかなかググってもでてこないんだけど、一応ググったら引っかかって、
要はセグメント(cs)とその中のアドレス(eip)を同時に移動、jmpする命令みたい。(素のjmpだと現セグメント内で、ということになるっぽい)
詳しくはこのページ参照。ちょっと違うケースではあるけど、大体同じだと思う。
普通にmov 0x08 %cs
とかやってしまうと、現状はすでにプロテクトモードなために、物理アドレス0x08ではなく、GDTを使用して 現在のセグメント内の0x08オフセットアドレスを見に行ってしまう。この時点で%csはまあ0x00、つまりnull descriptorを指しているのでerrorになる。
しかしリアルモードの時点で%csを書き換えてしまうと、その瞬間に別の場所に制御が写ってしまうためにやはり上手くいかない。このljmp命令はその循環問題を解決してくれるらしい。
ちなみに0x08は1つめ(0-originとして)のセグメントディスクリプタがある場所。一個8bytesでしたよね。
あとCPUの命令パイプラインのフラッシュの意味合いもあるらしい。こっちはよくわかんない。
.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
#↑ds,es,ssに実質的なセグメントの始まりである0x08をセット、fs,gsは用途不明だけどとりあえず1をセットしたらしい。
# Set up the stack pointer and call into C.
movl $start, %esp
call bootmain
スタックの頂点を0x7c00番にセットしている。これはブートセクタ(つまにこのアセンブリ)が読み込まれている番地(なにやら経緯があるらしい)であり、したがってこのコードの最初のラベルである$startでそれが参照できるため、そうしている。なんでここをスタックにするのかはわからない(伸びていく方向は下、つまりより低番地に向かって伸びているらしい)
そして、最後にbootmainを読み出す。なんでbootmain.cの関数を呼べるんだって思ったけどqemuで起動するためのMakefileでうまいことやられてるっぽい。
多分、本当はこのMakefileとqemuの仕組みについて勉強しないと行けないんだけど、とりあえずそれは現状本質的ではないので、先にxv6の中身について追求することにする。
↓はbootが失敗して処理が帰ってきた時の処理だからとりあえず気にしないことにする。
# 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
ようやっとcpuが32bit-modeになったくだすったらしい。次はカーネルを読み込むようだ。