C堆栈跟踪中缺少函数调用

Function call missing from C stack trace

本文关键字:函数调用 堆栈 跟踪      更新时间:2023-10-16

我正在代码中导入一个堆栈跟踪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上测试BasicScript()
  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就是我所需要的。但它只有在所有我的二进制文件(包括共享库)上应用时才会有效。

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

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

另一种选择是使用libunfold,我也尝试过,结果很好(在它的源代码中,你可以看到一些有用的应用内堆栈遍历技术)。

以上所有内容通常都不能很好地与优化(发布)的可执行文件配合使用,特别是如果它们不包含调试信息(尽管它可能已经生成并存储在一旁),则打印的堆栈将毫无用处(除了由于优化而跳过的帧之外)。

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

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

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

在发布生产应用程序需要诊断堆栈跟踪的某些情况下,这种方法效果非常好。

EDIT:我也尝试过的另一个选项是使用ptrace()(如果我记得很清楚的话,这也是上面提到的libunfold使用的技术之一,实际上也是GDB使用的技术)。其工作方式类似——通过fork()生成一个子进程,然后在其中调用ptrace(PTRACE_TRACEME);父进程然后可以发出各种CCD_ 15调用来检查子进程的堆栈(其恰好与父进程在CCD_。我认为libunfind源代码包含它的用途,所以您可以在那里检查它。

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

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