LoginSignup
3
0

More than 1 year has passed since last update.

xv6のソースコードを読んでブートローダを学習してみる

Last updated at Posted at 2022-12-17

初めに

大学の研究の関係でxv6-publicについて色々調べたので、本稿ではそのブートローダについて簡単にまとめたいと思いす。

そもそもxv6とは?

MITが開発した教育用のオペレーティングシステム(OS)です。RISC-V版とx86版があるのですが、本稿ではx86版を取り扱います。

必要なもの

多分これがあったら誰でも使用しているマシンでxv6-publicを動かせちゃいます。AppleのM1,M2はわからないです。ごめんなさい。

xv6-public本体

xv6-public

ここからクローンしてください、詳しくはMITのサイトに書いてるかも。

QEMU

プロセッサエミュレータです。xv6-publicはx86(32bit)とかRISC-Vで動くので、最近のCPUだと動作しません。そのため、プロセッサエミュレータを使ってx86(32bit)とかを動かします。

brew install qemu

これでインストールできるはず。

x86(32bit)用のgccとかgdbとか色々

アーキテクチャが異なるのでクロスコンパイル環境が必要です、(多分macの場合は)i386-jos-elf-系のツール一式が必要になります。

クロスコンパイラたち

このリンクに、必要なツールが揃っているので全部インストールしてください。

流行りのRISC-Vの方が良いという方でも、大体同じ手順で行けます。公式サイトはRISC-Vなので見ていただけると分かりやすいかもです。MITのサイト

動かしてみる

ターミナルとかシェルでクローンしたディレクトリにいきましょう

cd xv6-public

次にmakeコマンドを叩きます(makeとかMakefileの説明は後でするかも)

make

そして最後に、qemuから起動します

make qemu

するとこんな感じになると思います

スクリーンショット 2022-12-17 17.36.43.png

別タブでxv6のシェルが出現
スクリーンショット 2022-12-17 17.39.38.png

こんな感じでOSが起動します.

あれ?

別タブでシェルが起動し、OSを動かせてハッピーなわけですが、シェル上の方を見てみましょう

スクリーンショット 2022-12-17 17.39.38.png

OSが動くより前になんか色々動いてね?

当然と言われれば当然かもしれませんが、こんな小さなOSを動かすのにも当然ブート処理が必要なわけですね。qemuにはBIOSが搭載されているので、そのBIOSちゃんに何かしらのイメージディスクを読み込ませてるわけです。本記事ではそこに少しだけ着目します。

Makefileを見てみる

Makefileを見れば、起動ディスクの正体が何となく見えてきます。Makefileとはmakeコマンドを叩いたときに、記述したコードを全て実行してくれるみたいなやつです。

xv6.img

xv6のイメージディスクです、この場合はブートディスクと呼んだ方がいいかもしれません。

図1.png

このコードに着目すると、xv6.imgはbootblockというファイルとkernelというファイルで構成されていることが分かります。ddコマンドはブロック単位(512byte)でデータの読み書きを行うコマンドです。先頭の512byteにbootblockを書き込んで2ブロック目にkernelを書き込んでいますね。この時、ブートディスクの先頭1ブロックを「Master Boot Record」と呼び、BIOSは初めにこれをメモリにロードします。

スクリーンショット 2022-12-17 18.16.52.png

こんな感じです。kernelとはxv6-publicのカーネルなので、bootblockがkernelをロードしてくれるんだろなーと思いながら、bootblockの中身を追っていきましょう。

bootblock

再びMakefileの中身を確認します.

スクリーンショット 2022-12-17 17.58.22.png

bootasm.Sとbootmain.cで構成されていますね、ここで気づく方もいるかもしれませんが、低レイヤすぎて開始はx86(しかも16bit)直書きでスタートします、恐ろしい。

bootasm.S

bootasm.Sが何をしているか確認してみます。

.code16                       # Assemble for 16-bit mode
.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


ソースコードの一部です。簡単にいうと、起動時は16bitで動作するCPUを32bitで動作するように初期設定をしています。これをすることで、ようやく高級言語(bootmain.c)が動かせるようになる感じです。

bootmain.c

bootmain.c
#include "types.h"
#include "elf.h"
#include "x86.h"
#include "memlayout.h"

#define SECTSIZE  512

void readseg(uchar*, uint, uint);

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();
}

void
waitdisk(void)
{
  // Wait for disk ready.
  while((inb(0x1F7) & 0xC0) != 0x40)
    ;
}


bootmain.cの一部です。かなり複雑です。。

簡単に説明すると、bootblockの2ブロック目にはkernelがあるのですが、それをメモリにロードするプログラムです。

その他色々

もう一度Makefileを見てみましょう

スクリーンショット 2022-12-17 17.58.22.png

着目すべきポイントは101行目と104行目です。

101行目はldコマンドによってコンパイルしたbootasmとbootmainをbootblockにリンクしているのですが、格納される開始の番地が先頭ではなく0x7C00番になっています。これによって、メモリにbootblockが読み込まれる時、0x7C00番地からロードされます。

なぜ、0x7C00から格納するのかというのは、x86とBIOSの歴史をだいぶ遡る必要があるらしいので、こういうものだと思いましょう。

なぜx86ではMBRが"0x7C00"にロードされるのか?

102行目は作成したbootblockをperlで書かれたプログラムにぶちこんでいますね。perlのソースコードを見てみましょう。

sign.pl
open(SIG, $ARGV[0]) || die "open $ARGV[0]: $!";

$n = sysread(SIG, $buf, 1000);

if($n > 510){
  print STDERR "boot block too large: $n bytes (max 510)\n";
  exit 1;
}

print STDERR "boot block is $n bytes (max 510)\n";

$buf .= "\0" x (510-$n);
$buf .= "\x55\xAA";

open(SIG, ">$ARGV[0]") || die "open >$ARGV[0]: $!";
print SIG $buf;
close SIG;

このコードはbootblockの先頭1ブロック(512byte)の末尾に55 AAという値を書き込んでいます。これはBIOSに自身がMBRであることを教える、簡単に言えば「私は起動ディスクだ!!」と教えるためのもので、ブートシグニチャと呼ばれます。BIOSで起動する全ての起動ディスクにはこのブートシグニチャが書き込まれています。

これで何となくbootblockの全容が見えてきましたね

スクリーンショット 2022-12-17 18.50.54.png

こんな感じになると思います。

xv6起動までの道のり

ここまでで調べたことをまとめてみます。

1.make qemuを叩く

2.xv6.imgの先頭1ブロックの末尾にブートシグニチャ(55 AA)があるか確認

3.BIOSがxv6.imgのMBR(bootblock)をメモリの0x7C00番地にロード

4.bootblockのうちbootasmが動作し、16bit動作から32bit動作に切り替え

5.bootmain.cが動作してカーネルがロードされる

6.OSが起動

このようになります。

実際に0x7C00番地にbootblockがロードされているか、gdbを使って確認してみましょう

図1.png

図の左がgdb、右がbootblockをダンプしたものです。

設計通り0x7C00番地からロードされていることが確認できます、ここから起動の全てが始まるわけですね。

終わりに

今回はxv6-publicのブートについてまとめました。

BIOSで起動するものは大体この手順を踏むのだと思います。ブートの作成者はCPUレベルでコードを考える必要があるためかなり大変そうだと感じました。

UEFIの勉強がしたい方は、xv6をUEFI起動に魔改造した方がおられるのでそちらを使うと良いかもしれません。

最近はIoT機器がたくさん出てきており、低レイヤの知識やクロスコンパイル系の知識が何やかんや重要視されていると思うので、興味のある方は是非xv6とかから勉強を始めるといいかもしれません。

もっと詳しくしりたい方はめちゃくちゃ丁寧にまとめられている方がおられたのでそちらを共有しておきます。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0