如何获得调用堆栈回溯?(深入嵌入,没有库支持)

hug*_*gov 20 c c++ gcc arm newlib

我希望我的异常处理程序和调试函数能够打印调用堆栈回溯,基本上就像glibc中的backtrace()库函数一样.不幸的是,我的C库(Newlib)没有提供这样的调用.

我有这样的事情:

#include <unwind.h> // GCC's internal unwinder, part of libgcc
_Unwind_Reason_Code trace_fcn(_Unwind_Context *ctx, void *d)
{
    int *depth = (int*)d;
    printf("\t#%d: program counter at %08x\n", *depth, _Unwind_GetIP(ctx));
    (*depth)++;
    return _URC_NO_REASON;
}

void print_backtrace_here()
{
    int depth = 0;
    _Unwind_Backtrace(&trace_fcn, &depth);
}
Run Code Online (Sandbox Code Playgroud)

这基本上有效,但结果痕迹并不总是完整的.例如,如果我这样做

int func3() { print_backtrace_here(); return 0; }
int func2() { return func3(); }
int func1() { return func2(); }
int main()  { return func1(); }
Run Code Online (Sandbox Code Playgroud)

backtrace只显示func3()和main().(这是一个玩具示例,但我已经检查了反汇编并确认这些功能全部都在这里,并没有优化或内联.)

更新:我在旧的ARM7系统上尝试了这个回溯代码但是使用相同(或至少,尽可能等同)的编译器选项和链接描述文件,它打印出正确的完整回溯(即func1和func2不会丢失)和实际上它甚至会将过去的主要内容转移到启动初始化代码中.所以可能问题不在于链接器脚本或编译器选项.(另外,通过反汇编确认在此ARM7测试中也没有使用帧指针).

代码使用-fomit-frame-pointer编译,但我的平台(裸机ARM Cortex M3)定义了一个不使用帧指针的ABI.(该系统的先前版本在ARM7上使用旧的APCS ABI,具有强制堆栈帧和帧指针,以及类似于此处的回溯,它完美地工作).

整个系统使用-fexception进行编译,这确保了_Unwind使用的必要元数据包含在ELF文件中.(_Unwind是为我认为的异常处理而设计的).

所以,我的问题是: 在使用GCC的嵌入式系统中,是否存在"标准",可接受的可靠回溯方式?

我不介意在必要时乱用链接器脚本和crt0代码,但不想让工具链本身有任何机会.

谢谢!

Kin*_*umo 10

为此您需要-funwind-tables-fasynchronous-unwind-tables 在某些目标中,这是_Unwind_Backtrace正常工作所必需的!

  • 我不知道这个选项有什么作用,但您可能还需要在链接时指定 --no-merge-exidx-entries。http://old.nabble.com/Stack-backtrace-for-ARM-Thumb-td29264138.html (2认同)

Ian*_*anH 7

gcc会返回优化.在func1()和func2()中它不调用func2()/ func3() - 而不是这个,它跳转到func2()/ func3(),因此func3()可以立即返回main().

在你的情况下,func1()和func2()不需要设置堆栈帧,但如果他们会这样做(例如对于局部变量),如果函数调用是最后一条指令,gcc仍然可以进行优化 - 然后它会清理在跳转到func3()之前向上堆栈.

看一下生成的汇编程序代码来查看它.


编辑/更新:

要验证这是原因,请在函数调用之后执行某些操作,编译器无法对其进行重新排序(例如,使用返回值).或者只是尝试使用-O0进行编译.

  • 为了澄清:原始帖子中的简化玩具代码可以进行返回调用/跳转优化,但在实际代码中,调用的两侧都有一些东西(我已经验证它们不是)被优化了远.每个函数的开始和结束都有一个push/pop,并且使用blx指令(Thumb2)调用链中的下一个函数. (2认同)

dor*_*ron 7

由于ARM平台不使用帧指针,因此您永远不知道堆栈帧有多大,并且不能简单地将堆栈扩展到R14中的单个返回值之外.

在调查我们没有调试符号的崩溃时,我们只需转储整个堆栈并查找指令范围内每个项目的最近符号.它确实会产生大量误报,但对于调查崩溃仍然非常有用.

如果您正在运行纯ELF可执行文件,则可以将调试符号与发布可执行文件分开.然后,gdb可以帮助您了解标准unix核心转储的内容