Skip to content

Instantly share code, notes, and snippets.

@uchan-nos
Last active April 24, 2020 07:04
Show Gist options
  • Save uchan-nos/b5af3d033192c90ce1f4f1f0a005e551 to your computer and use it in GitHub Desktop.
Save uchan-nos/b5af3d033192c90ce1f4f1f0a005e551 to your computer and use it in GitHub Desktop.
MikanOSでrpnコマンド実行後にPCが再起動するバグを調査するメモ
  • MikanOS のリポジトリ https://github.com/uchan-nos/mikanos
  • 現象を確認したコミット b08a21e5d6b65f870da55e2bd1430762c0025ac2
    • このコミットは uchan のローカルマシンにしか存在しない可能性がある

現象の再現

QEMU にて MikanOS を起動させ,初期ターミナルにて「rpn 2 3 +」と入力しエンター。 画面に「app exited. ret = 5」と表示された後,いずれかのキーを入力すると MikanOS が再起動される(ブートローダーの処理からまた開始される)。

無関係に思えることをすると現象が治まる

次の 3 つのいずれかを行うと現象が治まる。(それぞれ,現象を治めるのに最小の変更かどうかは検証していない)

  • GDT に TSS を設定し,ltr する
  • 標準例外に対応するハンドラを作り IDT にセットする
  • Task::InitContext 内で Log() を使ってログを出す

GDT に TSS を設定し,ltr する

  • RSP0,IST1 にそれぞれ 8KiB のスタック領域を設定した TSS を作成。
  • GDT[5](および GDT[6]) に TSS を設定。
  • ltr で TR=5 をロード。

そもそも,このコミットの時点では,rpn コマンドはカーネルモード(CPL=0)で動く。 ターミナル内で rpn の ELF ファイルをロードし,エントリポイントを call 命令で呼び出すだけの簡単な仕組み。

CPL=0 以外で動くコンテキストは存在しないので,タイマー割り込みなどの割り込み発生時に CPL=3 → CPL=0 への遷移が起きず,TSS.RSP0 は使われないはずである。 また,いずれの IDT entry でも IST=0 としているため,TSS.IST1 も使われないはず。

だから,TSS を GDT に登録したこと自体は論理的には影響を及ぼさないはずである。(が,何故か現象は治まる)

標準例外に対応するハンドラを作り IDT にセットする

標準例外(DE, DB, BP, OF, BR, UD, NM, DF, TS, NP, SS, GP, PF, MF, AC, MC, XM, VE)に対応する割り込みハンドラを作り, それぞれの割り込みベクタに対応する IDT entry を設定する。

これらの割り込みハンドラは,割り込み発生時に積まれるスタックフレームの CS:RIP, RFLAGS, SS:RSP, オプションでエラーコードを 画面に表示した後,永久ループするハンドラとなっている。 いずれのハンドラも呼ばれないにも関わらず,現象は治まる。

Task::InitContext 内で Log() を使ってログを出す

タスク ID,Task::context_.rsp の値,Task::context_ のアドレスを画面に表示するだけのログ出力を Task::InitContext() に追加してみると,現象が治まる。

バグ調査メモ

まず,Task::InitContext 内で Log() を使ってログを出すときと出さないときの差を見てみたい。 まず,ログから得られた情報は次の通り。

TaskID RSP の初期値 ctx addr
2 0x00fe8ad8 0x00fe77b0
3 0x00feae78 0x00fe9b70

タスク用スタックは 4KiB を確保しているので,スタック領域は

  • Task2 が 0x00fe7ae0 - 0x00fe8ad8
  • Task3 が 0x00fe9e80 - 0x00feae78

となっている(8 バイト程度の誤差はあるかも)。 このことは,Task クラスを割り当てた直後にスタック領域を割り当てていることとも整合している。

rpn コマンドが終了した直後(ターミナルが "app exited. ret = %d" を表示しようとする行)である terminal.cpp:267 にブレークポイントを仕掛けてコンテキストの中身を見てみる。

(gdb) p /x *(TaskContext*)0x00fe77b0
$1 = {cr3 = 0x250000, rip = 0x10ec0d, rflags = 0x6, reserved1 = 0x0, cs = 0x100008, ss = 0x10, 
  fs = 0x10e0000, gs = 0x0, rax = 0xf8, rbx = 0x0, rcx = 0x10e0c20, rdx = 0x1, rdi = 0xfe6450, 
  rsi = 0xfe77b0, rsp = 0xfe8950, rbp = 0xfe8aa0, r8 = 0x80000000, r9 = 0x10fae, r10 = 0x0, 
  r11 = 0x30, r12 = 0x0, r13 = 0x0, r14 = 0x0, r15 = 0x0, fxsave_area = {__elems_ = {
      0x0 <repeats 28 times>, 0xff, 0xff, 0x0 <repeats 130 times>, 0xc7, 0x3, 
      0x0 <repeats 350 times>}}}

(gdb) p /x *(TaskContext*)0x00fe9b70
$3 = {cr3 = 0x250000, rip = 0x113ca8, rflags = 0x6, reserved1 = 0x0, cs = 0x110008, ss = 0xfe0010, 
  fs = 0x10d0000, gs = 0x0, rax = 0x1bd, rbx = 0xfe9b50, rcx = 0x10dcc00, rdx = 0x1, 
  rdi = 0xfe77b0, rsi = 0xfe9b70, rsp = 0xfeadb0, rbp = 0xfeadc0, r8 = 0xfebf30, r9 = 0x5e, 
  r10 = 0xfe9b50, r11 = 0xfc88c0, r12 = 0xfeadd0, r13 = 0xfeae08, r14 = 0xf00000007, 
  r15 = 0xfe9b50, fxsave_area = {__elems_ = {0x0 <repeats 28 times>, 0xff, 0xff, 
      0x0 <repeats 130 times>, 0xee, 0x2, 0x0 <repeats 350 times>}}}

RIP と RSP の値が重要なので抜き出してみると,RSP がちゃんとスタック領域に収まっており妥当な感じ。

TaskID RIP RSP スタック領域
2 0x0010ec0d 0x00fe8950 0x00fe7ae0 - 0x00fe8ad8
3 0x00113ca8 0x00feadb0 0x00fe9e80 - 0x00feae78

ログ無しのとき

次にログ出力している行をなくして実験。 次に示すように,2 つのタスクの Context 構造体のアドレスは,ログ出力があったときと完全に同じ。

Breakpoint 2 at 0x113b94: Task::InitContext. (2 locations)
(gdb) c
Continuing.

Breakpoint 2, TaskManager::TaskManager (this=0xfe6340) at task.cpp:75
75	    .InitContext(TaskIdle, 0)
(gdb) p /x idle.id_
$3 = 0x2
(gdb) p /x &idle.context_
$2 = 0xfe77b0
(gdb) c
Continuing.

Breakpoint 2, Task::InitContext (this=0xfe9b50, f=0x11a4d0 <TaskTerminal(unsigned long, long)>, 
    data=0) at task.cpp:18
18	  stack_.resize(stack_size);
(gdb) p /x id_
$4 = 0x3
(gdb) p /x &context_
$5 = 0xfe9b70
(gdb) c
Continuing.

この状態で rpn コマンド終了直後のコンテキストの値を見てみる。

(gdb) p /x *(TaskContext*)0x00fe77b0
$6 = {cr3 = 0x250000, rip = 0x10ec0d, rflags = 0x6, reserved1 = 0x0, cs = 0x100008, ss = 0x10, 
  fs = 0x10d0000, gs = 0x0, rax = 0xd6, rbx = 0x0, rcx = 0x10dfc10, rdx = 0x1, rdi = 0xfe6450, 
  rsi = 0xfe77b0, rsp = 0xfe8950, rbp = 0xfe8aa0, r8 = 0x7, r9 = 0x442a, r10 = 0x0, 
  r11 = 0x10dcbe0, r12 = 0x0, r13 = 0x0, r14 = 0x0, r15 = 0x0, fxsave_area = {__elems_ = {
      0x0 <repeats 28 times>, 0xff, 0xff, 0x0 <repeats 130 times>, 0xff, 0x3, 
      0x0 <repeats 350 times>}}}
(gdb) p /x *(TaskContext*)0x00fe9b70
$7 = {cr3 = 0x250000, rip = 0x113c88, rflags = 0x6, reserved1 = 0x0, cs = 0x110008, ss = 0xfe0010, 
  fs = 0xfe0000, gs = 0x0, rax = 0x1fa, rbx = 0xfe9b50, rcx = 0xfe8af0, rdx = 0x1, rdi = 0xfe77b0, 
  rsi = 0xfe9b70, rsp = 0xfeadb0, rbp = 0xfeadc0, r8 = 0xfebf30, r9 = 0x5e, r10 = 0xfe9b50, 
  r11 = 0xfc88c0, r12 = 0xfeadd0, r13 = 0xfeae08, r14 = 0xf00000007, r15 = 0xfe9b50, fxsave_area = {
    __elems_ = {0x0 <repeats 28 times>, 0xff, 0xff, 0x0 <repeats 130 times>, 0xcb, 0x2, 
      0x0 <repeats 350 times>}}}
TaskID RIP RSP スタック領域
2(ログ有り) 0x0010ec0d 0x00fe8950 0x00fe7ae0 - 0x00fe8ad8
2(ログ無し) 0x0010ec0d 0x00fe8950 0x00fe7ae0 - 0x00fe8ad8
3(ログ有り) 0x00113ca8 0x00feadb0 0x00fe9e80 - 0x00feae78
3(ログ無し) 0x00113c88 0x00feadb0 0x00fe9e80 - 0x00feae78

特に怪しそうなところがない。 この後 continue した後,rpn コマンドを実行しようとしてキー入力するとすぐに MikanOS が再起動する。

これだけだとよくわからんので 1 回目の rpn コマンドの実行後,2 回目のキー入力を開始する直前で止めて調査してみる。

^C
Program received signal SIGINT, Interrupt.
0x0000000000114531 in (anonymous namespace)::TaskIdle (task_id=2, data=0) at task.cpp:9
9	    while (true) __asm__("hlt");
(gdb) i r
rax            0x0	0
rbx            0x0	0
rcx            0x0	0
rdx            0x0	0
rsi            0x0	0
rdi            0x2	2
rbp            0xfe8ad0	0xfe8ad0
rsp            0xfe8ad0	0xfe8ad0
r8             0x0	0
r9             0x0	0
r10            0x0	0
r11            0x0	0
r12            0x0	0
r13            0x0	0
r14            0x0	0
r15            0x0	0
rip            0x114531	0x114531 <(anonymous namespace)::TaskIdle(unsigned long, long)+17>
eflags         0x202	[ IF ]
cs             0x8	8
ss             0x10	16
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0
(gdb) p /x *(TaskContext*)0x00fe77b0
$1 = {cr3 = 0x250000, rip = 0x10ec0d, rflags = 0x6, reserved1 = 0x0, cs = 0x100008, ss = 0x10, 
  fs = 0xfe0000, gs = 0x0, rax = 0xb6, rbx = 0x0, rcx = 0xfe8af0, rdx = 0x1, rdi = 0xfe77b0, 
  rsi = 0xfe77b0, rsp = 0xfe8950, rbp = 0xfe8aa0, r8 = 0x80000000, r9 = 0x992, r10 = 0x0, 
  r11 = 0x30, r12 = 0x0, r13 = 0x0, r14 = 0x0, r15 = 0x0, fxsave_area = {__elems_ = {
      0x0 <repeats 28 times>, 0xff, 0xff, 0x0 <repeats 130 times>, 0xb6, 0x2, 
      0x0 <repeats 350 times>}}}
(gdb) p /x *(TaskContext*)0x00fe9b70
$2 = {cr3 = 0x250000, rip = 0x113c88, rflags = 0x6, reserved1 = 0x0, cs = 0x110008, ss = 0xfe0010, 
  fs = 0xfe0000, gs = 0x0, rax = 0xaf, rbx = 0xfe9b50, rcx = 0xfe8af0, rdx = 0x1, rdi = 0xfe77b0, 
  rsi = 0xfe9b70, rsp = 0xfeadb0, rbp = 0xfeadc0, r8 = 0x10dab50, r9 = 0x16, r10 = 0xfe9b50, 
  r11 = 0xfeacc3, r12 = 0xfeadd0, r13 = 0xfeae08, r14 = 0xf00000007, r15 = 0xfe9b50, fxsave_area = {
    __elems_ = {0x0 <repeats 28 times>, 0xff, 0xff, 0x0 <repeats 130 times>, 0x75, 
      0x0 <repeats 351 times>}}}

特におかしいところはなさそう…

2 回目のキー入力によって発生する xHCI の割り込みハンドラ IntHandlerXHCI() の中で止めてみる。

Breakpoint 4, (anonymous namespace)::IntHandlerXHCI (
    frame=0x114531 <(anonymous namespace)::TaskIdle(unsigned long, long)+17>) at interrupt.cpp:35
35	    task_manager->SendMessage(1, Message{Message::kInterruptXHCI});
(gdb) i r
rax            0x0	0
rbx            0x0	0
rcx            0x0	0
rdx            0x0	0
rsi            0x0	0
rdi            0x2	2
rbp            0xfe8aa0	0xfe8aa0
rsp            0xfe8920	0xfe8920
r8             0x0	0
r9             0x0	0
r10            0x0	0
r11            0x0	0
r12            0x0	0
r13            0x0	0
r14            0x0	0
r15            0x0	0
rip            0x10ea98	0x10ea98 <(anonymous namespace)::IntHandlerXHCI(InterruptFrame*)+136>
eflags         0x2	[ ]
cs             0x8	8
ss             0x10	16
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0
(gdb) x /6gx $rbp
0xfe8aa0:	0x0000000000fe8ad0	0x0000000000114531
0xfe8ab0:	0x0000000000000008	0x0000000000000202
0xfe8ac0:	0x0000000000fe8ad0	0x0000000000000010

[RBP] には元の RBP の値,[RBP+8] からは割り込みハンドラのスタックフレームが書かれている。 スタックフレームを整理してみると次のようになる。

オフセット(バイト) レジスタ
+0 RIP 0x00114531
+8 CS 0x0008
+16 RFLAGS 0x00000202
+24 RSP 0x00fe8ad0
+32 SS 0x0010

TaskIdle のループ部分を実行中に割り込まれたことが分かる。各レジスタともに期待通りの値。

現象発生箇所を絞り込む

1 回目の rpn コマンドが完了した後,最初のキー入力で MikanOS が落ちることが分かっている。 更に詳細な現象発生箇所を突き止めたい。

画面をよく観察すると,普段はメインループがイベントを受信するたびに更新しているカウンタが カーソル用タイマのイベントに合わせて 50(0.5 秒)ずつカウントアップしている。 2 回目の rpn コマンドを実行しようとすると,50 の倍数ではないカウント値を表示したまま,しばらくすると MikanOS が落ちる。 これがヒントになりそうだ。

1 回目の rpn コマンド完了後に IntHandlerXHCI のブレークポイントを有効化し,ブレークさせる。 (最初からこのブレークポイントを有効化してしまうと,"rpn 2 3 +" と 1 文字入力するたびにブレークしてしまい面倒だ)

IntHandlerXHCI でブレークしたら SwitchContext にブレークポイントを仕掛け,タスクスイッチの様子を観察してみる。 すると,ある時点で RDI == RSI となっているのを発見した。 これは,今回の現象とは関係ないだろうが,別のバグである。 (同じタスクに切り替えようとしている。おかしな動作になるわけではないが明らかに無駄な処理だ)

SwitchContext の処理を ni コマンドでどんどん進め,処理を追っていると IntHandlerLAPICTimer からの iretq が参照するスタックフレームの値が明らかにおかしくなっていることに気づいた。

RIP == 0x10ec91,ちょうど iretq に差し掛かったときにスタックフレームを表示してみた。

(gdb) x /1i $rip
=> 0x10ec91 <(anonymous namespace)::IntHandlerLAPICTimer(InterruptFrame*)+273>:	iretq  
(gdb) x /5gx $rsp
0x24cd68 <kernel_main_stack+1047816>:	0x00000000000012b6	0x0000000000000008
0x24cd78 <kernel_main_stack+1047832>:	0x0000000000000a82	0x000000000024cd98
0x24cd88 <kernel_main_stack+1047848>:	0x0000000000000010

スタックフレーム上の RIP の値が 0x12b6 になっている。これは明らかにおかしい。 カーネルは 0x100000 から配置されてるから,こんなところに機械語は無いはずだ。 直前の SwitchContext で変な値にジャンプしてしまったと推測されるので,MikanOS 再起動後に再度観察することにする。

SwitchContext でブレークした際に RDI == RSI == 0xfe6450 であった。 このとき,SwitchContext の iretq に到達した時点でのスタックフレームは次の通り。

Breakpoint 8, 0x000000000010e890 in SwitchContext ()
(gdb) x /5gx $rsp
0x24cbe0 <kernel_main_stack+1047424>:	0x000000000010ec0d	0x0000000000100008
0x24cbf0 <kernel_main_stack+1047440>:	0x0000000000000006	0x000000000024cc10
0x24cc00 <kernel_main_stack+1047456>:	0x0000000000690010

RIP == 0x10ec0d は IntHandlerLAPICTimer の中の call LAPICTimerOnInterrup の次の命令。 RSP == 0x24cc10 から,割り込みのスタックフレームを探して表示してみる。

IntHandlerLAPICTimer の処理を追いかけてみると RSP + 0x108 + 80 がスタックフレームのアドレスだった。

(gdb) x /5gx 0x24cc10 + 0x108 + 80
0x24cd68 <kernel_main_stack+1047816>:	0x0000000000000a72	0x0000000000000008
0x24cd78 <kernel_main_stack+1047832>:	0x0000000000000202	0x000000000024cd98
0x24cd88 <kernel_main_stack+1047848>:	0x0000000000000010

CS == 0x8,RFLAGS == 0x202,RSP == 0x24cd98,SS == 0x10 はいかにもそれっぽい値だが,RIP == 0xa72 だけ明らかにおかしい。

時系列をまとめると次のような流れ:

  • 2 回目の rpn を実行するために何らかのキーを押す
  • IntHandlerXHCI が起動する
  • SwitchContext が呼ばれる。このときは RDI ≠ RSI,呼び出し元 RIP == 0x113c88
  • SwitchContext が呼ばれる。このときは RDI == RSI,呼び出し元 RIP == 0x10ec0d
    • 呼び出し元は IntHandlerLAPICTimer で,そのスタックフレームの RIP が変な値。

したがって IntHandlerXHCI 直後の SwitchContext を詳細に調べてみることにする。

1 回目の SwitchContext

IntHandlerXHCI 直後,初めて呼ばれる SwitchContext でブレークして詳細に調査する。

Breakpoint 7, 0x000000000010e782 in SwitchContext ()
(gdb) i r
rax            0x50	80
rbx            0x0	0
rcx            0xfe6780	16672640
rdx            0x0	0
rsi            0xfe77b0	16676784
rdi            0xfe6450	16671824
rbp            0xfe8aa0	0xfe8aa0
rsp            0xfe8948	0xfe8948
r8             0x80000000	2147483648
r9             0x4b0	1200
r10            0x0	0
r11            0x30	48
r12            0x0	0
r13            0x0	0
r14            0x0	0
r15            0x0	0
rip            0x10e782	0x10e782 <SwitchContext>
eflags         0x6	[ PF ]
cs             0x8	8
ss             0x10	16
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0

SwitchContext iretq 時点のスタックフレーム

(gdb) x /5gx $rsp
0xfe8920:	0x0000000000113c88	0x0000000000110008
0xfe8930:	0x0000000000000002	0x000000000024ced0
0xfe8940:	0x0000000000fe0010

RIP == 0x113c88 は Task::Sleep の中。call TaskManager::Sleep の次の命令。 RSP + 8 + 16 の位置に戻り先アドレスがあるので確認してみる。

(gdb) x /1gx 0x24ced0 + 8 + 16
0x24cee8 <kernel_main_stack+1048200>:	0x000000000010b7a8

RIP == 0x10b7a8 はメインループの Task::Sleep の次の sti 命令。 ここまでは想定通り… 0x10b7a8 でブレークして様子を見てみよう…

0x10b7a8 でちゃんとブレークできた。

その後実行を進めて見ると,SwitchContext を延々実行している。 で,そのうち MikanOS が死んでしまうことが分かった。

それぞれの SwitchContext でのレジスタの変化を見てみると何か分かるかも。 連続する 2 回の SwitchContext iretq でのレジスタ値は次の通り。

Breakpoint 8, 0x000000000010e890 in SwitchContext ()
2: /x *(unsigned long*)$rsp = 0x10ec0d
3: $rsp = (void *) 0x24cbe0 <kernel_main_stack+1047424>
(gdb) i r
rax            0x68	104
rbx            0x694b40	6900544
rcx            0xfe6780	16672640
rdx            0x0	0
rsi            0xfe6450	16671824
rdi            0xfe6450	16671824
rbp            0x24cd60	0x24cd60 <kernel_main_stack+1047808>
rsp            0x24cbe0	0x24cbe0 <kernel_main_stack+1047424>
r8             0x80000000	2147483648
r9             0x41a	1050
r10            0x0	0
r11            0x30	48
r12            0x693000	6893568
r13            0x695540	6903104
r14            0x694b64	6900580
r15            0x8	8
rip            0x10e890	0x10e890 <SwitchContext+270>
eflags         0x6	[ PF ]
cs             0x8	8
ss             0x10	16
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0
(gdb) c
Continuing.

Breakpoint 8, 0x000000000010e890 in SwitchContext ()
2: /x *(unsigned long*)$rsp = 0x10ec0d
3: $rsp = (void *) 0x24cbe0 <kernel_main_stack+1047424>
(gdb) i r
rax            0x69	105
rbx            0x694b40	6900544
rcx            0xfe6780	16672640
rdx            0x0	0
rsi            0xfe6450	16671824
rdi            0xfe6450	16671824
rbp            0x24cd60	0x24cd60 <kernel_main_stack+1047808>
rsp            0x24cbe0	0x24cbe0 <kernel_main_stack+1047424>
r8             0x80000000	2147483648
r9             0x41a	1050
r10            0x0	0
r11            0x30	48
r12            0x693000	6893568
r13            0x695540	6903104
r14            0x694b64	6900580
r15            0x8	8
rip            0x10e890	0x10e890 <SwitchContext+270>
eflags         0x6	[ PF ]
cs             0x8	8
ss             0x10	16
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0

うーん,RAX 以外は同じ値…(そして RAX は SwitchContext でブレークするたびに 1 ずつ増えるようだ)

IntHandlerLAPICTimer の入り口にブレークポイントを仕掛けると, 1 回目の SwitchContext のあと,IntHandlerLAPICTimer の入り口でブレークした。 このときのスタックフレーム上の RIP は正常な値。(RIP == 0x10f928 == Window::DrawTo の内部)

次に IntHandlerLAPICTimer の入り口で再度ブレークした。 このときのスタックフレーム上の RIP は異常な値。(RIP == 0x3fa)

0x3fa でブレークしてバックトレースを見てみる。

(gdb) bt
#0  0x00000000000003fa in ?? ()
#1  0x00000000001216d8 in usb::HIDBaseDriver::OnInterruptCompleted (
    this=0x694b40 <usb::memory_pool+15040>, ep_id=..., buf=<optimized out>, len=8)
    at usb/classdriver/hid.cpp:57
#2  0x000000000011e1d4 in usb::Device::OnInterruptCompleted (this=0x693000 <usb::memory_pool+8064>, 
    ep_id=..., buf=0x694b64 <usb::memory_pool+15076>, len=8) at usb/device.cpp:186
#3  0x0000000000120b7b in usb::xhci::Device::OnTransferEventReceived (
    this=0x693000 <usb::memory_pool+8064>, trb=...) at usb/xhci/device.cpp:249
#4  0x000000000011edc9 in (anonymous namespace)::OnEvent (xhc=..., trb=...) at usb/xhci/xhci.cpp:218
#5  usb::xhci::ProcessEvent (xhc=...) at usb/xhci/xhci.cpp:468
#6  0x000000000011f8eb in usb::xhci::ProcessEvents () at usb/xhci/xhci.cpp:537
#7  0x000000000010b86a in KernelMainNewStack (frame_buffer_config_ref=..., memory_map_ref=..., 
    acpi_table=..., volume_image=<optimized out>) at main.cpp:189
#8  0x000000000010e77f in KernelMain ()

1216d8 付近にはこんな命令がある

  1216d5:       ff 50 20                call   QWORD PTR [rax+0x20]
  1216d8:       48 89 c1                mov    rcx,rax

1216d5 でブレークしてみたら何か分かる気がする。実際にブレークしてみた。

(gdb) x /1i $rip
=> 0x1216d5 <usb::HIDBaseDriver::OnInterruptCompleted(usb::EndpointID, void const*, int)+101>:	
    call   QWORD PTR [rax+0x20]
(gdb) p /x $rax
$22 = 0x101ff8

[RAX + 0x20] == [0x102018] == 0x3 だが,ここにカーネルの機械語は置かれてない。これがバグの原因ぽい。

念の為,エラーになる前の RIP == 0x1216d5 でブレークしたらどんな様子だろうか。

(gdb) x /1gx 0x102018
0x102018 <_ZTVN3usb4xhci6DeviceE+48>:	0x00000000001209c0

[RAX + 0x20] == [0x102018] == 0x1209c0 であり,これは usb::xhci::Device::InterruptIn の先頭アドレス。 ということで 0x102018 番地に変なアドレスが書かれてしまうのが直接のバグ原因。

0x1209c0 にウォッチポイント(変数読み書きに対するブレークポイントのようなもの)を仕掛けて,書き換えてる犯人を探す。

Hardware watchpoint 21: *(unsigned long*)0x102018

Old value = 1182144
New value = 3
0x00000000010dee04 in ?? ()
2: /x *(unsigned long*)$rsp = 0xfead10
3: $rsp = (void *) 0xfeac98
(gdb) i r
rax            0x0	0
rbx            0x2	2
rcx            0x3	3
rdx            0x1	1
rsi            0x10e0fd0	17698768
rdi            0xfedf4c	16703308
rbp            0xfeaca0	0xfeaca0
rsp            0xfeac98	0xfeac98
r8             0x4	4
r9             0x0	0
r10            0xffffff33	4294967091
r11            0x2d	45
r12            0x10e0fa0	17698720
r13            0x10ddc10	17685520
r14            0xfead30	16690480
r15            0x0	0
rip            0x10dee04	0x10dee04
eflags         0x246	[ PF ZF IF ]
cs             0x8	8
ss             0x10	16
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0
(gdb) bt
#0  0x00000000010dee04 in ?? ()
#1  0x0000000000fead10 in ?? ()
#2  0x0000000000fead60 in ?? ()
#3  0x000000000011a470 in Terminal::ExecuteFile (this=0xfead10, file_entry=..., 
    command=0x12b28b <operator new(unsigned long)+27> "I\211\306H\205\300u)E1\366f.\017\037\204", 
    first_arg=<optimized out>) at terminal.cpp:264

直接の犯人は 0x10dee04 にある mov 命令だが,そもそもここにカーネルは無い。では何があるかというと rpn コマンドの本体。 このウォッチポイントに引っかかったのは,1 回目の rpn コマンドを実行しようとしてエンターキーを押した後, rpn コマンドが完了する前の段階だった。

RIP == 0x11a470 は Terminal::ExecuteFile で rpn コマンドのエントリポイントに対する call 命令の次の命令。 この call 命令でブレークして様子を見る。

(gdb) b *0x11a46e
Breakpoint 23 at 0x11a46e: file terminal.cpp, line 264.
(gdb) c
Continuing.

Breakpoint 23, Terminal::ExecuteFile (this=0xfedf20, file_entry=..., command=0xfedf44 "rpn", 
    first_arg=<optimized out>) at terminal.cpp:264
264	  auto ret = f(argv.size(), &argv[0]);
2: /x *(unsigned long*)$rsp = 0xfedf44
3: $rsp = (void *) 0xfeacb0
(gdb) i r
rax            0x10dece0	17689824
rbx            0xfead10	16690448
rcx            0xfedf4c	16703308
rdx            0x14a7c0	1353664
rsi            0x10e0fd0	17698768
rdi            0x4	4
rbp            0xfead60	0xfead60
rsp            0xfeacb0	0xfeacb0
r8             0x14a780	1353600
r9             0x0	0
r10            0x14a780	1353600
r11            0x0	0
r12            0x10e0fa0	17698720
r13            0x10ddc10	17685520
r14            0xfead30	16690480
r15            0x0	0
rip            0x11a46e	0x11a46e <Terminal::ExecuteFile(fat::DirectoryEntry const&, char*, char*)+750>
eflags         0x202	[ IF ]
cs             0x8	8
ss             0x10	16
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0

RAX == 0x10dece0 なので,rpn のエントリポイント(main 関数)がここにロードされていることが分かる。 main 関数の ret にブレークを仕掛けて実行してみると 0x10dee04 でウォッチポイント作動。

(gdb) b *0x10dee33
Breakpoint 24 at 0x10dee33
(gdb) c
Continuing.

Hardware watchpoint 21: *(unsigned long*)0x102018

Old value = 1182144
New value = 3
0x00000000010dee04 in ?? ()
2: /x *(unsigned long*)$rsp = 0xfead10
3: $rsp = (void *) 0xfeac98

この命令は,rpn コマンドで stack に値を書き込んでいる処理。

readelf -s rpn でシンボルテーブルを確認すると long stack[100] が 0x102010 に配置されている。 つまり,rpn コマンドの実行により 0x102018 が書き換えられていることが確定。

ログ有りで現象が治まることの検証

Task::InitContext でログを出さないときの _ZTVN3usb4xhci6DeviceE の位置とサイズ。

   980: 0000000000101fe8    64 OBJECT  GLOBAL DEFAULT    1 _ZTVN3usb4xhci6DeviceE

0x101fe8 + 48 = 0x102018

Task::InitContext でログを出すようにするとこのシンボルの位置が大分ずれる。

   980: 0000000000102008    64 OBJECT  GLOBAL DEFAULT    1 _ZTVN3usb4xhci6DeviceE

ログを出すようにしたことにより,不正なメモリ書き込みの影響が隠された,ということが分かった。

0x102038 + 48 = 0x102038

rpn 2 3 + とするとスタックの先頭 2 要素に書き込まれる。 long stack[100] が 0x102010 に配置されているので,0x102010 と 0x102018 である。

ログ無しでは,これがちょうど _ZTVN3usb4xhci6DeviceE+48 と重なるアドレスとなる。 ログ有りでは 0x20 だけずれており,現象が回避できた。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment