如何在C语言中获取堆栈跟踪?

92

我知道没有标准的C函数可以完成这个任务。我想知道在Windows和*nix上实现这个功能的技术是什么?(现在我的最重要的操作系统是Windows XP。)


我已经在以下网址详细测试了几种方法:https://dev59.com/Wm865IYBdhLWcg3wR8ah#54365144 - Ciro Santilli OurBigBook.com
9个回答

86

10
glibc 再次胜出。这是我认为在 C 编程方面 glibc 是绝对黄金标准的又一个原因(还有与之配套的编译器)。 - Trevor Boyd Smith
7
等等,还有更多!backtrace() 函数只提供了一个 void* 指针类型的数组,表示调用栈中的函数。"这不是很有用。arg." 不要担心!glibc 提供了一个函数,将所有 void* 地址(调用栈函数地址)转换为可读的字符串符号。char ** backtrace_symbols (void *const *buffer, int size) - Trevor Boyd Smith
1
注意:我认为只适用于C函数。@Trevor: 这只是在ELF表中按地址查找符号。 - Conrad Meyer
2
还有一个void backtrace_symbols_fd(void *const *buffer, int size, int fd)函数,可以将输出直接发送到stdout/err等。 - wkz
3
backtrace_symbols() 很烂。它需要导出所有符号,而且不支持 DWARF(调试)符号。在许多(大多数)情况下,libbacktrace 是一个更好的选择。 - Erwan Legrand
显示剩余2条评论

38

backtrace()backtrace_symbols()

来自手册页:

#include <execinfo.h>
#include <stdio.h>
...
void* callstack[128];
int i, frames = backtrace(callstack, 128);
char** strs = backtrace_symbols(callstack, frames);
for (i = 0; i < frames; ++i) {
    printf("%s\n", strs[i]);
}
free(strs);
...

使用更方便/OOP的方法之一是将backtrace_symbols()的结果保存在异常类构造函数中。这样,每当您抛出该类型的异常时,便会有堆栈跟踪。然后,只需提供一个打印输出的函数即可。例如:

class MyException : public std::exception {

    char ** strs;
    MyException( const std::string & message ) {
         int i, frames = backtrace(callstack, 128);
         strs = backtrace_symbols(callstack, frames);
    }

    void printStackTrace() {
        for (i = 0; i < frames; ++i) {
            printf("%s\n", strs[i]);
        }
        free(strs);
    }
};

...

try {
   throw MyException("Oops!");
} catch ( MyException e ) {
    e.printStackTrace();
}

完成了!

注意:启用优化标志可能会使得生成的堆栈跟踪不准确。理想情况下,应该在调试标志开启、优化标志关闭的情况下使用此功能。


@shuckc 只需要将地址转换为符号字符串,如果需要的话可以使用其他工具在外部完成此操作。 - Woodrow Barlow

22

对于Windows,可以使用StackWalk64() API(在32位Windows上也可用)。对于UNIX,您应该使用操作系统的本机方法进行堆栈跟踪,或者如果可用,则回退到glibc的backtrace()。

但需要注意的是,在本地代码中获取堆栈跟踪通常不是一个好主意 - 不是因为不可能,而是因为你通常试图实现错误的目标。

大多数情况下,人们尝试在异常情况下获取堆栈跟踪,比如捕获异常、断言失败或 - 最糟糕和最错误的是 - 当程序出现致命“异常”或信号时,比如分段违规。

考虑到最后一个问题,大多数API将要求您显式分配内存,或者可能会在内部执行此操作。在您的程序可能处于脆弱状态的情况下这样做实际上可能会使情况变得更糟。例如,崩溃报告(或核心转储)将不反映问题的实际原因,而是您处理它失败的尝试。

我假设您正在尝试实现致命错误处理,因为大多数人似乎在获取堆栈跟踪时都会尝试这样做。如果是这样,我建议您在开发过程中依赖于调试器,并让进程在生产环境中生成核心转储文件(或在Windows上生成迷你转储文件)。结合适当的符号管理,您应该没有任何麻烦找出导致问题的指令。


2
你关于在信号或异常处理程序中尝试内存分配容易出现问题是正确的。一种可能的解决方法是在程序启动时分配固定数量的“紧急”空间,或使用静态缓冲区。 - j_random_hacker
另一种方法是创建一个核心转储服务,该服务可以独立运行。 - Kobor42

6
您应该使用unwind库
unw_cursor_t cursor; unw_context_t uc;
unw_word_t ip, sp;
unw_getcontext(&uc);
unw_init_local(&cursor, &uc);
unsigned long a[100];
int ctr = 0;

while (unw_step(&cursor) > 0) {
  unw_get_reg(&cursor, UNW_REG_IP, &ip);
  unw_get_reg(&cursor, UNW_REG_SP, &sp);
  if (ctr >= 10) break;
  a[ctr++] = ip;
}

除非您从共享库中进行调用,否则您的方法也可以正常工作。

您可以在Linux上使用addr2line命令来获取相应PC的源函数/行号。


"源函数/行号"? 如果链接是为了减小代码大小而优化的,怎么办?我要说,这看起来像一个有用的项目。可惜没有办法获取寄存器。我一定会研究这个的。你知道它绝对是处理器无关的吗?它可以在任何有C编译器的东西上运行? - Mawg says reinstate Monica
1
好的,这个评论很值得,仅仅因为它提到了有用的addr2line命令! - Ogre Psalm33
addr2line在启用ASLR的系统上(即过去十年中大多数人使用的系统)无法处理可重定位代码。 - Erwan Legrand

5

对于 Windows 系统,CaptureStackBackTrace() 是另一个选择,相比 StackWalk64(),它需要用户端准备的代码更少。在我遇到的类似情况中,CaptureStackBackTrace() 的表现更可靠,比 StackWalk64() 更好。


4

没有跨平台的方法来做到这一点。

最接近的方法是在不进行优化的情况下运行代码。这样,您可以附加到进程(使用Visual C++调试器或GDB),并获得可用的堆栈跟踪。


1
当嵌入式计算机在现场发生崩溃时,这并不能帮助我。 :( - Kevin
@Kevin:即使在嵌入式设备上,通常也有一种方法可以获取远程调试器存根或至少核心转储。不过,一旦部署到现场,可能就没有了... - ephemient
如果您在您选择的平台上使用gcc-glibc运行,则backtrace()和backtrace_symbols()将在三个平台上均可工作,包括Windows / Linux / Mac。鉴于这种情况,我会使用“没有[可移植]的方法来做到这一点”的话。 - Trevor Boyd Smith

2
Solaris有pstack命令,该命令也被复制到Linux中。

1
有用,但不是真正的C语言(它是一个外部工具)。 - ephemient
1
此外,从描述(限制部分)中可以看出:“pstack目前仅适用于Linux系统,仅适用于运行32位ELF二进制文件的x86机器(不支持64位)。 - Ciro Costa

0

你可以通过反向遍历堆栈来实现。但实际上,更容易的方法是在每个函数的开头添加一个标识符,并在结尾处弹出它,然后只需遍历该标识符并打印其内容。这可能有点麻烦,但它能很好地工作,并最终节省你的时间。


2
你能更详细地解释一下“反向遍历堆栈”吗? - Spidey
@Spidey 有时在嵌入式系统中,这就是你所拥有的 - 我想这被投票否决是因为该平台是WinXP。但是如果没有支持堆栈遍历的libc,基本上你必须要"遍历"堆栈。你需要从当前的基址指针开始(在x86上,这是RBP寄存器的内容)。这将带您到堆栈上具有以下1.保存的先前的RBP(这是如何继续堆栈遍历的),和2.调用/分支返回地址(调用函数的保存RIP寄存器),告诉您函数是什么。然后,如果您可以访问符号表,就可以查找函数地址。 - Ted Middleton

0
在过去的几年中,我一直在使用Ian Lance Taylor的libbacktrace。它比GNU C库中需要导出所有符号的函数更加清晰。它提供了比libunwind更多的用于生成回溯的实用程序。最后但并非最不重要的是,它不会像需要外部工具(如addr2line)的方法那样被ASLR击败。
Libbacktrace最初是GCC发行版的一部分,但现在作者以BSD许可证的形式将其作为独立库提供。

https://github.com/ianlancetaylor/libbacktrace

在撰写本文时,除非我需要在不受libbacktrace支持的平台上生成回溯,否则我不会使用其他任何东西。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接