レガシーガジェット研究所

気になったことのあれこれ。

Unix xv6 ~ 起動イメージ ~

概要

xv6のコードリーティングを通してUnixの動作を追う。今回は起動イメージを見ていく。

リポジトリは以下。

https://github.com/mit-pdos/xv6-public

xv6

xv6は、ANSI Cによる、Sixth Edition Unixマルチプロセッサx86システムへの再実装である。 xv6はMITにおけるオペレーティングシステムエンジニアリング(6.828)コースにて、教育を目的として使われている。 LinuxBSDとは異なり、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

上記のようにMBRBIOSによって読み込まれ規定のアドレスに展開される。

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
  
  :

最後に呼び出しているbootmainC言語で記述された関数で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.Sentry:に対応している。

カーネルイメージ

カーネルイメージをダンプすると以下のように出力される。

$ 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

参考