Unix xv6 ~ イメージ ~
概要
xv6のコードリーティングを通してUnixの動作を追う。今回は起動イメージを見ていく。
リポジトリは以下。
https://github.com/mit-pdos/xv6-public
xv6
xv6は、ANSI Cによる、Sixth Edition Unixのマルチプロセッサx86システムへの再実装である。 xv6はMITにおけるオペレーティングシステムエンジニアリング(6.828)コースにて、教育を目的として使われている。 LinuxやBSDとは異なり、xv6は1セメスターで学習するのに十分なほどシンプルであり、Unixの重要な概念と構造を含んでいる。 引用: https://ja.wikipedia.org/wiki/Xv6
上記にもあるようにxv6自体は非常に小さなOSでありながら、Unixの重要な概念と構造を含んでおり、学習用に作られたというだけあって非常に理解が容易である。
このxv6を通じてUnixの最低限の動作をトレースできたらと思う。
イメージ作成
イメージの作成にはリポジトリのディレクトリ直下でmake
コマンドを用いて行う。
$ make gcc -fno-pic -static -fno-builtin -fno-strict-aliasing -O2 -Wall -MD -ggdb -m32 -Werror -fno-omit-frame-pointer -fno-stack-protector -fno-pie -no-pie -fno-pic -O -nostdinc -I. -c bootmain.c gcc -fno-pic -static -fno-builtin -fno-strict-aliasing -O2 -Wall -MD -ggdb -m32 -Werror -fno-omit-frame-pointer -fno-stack-protector -fno-pie -no-pie -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) 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 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.0338458 s, 151 MB/s dd if=bootblock of=xv6.img conv=notrunc 1+0 records in 1+0 records out 512 bytes copied, 0.000193774 s, 2.6 MB/s dd if=kernel of=xv6.img seek=1 conv=notrunc 349+1 records in 349+1 records out 178900 bytes (179 kB, 175 KiB) copied, 0.00128119 s, 140 MB/s
生成されたイメージは以下。
$ ls -l xv6.img -rw-rw-r-- 1 ubuntu ubuntu 5120000 May 22 22:58 xv6.img
Imageの構造
make
コマンドの出力から以下のような流れでイメージが作成されていることがわかる。
# 5MBのファイルを作成 $ 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.0346979 s, 148 MB/s # ブートセクタ $ dd if=bootblock of=xv6.img conv=notrunc 1+0 records in 1+0 records out 512 bytes copied, 0.000195647 s, 2.6 MB/s # カーネルイメージ(ブートセクタを呼ばして2番目のセクタから書き込む) $ dd if=kernel of=xv6.img seek=1 conv=notrunc 349+1 records in 349+1 records out 178900 bytes (179 kB, 175 KiB) copied, 0.00125081 s, 143 MB/s
最初のセクタはブートセクタ(bootblock
)となっており、その次のセクタ(kernel
)からカーネルイメージが開始するのがわかる。
ブートセクタ
MBRはHDDの先頭セクタ(cylinder 0, head 0, sector 1の位置)に512byte書込まれていて、各パーティションの先頭セクタに存在するPBRを読出すための小さなプログラムと各パーティションのディスク情報が含まれています。 引用:http://caspar.hazymoon.jp/OpenBSD/arch/i386/stand/mbr/mbr_structure.html
イメージに書き込まれていたbootblock
のサイズは512バイトと単一セクタのサイズとなっており、以下のfile
コマンドの出力からもマスターブートレコードを保持したブートセクタだとわかる。
$ ls -l bootblock -rwxrwxr-x 1 ubuntu ubuntu 512 May 22 19:35 bootblock $ file bootblock bootblock: DOS/MBR boot sector
boolblock
のダンプ結果は以下。(出力はメモリに展開される形で表示させるためエンディアンを逆にしている)
$ od --address-radix=x --format=x --endian=big bootblock 000000 fa31c08e d88ec08e d0e464a8 0275fab0 000010 d1e664e4 64a80275 fab0dfe6 600f0116 000020 787c0f20 c06683c8 010f22c0 ea317c08 000030 0066b810 008ed88e c08ed066 b800008e 000040 e08ee8bc 007c0000 e8ee0000 0066b800 000050 8a6689c2 66ef66b8 e08a66ef ebfe6690 000060 00000000 00000000 ffff0000 009acf00 000070 ffff0000 0092cf00 1700607c 00005589 000080 e5baf701 0000ec83 e0c03c40 75f85dc3 000090 5589e557 538b5d0c e8e1ffff ffb80100 0000a0 0000baf2 010000ee baf30100 0089d8ee 0000b0 89d8c1e8 08baf401 0000ee89 d8c1e810 0000c0 baf50100 00ee89d8 c1e81883 c8e0baf6 0000d0 010000ee b8200000 00baf701 0000eee8 0000e0 9affffff 8b7d08b9 80000000 baf00100 0000f0 00fcf36d 5b5f5dc3 5589e557 56538b5d 000100 088b7510 89df037d 0c89f025 ff010000 000110 29c3c1ee 0983c601 39df7617 5653e86d 000120 ffffff81 c3000200 0083c601 83c40839 000130 df77e98d 65f45b5e 5f5dc355 89e55756 000140 5383ec0c 6a006800 10000068 00000100 000150 e8a3ffff ff83c40c 813d0000 01007f45 000160 4c467408 8d65f45b 5e5f5dc3 a11c0001 000170 008d9800 0001000f b7352c00 0100c1e6 000180 0501de39 f3720fff 15180001 00ebd583 000190 c32039de 76f18b7b 0cff7304 ff731057 0001a0 e853ffff ff8b4b14 8b431083 c40c39c1 0001b0 76dd01c7 29c1b800 000000fc f3aaebcf 0001c0 00000000 00000000 00000000 00000000 0001d0 00000000 00000000 00000000 00000000 0001e0 00000000 00000000 00000000 00000000 0001f0 00000000 00000000 00000000 000055aa
パーティションテーブルは存在せず最後の2バイトにMBRのシグニチャを保持していることがわかる。
MBR領域はPCの電源がONされたときにBIOSによってメモリー上の
0000:7C00
番地に読込まれます。 引用:http://caspar.hazymoon.jp/OpenBSD/arch/i386/stand/mbr/mbr_structure.html
上記のようにMBRがBIOSによって読み込まれ規定のアドレスに展開される。
objdump
コマンドでブートセクタをダンプする。
$ objdump -D -b binary -m i386 bootblock bootblock: file format binary Disassembly of section .data: 00000000 <.data>: 0: fa cli 1: 31 c0 xor %eax,%eax 3: 8e d8 mov %eax,%ds 5: 8e c0 mov %eax,%es 7: 8e d0 mov %eax,%ss 9: e4 64 in $0x64,%al b: a8 02 test $0x2,%al d: 75 fa jne 0x9 f: b0 d1 mov $0xd1,%al 11: e6 64 out %al,$0x64 13: e4 64 in $0x64,%al 15: a8 02 test $0x2,%al 17: 75 fa jne 0x13 19: b0 df mov $0xdf,%al 1b: e6 60 out %al,$0x60 1d: 0f 01 16 lgdtl (%esi) 20: 78 7c js 0x9e 22: 0f 20 c0 mov %cr0,%eax 25: 66 83 c8 01 or $0x1,%ax 29: 0f 22 c0 mov %eax,%cr0 2c: ea 31 7c 08 00 66 b8 ljmp $0xb866,$0x87c31 33: 10 00 adc %al,(%eax) 35: 8e d8 mov %eax,%ds 37: 8e c0 mov %eax,%es 39: 8e d0 mov %eax,%ss 3b: 66 b8 00 00 mov $0x0,%ax 3f: 8e e0 mov %eax,%fs 41: 8e e8 mov %eax,%gs 43: bc 00 7c 00 00 mov $0x7c00,%esp 48: e8 f0 00 00 00 call 0x13d :
上記の出力からbootasm.S
のコードと同じだということがわかる。
.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 # 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 .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
はC言語で記述された関数でbootmain.c
で定義されている。bootmain()
関数ではカーネルイメージをメモリアドレスの0x10000
に展開し、最終的にその展開したカーネルイメージのentry
に処理を移す。
// bootmain.c void bootmain(void) { struct elfhdr *elf; struct proghdr *ph, *eph; void (*entry)(void); uchar* pa; elf = (struct elfhdr*)0x10000; // scratch space // Read 1st page off disk readseg((uchar*)elf, 4096, 0); // Is this an ELF executable? if(elf->magic != ELF_MAGIC) return; // let bootasm.S handle error // Load each program segment (ignores ph flags). ph = (struct proghdr*)((uchar*)elf + elf->phoff); eph = ph + elf->phnum; for(; ph < eph; ph++){ pa = (uchar*)ph->paddr; readseg(pa, ph->filesz, ph->off); if(ph->memsz > ph->filesz) stosb(pa + ph->filesz, 0, ph->memsz - ph->filesz); } // Call the entry point from the ELF header. // Does not return! entry = (void(*)(void))(elf->entry); entry(); }
entry()
関数はentry.S
のentry:
に対応している。
カーネルイメージ
カーネルイメージをダンプすると以下のように出力される。
$ objdump -D kernel kernel: file format elf32-i386 Disassembly of section .text: 80100000 <multiboot_header>: 80100000: 02 b0 ad 1b 00 00 add 0x1bad(%eax),%dh 80100006: 00 00 add %al,(%eax) 80100008: fe 4f 52 decb 0x52(%edi) 8010000b: e4 in $0xf,%al 8010000c <entry>: 8010000c: 0f 20 e0 mov %cr4,%eax 8010000f: 83 c8 10 or $0x10,%eax 80100012: 0f 22 e0 mov %eax,%cr4 80100015: b8 00 90 10 00 mov $0x109000,%eax 8010001a: 0f 22 d8 mov %eax,%cr3 8010001d: 0f 20 c0 mov %cr0,%eax 80100020: 0d 00 00 01 80 or $0x80010000,%eax 80100025: 0f 22 c0 mov %eax,%cr0 80100028: bc c0 b5 10 80 mov $0x8010b5c0,%esp 8010002d: b8 b0 2f 10 80 mov $0x80102fb0,%eax 80100032: ff e0 jmp *%eax :
これは前述のentry.S
に対応していることがわかる。
// entry.S multiboot_header: #define magic 0x1badb002 #define flags 0 .long magic .long flags .long (-magic-flags) # 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