MikanOSをデバッグする話その1(おま環?)

どんなバグ?

MikanOSで「ゼロからのOS自作入門」通りに勉強していくとおそらくタグのosbook_day18cでrpnというアプリケーションが出てくると思うんですが、これがずっと実行できないのでデバッグしていきます。
osbook_day20dで、OSクラッシュしたときにログを吐くようになるんですが、ターミナルでrpnを叩くとなんとページフォルトが起きていることがわかりました。こいつをどうにかしたいです。

環境

何を書けばいいかわからないのでとりあえず羅列します...
OS : Ubuntu 20.04 focal
kernel : x86_64 Linux 5.13.0-40-generic
fish, version 3.1.0
GNU bash, バージョン 5.0.17(1)-release (x86_64-pc-linux-gnu)
clang version 7.0.1-12 (tags/RELEASE_701/final)
LLD 10.0.0 (compatible with GNU linkers)
QEMU emulator version 4.2.1 (Debian 1:4.2-3ubuntu6.21)
こんなもんですかね...

どこでクラッシュしてるのか

とりあえずタグはosbook_day20eで話を進めます(現在の進捗的に)。 この前にもデバッグしようとしたことはありましたが断念しています。その時の記憶を頼りに

__asm__("int3");

をterminal.cpp(下のやつ)の403行目と404行目の間に挟んでみます。

403     auto file_entry = fat::FindFile(command);
404     if (!file_entry) {
405       Print("no such command: ");
406       Print(command);
407       Print("\n");   
408     } else if (auto err = ExecuteFile(*file_entry, command, first_arg)) {
409       Print("failed to exec file: ");

すると

#BPはBreakPoint Exceptionの略で、CPUの仕様で割り込みベクタ番号の3に割り当てられるんでしたっけ...
こんな感じにクラッシュしてログが吐かれました。int3は割り込みベクタ番号の3の割り込みを発生させます。
次に、このint3を取り除いた上で408行目と409行目の間にint3を挟み込みます。すると
普段使われないはずの右上の領域に黒文字でログが書かれるのいいですよね...
とまぁ、問題のページフォルトが発生するわけですね。一応405~407行が実行されていないことを確認するために404,405行目の間にint3を挟んでも上の図の通りページフォルトが発生しました。
つまり、問題はExecuteFile関数にありそうですね。この調子でどこでクラッシュしているか探していきます。

下はExecuteFile関数の中の一部です。

452   auto entry_addr = elf_header->e_entry;
453   CallApp(argc.value, argv, 4 << 3 | 3, 3 << 3 | 3, entry_addr,
454       stack_frame_addr.value + 4096 - 8);

rpnを実行するときの話になりますが、rpnのバイナリのELFヘッダからプログラムのエントリポイントを指すelf_header->e_entryをCallAppに渡してrpn自身を実行します。そこで、int3を452, 453行目の間にint3を挿入して実行するとページフォルトが返されました。ここの行以前でクラッシュしていそうです(CallAppの処理をよく把握していないのでホッとしている)。とまぁ、こんな感じにどんどん特定を進めると

429   if (auto err = LoadELF(elf_header)) {
430     return err;
431   }

ここでクラッシュしていることがわかりました。LoadELF(elf_header)が実行されていますね。LoadELF関数は同じくterminal.cppの173行目にあります。このLoadELF関数のどこでクラッシュしているかを探ると

183   if (auto err = CopyLoadSegments(ehdr)) {
184     return err;
185   }

ファイル内を検索しても他にCopyLoadSegments関数を呼び出している箇所はないのでさらにこの関数の中にint3を差し込みます。
そして特定がとうとう完了しました(唐突)!

165     const auto src = reinterpret_cast<uint8_t*>(ehdr) + phdr[i].p_offset;
166     const auto dst = reinterpret_cast<uint8_t*>(phdr[i].p_vaddr);
167     memcpy(dst, src, phdr[i].p_filesz);    //←ここ

ここです。ここはforループの中ですが、i=3のときのメモリのコピーをするのに失敗している感じですかね。

何が起こっているのか

166     const auto src = reinterpret_cast<uint8_t*>(ehdr) + phdr[i].p_offset;
167     const auto dst = reinterpret_cast<uint8_t*>(phdr[i].p_vaddr);
168     char str[64];
169     sprintf(str, "ehdr: 0x%p",ehdr);  
170     WriteString(*screen_writer, {500, 100+i*68}, str, {0,0,0});  
171     sprintf(str, "phdr[%d].p_offset: 0x%0p", i, phdr[i].p_offset);
172     WriteString(*screen_writer, {500, 117+i*68}, str, {0,0,0});
173     sprintf(str, "phdr[%d].p_vaddr: 0x%0p", i, phdr[i].p_vaddr);
174     WriteString(*screen_writer, {500, 134+i*68}, str, {0,0,0});
175     sprintf(str, "phdr[%d].p_filesz: 0x%0p", i, phdr[i].p_filesz);
176     WriteString(*screen_writer, {500, 151+i*68}, str, {0,0,0}); 
177     memcpy(dst, src, phdr[i].p_filesz);

sptintf関数を使うためにインクルードファイルにstdio.hを増やしました。(その関係で行番号がずれています。) 右上に黒文字で出るクラッシュログのコードを参考にmemcpyに渡される引数を構成する変数を表示するようにしました。 その結果がこちらです。

ログがけっこうでかく出るのでウィンドウは退避させます
これらが実際に作成されているrpnのバイナリと合致するかをまず確認します。
画像そのものを編集するのが面倒でウィンドウの大きさを変えてAlt + PrtSc
readelfにより表示させました。値を見ると、確かにファイルから読み込まれた値に間違いがないことがわかります。

と、こんなところで今日は疲れたのでまたこんど。