如何使用gcc获取带有行号信息的C++堆栈跟踪?
如何使用gcc获取带有行号信息的C++堆栈跟踪?
我们在专有的assert
宏中使用堆栈跟踪来捕捉开发人员的错误——当错误被捕捉到时,会打印出堆栈跟踪。\n我发现gcc的backtrace()
/backtrace_symbols()
方法不够用:\n
- \n
- 函数名被混淆了
- 没有行信息
\n
\n
\n第一个问题可以通过abi::__cxa_demangle解决。\n然而第二个问题更难解决。我找到了替代backtrace_symbols()的方法。\n这比gcc的backtrace_symbols()更好,因为它可以获取行号(如果使用-g编译),而且你不需要使用-rdynamic来编译。\n然而,这段代码是GNU许可的,所以我认为我不能在商业代码中使用它。\n有什么建议吗?\nP.S.\ngdb可以打印出传递给函数的参数。\n 可能要求太多了 :)\nPS 2\n类似的问题(感谢nobar)
如何使用gcc获取带有行号信息的C++堆栈跟踪?
在stackoverflow上有一个关于如何生成C++应用程序崩溃时的堆栈跟踪的讨论。讨论中提供了许多建议,包括关于如何在运行时生成堆栈跟踪的讨论。
其中一个答案是启用core dump,它允许您在崩溃时查看完整的应用程序状态(包括函数参数、行号和未解析的名称)。这种方法的另一个好处是它不仅适用于断言(asserts),还适用于段错误(segmentation faults)和未处理的异常(unhandled exceptions)。
不同的Linux shell使用不同的命令来启用core dump,但是您可以在应用程序代码中使用类似以下的代码来实现:
#include... struct rlimit core_limit = { RLIM_INFINITY, RLIM_INFINITY }; assert( setrlimit( RLIMIT_CORE, &core_limit ) == 0 ); // enable core dumps for debug builds
崩溃后,运行您喜欢的调试器来检查程序状态。
$ kdbg executable core
下面是一些示例输出:
#0 0x00007f4189be5fb5 in raise () from /lib/libc.so.6 #1 0x00007f4189be7bc3 in abort () from /lib/libc.so.6 #2 0x00007f4189bdef09 in __assert_fail () from /lib/libc.so.6 #3 0x00000000004007e8 in recursive (i=5) at ./demo1.cpp:18 #4 0x00000000004007f3 in recursive (i=4) at ./demo1.cpp:19 #5 0x00000000004007f3 in recursive (i=3) at ./demo1.cpp:19 #6 0x00000000004007f3 in recursive (i=2) at ./demo1.cpp:19 #7 0x00000000004007f3 in recursive (i=1) at ./demo1.cpp:19 #8 0x00000000004007f3 in recursive (i=0) at ./demo1.cpp:19 #9 0x0000000000400849 in main (argc=1, argv=0x7fff2483bd98) at ./demo1.cpp:26
还可以在命令行中从core dump中提取堆栈跟踪:
$ ( CMDFILE=$(mktemp); echo "bt" >${CMDFILE}; gdb 2>/dev/null --batch -x ${CMDFILE} temp.exe core )
gdb用于后期分析。如果您想要在程序不终止的情况下生成堆栈跟踪,我发布了另一个回答来解决这个要求。
如果您的二进制文件是在外部系统上构建的(来自开源代码),则您无法理解您得到的core dump。而且您无法要求某个用户运行这些命令。
如何使用gcc为C++获取带有行号信息的堆栈跟踪?
在不久前,我回答了一个类似的问题。您应该查看方法#4中提供的源代码,该方法还打印行号和文件名。
方法#4:
对方法#3进行了一点改进,以打印行号。这个方法可以复制到方法#2上工作。
基本上,它使用addr2line将地址转换为文件名和行号。
下面的源代码打印所有本地函数的行号。如果调用了来自其他库的函数,您可能会看到一些"??:0"而不是文件名。
#include#include #include #include #include void bt_sighandler(int sig, struct sigcontext ctx) { void *trace[16]; char **messages = (char **)NULL; int i, trace_size = 0; if (sig == SIGSEGV) printf("Got signal %d, faulty address is %p, " "from %p\n", sig, ctx.cr2, ctx.eip); else printf("Got signal %d\n", sig); trace_size = backtrace(trace, 16); /* overwrite sigaction with caller's address */ trace[1] = (void *)ctx.eip; messages = backtrace_symbols(trace, trace_size); /* skip first stack frame (points here) */ printf("[bt] Execution path:\n"); for (i=1; i
此代码应编译为:gcc sighandler.c -o sighandler -rdynamic
程序输出:
Got signal 11, faulty address is 0xdeadbeef, from 0x8048975
[bt] Execution path:
[bt] #1 ./sighandler(func_a+0x1d) [0x8048975]
/home/karl/workspace/stacktrace/sighandler.c:44
[bt] #2 ./sighandler(func_b+0x20) [0x804899f]
/home/karl/workspace/stacktrace/sighandler.c:54
[bt] #3 ./sighandler(main+0x6c) [0x8048a16]
/home/karl/workspace/stacktrace/sighandler.c:74
[bt] #4 /lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe6) [0x3fdbd6]
??:0
[bt] #5 ./sighandler() [0x8048781]
??:0
请记住使用-rdynamic编译您的应用程序。
关于GPL。如果GPL(不是GNU,而是GPL许可证)代码与另一个代码链接(使用ld.so或ld),则GPL要求另一个代码在GPL下可用。这一切只在将应用程序传递给其他人时才成立。您可以自己对GPL代码做任何事情并将其与其他内容链接。
如果您的应用程序使用系统目标上可用的动态链接的GPL库会发生什么情况?相同的规则?
对于许多动态库,有一个LGPL许可证,允许链接。
我接受这个答案,因为答案最接近我想要的。
别忘了给他颁发奖励(在接受的勾选框下方!!)
我尝试了您的解决方案并查看了Linux Journal上的文章,但我需要在程序中没有崩溃的情况下执行与该程序中所做的相同操作。基本上,我正在尝试实现一个自定义异常类,当捕获到异常时,它将打印出回溯和行号等信息。对于如何实现这一点,有什么想法吗?
错误:'struct sigcontext'没有名为'eip'的成员;您是否指的是'rip'?
我按照建议修复了错误,将'eip'替换为'rip'。
我认为这个答案不再有效。我在Ubuntu 18上编译了代码(应用eip->rip修复),没有一行是正确的:
Got signal 11, faulty address is 0x10202, from (nil) [bt] Execution path: [bt] #1 [(nil)] sh: 1: Syntax error: word unexpected (expecting ")") [bt] #2 ./sighandler(func_a+0x20) [0x55b0d4f96dad] ??:0 [bt] #3 ./sighandler(func_b+0x1e) [0x55b0d4f96dd5] ??:0 [bt] #4 ./sighandler(main+0x7e) [0x55b0d4f96e5e] ??:0
在GDB中运行它并识别有问题的行。
这对于ASLR不起作用。
如何使用gcc获得C++的带有行号信息的堆栈跟踪?
你想要一个打印堆栈跟踪的独立函数,具有gdb堆栈跟踪的所有功能,并且不终止应用程序。答案是自动化启动gdb以非交互模式执行您想要的任务。
通过使用fork()在子进程中执行gdb,并对其进行脚本处理以显示堆栈跟踪,同时您的应用程序等待其完成,可以执行此操作,无需使用核心转储并且不会中止应用程序。我通过查看这个问题学会了如何做到这一点:How it's better to invoke gdb from program to print it's stacktrace?
该问题中发布的示例对我来说并不完全有效,因此这是我的“修复”版本(我在Ubuntu 9.04上运行)。
#include#include #include #include #include void print_trace() { char pid_buf[30]; sprintf(pid_buf, "%d", getpid()); char name_buf[512]; name_buf[readlink("/proc/self/exe", name_buf, 511)]=0; prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0); int child_pid = fork(); if (!child_pid) { dup2(2,1); // redirect output to stderr - edit: unnecessary? execl("/usr/bin/gdb", "gdb", "--batch", "-n", "-ex", "thread", "-ex", "bt", name_buf, pid_buf, NULL); abort(); /* If gdb failed to start */ } else { waitpid(child_pid,NULL,0); } }
如引用的问题所示,gdb提供了您可以使用的其他选项。例如,使用“bt full”而不是“bt”会生成更详细的报告(输出包括局部变量)。gdb的manpages相对比较简单,但可以在这里找到完整的文档。
由于这是基于gdb的,所以输出包括解码的名称、行号、函数参数,甚至可以选择包括局部变量。此外,gdb是线程感知的,因此您应该能够提取一些线程特定的元数据。
以下是使用此方法看到的堆栈跟踪示例。
0x00007f97e1fc2925 in waitpid () from /lib/libc.so.6 [Current thread is 0 (process 15573)] #0 0x00007f97e1fc2925 in waitpid () from /lib/libc.so.6 #1 0x0000000000400bd5 in print_trace () at ./demo3b.cpp:496 2 0x0000000000400c09 in recursive (i=2) at ./demo3b.cpp:636 3 0x0000000000400c1a in recursive (i=1) at ./demo3b.cpp:646 4 0x0000000000400c1a in recursive (i=0) at ./demo3b.cpp:646 5 0x0000000000400c46 in main (argc=1, argv=0x7fffe3b2b5b8) at ./demo3b.cpp:70
注意:我发现这与使用valgrind不兼容(可能是由于Valgrind使用了虚拟机)。当您在gdb会话中运行程序时,它也无法工作(无法将“ptrace”的第二个实例应用于进程)。
+1好!如果它打印行号会更好。
它对我来说确实打印了行号。你为什么说它没有?
因为在我的系统上没有!而且我是使用-rdynamic和-g编译的。你是如何编译测试应用程序的?我使用的是GDB 7.1,你呢?
:我只是使用“-g”进行编译。我的gdb版本是“6.8-debian”。当前的gdb文档称,它将在回溯中打印行号:“回溯还显示源文件名和行号,以及函数的参数。”您的测试应用程序是否与调试器一起工作(您可以逐行单步执行源代码)?
对不起,现在我可以看到:#3 0x080489d5 in main () at stacktrace_test.cpp:29
你应该在其他问题中添加对这个答案的引用,该问题尚未得到回答。谢谢。
:太棒了!我编辑了我的答案以包括示例输出。
不要使用这个!我在我的程序中逐字使用了上面的函数,在Ubuntu 12.04上它完全崩溃了X Server。
你运行的是什么样的程序?它是低级程序吗?这种方法在我使用的Fedora 17中完全正常。
我意识到这可以重新用于提供一个交互式调试器会话,以检查未完成的进程,只需删除“--batch”。我想知道是否有一种简单的方法使用gdb使进程从中断的地方恢复,导致原始信号被重新抛出并被gdb捕获。
:不,一个普通的qt桌面应用程序。也许它是由于连接到原始应用程序并进行fork的某种输入封装(例如DBus之类的)导致的,并且然后阻塞输入。
而且情况正在变得更糟:现在不再允许对父进程进行ptrace。但是也许你可以使用prctl
设置一个标志?
:感谢您的指点。一个可能的解决办法是使用sudo
运行。
您可以在fork()
之前通过#include
prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0);
绕过它。
execl
比execlp
更安全,而且也完美工作
所有人,我已经更新了答案,使其再次正常工作(没有prctl()
就不行),根据评论。谢谢!我还删除了fprintf()
调用,因为它没有输出任何内容,而且我不确定dup2()
调用是否是必要的/有用的。
它确实有效;我不得不将gdb
更改为/usr/bin/gdb
才能使其正常工作,如我的编辑所示。那是我从阅读文档中了解到的需要做的事情,它起作用了。