Armにおけるスタックフレームベースのバックトレース
ベアメタルの開発環境でもエラー発生時にバックトレースを表示したいと思ったことありませんか。特にCPU例外発生時、バックトレースがあるのとないのとでは開発効率が段違いです。しかしながらCortex-Mでは、ターゲットアーキテクチャやツールチェーンによって、できたりできなかったりします。
本稿ではランタイムのバックトレース表示ができる環境とできない環境について述べ、限定的な状況での実現方法を説明します。
まとめ
- いずれの環境でも
-fno-omit-frame-pointer
が必要 - A64(AArch64) →
X29
がフレームレジスタとして定義されており、x29
,lr
をたどってunwindingできる
(参考: https://github.com/torvalds/linux/blob/v5.18/arch/arm64/kernel/stacktrace.c) - A32(Arm) →
r11
がフレームレジスタとして定義されており、r11
,lr
をたどってunwindingできる
(Thumb interworkを使用する場合、Thumbはフレームレジスタが違うのでうまく動かないことがある) - T32(Thumb-2) →
r7
がフレームレジスタとして定義されており、Clangはr7
,lr
をたどってunwindingできるが、GCCはできない - Thumb(Armv6まで) →
r7
がフレームレジスタとして定義されており、r7
,lr
をたどってunwindingできる
ISA | コンパイラ | フレームレジスタ | スタックフレームベースのunwinding可能か |
---|---|---|---|
A64 (AArch64) | GCC | x29 | yes |
A64 (AArch64) | Clang | x29 | yes |
A32 (Arm) | GCC | r11 | yes |
A32 (Arm) | Clang | r11 | yes |
T32 (Thumb-2) interworkなし | GCC | r7 | no |
T32 (Thumb-2) interworkなし | Clang | r7 | yes |
A32, T32 (interworkあり) | GCC | – | no |
A32, T32 (interworkあり) | Clang | – | no |
Thumb | GCC | r7 | yes |
Thumb | Clang | r7 | yes |
コンパイラ | アーキテクチャ | スタックフレームベースのunwinding可能か |
---|---|---|
arm-none-eabi-gcc | -march=armv6-m | yes |
arm-none-eabi-gcc | -march=armv7-m | no |
arm-none-eabi-gcc | -march=armv8-m.base | yes |
arm-none-eabi-gcc | -march=armv8-m.main | no |
clang | -march=armv6-m | yes |
clang | -march=armv7-m | yes ※Thumb interworkしない場合 |
clang | -march=armv8-m.base | yes |
clang | -march=armv8-m.main | yes ※Thumb interworkしない場合 |
課題
Armの32ビットアーキテクチャでは、アーキテクチャ仕様としてはスタックフレームベースのunwindingをサポートしておらず(AAPCSで定義はあるがmandatoryではない)、処理系によってできたりできなかったりします。具体的にはClangはThumb interworkを使わなければ実現可能で、GCCはT32(Thumb-2)のときほぼ動きません。
現実的なシチュエーションとして、Thumb-2サポートのあるArmv7以降のCortex-Mのベアメタル環境でランタイムにバックトレース表示を実現したいというとき、GCCではほぼできません。動く条件は-march=armv6m
または-march=armv8m.base
またはコンパイラ==Clangです。
Thumb interworkにおける本質的な困難
A32のフレームレジスタはr11
でT32(Thumb-2)のフレームレジスタはr7
です。Thumb interworkを使用する際、呼び出し元がA32ステートなのかT32ステートなのかわかりません(lr & 1
でわかりますが、prologueでフレームレジスタを保存する際にチェックして切り替える機能はコンパイラにありません)。
したがってThumb interworkを使用する場合はコンパイラによらずスタックフレームベースのバックトレース表示はできません。
GCCが生成するスタックフレーム
以下のコードはTF-A v2.2のvprintf
実装をarm-none-eabi-gcc -fno-omit-frame-pointer -march=armv7-m
でコンパイルした結果です。
Thumb-2のフレームレジスタはr7
ですが、-march=armv7-m
や-march=armv8-m.main
でコンパイルすると以下のようなprologueが生成されます。スタックのレイアウトが不定なので、スタックフレームベースのunwindingができません。
vprintf():
38000458: e92d 4fb0 stmdb sp!, {r4, r5, r7, r8, r9, sl, fp, lr}
3800045c: b094 sub sp, #80 ; 0x50
3800045e: af02 add r7, sp, #8
38000460: 6178 str r0, [r7, #20]
38000462: 6139 str r1, [r7, #16]
-march=armv6-m
(Thumbのみ)や-march=armv8-m.base
(stmdb
なし)とした場合、以下のようなコードが生成され、スタックフレームの先頭がlr
, r7
となるのでスタックフレームベースのunwindingができます(-fno-omit-frame-pointer
が必要)。
vprintf():
38000482: b5b0 push {r4, r5, r7, lr}
38000484: b094 sub sp, #80 ; 0x50
38000486: af02 add r7, sp, #8
38000488: 6178 str r0, [r7, #20]
3800048a: 6139 str r1, [r7, #16]
Clangが生成するスタックフレーム
Clangは-march=armv7-m
や-march=armv8-m.main
であっても以下のようなコードが生成され、必ずスタックフレームの先頭がlr
, r7
となるのでスタックフレームベースのunwindingができます(-fno-omit-frame-pointer
が必要)。
vprintf():
380003b8: b580 push {r7, lr}
380003ba: 466f mov r7, sp
2016年に以下のコミットで対応されました。
GCCの対応パッチ (提案してリジェクトされている)
GCCでもClangと同様の実装にする提案が行われましたが、却下されました。
ベアメタルでスタックフレームベースのバックトレースを実現する方法
Armv8-MではCPU例外が発生した際、例外ハンドラにきたときスタックは以下のレイアウトになっています。
| |
+----------------+ <- 例外ハンドラ先頭でのSP
| R0 |
| R1 |
| R2 |
| R3 |
| R12 |
| R14 (LR) |
| Return address |
| xPSR |
+----------------+ <- 例外発生前のSP
| |
例外発生時のPCがスタック上のReturn address (sp + 24
)で、フレームポインタはr7
に入っています。
例外発生時にバックトレースをアドレスで表示するコードを以下に示します。ツールチェーンの実装に依存しておりポータブルではありませんが、これまでに説明した条件を満たしていれば概ね動作します。-g
つきでコンパイルした.elf
があればaddr2line -fe image.elf 38000796 380007af 380007ef 3800002d
などとして行番号まで簡単に特定可能です。デバッグ用としては十分に実用的だと思います。
__attribute__((naked))
void __default_exception_handler(void)
{
asm("mov r0, r7");
asm("mov r1, sp");
asm("add r1, #24");
asm("ldr r1, [r1]");
asm("b print_backtrace_fp");
}
void print_backtrace_fp(unsigned long fp, unsigned long lr)
{
while (1) {
printf("%08lx\n", lr);
if (fp < (unsigned long)__stack_top || fp >= (unsigned long)__stack_bottom) {
break;
}
lr = *(unsigned long *)(fp + sizeof(unsigned long));
fp = *(unsigned long *)(fp);
}
while (1)
;
}