xv6

近頃の話題はCPUが溶けちゃうという、どこかの国でも起こった事のCPU版だが、こんな石を作っていても、ロイターさんは評価してんのね。

トムソン・ロイターによる世界のテクノロジー企業トップ100、Microsoftが1位に

で、FreeBSDでもこの緩和策が話題になってた。

CPUマイクロコードのアップデート

リナだと、ファームウェアアップデートってのに含まれているのかな。

マイクロコードを配っているのはは、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も決まりきったもんが設定されてるかと思ったら、そうでもないのね。 何をしてるんだろう?

etc

x86(i386)向けのOS作成日記 ローカル保存分

The little book about OS development

Welcome to OSDev.org

James mのカーネル開発チュートリアル

六本木戦記