「ゼロからのOS自作入門」 03日目

参考文献

こちらの記事を参考にいたしました! 良記事ありがとうございます。


試行錯誤メモ

QEMU モニタ

QEMU の実行コマンドに -monitor stdio のオプションを付けることで実行中のレジスタ/メモリの内容が見れる。

$ qemu-system-x86_64 \
  -drive if=pflash,file=$HOME/Code/mikanos-build/devenv/OVMF_CODE.fd \
  -drive if=pflash,file=$HOME/Code/mikanos-build/devenv/OVMF_VARS.fd \
  -hda day01/c/disk.img -monitor stdio
(qemu) # 入力待ちになる

このときに下記のようにレジスタの内容を確認できる。

(qemu) info registers 
RAX=0000000000000000 RBX=0000000000000001 RCX=0000000007b7b1c0 RDX=0000000000000002
RSI=0000000000000400 RDI=0000000007ea92d0 RBP=000000000000002e RSP=0000000007ea88a0
R8 =00000000000000af R9 =0000000000000288 R10=0000000000000050 R11=0000000000000000
R12=00000000066c55c0 R13=0000000007ea8930 R14=0000000007255ca0 R15=0000000007ea88e8
RIP=00000000066c5519 

(qemu) x /4xb 0x66c5519 
00000000066c5519: 0xeb 0xfe 0x48 0x83

(qemu) x /2i 0x66c5519 
0x066c5519:  eb fe                    jmp      0x66c5519
0x066c551b:  48 83 ec 28              subq     $0x28, %rsp

レジスタ

CPU の演算や設定に使用される高速なメモリ領域。汎用レジスタと特殊レジスタに分類される。

汎用レジスタ

  • 一般の演算で使われるレジスタ
  • サイズはすべて 8 バイト(=64 ビット)
  • 数は 16 個(RAX/RBX/RCX/RDX/RBP/RSI/RDI/RSP/R8 ~ R15)
  • アクセスしたいデータサイズによって表記が変わる(AX は RAX の下位 16 ビットを表す)
名称説明とか由来
RAXアキュームレーターの A。はじめは計算結果を格納するための作られた。
RBXベースの B
RCXカウンタレジスタの C。繰り返し回数の設定につかう(ことが多い)。
RDXデータレジスタの D
RSIソースインデックスレジスタの SI
RDIディスティネーションインデックスレジスタの DI
RBPベースポインタの BP。スタック内のアクセスに使われることが多い。
RSPスタックポインタの SP。サブルーチンの戻りアドレスを格納したり PUSH/POP でレジスタを一時的に退避/復帰することに使われる。
R8拡張汎用レジスタ。
R9拡張汎用レジスタ。
R10拡張汎用レジスタ。
R11拡張汎用レジスタ。
R12拡張汎用レジスタ。
R13拡張汎用レジスタ。
R14拡張汎用レジスタ。
R15拡張汎用レジスタ。

register

特殊レジスタ

  • CPU の設定、タイマ、機能制御で使われるレジスタ
  • RIP: CPU が次に実行する命令のメモリアドレスを保持するレジスタ
  • RFLAGS: 命令の実行結果のよって変化するフラグを集めたレジスタ
  • CR0: CPU の重要な設定を集めたレジスタ

初めてのカーネル

いよいよブートローダーから C で書かれたカーネル本体を呼び出して実行する段階。

extern "C" void KernelMain() {
  while(1) __asm__("hlt");
}
$ cd mikanos/kernel
$ git checkout osboook_day03a
$ clang++ -O2 -Wall -g --target=x86_64-elf -ffreestanding -mno-red-zone -fno-exceptions -fno-rtti -std=c++17 -c main.cpp -o main.o
$ ld.lld --entry KernelMain -z norelro --image-base 0x100000 --static -z separate-code -o kernel.elf main.o

llvm@10 以降では、リンク後のファイルサイズ削減のために ELF ファイルのセクション間のゼロ埋め処理がデフォルトで省略される動きになったので、書籍通りのコマンドでは正しく動かない。

具体的には、lld@10以降でリンクした elf ファイルを readelf で見たときに、 エントリーポイントのアドレスが 0x101120 と表示されるものの、 実際にメモリ上にカーネルが展開されたときに命令が存在するアドレスは 0x00100120 なので正しく命令が実行できない模様。 (ld.lld@9では命令が存在する箇所まで0x00で埋まっており、カーネル展開後はエントリポイントのアドレスと命令が存在するアドレスが一致していた)

なので、

  • ゼロ埋め無しでエントリポイントのアドレスを正しく指定する(0x101120 ではなく 0x00100120 と指定する方法)
  • ゼロ埋め有りでエントリポイントのアドレスと実命令のアドレスを一致させる(ゼロ埋めを復活させる方法)

の2パターンが解決策として考えられたが、ひとまずは llvm@9 系と同じくゼロ埋めありでリンクする -z separate-code というオプションを見つけたので、書籍どおりの結果を得ることが出来た。

後はいつものように起動ディスクイメージを作成して QEMU で実行!

$ hdiutil attach disk.img
$ cp ~/Code/mikanos/kernel/kernel.elf /Volumes/MIKAN\ OS/kernel.elf
$ hdiutil detach /Volumes/MIKAN\ OS

$ qemu-system-x86_64 \
  -drive if=pflash,file=$HOME/Code/mikanos-build/devenv/OVMF_CODE.fd \
  -drive if=pflash,file=$HOME/Code/mikanos-build/devenv/OVMF_VARS.fd \
  -hda day01/c/disk.img -monitor stdio

day03a

(qemu) info registers
RAX=0000000000100000 RBX=0000000007eaca20 RCX=0000000000000000 RDX=0000000000000000
RSI=00000000067d5a98 RDI=0000000007eac9d8 RBP=0000000007ea8830 RSP=0000000007ea8830
R8 =0000000007ea87c4 R9 =0000000007b7b48f R10=0000000007bcd018 R11=0000000000000000
R12=0000000007255ca0 R13=0000000007eac8d0 R14=0000000007ea9170 R15=00000000066c47e0
RIP=0000000000101011 RFL=00000002 [-------] CPL=0 II=0 A20=1 SMM=0 HLT=1
ES =0030 0000000000000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
CS =0038 0000000000000000 ffffffff 00af9a00 DPL=0 CS64 [-R-]
SS =0030 0000000000000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
DS =0030 0000000000000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
FS =0030 0000000000000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
GS =0030 0000000000000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
LDT=0000 0000000000000000 0000ffff 00008200 DPL=0 LDT
TR =0000 0000000000000000 0000ffff 00008b00 DPL=0 TSS64-busy
GDT=     0000000007bee698 00000047
IDT=     0000000007306018 00000fff
CR0=80010033 CR2=0000000000000000 CR3=0000000007c01000 CR4=00000668
DR0=0000000000000000 DR1=0000000000000000 DR2=0000000000000000 DR3=0000000000000000 
DR6=00000000ffff0ff0 DR7=0000000000000400
EFER=0000000000000500
FCW=037f FSW=0000 [ST=0] FTW=00 MXCSR=00001f80
FPR0=0000000000000000 0000 FPR1=0000000000000000 0000
FPR2=0000000000000000 0000 FPR3=0000000000000000 0000
FPR4=0000000000000000 0000 FPR5=0000000000000000 0000
FPR6=0000000000000000 0000 FPR7=0000000000000000 0000
XMM00=00000000000000000000000000000000 XMM01=00000000000000000000000000000000
XMM02=00000000000000000000000000000000 XMM03=00000000000000000000000000000000
XMM04=00000000000000000000000000000000 XMM05=00000000000000000000000000000000
XMM06=00000000000000000000000000000000 XMM07=00000000000000000000000000000000
XMM08=00000000000000000000000000000000 XMM09=00000000000000000000000000000000
XMM10=00000000000000000000000000000000 XMM11=00000000000000000000000000000000
XMM12=00000000000000000000000000000000 XMM13=00000000000000000000000000000000
XMM14=00000000000000000000000000000000 XMM15=00000000000000000000000000000000
(qemu) x /2i 0x101011
0x00101011:  eb fd                    jmp      0x101010
0x00101013:  cc                       int3     
(qemu) x /2i 0x101010
0x00101010:  f4                       hlt      
0x00101011:  eb fd                    jmp      0x101010
(qemu) 

おぉ、ちゃんと hlt のにジャンプして停止になるような状態ですね。すごい。


ブートローダーからピクセルを描く

ピクセル描画に必要なもの。

項目説明
フレームバッファの先頭アドレス画面描画するためのメモリ領域。
任意のアドレスに値を書き込むことで画面描画できる。
フレームバッファの高さと幅=解像度
フレームバッファの非表示領域を含めた幅右側に非表示の領域がある場合がある。
1 ピクセルのデータ形式1 ピクセルの表現バイト数/RGB の並びとビット数など

ブートローダーから塗りつぶす

肝はこの部分で、フレームバッファの全ビットを 0xff で埋めることで RGB 0xffffff のような感じにして白色にしている。

UINT8* frame_buffer = (UINT8*)gop->Mode->FrameBufferBase;
for (UINTN i = 0; i < gop->Mode->FrameBufferSize; ++i) {
  frame_buffer[i] = 255;
}

では、ビルド&動作確認。

# OSブランチの変更
$ cd ~/Code/mikanos
$ git checkout osbook_day03b

# EDKでブートローダーをビルド
$ cd ~/Code/edk
$ source edksetup.sh
$ build

# カーネルをビルド
$ cd ~/Code/mikanos-build
$ mkdir ./BUILD && mkdir ./DISK/EFI/BOOT
$ clang++ -O2 -Wall -g --target=x86_64-elf -ffreestanding -mno-red-zone -fno-exceptions -fno-rtti -std=c++17 -c ~/Code/mikanos/kernel/main.cpp -o ./BUILD/main.o
$ ld.lld --entry KernelMain -z norelro --image-base 0x100000 --static -z separate-code -o ./DISK/kernel.elf ./BUILD/main.o
$ cp ~/Code/edk2/Build/MikanLoaderX64/DEBUG_CLANGPDB/X64/Loader.efi ./DISK/EFI/BOOT/BOOTX64.EFI

# QEMU で実行(簡単化のためにイメージを作らずローカルディレクトリを直接実行)
$ tree ./DISK
DISK
├─ EFI
│   └─ BOOT
│       └─ BOOTX64.EFI
└─ kernel.elf
$ qemu-system-x86_64 \
  -drive if=pflash,file=$HOME/Code/mikanos-build/devenv/OVMF_CODE.fd \
  -drive if=pflash,file=$HOME/Code/mikanos-build/devenv/OVMF_VARS.fd \
  -hda fat:rw:./DISK -monitor stdio

おぉー、白くなった(色んな情報表示してるけど真っ白に塗りつぶされてしまった 😂)。

day03b

塗りつぶしの処理部分をコメントアウトすると各種情報が読める。

1,921,024[byte] ÷ ( 800[px] x 600[px] ) = 4[byte] ということで、「PixelBlueGreenRedReserved8bitPerColor(=RGB+予約 8byte)」と一致ということだね。なるほど。

day03b2

カーネルから塗りつぶす

今回の一番の山場。UEFI からカーネルに処理を引き継いで、カーネル側でピクセルを操作してみる。

ミソは、KernelMain がフレームバッファに関する引数をとって、それを操作しているところ。

#include <cstdint>

extern "C" void KernelMain(uint64_t frame_buffer_base,
                           uint64_t frame_buffer_size) {
  uint8_t* frame_buffer = reinterpret_cast<uint8_t*>(frame_buffer_base);
  for (uint64_t i = 0; i < frame_buffer_size; ++i) {
    frame_buffer[i] = i % 256;
  }
  while (1) __asm__("hlt");
}

また、今回は Mac OS 上の EDK で TOOL_CHAIN_TAG を CLANGPDB にしてビルドしているので、 ブートローダーからカーネルを呼び出す際に Microsoft x64 ABI の形で引数を渡そうとしてしまう。

以下のようにマクロを付与することで System V AMD64 ABI の形式に固定して引数を渡すことができるとのこと。ほほー。

typedef void EntryPointType(UINT64, UINT64);
// ↓ 強制的に SystemV AMD64 ABI 方式で引数を渡せるようにする。
typedef void __attribute__((sysv_abi)) EntryPointType(UINT64, UINT64);

では、こちらもビルド&動作確認。

# OSブランチの変更
$ cd ~/Code/mikanos
$ git checkout osbook_day03c

# EDKでブートローダーをビルド
$ cd ~/Code/edk
$ source edksetup.sh
$ build

# カーネルをビルド
$ cd ~/Code/mikanos-build
# (ソースを展開しているディレクトリが書籍と違うので適宜パスを変更)
$ vim ./devenv/buildenv.sh
# (環境変数を反映)
$ source ./devenv/buildenv.sh
# (なんだか環境変数展開がうまく行かないので苦肉の策…。まじ謎🤔🤔🤔🤔🤔)
$ clang++ $(echo $CPPFLAGS) -O2 --target=x86_64-elf -ffreestanding -fno-exceptions -c ~/Code/mikanos/kernel/main.cpp -o ./BUILD/main.o
$ ld.lld $(echo $LDFLAGS) --entry KernelMain -z norelro --image-base 0x100000 --static -z separate-code -o ./DISK/kernel.elf ./BUILD/main.o
$ cp ~/Code/edk2/Build/MikanLoaderX64/DEBUG_CLANGPDB/X64/Loader.efi ./DISK/EFI/BOOT/BOOTX64.EFI

# QEMU で実行(簡単化のためにイメージを作らずローカルディレクトリを直接実行)
$ tree ./DISK
DISK
├─ EFI
│   └─ BOOT
│       └─ BOOTX64.EFI
└─ kernel.elf
$ qemu-system-x86_64 \
  -drive if=pflash,file=$HOME/Code/mikanos-build/devenv/OVMF_CODE.fd \
  -drive if=pflash,file=$HOME/Code/mikanos-build/devenv/OVMF_VARS.fd \
  -hda fat:rw:./DISK -monitor stdio

お〜、シマシマや!!

day03c


今回は、llvm@11 でもカーネルをビルド&リンクできることがわかったので良かったです。 引き続き Mac & QEMU で学習を進めていきまーす。

今回はこの辺りでおしまい!

© 2021 czu.jp