20

我希望我的异常处理程序和调试函数能够打印调用堆栈回溯,基本上就像 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);
}

这基本上可以工作,但生成的跟踪并不总是完整的。例如,如果我这样做

int func3() { print_backtrace_here(); return 0; }
int func2() { return func3(); }
int func1() { return func2(); }
int main()  { return func1(); }

回溯只显示 func3() 和 main()。(这是一个玩具示例,但我检查了反汇编并确认这些功能都在这里完整,没有优化或内联。)

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

代码是用 -fomit-frame-pointer 编译的,但我的平台(裸机 ARM Cortex M3)定义了一个无论如何都不使用帧指针的 ABI。(该系统的早期版本使用 ARM7 上的旧 APCS ABI,具有强制堆栈帧和帧指针,以及类似此处的回溯,效果很好)。

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

所以,我的问题是: 在使用 GCC 的嵌入式系统中,是否有一种“标准”、可接受的方式来获得可靠的回溯?

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

谢谢!

4

6 回答 6

10

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

于 2011-08-04T19:07:26.797 回答
8

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

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

如果您正在运行纯 ELF 可执行文件,则可以将调试符号从发布的可执行文件中分离出来。然后 gdb 可以帮助您从标准 unix 核心转储中找出发生了什么

于 2010-08-03T16:57:58.073 回答
7

gcc 确实返回优化。在 func1() 和 func2() 中,它不调用 func2()/func3() - 取而代之的是,它跳转到 func2()/func3(),因此 func3() 可以立即返回到 main()。

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

查看生成的汇编代码以查看它。


编辑/更新:

为了验证这是否是原因,在函数调用之后做一些编译器无法重新排序的事情(例如使用返回值)。或者只是尝试使用 -O0 进行编译。

于 2010-08-03T16:40:25.310 回答
3

一些编译器,如 GCC 优化函数调用,就像您在示例中提到的那样。对于代码片段的操作,不需要在调用链中存储中间返回指针。从 to 返回是完全可以的func3()main()因为中间函数除了调用另一个函数之外不做任何额外的事情。

它与代码消除不同(实际上可以将中间函数完全优化掉),并且单独的编译器参数可能会控制这种优化。

如果您使用 GCC,请尝试-fno-optimize-sibling-calls

另一个方便的 GCC 选项是-mno-sched-prolog,它可以防止函数序言中的指令重新排序,如果你想逐字节解析代码,这很重要,就像在这里完成的那样:http: //www.kegel.com/stackcheck/ checkstack-pl.txt

于 2014-02-25T14:25:34.410 回答
1

这很 hacky,但考虑到所需的代码/RAM 空间量,我发现它足够好用:

假设您使用的是 ARM THUMB 模式,请使用以下选项进行编译:

-mtpcs-frame -mtpcs-leaf-frame  -fno-omit-frame-pointer

以下函数用于检索调用堆栈。有关更多信息,请参阅评论:

/*
 * This should be compiled with:
 *  -mtpcs-frame -mtpcs-leaf-frame  -fno-omit-frame-pointer
 *
 *  With these options, the Stack pointer is automatically pushed to the stack
 *  at the beginning of each function.
 *
 *  This function basically iterates through the current stack finding the following combination of values:
 *  - <Frame Address>
 *  - <Link Address>
 *
 *  This combination will occur for each function in the call stack
 */
static void backtrace(uint32_t *caller_list, const uint32_t *caller_list_end, const uint32_t *stack_pointer)
{
    uint32_t previous_frame_address = (uint32_t)stack_pointer;
    uint32_t stack_entry_counter = 0;

    // be sure to clear the caller_list buffer
    memset(caller_list, 0, caller_list_end-caller_list);

    // loop until the buffer is full
    while(caller_list < caller_list_end)
    {
        // Attempt to obtain next stack pointer
        // The link address should come immediately after
        const uint32_t possible_frame_address = *stack_pointer;
        const uint32_t possible_link_address = *(stack_pointer+1);

        // Have we searched past the allowable size of a given stack?
        if(stack_entry_counter > PLATFORM_MAX_STACK_SIZE/4)
        {
            // yes, so just quite
            break;
        }
        // Next check that the frame addresss (i.e. stack pointer for the function)
        // and Link address are within an acceptable range
        else if((possible_frame_address > previous_frame_address) &&
                ((possible_frame_address < previous_frame_address + PLATFORM_MAX_STACK_SIZE)) &&
               ((possible_link_address  & 0x01) != 0) && // in THUMB mode the address will be odd
                (possible_link_address > PLATFORM_CODE_SPACE_START_ADDRESS &&
                 possible_link_address < PLATFORM_CODE_SPACE_END_ADDRESS))
        {
            // We found two acceptable values

            // Store the link address
            *caller_list++ = possible_link_address;

            // Update the book-keeping registers for the next search
            previous_frame_address = possible_frame_address;
            stack_pointer = (uint32_t*)(possible_frame_address + 4);
            stack_entry_counter = 0;
        }
        else
        {
            // Keep iterating through the stack until be find an acceptable combination
            ++stack_pointer;
            ++stack_entry_counter;
        }
    }

}

您需要为您的平台更新#defines。

然后调用以下命令以使用当前调用堆栈填充缓冲区:

uint32_t callers[8];
uint32_t sp_reg;
__ASM volatile ("mov %0, sp" : "=r" (sp_reg) );
backtrace(callers, &callers[8], (uint32_t*)sp_reg);

再一次,这是相当hacky,但我发现它工作得很好。缓冲区将填充调用堆栈中每个函数调用的链接地址。

于 2017-04-05T22:39:13.817 回答
0

您的可执行文件是否包含使用该-g选项编译的调试信息?我认为这是在没有帧指针的情况下获得完整的堆栈跟踪所必需的。

您可能需要-gdwarf-2确保它使用包含展开信息的格式。

于 2010-08-04T11:54:42.160 回答