で使ってるカンペの内容です
プロテクトモード移行
こんなん見てる人全員やりかた知ってるだろ
カンペ
cpu 486
lgdt cs:gdtr
mov eax, cr0
or eax, 1
mov cr0, eax
o32 jmp 0x10:0xffff0000
gdt:
; selector[0x00]
dq 0
; selector[0x08] : full data access
dq ((0xc)<<52) | (0xf<<48) | (0x93<<40) | (0xffff<<0)
; selector[0x10] : full text access
dq ((0xc)<<52) | (0xf<<48) | (0x9b<<40) | (0xffff<<0)
; selector[0x18] : initial cs compatible, base=0xf0000, limit=0xffff, 16bit, byte granularity
dq ((0x0)<<52) | (0x0<<48) | (0x9b<<40) | (0xf0000<<16) | (0xffff<<0)
; selector[0x20] : initial ds compatible, base=0xf0000, limit=0xffff, 16bit, byte granularity
dq ((0x0)<<52) | (0x0<<48) | (0x93<<40) | (0xf0000<<16) | (0xffff<<0) ; selector[0x20] : initial ds compatible
gdtr:
dw (8*5)-1
dd gdt
注意点としては、ROM に置くときは A : access bit を立てておかないと、書きこみしようとして止まるっぽい。
アドレス配置
メモリマップを考えておく
リセット直後の real mode は 4KiBぐらいでいいか。0xf_f000 - 0xf_ffff に置く
起動直後は、ROM は 64KiB しか見えないので、プロテクトモードも最初は0xffff_0000-0xffff_f000 の 60KiB に置く。
これでROMの最後64KiBを使う。
DRAM 使えるようになったら、0xfffc_0000 - 0xfffe_ffff の 192KiB をDRAM 0x10_0000 に移動して、本体とする。
ROMにOSを入れるとか、そういう自分だけの自作BIOSを満喫したいならもっと詰めたほうがいいが、
今回はそんなに難しいことをする予定はないので、アドレス配置はメモを見なくても覚えられる値にしておく。
最初の reset 直後の real mode を stage1 とする
DRAM が起動するまでの protected mode を stage2 とする
DRAM が起動して以降は stage3 とする。
ROM の配置としては、
- 0xfffc_0000 - 0xfffe_ffff : 192 KiB の stage3.bin
- 0xffff_0000 - 0xffff_efff : 60KiB の stage2.bin (protected mode)
- 0xffff_f000 - 0xffff_ffff (0x0000f_f000-0x000f_ffff のミラーも使う) 4KiB の stage1.bin (real mode)
を cat でくっつけて作るようにしよう。
カンペ : ファイルサイズ拡張は truncate -s 192K
リンカスクリプト
stage2 はあとで C も使うのでリンカスクリプトを書く。
0xffff_0000 から順に置いていく。
最初だけアドレス固定
カンペ
OUTPUT_ARCH("i386")
OUTPUT_FORMAT("elf32-i386")
ENTRY(entry)
SECTIONS {
. = 0xffff0000;
.text : {
*(.stage2_entry)
*(.text .text.*)
}
.rodata : {
*(.rodata .rodata.* .rdata*)
}
/DISCARD/ : { *(.note.* .eh_frame .comment ) }
}
ld -T stage2.lds $(OBJS) -o $@ -Map stage2.map
objcopy -O binary $< $@
もうこんなの覚える意味ないので、みんなはcoding agentに
「C言語がリンクできて、0xffff0000 から配置するリンカスクリプトを書いて」とか指示してなんとかしような。
キャッシュを有効にする
BIOS プログラミングで普通のプログラミングと一番違うのは、メインメモリが最初使えんという点。
DRAM って結構使いづらくて、アドレスが一次元ではなくrank, bank, row, col で指定しないといけない上に
row -> col は時間で遷移する状態遷移を回さないといけない。
さらに性能を出すためには、bank方向には独立して状態を持てることを考慮してスケジュールしたほうがいい。
かなり面倒である。
これはハードウェアが面倒見てくれる。メモリコントローラというハードウェアが、ノースブリッジに入ってて、
基本的にはCPUからは物理アドレスを含むメモリアクセス要求を投げると、よしなにやってくれる。
ただ、このメモリコントローラは、初期化しないと使えない。
メモリコントーラの初期化については、次回詳しく話す予定だが、
- 物理アドレスと rank,bank,row,col の割り当て
- メモリコントローラ - DRAM 間のスピードの設定
などが必要である。
この初期化が終わるまでは、DRAMは使えないため、メインメモリなしでプログラムを動かさないといけない。
まあ、今使ってる SDR SDRAM の世代くらいなら、レジスタだけでメモリコントローラ初期化するのも不可能ではない気がするが、
はっきり言って苦行なので、やりたくはない。
幸い、CPU にはデータキャッシュというものが付いており、16KiB-32KiB 程度なら、メインメモリなしでもデータを保持できる。
キャッシュはway の衝突回数を way数内に抑えれば、eviction は発生しない可能性が高い。
雑に考えたら、連続領域アクセスであれば、way 衝突は最小回数に抑えられる可能性が高く、どこかの16KiB程度の連続領域だけをキャッシュ可能にして、
キャッシュを有効にすれば、eviction なしで、読み書きできるようになるはずである。
これは完全に実装依存ではあるが、まあ、やってみると普通に動くので OK とする。こうやってキャッシュをRAMがわりにするのを、car (cache as ram) と呼ぶ。
ある程度新しい Intel CPU は、NEM というので、eviction を制御できるようになっており、実装依存ではなく仕様の保証ありで安全に使うことができるようになってる。
coreboot が使っていたので知った。
というわけで、キャッシュを有効にする。
やることは、
- 領域を決める
- mtrr でそこだけキャッシュ可能にする
- PAT と MTRR というのがあって、これでアドレスごとにキャッシュ可能かどうかを制御できる
- PAT はページ毎にキャッシュ可能属性を付けられる
- MTRR は、有限個のメモリ領域に対してキャッシュ可能属性を付けられる
- この世代の Pentium2 は PAT ないので、MTRRだけ考えればいい
- cr0 の CD (1<<30) を落とす
である。
領域は、DRAM アクセスにならない領域を使う。これはメモリコントローラ初期化が終わる前に、メモリアクセス要求を出してしまうとよくないため。
DDR3 SDRAMとかを使うメモリコントローラだと、初期化前にメモリアクセス要求を出すとハングしてしまうようだ。
簡単に実験したところ、この440BXは、SDR SDRAMで、古いタイプなので、特に初期化しないでも動いているような気配があった。
が、まあ DRAM 初期化中に変なリクエストを投げるとよくないのは確実なので、キャッシュ可能領域はDRAMではないところに置こう。
とはいえ、I/O 領域にMMIOアクセス要求出していいが、メモリに対して要求を出してはいけないというのもかなり実装依存っぽい挙動で、全CPUでそうなのかというのは
よく知らないが、まあ見てるかぎり、PC x86 はそういう実装っぽい。
ChatGPT に聞いたところ、
- PCI は、mater abort の仕様がある
- ISA はそもそも一定時間内にレスポンス返せという仕様なので、レスポンスが返ってこないという事態が発生しない(未定義値を読む)
なので、MMIO は誰もレスポンス返さなかったとしてもハングアップはしないという仕様ではという見解を出した。そんな気がする。
なので、MMIO 領域を使う。
coreboot は 0xC_0000-0xC_FFFF あたりの領域を使ってるっぽいので、それにあわせる。
この領域は、VGA の ROM がマップされてることが多い領域で、少なくともlegacy BIOSが動いてた世代までは、初期状態だと MMIO になる。
なんでC0000をわざわざ使ってるのかはあんまりよく知らないです。
まあぱっと思い付くメリットとしては、
- real mode でも到達できる範囲にある
- 何かが繋がっていたとしても ROM の可能性高いので実装依存の動作で write request が出ても問題起こりづらそう
- DRAM の容量に関係なく DRAM がマップされないこと
あたりがあるんじゃない?
こうすれば、スタックを使えるようになって C 言語が使える。というわけで、
- cache を有効にする
- 領域を決める 0xC0000 - 0xC1FFF の 8Kにしよう
- mtrr で この領域をキャッシュ可にする
- cr0 の CD をおとす
mtrr は、
初期値は、
E=0, FE=0で無効
fixed 領域は多分未定義なので全初期化がいる
variable 領域は、リセット後 V=0 保証なので、なにもしなくていい。
mtrr ってなんやねん
物理アドレス 4KiB 単位ごとに、キャッシュ属性を決められる。
(Pentium2 は pat ないので pat は忘れる)
addr と mask が設定できて、 (使うaddr & mask) == (mtrrのaddr) のとき、そのキャッシュ属性が適用される。
先頭1MiB の領域は、DOS との互換のために、専用のレジスタが付いてて、0xC0000-0xFFFFF の範囲は 4KiB 単位で個別に指定できる。
0xC0000 - 0xC1FFF は、1MiB に入ってるので、 fixed を使う。
fixed の msr アドレスは不規則なので、一個一個手で書いたほうがいい
msr 0x250, 0x258, 0x259, 0x268, 0x269, 0x26a, 0x26b, 0x26c, 0x26d, 0x26e, 0x26f を 0 初期化する。これで uc になる。
そのあと、0xC0000 と対応する、0x268 の下 2byte (C0000-C1FFFF) を 0x0606 にする。6 = WB なので
xor eax,eax
xor edx,edx
mov ecx, 0x250
wrmsr
mov ecx, 0x258
wrmsr
...
mov ecx, 0x26f
wrmsr
としたあと、
mov eax, 0x0606
mov ecx, 0x268
wrmsr
とする。
- CD=1 にする。(resetでなってる)
- fixed MTRR 全部 = UC (0)
- CAR に使う 0xC0000-0xC1FFF を含む fixed MTRR (0x268) を 0x0606
- IA32_MTRR_DEF_TYPE:
E = 1
FE = 1
default type = UC(0) - CR0.CD = 0, CR0.NW = 0
mov eax, cr0
and eax, 0x9FFFFFFF
mov cr0, eax - esp = 0xC2000
これで push/pop call/ret できるはずや
逆に言うと、実はPCはDRAM付けてなくてもキャッシュに入る範囲ならDRAMなくても動作可能である。
ただ、実験してる限り、ある程度新しい CPU だと命令キャッシュとデータキャッシュを同期するのにDRAMが必要らしく、CPUで作った命令を実行することはできず、ROM に入ってる命令を直接実行することしかできない。MS-DOS ぐらいなら 32KiB キャッシュあれば動くのではと思っていた時期があったが、ディスクからプログラムをロードすることができなくて詰む。
じゃあ pentium2 はどうやねん。みんなもやってみよう
pentium2 は、512KiB の L2キャッシュがあるので、これを使えば、使える領域はもう少し増えるかもしれない。
ただし、これは外付けなので、タグなどの初期化が別途必要になる。今回は面倒なので使わない。
x86 ではタグの状態が外から観測できるキャッシュは貴重なので、pentium2 を入手できた人は遊んでみよう。