xv6
近頃の話題はCPUが溶けちゃうという、どこかの国でも起こった事のCPU版だが、こんな石を作っていても、ロイターさんは評価してんのね。
トムソン・ロイターによる世界のテクノロジー企業トップ100、Microsoftが1位に
で、FreeBSDでもこの緩和策が話題になってた。
リナだと、ファームウェアアップデートってのに含まれているのかな。
マイクロコードを配っているのはは、portsの資料によれば、(/usr/ports/sysutils/devcpu-data)
MASTER_SITES= https://downloadmirror.intel.com/27337/eng/:intel \ LOCAL/sbruno:amd
マイクロコードだから、CISCを動かすための更にマイクロなものだ。だれかこの暗号コードを 逆解析して、内部のRISCっぽい石を想像出来ないかね。あの優秀なググルの人達なら、なんなく やっちゃうんだろうね。
そんなのは無理としても、/usr/local/etc/rc.d/microcode_updateぐらいは、読めるよ。
CMT="/usr/sbin/cpucontrol" microcode_update_prepare() { if ! kldstat -q -m cpuctl; then if ! kldload cpuctl > /dev/null 2>&1; then warn "Can't load cpuctl module." return 1 fi fi } microcode_update_start() { echo "Updating CPU Microcode..." if [ "${microcode_cpus}" = "ALL" ]; then ncpu=`/sbin/sysctl -n hw.ncpu` cpus=`jot ${ncpu} 0`; else cpus=${microcode_cpus} fi for i in ${cpus}; do ${CMT} -u ${microcode_update_flags} \ -d "${microcode_update_datadir}" /dev/cpuctl${i} 2>&1 | \ logger -p daemon.notice -t microcode_update || \ (echo "Microcode Update Failed." && exit 1) done :
これを見てて、cpucontrolってコマンドを見れば、ヒントが有るかもと思った。ソースの所へ 飛んで行ったら、インテル氏、amd氏、via氏用に対応してるって判明。via氏は知らないなあ。 とか、言ってたら、VIAがx86互換CPUの新製品を発表。中国市場向けなんて記事に遭遇。
インテル氏のヘッダーを見たら、
typedef struct intel_fw_header { uint32_t header_version; /* Version of the header. */ int32_t revision; /* Unique version number. */ uint32_t date; /* Date of creation in BCD. */ uint32_t cpu_signature; /* Extended family, extended model, type, family, model and stepping. */ uint32_t checksum; /* Sum of all DWORDS should be 0. */ uint32_t loader_revision; /* Version of the loader required to load update. */ uint32_t cpu_flags; /* Platform IDs encoded in the lower 8 bits. */ uint32_t data_size; uint32_t total_size; uint8_t reserved[12]; } intel_fw_header_t;
さすがに、データのフォーマットは公開されていない。インテル氏の闇、秘密は想像してください。それしかありませんです。/sys/dev/cpuctl/cpuctl.cを見ても、手がかりは無いし。
で、別な方面から攻めてみる。
IA-32 インテル®アーキテクチャソフトウェア・デベロッパーズ・マニュアル
IA-32 インテル アーキテクチャ ソフトウェア デベロッパーズ マニュアル 下巻-システムプログラミングガイド.pdf
あれれ、64Bitの石はどうした?と言うのは、取り合えず無しね。
xv6 on debian
取り合えず、動かしてみた図。
deb9:xv6-public$ make qemu-nox gcc -Werror -Wall -o mkfs mkfs.c gcc -fno-pic -static -fno-builtin -fno-strict-aliasing -O2 -Wall -MD -ggdb -m32 -Werror -fno-omit-frame-pointer -fno-stack-protector -c -o ulib.o ulib.c gcc -m32 -gdwarf-2 -Wa,-divide -c -o usys.o usys.S : objdump -S kernel > kernel.asm objdump -t kernel | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$/d' > kernel.sym dd if=/dev/zero of=xv6.img count=10000 10000+0 records in 10000+0 records out 5120000 bytes (5.1 MB, 4.9 MiB) copied, 0.0108031 s, 474 MB/s dd if=bootblock of=xv6.img conv=notrunc 1+0 records in 1+0 records out 512 bytes copied, 0.000109367 s, 4.7 MB/s dd if=kernel of=xv6.img seek=1 conv=notrunc 349+1 records in 349+1 records out 179020 bytes (179 kB, 175 KiB) copied, 0.000449726 s, 398 MB/s qemu-system-i386 -nographic -drive file=fs.img,index=1,media=disk,format=raw -drive file=xv6.img,index=0,media=disk,format=raw -smp 2 -m 512 xv6... cpu1: starting 1 cpu0: starting 0 sb: size 1000 nblocks 941 ninodes 200 nlog 30 logstart 2 inodestart 32 bmap start 58 init: starting sh $
コンパイルして、起動の様子。
The target architecture is assumed to be i8086 [f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b 0x0000fff0 in ?? () + symbol-file kernel (gdb) si [f000:e05b] 0xfe05b: cmpl $0x0,%cs:0x70c8 [f000:e062] 0xfe062: jne 0xfd414 [f000:e066] 0xfe066: xor %dx,%dx [f000:e068] 0xfe068: mov %dx,%ss [f000:e06a] 0xfe06a: mov $0x7000,%esp [f000:e070] 0xfe070: mov $0xf2d4e,%edx [f000:e076] 0xfe076: jmp 0xfff00 [f000:ff00] 0xfff00: cli [f000:ff01] 0xfff01: cld [f000:ff02] 0xfff02: mov %eax,%ecx [f000:ff05] 0xfff05: mov $0x8f,%eax [f000:ff0b] 0xfff0b: out %al,$0x70 [f000:ff0d] 0xfff0d: in $0x71,%al [f000:ff0f] 0xfff0f: in $0x92,%al :
上記は、gdbを有効にして、何ステップか実行させた軌跡。
こうしていても、なかなか先に進まないので、ちょいとずる賢い事を考える。
xv6.imgの成分分析
先の起動の例から、qemuに渡すのはfs.imgとxv6.imgだ。fsってのは、ユーザーアプリとかデータが入ったHDDの積りなんだろう。そうするとxv6.imgは、文字通りカーネルが入ったものだな。
deb9:xv6-public$ file xv6.img xv6.img: DOS/MBR boot sector
正体は、DOS用のMBRとの事。その先にカーネルが隠れているんだろう。何たって、HDDの積りですから。こういう時は、どうやってxv6.imgが出来上がるか、makeのログを取って眺めるのが楽だ。(Makefileと格闘なんてしないのさ)
dd if=/dev/zero of=xv6.img count=10000 10000+0 records in 10000+0 records out 5120000 bytes (5.1 MB, 4.9 MiB) copied, 0.0127954 s, 400 MB/s dd if=bootblock of=xv6.img conv=notrunc 1+0 records in 1+0 records out 512 bytes copied, 7.6769e-05 s, 6.7 MB/s dd if=kernel of=xv6.img seek=1 conv=notrunc 349+1 records in 349+1 records out 179020 bytes (179 kB, 175 KiB) copied, 0.000545463 s, 328 MB/s
ふむ、これを見ると、5MのZEROクリアしたファイルxv6.imgを作る。次に1ブロックのbootblockを上書き。続いて、先に書いたブロックを飛ばして、kernelを書き込むとな。
deb9:xv6-public$ file bootblock kernel bootblock: DOS/MBR boot sector kernel: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, not stripped
2つのプログラムの素性を調べてみた。
deb9:xv6-public$ readelf -h kernel ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Intel 80386 Version: 0x1 Entry point address: 0x10000c Start of program headers: 52 (bytes into file) Start of section headers: 178300 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 2 Size of section headers: 40 (bytes) Number of section headers: 18 Section header string table index: 17
どんな風にこのプログラムが作られるか? まずは、bootblockの方
gcc - -fno-pic -O -nostdinc -I. -c bootmain.c gcc - -fno-pic -nostdinc -I. -c bootasm.S ld -m elf_i386 -N -e start -Ttext 0x7C00 -o bootblock.o bootasm.o bootmain.o objdump -S bootblock.o > bootblock.asm objcopy -S -O binary -j .text bootblock.o bootblock ./sign.pl bootblock boot block is 448 bytes (max 510)
bootmain.cとbootasm.Sを合体させたものから、コードだけを抜き出したもの。そいつにperlでMBRだよってマークの55AAを書き込んでいる。(512byteにして、えせMBRにしてるのも注目)
次は、本体のkernel部だな。
gcc -m32 -gdwarf-2 -Wa,-divide -c -o entry.o entry.S gcc - -fno-pic -nostdinc -I. -c entryother.S ld -m elf_i386 -N -e start -Ttext 0x7000 -o bootblockother.o entryother.o objcopy -S -O binary -j .text bootblockother.o entryother objdump -S bootblockother.o > entryother.asm gcc - -nostdinc -I. -c initcode.S ld -m elf_i386 -N -e start -Ttext 0 -o initcode.out initcode.o objcopy -S -O binary initcode.out initcode
entry.S,entryother.Sで、CPU起動処理ルーチンを作る。それから、initcode.Sっていう、カーネルが面倒をみる唯一のユーザーランドあぷり initを作る。
ld -m elf_i386 -T kernel.ld -o kernel entry.o bio.o console.o exec.o file.o \ fs.o ide.o ioapic.o kalloc.o kbd.o lapic.o log.o main.o mp.o picirq.o pipe.o \ proc.o sleeplock.o spinlock.o string.o swtch.o syscall.o sysfile.o sysproc.o \ trapasm.o trap.o uart.o vectors.o vm.o -b binary initcode entryother
そして、前もってコンパイルしておいたオブジェクトと先ほど作ったinitcodeとかを混ぜ合わせて、最終的なkernelに仕立てあげる。この時、kernel.ldというリンカーを制御するスクリプトを介在させて、重要な情報をkernelに埋め込んでいるとな。
kernelが起動するまで
上の解析によると、えせMBRがカーネルをロードしてるようだ。BIOSにより0x7c00から起動される。起動の作法により、時代の遺物セグメントをクリア、A20ラインを有効化してメモリーを 広く使えるようにする。
それからGDTを設定しておいて、プロテクトモードに移行。GDTは、論理アドレスと物理アドレスが同一になるような設定がなされている。全く、無駄の極みだ。 それからスタックぽインタを設定。(0x7c00から、アドレスの若い方に伸びる)
これで、bootmainを呼び出す。C語の世界だ。ほっとするよ。
で、DISKの頭から4kを読み込み。軽くELFファイルである事を確認したら、きっとカーネルだろうってんで、読み込んじゃう。これでカーネルがメモリーに載った。随分とあっさりしたローダーである。
次は、カーネルに制御を移す。
// Call the entry point from the ELF header. // Does not return! entry = (void(*)(void))(elf->entry); entry();
と書いてあるので、readelfして出て来たentry pointにBPを置いて、動かしてみた。
(gdb) b *0x10000c Breakpoint 1 at 0x10000c (gdb) c Continuing. The target architecture is assumed to be i386 => 0x10000c: mov %cr4,%eax Thread 1 hit Breakpoint 1, 0x0010000c in ?? ()
この段階では、ページングがまだ有効になっていない。 ぼちぼちとステップ実行していくと
=> 0x100020: or $0x80010000,%eax => 0x100025: mov %eax,%cr0 => 0x100028: mov $0x8010b5c0,%esp => 0x10002d: mov $0x80102e80,%eax => 0x100032: jmp *%eax => 0x80102e80 <main>: lea 0x4(%esp),%ecx main () at main.c:19 19 {
仮想記憶が有効になった瞬間を捉えたぞ。後は、mainが粛々と実行されて行きます。
# By convention, the _start symbol specifies the ELF entry point. # Since we haven't set up virtual memory yet, our entry point is # the physical address of 'entry'. .globl _start _start = V2P_WO(entry) # Entering xv6 on boot processor, with paging off. .globl entry entry: # Turn on page size extension for 4Mbyte pages movl %cr4, %eax orl $(CR4_PSE), %eax movl %eax, %cr4 # Set page directory movl $(V2P_WO(entrypgdir)), %eax movl %eax, %cr3 # Turn on paging. movl %cr0, %eax orl $(CR0_PG|CR0_WP), %eax movl %eax, %cr0 # Set up the stack pointer. movl $(stack + KSTACKSIZE), %esp # Jump to main(), and switch to executing at # high addresses. The indirect call is needed because # the assembler produces a PC-relative instruction # for a direct jump. mov $main, %eax jmp *%eax
CR4_PSEで4M割り当てのモードにし、
// The boot page table used in entry.S and entryother.S. // Page directories (and page tables) must start on page boundaries, // hence the __aligned__ attribute. // PTE_PS in a page directory entry enables 4Mbyte pages. __attribute__((__aligned__(PGSIZE))) pde_t entrypgdir[NPDENTRIES] = { // Map VA's [0, 4MB) to PA's [0, 4MB) [0] = (0) | PTE_P | PTE_W | PTE_PS, // Map VA's [KERNBASE, KERNBASE+4MB) to PA's [0, 4MB) [KERNBASE>>PDXSHIFT] = (0) | PTE_P | PTE_W | PTE_PS, };
こんなentrypgdirっていう配列のうち、2つ分のエントリーがハードコートされたやつを 使っている。
gdbutil
なんていうgdb用のユーティリティが付いていた。どんな芸が出来るか、ファイルを開いてみたら、
# Utility functions to pretty-print x86 segment/interrupt descriptors. # To load this file, run "source gdbutil" in gdb. # printdesc and printdescs are the main entry points.
糞石の遺物、セグメント・ディスクリプターと割り込み・ディスクリプターを綺麗に表示してくれるやつらしい。一つだけ表示するprintdescと複数を表示するprintdescsをサポートしてるとな。
異物なんで近代的なOSには邪魔なものだけど、設定しておかないとOSが動かないので、いやいや 設定するはめになるんだな。間違った設定をすると、勿論OSは動かないので、間違いを容易に 調べられるようにという親心。有り難く試してみる。
まずは、セグメントの方。これが正しくないと、仮想記憶が動かない。論理アドレス(プログラム上)を、このディスクリプターを通して、リニアアドレスに変換。出て来たアドレスを仮想記憶機構を使って、物理アドレスに変換するって訳だな。
(gdb) printdescs cpus[0].gdt 5 [0] P = 0 (Not present) [1] type = code|STA_R base = 0x00000000 limit = 0xffffffff AVL = 0 D = 32-bit (1) DPL = DPL_KERN (0) [2] type = data|STA_W|STA_A base = 0x00000000 limit = 0xffffffff AVL = 0 B = big (1) DPL = DPL_KERN (0) [3] type = code|STA_R base = 0x00000000 limit = 0xffffffff AVL = 0 D = 32-bit (1) DPL = DPL_USER (3) [4] type = data|STA_W|STA_A base = 0x00000000 limit = 0xffffffff AVL = 0 B = big (1) DPL = DPL_USER (3)
このディスクリプターは、CPU毎に持ってるので、CPUの情報をまとめた配列cpusの中に構造体という形で収めてある。codeとデータ領域って事でそれぞれに表がある。そしてそのセットが、カーネル用とユーザー用に分かれている。
大事なのは、baseで何処からそのエリアが始まり、limitでエリアの幅を指定するって点。codeの方は読むだけよ、dataの方は書き込んでもいいよって属性が付与されてる。
次は、割り込みテーブルの方。
(gdb) printdescs idt 3 [0] type = STS_IG32 (0xe) CS = SEG_KCODE<<3 (8) Offset = 0x80105ac5 <vector0> DPL = DPL_KERN (0) [1] type = STS_IG32 (0xe) CS = SEG_KCODE<<3 (8) Offset = 0x80105ace <vector1> DPL = DPL_KERN (0) [2] type = STS_IG32 (0xe) CS = SEG_KCODE<<3 (8) Offset = 0x80105ad7 <vector2> DPL = DPL_KERN (0)
255種類あるんだけど、最初の方だけダンプした。決まりきった設定になるんで、makeする時にperlさんの力を借りて、機械的にコードを発生させている。
perl vectors.pl > vectors.S
で、gdbutiliのコードを見ていたら、xv6-specificっつう事で、SEG_TSSなんてのが出てた。 使ってる所を調べてみた。
(gdb) bt #0 switchuvm (p=<optimized out>) at vm.c:174 #1 0x80103ab3 in scheduler () at proc.c:343 #2 0x80102e5f in mpmain () at main.c:57 #3 0x80102f9f in main () at main.c:37
(gdb) printdescs mycpu()->gdt 7 [0] P = 0 (Not present) [1] type = code|STA_R base = 0x00000000 limit = 0xffffffff AVL = 0 D = 32-bit (1) DPL = DPL_KERN (0) [2] type = data|STA_W|STA_A base = 0x00000000 limit = 0xffffffff AVL = 0 B = big (1) DPL = DPL_KERN (0) [3] type = code|STA_R base = 0x00000000 limit = 0xffffffff AVL = 0 D = 32-bit (1) DPL = DPL_USER (3) [4] type = data|STA_W base = 0x00000000 limit = 0xffffffff AVL = 0 B = big (1) DPL = DPL_USER (3) [5] type = code|STA_A base = 0x80112788 limit = 0x00000067 AVL = 0 D = 32-bit (1) DPL = DPL_KERN (0) [6] P = 0 (Not present)
今まで、baseもlimitも決まりきったもんが設定されてるかと思ったら、そうでもないのね。 何をしてるんだろう?