katsuhiro -> katsuhiro/refmon -> katsuhiro/refmon/stack_dump
スタックバックトレースについて
for ARM†
参考
katsu/refmon/stack_image:ARM のスタックイメージと、その実例について
参考になりそうな情報†
ARM ではスタックの積み方が一定ではないため、強引な仮定をおかない限り、スタックトレースが取れない。
gdb のスタックトレースを行うコードが役に立つかもしれない。
gdb/arm-linux-tdep あたりがあやしい??
ARM のスタックフレーム†
今までの知見から大別してみる。
fpを使うスタックフレーム
- プロローグコード
- ip レジスタに sp の値をバックアップする
- fp, ip, lr, pc をスタックに積む
- fp レジスタにスタックフレームの開始位置を指す位置を入れる
- (ローカル変数領域の確保とか、他の処理)
- エピローグコード
fpを使わないスタックフレーム
- プロローグコード
- レジスタのバックアップをスタックに積む。バックアップが二回に分かれることがある。
- レジスタのバックアップは stmdb 命令で積むが、レジスタ一つだけの場合は str 命令で積む時がある。
- lr を含まない:lr にリターンアドレスがある
- lr を含む:lr がスタックに積まれ、エピローグコードにてその値が pc にロードされてリターンする
- 浮動小数点レジスタのバックアップを積む
- sp を減算して、ローカル変数領域を確保する
- エピローグコード
- プロローグコードで使用したスタックのサイズ(ローカル変数)があらかじめハードコーディングされていて、sp にそのサイズを加算することでまき戻す。
- 浮動小数点レジスタのバックアップから、データを復旧する。
- レジスタのバックアップから、データを復旧する。このとき lr の値が pc にロードされ、リターンする。
- r1, r2, r3, lr の順で積んだならば、復旧するときは r1, r2, r3, pc のように読む。
バックトレース処理の流れ†
基本的には以下の処理の繰り返しである
- 関数の先頭にある命令を読み出して関数がどのようなスタックフレームを構成するか推定する。
- デバッグ情報などで、関数の先頭位置がわかっていなければならない。
- 以前の fp と以前の sp と以前の pc(リターンアドレス) を取得して、次のスタックフレームのバックトレース処理を続ける。
プロローグコードで意味のありそうな命令
- fp を使う関数で出現する sp のバックアップ:mov ip, sp
- レジスタのバックアップ:stmdb 命令または str 命令
- 浮動小数点レジスタのバックアップ:stc 命令
- ローカル変数領域の確保:sub 命令
- 機械語について
- 0x01a0c00d mov ip, sp
- 0xe52de004 str lr, [sp, #-4]!
気になるポイント
- fp を使うか使わないかの識別が難しそう。関数先頭の mov ip, sp の有無だけで判断してよいのだろうか?
バックトレース処理の詳細†
具体的な処理内容について。
まずは fp を使うスタックフレームの場合。
- fp を使うスタックフレームのバックトレース
- fp の指す位置から、以下の情報を読み出す。
- (位置 fp - 12)fp <- fp(積んだときの値)= 以前の fp
- (位置 fp - 8)sp <- ip(積んだときの値)
- (位置 fp - 4)pc <- lr(積んだときの値)= リターンアドレス
- これだけ取得できれば次のスタックフレームにトライできるはず。
- 冒頭で pc を積む意味がわからんが、fp とリターンアドレスが取得できているので OK かな?
次に fp を使わないスタックフレームの場合。
- fp を使わないスタックフレームのバックトレース
- (前提条件)プロローグコードが終わってから、関数が終わるまで、または解析対象の関数が呼び出されるまで、sp の値は一定である。
- システムコールが発行された時点でスタック調査を始める。つまりこの時 pc は関数の真ん中あたりを指している。
- このバックトレース処理では、先頭の命令からスタックをどのくらい使うか推測するために、関数の初期化処理が終わった直後と、システムコールが呼ばれた地点での sp が異なると推測を間違ってしまう。
- 処理
- レジスタのバックアップ命令を解析し、lr(r14) より番号の若いレジスタをいくつ積んでいるか見る。積んでいる個数を r_backup_lr とする。
- 2回に分けて積む場合もあるので注意
stmdb sp!, {r1, r2, r3}
stmdb sp!, {r4, r5, r6, r7, lr}
- ストア命令を用いることもあるので注意
str lr, [sp, #-4]!
- 上記の複合でくることもあるので注意
stmdb sp!, {r1, r2, r3}
str lr, [sp, #-4]!
- fp(r11) より番号の若いレジスタをいくつ積んでいるか見る。積んでいる個数を r_backup_fp とする。
- 今後、fp を使うスタックフレームがあるかもしれないため、元の fp を取得する必要がある。
- レジスタを全部でいくつ積んでいるか見る。積んでいる個数を r_backup とする。
- 浮動小数点レジスタなどを積んでいるか見る。積んでいるサイズを r_float とする。
- ローカル変数領域をどれだけ確保しているか見る。領域のサイズを r_local とする。
- sp に上記の数値を加えた数値が指すアドレスに、レジスタのバックアップが存在する「はず」である。
- fp: sp + r_local + r_float + (r_backup_fp * 4) = 以前の fp
- lr: sp + r_local + r_float + (r_backup_lr * 4) = リターンアドレス
- sp: sp + r_local + r_float + (r_backup * 4) = 以前の sp
- r_float は、stc 命令に指定されているオフセットに依存する。
参考資料、レジスタの別名と汎用レジスタの関係
- 参考:レジスタの別名一覧
- sb: r9
- sl: r10
- fp: r11
- ip: r12
- sp: r13
- lr: r14
- pc: r15
まあ GCC なんだからソース見てみろって話ですけどね。
gdb には bt があるのでそれを参考にしても良いかもしれないですね。