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)GCCx29yes
A64 (AArch64)Clangx29yes
A32 (Arm)GCCr11yes
A32 (Arm)Clangr11yes
T32 (Thumb-2) interworkなしGCCr7no
T32 (Thumb-2) interworkなしClangr7yes
A32, T32 (interworkあり)GCCno
A32, T32 (interworkあり)Clangno
ThumbGCCr7yes
ThumbClangr7yes
コンパイラアーキテクチャスタックフレームベースのunwinding可能か
arm-none-eabi-gcc-march=armv6-myes
arm-none-eabi-gcc-march=armv7-mno
arm-none-eabi-gcc-march=armv8-m.baseyes
arm-none-eabi-gcc-march=armv8-m.mainno
clang-march=armv6-myes
clang-march=armv7-myes ※Thumb interworkしない場合
clang-march=armv8-m.baseyes
clang-march=armv8-m.mainyes ※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年に以下のコミットで対応されました。

https://github.com/llvm/llvm-project/commit/f8b0a7af52f8c4ec6b4ddcfe3a6fa75098c9507c#diff-8d2c99adf5ecc940883de7fda1462a4f6938b5ac289861ec72c4c7b6ff682c0b

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)
        ;
}