「ゼロからのOS自作入門」3日目にてKernelが起動しない原因をちゃんと理解する
はじめに
ゼロからのOS自作入門を購入したものの積んでしまっていたがとある機会に再開したところ, Ubuntu22.04でないとAnsible周りの実装が動かなくなっていたので, 再度おさらいを実施.
その最中, 3日目のkernelロード周りが動作しないことが分かった.
この問題に関してはIssueにて起票はされており, 解決策も既に分かっている.
解決策1: 先に4日目のELFローダを実装する.
解決策2: ldd-7を使用する.
一方で, Issueのコメントを眺めていてもこの問題の原因について私はイマイチ理解が追いついていなかった.
ELFに対する実践的な理解度が不足しているためだと思われる.
そのため, 自身の理解度に落とし込むために色々を解析を進めてみた.
対象読者
- ldd-14が出力するELFの内容に興味がある方
- 「ゼロからのOS自作入門」を進めていて, 3日目のkernel起動Issueを解決した際に本Issueの理解をせずに進めた方
非対象読者
- readelfを見ただけで本Issueの原因が良くわかる方
- 特にこの辺りの技術領域に興味がない方
前提条件
- X86 WindowsOS, WSL2 Ubuntu22.04を使用.
- QEMUでのOS起動を実施.
- 2023/08/07時点の情報. この時点のlddはldd-14である.
- 環境としては「day03a時点」と「day03a直後にday04dのELFローダ実装後」の2パターンに対して解析.
kernel.elfの理解
何を解析するにせよ, kernel.elfが理解できていないと始まらないので, 色々と解析を行う.
readelfによる解析
readelfコマンドを使うことでELF formatは解析を行うことができる.
とりあえず全ての解析情報を眺めるため, allオプションで実行.
readelf -a kernel.elf
以下の情報が得られる. 注目ポイントのみ抜粋.
ELF Header:
...
Entry point address: 0x101120
...
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000100040 0x0000000000100040
0x00000000000000e0 0x00000000000000e0 R 0x8
LOAD 0x0000000000000000 0x0000000000100000 0x0000000000100000
0x0000000000000120 0x0000000000000120 R 0x1000
LOAD 0x0000000000000120 0x0000000000101120 0x0000000000101120
0x0000000000000013 0x0000000000000013 R E 0x1000
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x0
...
Symbol table '.symtab' contains 3 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.cpp
2: 0000000000101120 19 FUNC GLOBAL DEFAULT 1 KernelMain
このreadelfの結果を見て, 初心者レベルで理解できるのは「0x101120がエントリポイントらしい?」というくらいではないだろうか.
実際QEMU上でEntryAddressをPrint文でチェックすると0x101120が取得できる.
objdumpによる逆アセンブル
これからメモリダンプしてKernelMainがどこにあるのか探したかったので, そもそもの命令内容を調べてみた.
objdump -d kernel.elf
KernelMainについては以下の情報である.
0000000000101120 <KernelMain>:
101120: 55 push %rbp
101121: 48 89 e5 mov %rsp,%rbp
101124: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1)
10112b: 00 00 00
10112e: 66 90 xchg %ax,%ax
101130: f4 hlt
101131: eb fd jmp 101130 <KernelMain+0x10>
QEMUのメモリダンプにて, Hexかつ64bitで調べるので,
x86がリトルエンディアンであることを踏まえて, メモリダンプする際には0x1f0f2e66e5894855とダンプされる個所を探すことになる.
QEMUでのメモリダンプ
それぞれのケースでメモリダンプ結果を調べてみる.
day03a時点
0x100120にそれらしきデータが格納されていることがわかる...
つまり0x101120をエントリポイントとしたコードを記述しても, KernelMainは実行されないということになる.
(qemu) xp /10xg 0x100120
0000000000100120: 0x1f0f2e66e5894855 0x9066000000000084
0000000000100130: 0x2525011101fdebf4 0x1710177225030513
0000000000100140: 0x177306121b11251b 0x121b11002e020000
0000000000100150: 0x3a2503197a184006 0x000000193f0b3b0b
0000000000100160: 0x080100050000002b 0x0021000100000000
day03a直後にday04dのELFローダ実装後
このケースではEntrypoint通り, 0x101120に配置されていることがわかる.
(qemu) xp /10xg 0x101120
0000000000101120: 0x1f0f2e66e5894855 0x9066000000000084
0000000000101130: 0x0000000000fdebf4 0x0000000000000000
0000000000101140: 0x0000000000000000 0x0000000000000000
0000000000101150: 0x0000000000000000 0x0000000000000000
0000000000101160: 0x0000000000000000 0x0000000000000000
2つの挙動について
「day03a時点」というのは, 以下の順で処理が行われる.
- kernel.elfを0x100000に確保したメモリ上にロード. 0x100000は事前に決めたアドレス.
- ELFヘッダからEntrypointの取得.
- EntryPointを実行.
「day03a直後にday04dのELFローダ実装後」というのは4日目の実装をちゃんと見ればわかるが以下の処理が行われる.
- kernel.elfを一時領域にロード.
- kernel.elfのプログラムヘッダに存在するLOADセクションを解析し, 確保すべきアドレス領域とサイズを取得.
- 解析情報を元にメモリを確保.
- kernel.elfのデータをLOADセクション情報を元にコピー.
- 一時領域を開放.
つまり以下の違いがある.
day03a時点 | day03a直後にday04dのELFローダ実装後 |
---|---|
kernel.elfがそのままメモリに残った状態 | kernel.elfのプログラムヘッダ通りにメモリ配置しなおした状態 |
kernel.elfそのものをダンプしてみる
「kernel.elfがそのままメモリに残った状態」というのがすなわちどういうことか理解するため,
objdumpでkernel.elfをダンプしてみる.
od -t x1 -A x kernel.elf
すると0x120のオフセットにてKernelMainが存在することが把握できる.
000120 55 48 89 e5 66 2e 0f 1f 84 00 00 00 00 00 66 90
つまり, 「kernel.elfの中身とELFで示すEntryPointは不一致である」というのが動作しない原因だったということが分かる.
readelfのおさらい
ここでIssue起票はreadelfだけで原因を理解できていたことから, 再度readelfの結果を見てみる.
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000100040 0x0000000000100040
0x00000000000000e0 0x00000000000000e0 R 0x8
LOAD 0x0000000000000000 0x0000000000100000 0x0000000000100000
0x0000000000000120 0x0000000000000120 R 0x1000
LOAD 0x0000000000000120 0x0000000000101120 0x0000000000101120
0x0000000000000013 0x0000000000000013 R E 0x1000
実はプログラムヘッダの読み方が分かっていれば, 以下の情報はわかるようになっている.
- Kernelmainの実態が0x120に存在している. 2つ目のLOADセクションのOffsetがそういった意味.
- EntryPoint自体はVirtAddrにあるように0x101120であるため, kernel.elfでは0x100120にあっても, 実行時は0x101120に配置されていることになる.
- これらはローダが配置するため, kernel.elfを直接メモリにロードした場合とは状況が異なる.
Issue起票者のコメントも上記のことを述べているのだろう.
lld14では、ファイル上の余分なパディングが消えて一つ目のLOAD領域の直後にエントリーポイントのLOAD領域が保存されています。なので、(0x00100000 + 24)に記載されているアドレスに処理を移しても、ファイル上のオフセットが一致していないため誤作動し、何かしらの例外を引き当てているように思えます。
最後に
この辺りはリンカローダを理解していれば分かる内容ではあるが,
また一つ賢くなった…かもしれない?
Discussion