【问题标题】:Function call missing from C stack traceC 堆栈跟踪中缺少函数调用
【发布时间】:2015-12-15 20:33:37
【问题描述】:

我正在我的代码中导入一个堆栈跟踪 C 代码(在 Stack Overflow 上的某处)来跟踪内存块的分配位置:

struct layout
{
  struct layout *ebp;
  void *ret;
};

struct layout *fr;
__asm__("movl %%ebp, %[fp]" :  /* output */ [fp] "=r" (fr));
for (int i=1 ; i<8 && (unsigned char*) fr > dsRAM; i++) {
  x[i] = (size_t) fr->ret;
  fr = fr->ebp;
}

一切运行良好,除了在某些调用中,代码在堆栈顶部附近缺少一些函数,例如GDB 将报告:

  1. main.cpp 中的 malloc()
  2. 来自 libstdc++.so.6 的运算符 new()
  3. BasicScript.cpp 中的 TestBasicScript()
  4. main.cpp 中的 main()

虽然代码用 malloc、new 运算符和 main() 的地址填充 x[],但缺少 TestBasicScript。

代码由 g++ 4.5.1(用于自制控制台编程的旧 devkit)编译,带有以下标志:

CFLAGS += -I libgeds/source/ -I wrappers -I $(DEVKITPRO)/include -DARM9 \
   -include wrappers/nds/system.h -include wrappers/fake.h
CFLAGS += -m32 -Duint=uint32_t -g -Wall -Weffc++ -fno-omit-frame-pointer

我尝试改用__builtin_return_address(),但我得到了几乎相同的结果,但代码更长。

编辑:我注意到我系统地缺少operator new 的调用者,如果_Znwj 的代码没有设置堆栈帧,这可以解释。所以问题列表变成了:

  • 如果 TestBasicScript() 函数调用不在堆栈帧列表中,GDB 如何设法找到它?

  • 如何配置链接步骤以便使用 libstdc++ 的调试友好变体(如果有)?

原始子问题“是否有编译时选项可以保证我可以跟踪 100% 对我的 malloc 克隆的调用?”因此由@chqrlie 回答:-O0 是我所需要的。但它只有在应用于所有我的二进制文件(包括共享库)时才会有效。

【问题讨论】:

    标签: c++ c debugging gcc stack-trace


    【解决方案1】:

    可能会省略某些帧的原因有很多,例如内联和优化(尽管提供的 CFLAGS 不包含优化标志,默认为 AFAIK 无优化)。

    无论如何,对于 GCC,通过使用 backtrace()backtrace_symbols() 以及可能与 abi::__cxa_demangle() 结合使用,内置对堆栈遍历的支持,您也可以尝试这些。

    其他选项是使用libunwind,我也尝试过,结果非常好(在其源代码中,您可以看到一些有用的应用内堆栈遍历技术)。

    以上所有内容通常不适用于优化(发布)可执行文件,特别是如果它们不包含调试信息(尽管它可能已经生成并存储在一边)打印堆栈将无用(除了跳过帧,因为优化)。

    一种甚至适用于优化代码的终极技术是生成核心转储。那里有关于堆栈的所有信息(二进制文件本身不需要包含调试信息,它可以放在一边,仅用于离线检查核心),作为堆栈上所有变量的奖励值,信息关于当前正在运行的所有线程等。 对于跟踪内存分配,它可能是一种过度杀伤(它也很慢),但有时它可能非常有用。在我的一个项目中,我创建了这种核心转储程序的工作实现,它仍然存在于生产代码中。

    请注意,您实际上可以在不终止应用程序的情况下生成应用程序的核心转储 - 我创建的实现基本上如下工作:

    • fork() 应该生成核心转储点的进程
    • 子进程调用abort()生成核心转储(forked进程的调用栈与原进程相同),即只有forked进程被abort()终止
    • 原始父进程使用waitpid() 等待子进程生成核心转储并终止(使用保护计数器不会永远等待)
    • 然后原始进程继续运行(并将已生成诊断核心以及用于生成核心的分叉进程的 PID 写入日志)

    事实证明,在发布生产应用程序需要诊断堆栈跟踪的某些情况下,这非常有效。

    编辑:我也尝试过的另一个选项是使用ptrace()(如果我没记错的话,这也是上面提到的 libunwind 使用的技术之一,实际上也是 GDB 使用的技术之一)。其工作方式类似——通过fork() 生成一个子进程,然后在其中调用ptrace(PTRACE_TRACEME);然后父进程可以发出各种ptrace() 调用来检查子进程的堆栈(恰好与fork() 处的父进程堆栈相同)。我认为 libunwind 源代码包含它的使用,因此您可以在那里检查它。

    【讨论】:

    • 一开始我以为我会使用backtrace(),但不幸的是,由于某种原因,它依赖于malloc。正如您自己所说,核心转储在这里会过大,并且很难进行后期处理。我将保留该技术以供其他用途。
    • 你也可以尝试使用ptrace(),它也是gdb使用的AFAIK(更新了帖子)
    • 谢谢,但我认为你错过了核心观点。这不是关于我可以使用什么来获取堆栈帧列表,它更多的是关于“当没有堆栈帧支持时,GDB 使用哪些附加信息允许它在调用跟踪中向我显示一个项目”。
    • 检查 libunwind 源代码,据我所知,在某些情况下重建适当的堆栈帧有一些“魔法”。正如我所提到的,GDB 似乎正在使用 ptrace() 调用,这可能会提供更多信息(您也可以在网上搜索 ptrace 源代码,以查看 ptrace 检索堆栈信息的作用)。
    【解决方案2】:

    编译器可能不会总是生成带有%ebp 指向前一帧的堆栈帧。对于某些函数,它可能会生成使用基于%esp 的寻址来检索参数的代码,对于其他函数,它可能会生成带有跳转而不是调用/调用序列的尾递归。您尝试扫描的堆栈跟踪可能不完整。

    尝试在禁用优化的情况下编译整个项目 (-O0)。

    【讨论】:

    • 谢谢,但不幸的是,-O0 没有改变任何东西。 -mno-omit-leaf-frame-pointer也没有。
    • @PypeBros:您是否查看了为跟踪中缺少的函数生成的代码?
    • 不过,我可能遗漏了一些东西。我的代码是用 -O0 编译的,但不是 libstdc++.so ...所以如果 _Znwj(operator new)不提供堆栈帧,它将中断跟踪,显示 operator new 的调用者的堆栈帧,返回地址为在操作员新...
    猜你喜欢
    • 2015-06-18
    • 2020-05-16
    • 2018-07-13
    • 2013-03-02
    • 2021-08-16
    • 1970-01-01
    • 1970-01-01
    • 2021-08-06
    • 2012-04-30
    相关资源
    最近更新 更多