我需要在c++ Linux应用程序崩溃时转储堆栈跟踪。我用backtrace()
和backtrace_symbols()
成功地做到了这一点。现在,我还想获得崩溃的行号。它是怎么做到的?
http://www.linuxjournal.com/files/linuxjournal.com/linuxjournal/articles/063/6391/6391l2.html和http://www.linuxjournal.com/article/6391?page=0,0提供示例代码,展示如何实现这一点。
基本上,它是关于在信号处理程序中放置堆栈回溯,并让后者捕获程序可以接收到的所有"坏"信号(SIGSEGV, SIGBUS, SIGILL, SIGFPE等)。这样,如果您的程序不幸崩溃,并且您没有使用调试器运行它,您就可以获得堆栈跟踪并知道错误发生的位置。该技术还可以用于了解程序在哪里循环,以防它停止响应…
下面的代码为跟踪中的每个地址运行外部程序addr2line,将其转换为文件名和行号。
下面的源代码打印所有局部函数的行号。如果调用另一个库中的函数,您可能会看到一对?? ?:0代替文件名。
#include <stdio.h>
#include <signal.h>
#include <execinfo.h>
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 %pn", sig, ctx.cr2, ctx.eip);
else
printf("Got signal %dn", 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<trace_size; ++i)
{
printf("[bt] #%d %sn", i, messages[i]);
char syscom[256];
sprintf(syscom,"addr2line %p -e sighandler", trace[i]); //last parameter is the name of this app
system(syscom);
}
exit(0);
}
int func_a(int a, char b) {
char *p = (char *)0xdeadbeef;
a = a + b;
*p = 10; /* CRASH here!! */
return 2*a;
}
int func_b() {
int res, a = 5;
res = 5 + func_a(a, 't');
return res;
}
int main() {
/* Install our signal handler */
struct sigaction sa;
sa.sa_handler = (void *)bt_sighandler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sigaction(SIGSEGV, &sa, NULL);
sigaction(SIGUSR1, &sa, NULL);
/* ... add any other signal here */
/* Do something */
printf("%dn", func_b());
}
此代码应编译为: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
只有当程序已经编译了调试信息(即gcc -Wall -g
或g++ -Wall -g
)时才有可能。没有-g
,可执行文件不包含任何源行信息。如果使用gcc
,您可以同时编译优化&调试信息(例如g++ -Wall -g -O2
),但有时行位置会"令人惊讶"。
-Wall
标志要求GCC显示所有警告。它非常有用(因此我建议使用它),但与-g
或调试信息无关。
关于如何提取行号,最简单的方法是fork一个gdb
进程。或者,您可以获取调试信息(DWARF格式)并解析它,也许可以使用ELF工具链中的libdwarf
。我不确定是否值得……
要获得反向跟踪,您可以简单地通过gdb
运行您的程序,也许作为gdb --args yourprogram itsarguments
…
附录
您也可以使用最近的GCC中的libbacktrace(实际上是Ian Taylor的libbacktrace),它旨在解决您的问题(它"解释"当前可执行文件的DWARF格式,您将使用g++ -O -g
进行编译)。
正如Saqlain指出的,addr2line可以用来获取行号。
如果首选库,请查看LPT工具包。关于如何安装的说明在这里。LPT依赖于bfd库。
system()
需要#include <stdlib.h>
似乎少了几样东西。
$ g++-8 -g -o dump dump.cpp
dump.cpp: In function ‘void bt_sighandler(int, sigcontext)’:
dump.cpp:15:43: error: ‘struct sigcontext’ has no member named
‘eip’; did > you mean ‘rip’?
"from %pn", sig, ctx.cr2, ctx.eip);
^~~
rip
dump.cpp:21:26: error: ‘struct sigcontext’ has no member
named ‘eip’;
did you mean ‘rip’?
trace[1] = (void *)ctx.eip;
^~~
rip
dump.cpp: In function ‘int main()’:
dump.cpp:64:19: error: invalid conversion from ‘void*’ to
‘__sighandler_t’ > {aka ‘void (*)(int)’} [-fpermissive]
sa.sa_handler = (void *)bt_sighandler;
^~~~~~~~~~~~~~~~~~~~~
好的,所以当我被这张漂亮的便条所吸引时,我有一个Xavier (NVIDIA Jetpack 4.4 Ubuntu 18.04.5)在我面前打开。我很懊恼地发现这些代码不能在Xavier上工作,Xavier基本上是一个aarch64架构,其中一些信号结构是不同的。
所以我设法拼凑了一个在X86_64和aarch64上都能工作的反例。
#include <stdio.h>
#include <signal.h>
#include <execinfo.h>
#include <stdlib.h>
#include <ucontext.h>
#include <string.h>
#ifdef __aarch64__
#define _PC pc
#define _SP sp
#elif defined(__x86_64__)
#define _PC gregs[REG_RSP]
#define _SP gregs[REG_RIP]
#else
#error architecture not supported
#endif
static void addr2line(void *traceP, void *messageP) {
char syscom[256];
#ifdef __aarch64__
char message[4096];
strcpy(message, (char *) messageP);
char *saveP = NULL, *token = strtok_r(message, "(", &saveP);
if(token) {
token = strtok_r(NULL, "+", &saveP);
if(token) {
char *term = strchr(token,')');
if(term)
*term = 0;
}
}
if(token==NULL) {
token = message;
}
sprintf(syscom,"addr2line %s -e sighandler", token);
#elif defined(__x86_64__)
sprintf(syscom,"addr2line %p -e sighandler", traceP); //last parameter is the name of this app
#endif
system(syscom);
}
void bt_sighandler(int sig, siginfo_t *psi, void *ctxarg) {
void *trace[16];
char **messages = (char **)NULL;
int i, trace_size = 0;
mcontext_t *ctxP = &((ucontext_t *) ctxarg)->uc_mcontext;
if (sig == SIGSEGV)
printf("Got signal %d, faulty address is %p, "
"from %pn", sig, (void *) ctxP->_PC, (void *) ctxP->_SP);
else
printf("Got signal %dn", sig);
trace_size = backtrace(trace, 16);
/* overwrite sigaction with caller's address */
trace[1] = (void *)ctxP->_SP;
messages = backtrace_symbols(trace, trace_size);
/* skip first stack frame (points here) */
printf("[bt] Execution path:n");
for (i=1; i<trace_size; ++i)
{
printf("[bt] #%d %sn", i, messages[i]);
addr2line(trace[i], messages[i]);
}
exit(0);
}
int func_a(int a, char b) {
char *p = (char *)0xdeadbeef;
a = a + b;
*p = 10; /* CRASH here!! */
return 2*a;
}
int func_b() {
int res, a = 5;
res = 5 + func_a(a, 't');
return res;
}
int main() {
/* Install our signal handler */
struct sigaction sa;
sa.sa_sigaction = bt_sighandler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART|SA_SIGINFO;
sigaction(SIGSEGV, &sa, NULL);
sigaction(SIGUSR1, &sa, NULL);
/* ... add any other signal here */
/* Do something */
printf("%dn", func_b());
}
基本上:
- 信号处理程序包括SA_INFO,其结构不同,但看起来在两种体系结构中都支持。注意,信号处理程序必须填充sa_sigaction而不是sa_handler。
- 信号处理程序的第三个参数"void *ctxarg"被强制转换为ucontext_t,其中包括mcontext_t,其中包括PC和SP字段。它们在x86_64和aarch64体系结构中的结构不同,因此代码必须是特定于体系结构的。
- add2line不能处理trace[i]中提供的地址。我还没有找到为什么-这些地址前缀为0x55 -如果你剥离这个偏移量,他们的工作,但它来自哪里是目前一个谜对我来说。然而,消息[i]包含一个可以在其位置上使用的偏移量,只需要一些字符串操作来获取它。
- 在回溯中有一个额外的条目(没有去寻找解释为什么)
代码现在运行在x86_64上,并给出了类似的,可用的结果。
xavier $ ./sighandler 3
Got signal 11, faulty address is 0x5563baadf4, from 0x7fd4439350
[bt] Execution path:
[bt] #1 [0x7fd4439350]
??:0
[bt] #2 ./sighandler(+0xdf4) [0x5563baadf4]
/home/jsaari/project/radar_ars/alfalfa/cuda/debug/sighandler.cpp:79
[bt] #3 ./sighandler(+0xe24) [0x5563baae24]
/home/jsaari/project/radar_ars/alfalfa/cuda/debug/sighandler.cpp:89
[bt] #4 ./sighandler(+0xea4) [0x5563baaea4]
/home/jsaari/project/radar_ars/alfalfa/cuda/debug/sighandler.cpp:109
[bt] #5 /lib/aarch64-linux-gnu/libc.so.6(__libc_start_main+0xe0) [0x7f9ae316e0]
??:0
[bt] #6 ./sighandler(+0xa94) [0x5563baaa94]
:?
x86_64 $ ./sighandler 3
Got signal 11, faulty address is 0x7ffe291d27d0, from 0x40095a
[bt] Execution path:
[bt] #1 ./sighandler() [0x40095a]
/home/jsaari/common/experiment/backtrace/sighandler.cpp:79
[bt] #2 ./sighandler() [0x40095a]
/home/jsaari/common/experiment/backtrace/sighandler.cpp:79
[bt] #3 ./sighandler() [0x400982]
/home/jsaari/common/experiment/backtrace/sighandler.cpp:89
[bt] #4 ./sighandler() [0x4009f4]
/home/jsaari/common/experiment/backtrace/sighandler.cpp:109
[bt] #5 /lib64/libc.so.6(__libc_start_main+0xf5) [0x7f294a36d495]
??:0
[bt] #6 ./sighandler() [0x4006d9]
??:?
IMHO -现在NVIDIA已经收购了ARM, ARM将接管物联网(如果它还没有),英特尔很可能开始挣扎。
很明显,这里有一个地址混乱的记录:
https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=860394显然,这是由于gcc只构建与位置无关的代码。如果你使用"-no-pie"构建应用程序,那么trace[i]条目将与addr2line一起工作。
除了我编写的过程之外,我还没有找到如何管理这个。
从这个帖子:
如何找到一个PIE二进制的负载重定位?
重定位值可从
中获得#include <link.h>
. . .
uintptr_t relocation = _r_dump.r_map->l_addr;
可以从trace[i]中减去重定位值,以获得addr2line可以使用的地址(我正在使用的xavier aarch64 box的结果)。
对于非重定位二进制,重定位的值为"0"(我使用的x86_64盒子的结果)。