当我的程序崩溃时,如何自动生成堆栈跟踪

我使用GCC编译器在Linux上工作。当我的c++程序崩溃时,我希望它自动生成一个堆栈跟踪。

我的程序由许多不同的用户运行,它也可以在Linux、Windows和Macintosh上运行(所有版本都使用gcc编译)。

我希望我的程序能够在崩溃时生成堆栈跟踪,下次用户运行它时,它会问他们是否可以将堆栈跟踪发送给我,这样我就可以跟踪到问题。我可以处理发送信息给我,但我不知道如何生成跟踪字符串。什么好主意吗?

461255 次浏览
ulimit -c unlimited

是一个系统变量,它将允许在应用程序崩溃后创建一个核心转储。在这种情况下是无限的。在同一目录中查找一个名为core的文件。确保在编译代码时启用了调试信息!

问候

一些版本的libc包含处理堆栈跟踪的函数;你可能会用到它们:

http://www.gnu.org/software/libc/manual/html_node/Backtraces.html

我记得很久以前使用libunwind来获取堆栈跟踪,但您的平台可能不支持它。

您没有指定您的操作系统,因此这个问题很难回答。如果你使用的是基于gnu libc的系统,你可以使用libc函数backtrace()

GCC还有两个内置组件可以帮助您,但它们可能在体系结构上完全实现,也可能不完全实现,它们是__builtin_frame_address__builtin_return_address。两者都需要一个即时整数级别(这里的即时是指它不能是一个变量)。如果给定级别的__builtin_frame_address非零,那么获取相同级别的返回地址应该是安全的。

看:

男人3回溯

和:

#include <exeinfo.h>
int backtrace(void **buffer, int size);

这些是GNU扩展。

需要注意的是,一旦生成了核心文件,就需要使用gdb工具来查看它。为了让gdb理解你的核心文件,你必须告诉gcc用调试符号来检测二进制文件:要做到这一点,你需要使用-g标志进行编译:

$ g++ -g prog.cpp -o prog

然后,您可以设置“ulimit -c unlimited”来转储一个核心,或者只是在gdb中运行您的程序。我更喜欢第二种方法:

$ gdb ./prog
... gdb startup output ...
(gdb) run
... program runs and crashes ...
(gdb) where
... gdb outputs your stack trace ...

我希望这能有所帮助。

我将使用为视觉检漏仪中的泄漏内存生成堆栈跟踪的代码。不过,这只适用于Win32。

我可以帮助Linux版本:函数backtrace, backtrace_symbols和backtrace_symbols_fd可以使用。请参见相应的手册。

< p > * nix: 你可以拦截SIGSEGV信号(通常在崩溃前会发出这个信号),并将信息保存到一个文件中。(除了你可以使用GDB进行调试的核心文件之外) < p >赢得: 检查msdn中的

你也可以看看谷歌的chrome代码,看看它是如何处理崩溃的。它有一个很好的异常处理机制。

在Linux/unix/MacOSX上使用核心文件(您可以使用ulimit或兼容的系统调用启用它们)。在Windows上使用Microsoft错误报告(您可以成为合作伙伴并访问您的应用程序崩溃数据)。

ulimit -c <value>设置unix上的核心文件大小限制。缺省情况下,内核文件大小限制为0。你可以用ulimit -a看到你的ulimit值。

同样,如果您从gdb内部运行程序,它将在“分段违规”(SIGSEGV,通常当您访问未分配的内存块时)时停止程序,或者您可以设置断点。

DDD和nemiver是GDB的前端,这使得新手更容易使用GDB。

我忘记了GNOME的“apport”技术,但我不太了解如何使用它。它用于生成堆栈跟踪和其他用于处理的诊断,并可以自动归档错误。这当然值得一看。

对于Linux和Mac OS X,如果您正在使用gcc或任何使用glibc的编译器,您可以使用execinfo.h中的backtrace()函数来打印堆栈跟踪,并在遇到分割错误时优雅地退出。文档可以在在libc手册中中找到。

下面是一个示例程序,它安装了SIGSEGV处理程序,并在stderr发生分段错误时将堆栈跟踪输出到stderr。这里的baz()函数会导致触发处理程序的段错误:

#include <stdio.h>
#include <execinfo.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>




void handler(int sig) {
void *array[10];
size_t size;


// get void*'s for all entries on the stack
size = backtrace(array, 10);


// print out all the frames to stderr
fprintf(stderr, "Error: signal %d:\n", sig);
backtrace_symbols_fd(array, size, STDERR_FILENO);
exit(1);
}


void baz() {
int *foo = (int*)-1; // make a bad pointer
printf("%d\n", *foo);       // causes segfault
}


void bar() { baz(); }
void foo() { bar(); }




int main(int argc, char **argv) {
signal(SIGSEGV, handler);   // install our handler
foo(); // this will call foo, bar, and baz.  baz segfaults.
}

使用-g -rdynamic编译可以在输出中获得符号信息,glibc可以使用它来创建一个漂亮的stacktrace:

$ gcc -g -rdynamic ./test.c -o test

执行此命令将得到以下输出:

$ ./test
Error: signal 11:
./test(handler+0x19)[0x400911]
/lib64/tls/libc.so.6[0x3a9b92e380]
./test(baz+0x14)[0x400962]
./test(bar+0xe)[0x400983]
./test(foo+0xe)[0x400993]
./test(main+0x28)[0x4009bd]
/lib64/tls/libc.so.6(__libc_start_main+0xdb)[0x3a9b91c4bb]
./test[0x40086a]

这显示了堆栈中每个帧的加载模块、偏移量和函数。在这里,您可以看到堆栈顶部的信号处理程序,以及main之前的libc函数,此外还有mainfoobarbaz

参见王牌(自适应通信环境)中的堆栈跟踪功能。它已经被编写为涵盖所有主要平台(以及更多)。这个库是bsd风格授权的,所以如果你不想使用ACE,你甚至可以复制/粘贴代码。

可能值得一看谷歌Breakpad,这是一个跨平台崩溃转储生成器和处理转储的工具。

我研究这个问题有一段时间了。

深埋在谷歌性能工具README

http://code.google.com/p/google-perftools/source/browse/trunk/README

谈到libunwind

http://www.nongnu.org/libunwind/

很乐意听听对这个图书馆的意见。

使用-rdynamic的问题是,在某些情况下,它会相对显著地增加二进制文件的大小

Linux

虽然在execinfo.h中使用backtrace()函数来打印堆栈跟踪并在出现分段错误时优雅地退出,但我没有看到提到确保所产生的回溯指向错误的实际位置所必需的复杂性(至少对于一些架构- x86 &手臂)。

进入信号处理程序时,堆栈帧链中的前两个条目在信号处理程序中包含一个返回地址,在libc中的sigaction()中包含一个返回地址。在信号(即故障位置)之前调用的最后一个函数的堆栈帧丢失。

代码

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#ifndef __USE_GNU
#define __USE_GNU
#endif


#include <execinfo.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ucontext.h>
#include <unistd.h>


/* This structure mirrors the one found in /usr/include/asm/ucontext.h */
typedef struct _sig_ucontext {
unsigned long     uc_flags;
ucontext_t        *uc_link;
stack_t           uc_stack;
sigcontext_t      uc_mcontext;
sigset_t          uc_sigmask;
} sig_ucontext_t;


void crit_err_hdlr(int sig_num, siginfo_t * info, void * ucontext)
{
void *             array[50];
void *             caller_address;
char **            messages;
int                size, i;
sig_ucontext_t *   uc;


uc = (sig_ucontext_t *)ucontext;


/* Get the address at the time the signal was raised */
#if defined(__i386__) // gcc specific
caller_address = (void *) uc->uc_mcontext.eip; // EIP: x86 specific
#elif defined(__x86_64__) // gcc specific
caller_address = (void *) uc->uc_mcontext.rip; // RIP: x86_64 specific
#else
#error Unsupported architecture. // TODO: Add support for other arch.
#endif


fprintf(stderr, "signal %d (%s), address is %p from %p\n",
sig_num, strsignal(sig_num), info->si_addr,
(void *)caller_address);


size = backtrace(array, 50);


/* overwrite sigaction with caller's address */
array[1] = caller_address;


messages = backtrace_symbols(array, size);


/* skip first stack frame (points here) */
for (i = 1; i < size && messages != NULL; ++i)
{
fprintf(stderr, "[bt]: (%d) %s\n", i, messages[i]);
}


free(messages);


exit(EXIT_FAILURE);
}


int crash()
{
char * p = NULL;
*p = 0;
return 0;
}


int foo4()
{
crash();
return 0;
}


int foo3()
{
foo4();
return 0;
}


int foo2()
{
foo3();
return 0;
}


int foo1()
{
foo2();
return 0;
}


int main(int argc, char ** argv)
{
struct sigaction sigact;


sigact.sa_sigaction = crit_err_hdlr;
sigact.sa_flags = SA_RESTART | SA_SIGINFO;


if (sigaction(SIGSEGV, &sigact, (struct sigaction *)NULL) != 0)
{
fprintf(stderr, "error setting signal handler for %d (%s)\n",
SIGSEGV, strsignal(SIGSEGV));


exit(EXIT_FAILURE);
}


foo1();


exit(EXIT_SUCCESS);
}

输出

signal 11 (Segmentation fault), address is (nil) from 0x8c50
[bt]: (1) ./test(crash+0x24) [0x8c50]
[bt]: (2) ./test(foo4+0x10) [0x8c70]
[bt]: (3) ./test(foo3+0x10) [0x8c8c]
[bt]: (4) ./test(foo2+0x10) [0x8ca8]
[bt]: (5) ./test(foo1+0x10) [0x8cc4]
[bt]: (6) ./test(main+0x74) [0x8d44]
[bt]: (7) /lib/libc.so.6(__libc_start_main+0xa8) [0x40032e44]

在信号处理程序中调用backtrace()函数的所有危险仍然存在,不应忽视,但我发现这里描述的功能对调试崩溃非常有帮助。

需要注意的是,我提供的示例是在x86的Linux上开发/测试的。我还成功地在ARM上使用uc_mcontext.arm_pc而不是uc_mcontext.eip实现了这一点。

下面是我了解这个实现细节的文章的链接: # EYZ0 < / p >

尽管已经提供了正确的答案,描述了如何使用GNU libc backtrace()函数1,并且我提供了我自己的回答,描述了如何确保从信号处理程序指向错误2的实际位置,但我没有看到从回溯中输出的demangling c++符号。

当从c++程序中获得回溯时,输出可以通过c++filt1来要求符号,或者直接使用abi::__cxa_demangle1

    Linux &OS X 注意,c++filt__cxa_demangle是GCC特定的
  • # EYZ0 Linux

下面的c++ Linux示例使用了与我的其他的答案相同的信号处理程序,并演示了如何使用c++filt来要求符号。

# EYZ0:

class foo
{
public:
foo() { foo1(); }


private:
void foo1() { foo2(); }
void foo2() { foo3(); }
void foo3() { foo4(); }
void foo4() { crash(); }
void crash() { char * p = NULL; *p = 0; }
};


int main(int argc, char ** argv)
{
// Setup signal handler for SIGSEGV
...


foo * f = new foo();
return 0;
}

# EYZ1 (# EYZ0):

signal 11 (Segmentation fault), address is (nil) from 0x8048e07
[bt]: (1) ./test(crash__3foo+0x13) [0x8048e07]
[bt]: (2) ./test(foo4__3foo+0x12) [0x8048dee]
[bt]: (3) ./test(foo3__3foo+0x12) [0x8048dd6]
[bt]: (4) ./test(foo2__3foo+0x12) [0x8048dbe]
[bt]: (5) ./test(foo1__3foo+0x12) [0x8048da6]
[bt]: (6) ./test(__3foo+0x12) [0x8048d8e]
[bt]: (7) ./test(main+0xe0) [0x8048d18]
[bt]: (8) ./test(__libc_start_main+0x95) [0x42017589]
[bt]: (9) ./test(__register_frame_info+0x3d) [0x8048981]

# EYZ1 (# EYZ0):

signal 11 (Segmentation fault), address is (nil) from 0x8048e07
[bt]: (1) ./test(foo::crash(void)+0x13) [0x8048e07]
[bt]: (2) ./test(foo::foo4(void)+0x12) [0x8048dee]
[bt]: (3) ./test(foo::foo3(void)+0x12) [0x8048dd6]
[bt]: (4) ./test(foo::foo2(void)+0x12) [0x8048dbe]
[bt]: (5) ./test(foo::foo1(void)+0x12) [0x8048da6]
[bt]: (6) ./test(foo::foo(void)+0x12) [0x8048d8e]
[bt]: (7) ./test(main+0xe0) [0x8048d18]
[bt]: (8) ./test(__libc_start_main+0x95) [0x42017589]
[bt]: (9) ./test(__register_frame_info+0x3d) [0x8048981]

下面的代码构建在我的原来的答案中的信号处理程序之上,可以替换上面示例中的信号处理程序,以演示如何使用abi::__cxa_demangle来要求符号。此信号处理程序产生与上面示例相同的需求输出。

# EYZ0:

void crit_err_hdlr(int sig_num, siginfo_t * info, void * ucontext)
{
sig_ucontext_t * uc = (sig_ucontext_t *)ucontext;


void * caller_address = (void *) uc->uc_mcontext.eip; // x86 specific


std::cerr << "signal " << sig_num
<< " (" << strsignal(sig_num) << "), address is "
<< info->si_addr << " from " << caller_address
<< std::endl << std::endl;


void * array[50];
int size = backtrace(array, 50);


array[1] = caller_address;


char ** messages = backtrace_symbols(array, size);


// skip first stack frame (points here)
for (int i = 1; i < size && messages != NULL; ++i)
{
char *mangled_name = 0, *offset_begin = 0, *offset_end = 0;


// find parantheses and +address offset surrounding mangled name
for (char *p = messages[i]; *p; ++p)
{
if (*p == '(')
{
mangled_name = p;
}
else if (*p == '+')
{
offset_begin = p;
}
else if (*p == ')')
{
offset_end = p;
break;
}
}


// if the line could be processed, attempt to demangle the symbol
if (mangled_name && offset_begin && offset_end &&
mangled_name < offset_begin)
{
*mangled_name++ = '\0';
*offset_begin++ = '\0';
*offset_end++ = '\0';


int status;
char * real_name = abi::__cxa_demangle(mangled_name, 0, 0, &status);


// if demangling is successful, output the demangled function name
if (status == 0)
{
std::cerr << "[bt]: (" << i << ") " << messages[i] << " : "
<< real_name << "+" << offset_begin << offset_end
<< std::endl;


}
// otherwise, output the mangled function name
else
{
std::cerr << "[bt]: (" << i << ") " << messages[i] << " : "
<< mangled_name << "+" << offset_begin << offset_end
<< std::endl;
}
free(real_name);
}
// otherwise, print the whole line
else
{
std::cerr << "[bt]: (" << i << ") " << messages[i] << std::endl;
}
}
std::cerr << std::endl;


free(messages);


exit(EXIT_FAILURE);
}

它甚至比“man backtrace”更简单,有一个很少有文档的库(GNU专用)作为libSegFault与glibc一起分发。所以,我相信这是由Ulrich Drepper写的,以支持程序catchsegv(见“man catchsegv”)。

这给了我们3种可能性。而不是运行“program -o hai”:

  1. 在catchsegv中运行:

    $ catchsegv program -o hai
    
  2. Link with libSegFault at runtime:

    $ LD_PRELOAD=/lib/libSegFault.so program -o hai
    
  3. Link with libSegFault at compile time:

    $ gcc -g1 -lSegFault -o program program.cc
    $ program -o hai
    

In all 3 cases, you will get clearer backtraces with less optimization (gcc -O0 or -O1) and debugging symbols (gcc -g). Otherwise, you may just end up with a pile of memory addresses.

You can also catch more signals for stack traces with something like:

$ export SEGFAULT_SIGNALS="all"       # "all" signals
$ export SEGFAULT_SIGNALS="bus abrt"  # SIGBUS and SIGABRT

输出看起来像这样(注意底部的反向跟踪):

*** Segmentation fault Register dump:


EAX: 0000000c   EBX: 00000080   ECX:
00000000   EDX: 0000000c  ESI:
bfdbf080   EDI: 080497e0   EBP:
bfdbee38   ESP: bfdbee20


EIP: 0805640f   EFLAGS: 00010282


CS: 0073   DS: 007b   ES: 007b   FS:
0000   GS: 0033   SS: 007b


Trap: 0000000e   Error: 00000004
OldMask: 00000000  ESP/signal:
bfdbee20   CR2: 00000024


FPUCW: ffff037f   FPUSW: ffff0000
TAG: ffffffff  IPOFF: 00000000
CSSEL: 0000   DATAOFF: 00000000
DATASEL: 0000


ST(0) 0000 0000000000000000   ST(1)
0000 0000000000000000  ST(2) 0000
0000000000000000   ST(3) 0000
0000000000000000  ST(4) 0000
0000000000000000   ST(5) 0000
0000000000000000  ST(6) 0000
0000000000000000   ST(7) 0000
0000000000000000


Backtrace:
/lib/libSegFault.so[0xb7f9e100]
??:0(??)[0xb7fa3400]
/usr/include/c++/4.3/bits/stl_queue.h:226(_ZNSt5queueISsSt5dequeISsSaISsEEE4pushERKSs)[0x805647a]
/home/dbingham/src/middle-earth-mud/alpha6/src/engine/player.cpp:73(_ZN6Player5inputESs)[0x805377c]
/home/dbingham/src/middle-earth-mud/alpha6/src/engine/socket.cpp:159(_ZN6Socket4ReadEv)[0x8050698]
/home/dbingham/src/middle-earth-mud/alpha6/src/engine/socket.cpp:413(_ZN12ServerSocket4ReadEv)[0x80507ad]
/home/dbingham/src/middle-earth-mud/alpha6/src/engine/socket.cpp:300(_ZN12ServerSocket4pollEv)[0x8050b44]
/home/dbingham/src/middle-earth-mud/alpha6/src/engine/main.cpp:34(main)[0x8049a72]
/lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe5)[0xb7d1b775]
/build/buildd/glibc-2.9/csu/../sysdeps/i386/elf/start.S:122(_start)[0x8049801]

如果你想知道血淋淋的细节,最好的来源是:参阅http://sourceware.org/git/?p=glibc.git;a=blob;f=debug/segfault.c和它的父目录http://sourceware.org/git/?p=glibc.git;a=tree;f=debug

你可以使用DeathHandler——一个小的c++类,它为你做所有的事情,可靠。

感谢calorticgeek让我注意到addr2line实用程序。

我写了一个快速和肮脏的脚本来处理答案的输出提供在这里: (非常感谢jschmier!)使用addr2line实用程序

脚本只接受一个参数:包含jschmier实用程序输出的文件名。

对于跟踪的每一层,输出应该打印如下内容:

BACKTRACE:  testExe 0x8A5db6b
FILE:       pathToFile/testExe.C:110
FUNCTION:   testFunction(int)
107
108
109           int* i = 0x0;
*110           *i = 5;
111
112        }
113        return i;

代码:

#!/bin/bash


LOGFILE=$1


NUM_SRC_CONTEXT_LINES=3


old_IFS=$IFS  # save the field separator
IFS=$'\n'     # new field separator, the end of line


for bt in `cat $LOGFILE | grep '\[bt\]'`; do
IFS=$old_IFS     # restore default field separator
printf '\n'
EXEC=`echo $bt | cut -d' ' -f3 | cut -d'(' -f1`
ADDR=`echo $bt | cut -d'[' -f3 | cut -d']' -f1`
echo "BACKTRACE:  $EXEC $ADDR"
A2L=`addr2line -a $ADDR -e $EXEC -pfC`
#echo "A2L:        $A2L"


FUNCTION=`echo $A2L | sed 's/\<at\>.*//' | cut -d' ' -f2-99`
FILE_AND_LINE=`echo $A2L | sed 's/.* at //'`
echo "FILE:       $FILE_AND_LINE"
echo "FUNCTION:   $FUNCTION"


# print offending source code
SRCFILE=`echo $FILE_AND_LINE | cut -d':' -f1`
LINENUM=`echo $FILE_AND_LINE | cut -d':' -f2`
if ([ -f $SRCFILE ]); then
cat -n $SRCFILE | grep -C $NUM_SRC_CONTEXT_LINES "^ *$LINENUM\>" | sed "s/ $LINENUM/*$LINENUM/"
else
echo "File not found: $SRCFILE"
fi
IFS=$'\n'     # new field separator, the end of line
done


IFS=$old_IFS     # restore default field separator

除了上面的答案,这里还介绍了如何让Debian Linux操作系统生成核心转储

  1. 在用户的主文件夹中创建一个“coredumps”文件夹
  2. 进入/etc/security/limits.conf。在“”行下面,键入“soft core unlimited”,如果为根启用核心转储,则键入“root soft core unlimited”,以允许为核心转储提供无限空间。
  3. 注意:“* soft core unlimited”不包括root,这就是为什么root必须在它自己的行中指定。
  4. 要检查这些值,请注销,重新登录,并键入“ulimit -a”。“核心文件大小”应设置为无限。
  5. 检查.bashrc文件(用户文件和root文件,如果适用的话),以确保那里没有设置ulimit。否则,上面的值将在启动时被覆盖。
  6. <李> /etc/sysctl.conf开放。 在底部输入以下内容:" kernel. "Core_pattern = /home//coredumps/%e_%t.dump "。(%e为进程名,%t为系统时间)
  7. 退出并输入“sysctl -p”加载新配置 检查/proc/sys/kernel/core_pattern,并验证它是否与您刚刚输入的内容相匹配
  8. 可以通过在命令行上运行一个进程(“&”)来测试核心转储,然后使用“kill -11”终止它。如果核心转储成功,您将在分段故障指示后看到“(核心转储)”。
我在这里看到了很多答案执行一个信号处理程序,然后退出。 这就是方法,但请记住一个非常重要的事实:如果您想获得生成错误的核心转储,就不能调用exit(status)。而是调用abort() !< / p >

我发现@tgamblin解决方案不完整。 它不能处理stackoverflow。 我想因为默认情况下信号处理程序是用相同的堆栈和调用的 SIGSEGV被抛出两次。为了保护,你需要为信号处理器注册一个独立的堆栈

您可以使用下面的代码进行检查。默认情况下,处理程序失败。使用已定义的宏STACK_OVERFLOW就可以了。

#include <iostream>
#include <execinfo.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <string>
#include <cassert>


using namespace std;


//#define STACK_OVERFLOW


#ifdef STACK_OVERFLOW
static char stack_body[64*1024];
static stack_t sigseg_stack;
#endif


static struct sigaction sigseg_handler;


void handler(int sig) {
cerr << "sig seg fault handler" << endl;
const int asize = 10;
void *array[asize];
size_t size;


// get void*'s for all entries on the stack
size = backtrace(array, asize);


// print out all the frames to stderr
cerr << "stack trace: " << endl;
backtrace_symbols_fd(array, size, STDERR_FILENO);
cerr << "resend SIGSEGV to get core dump" << endl;
signal(sig, SIG_DFL);
kill(getpid(), sig);
}


void foo() {
foo();
}


int main(int argc, char **argv) {
#ifdef STACK_OVERFLOW
sigseg_stack.ss_sp = stack_body;
sigseg_stack.ss_flags = SS_ONSTACK;
sigseg_stack.ss_size = sizeof(stack_body);
assert(!sigaltstack(&sigseg_stack, nullptr));
sigseg_handler.sa_flags = SA_ONSTACK;
#else
sigseg_handler.sa_flags = SA_RESTART;
#endif
sigseg_handler.sa_handler = &handler;
assert(!sigaction(SIGSEGV, &sigseg_handler, nullptr));
cout << "sig action set" << endl;
foo();
return 0;
}

忘记改变你的源代码,用backtrace()函数或宏来做一些hack -这些只是糟糕的解决方案。

作为一个有效的解决方案,我的建议是:

  1. 用"-g"标志编译程序,用于将调试符号嵌入二进制(不用担心,这不会影响您的性能)。
  2. 在linux上运行下一个命令:"ulimit -c unlimited" -允许系统进行大崩溃转储。
  3. 当你的程序崩溃时,在工作目录中你会看到文件“core”。
  4. gdb -batch -ex "backtrace" ./your_program_exe ./core . exe .执行下一个命令,将backtrace输出到标准输出
这将以人类可读的方式打印您的程序的正确可读的反向跟踪(包含源文件名和行号)。 此外,这种方法将给你自由自动化你的系统: 编写一个简短的脚本,检查进程是否创建了核心转储,然后通过电子邮件向开发人员发送回溯信息,或将其记录到一些日志系统中
镇上的新国王来了 # EYZ0 < / p >

在代码中放置1个头文件,安装1个库。

我个人使用这个函数来调用它

#include "backward.hpp"
void stacker() {


using namespace backward;
StackTrace st;




st.load_here(99); //Limit the number of trace depth to 99
st.skip_n_firsts(3);//This will skip some backward internal function from the trace


Printer p;
p.snippet = true;
p.object = true;
p.color = true;
p.address = true;
p.print(st, stderr);
}

作为仅适用于windows的解决方案,您可以使用Windows错误报告获得与堆栈跟踪相当的信息(其中包含更多信息)。只需几个注册表项,就可以设置为收集用户模式转储:

从Windows Server 2008和Windows Vista with Service Pack 1 (SP1)开始,可以配置Windows错误报告(WER),以便在用户模式应用程序崩溃后收集完整的用户模式转储并存储在本地。[…]

默认情况下不启用此特性。启用该特性需要管理员权限。要启用和配置该特性,请在HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows错误报告\LocalDumps键下使用以下注册表值。

您可以从具有所需权限的安装程序中设置注册表项。

创建用户模式转储比在客户端生成堆栈跟踪有以下优点:

  • 系统中已经实现了。如果需要对转储的信息量进行更细粒度的控制,可以使用上面概述的WER,也可以自己调用MiniDumpWriteDump。(请确保从不同的进程调用它。)
  • 道路比堆栈跟踪更完整。其中,它可以包含局部变量、函数参数、其他线程的堆栈、加载的模块等等。数据量(以及大小)是高度可定制的。
  • 不需要附带调试符号。这既大大减小了部署的大小,也使得对应用程序进行逆向工程变得更加困难。
  • 很大程度上独立于你使用的编译器。使用WER甚至不需要任何代码。不管怎样,有一种获得符号数据库(PDB)的方法对脱机分析非常很有用。我相信GCC可以生成PDB,或者有工具可以将符号数据库转换为PDB格式。

请注意,WER只能由应用程序崩溃触发(即系统由于未处理的异常而终止进程)。MiniDumpWriteDump可以在任何时候被调用。如果您需要转储当前状态以诊断崩溃以外的问题,这可能会很有帮助。

如果你想评估mini dump的适用性,必读:

它看起来像在最后一个c++增强版本中出现的库提供了你想要的东西,可能代码会是多平台的。 它是boost::加,你可以像这样使用在增强样本中:

#include <filesystem>
#include <sstream>
#include <fstream>
#include <signal.h>     // ::signal, ::raise
#include <boost/stacktrace.hpp>


const char* backtraceFileName = "./backtraceFile.dump";


void signalHandler(int)
{
::signal(SIGSEGV, SIG_DFL);
::signal(SIGABRT, SIG_DFL);
boost::stacktrace::safe_dump_to(backtraceFileName);
::raise(SIGABRT);
}


void sendReport()
{
if (std::filesystem::exists(backtraceFileName))
{
std::ifstream file(backtraceFileName);


auto st = boost::stacktrace::stacktrace::from_dump(file);
std::ostringstream backtraceStream;
backtraceStream << st << std::endl;


// sending the code from st


file.close();
std::filesystem::remove(backtraceFileName);
}
}


int main()
{
::signal(SIGSEGV, signalHandler);
::signal(SIGABRT, signalHandler);


sendReport();
// ... rest of code
}

在Linux中编译上面的代码:

g++ --std=c++17 file.cpp -lstdc++fs -lboost_stacktrace_backtrace -ldl -lbacktrace

提高文档复制的反向跟踪示例:

0# bar(int) at /path/to/source/file.cpp:70
1# bar(int) at /path/to/source/file.cpp:70
2# bar(int) at /path/to/source/file.cpp:70
3# bar(int) at /path/to/source/file.cpp:70
4# main at /path/to/main.cpp:93
5# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
6# _start

如果你仍然想像我一样单独做,你可以链接到bfd,避免使用addr2line,就像我在这里做的那样:

https://github.com/gnif/LookingGlass/blob/master/common/src/platform/linux/crash.c

这将产生输出:

[E]        crash.linux.c:170  | crit_err_hdlr                  | ==== FATAL CRASH (a12-151-g28b12c85f4+1) ====
[E]        crash.linux.c:171  | crit_err_hdlr                  | signal 11 (Segmentation fault), address is (nil)
[E]        crash.linux.c:194  | crit_err_hdlr                  | [trace]: (0) /home/geoff/Projects/LookingGlass/client/src/main.c:936 (register_key_binds)
[E]        crash.linux.c:194  | crit_err_hdlr                  | [trace]: (1) /home/geoff/Projects/LookingGlass/client/src/main.c:1069 (run)
[E]        crash.linux.c:194  | crit_err_hdlr                  | [trace]: (2) /home/geoff/Projects/LookingGlass/client/src/main.c:1314 (main)
[E]        crash.linux.c:199  | crit_err_hdlr                  | [trace]: (3) /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7f8aa65f809b]
[E]        crash.linux.c:199  | crit_err_hdlr                  | [trace]: (4) ./looking-glass-client(_start+0x2a) [0x55c70fc4aeca]
gdb -ex 'set confirm off' -ex r -ex bt -ex q <my-program>

你可能不会这样,我能说的是它适合我,和我有相似但不相同的要求:我写编译器/ transpiler 1970的algol风格使用C语言的输出,然后编译C用户而言,他们通常不知道C参与,因此,尽管你可以把这称为transpiler,它实际上是一个用C编译器的中间代码。正在编译的语言在原始本机编译器中提供良好的诊断和完整的回溯历史。我已经能够找到gcc编译器标志和库等,它们允许我捕获原始编译器所做的大多数运行时错误(尽管有一个明显的例外-未赋值变量捕获)。当发生运行时错误(例如算术溢出、除零、数组索引越界等)时,原始编译器会向控制台输出一个回溯,列出每个活动过程调用的堆栈帧中的所有变量。我努力在C语言中获得这种效果,但最终只能用hack来形容……当程序被调用时,提供C "main"查看它的argv,如果没有特殊的选项,它会在GDB下重新启动自己,并使用一个修改后的argv,其中包含GDB选项和程序本身的'magic'选项字符串。然后,这个重新启动的版本在调用用我们的语言编写的代码的主块之前,通过恢复原始参数,从用户的代码中隐藏这些字符串。当错误发生时(只要不是用户代码显式地在程序中捕获的错误),它将退出到gdb,并打印所需的回溯。

启动序列中的关键代码行包括:

  if ((argc >= 1) && (strcmp(origargv[argc-1], "--restarting-under-gdb")) != 0) {
// initial invocation
// the "--restarting-under-gdb" option is how the copy running under gdb knows
// not to start another gdb process.

而且

  char *gdb [] = {
"/usr/bin/gdb", "-q", "-batch", "-nx", "-nh", "-return-child-result",
"-ex", "run",
"-ex", "bt full",
"--args"
};
原始参数被附加到上面的gdb选项中。这应该足以提示您在自己的系统中执行类似的操作。 我确实看了其他库支持的回溯选项(例如libbacktrace, https://codingrelic.geekhold.com/2010/09/gcc-function-instrumentation.html,等等),但是它们只输出过程调用堆栈,而不是局部变量。然而,如果有人知道任何清洁机制来获得类似的效果,请告诉我们。这样做的主要缺点是变量是用C语法打印出来的,而不是用户使用的语言的语法。并且(直到我添加合适的#line指令在C的每一行中:-()反向跟踪列出了C源文件和行号 < p > G PS我使用的gcc编译选项是:

 GCCOPTS=" -Wall -Wno-return-type -Wno-comment -g -fsanitize=undefined
-fsanitize-undefined-trap-on-error -fno-sanitize-recover=all -frecord-gcc-switches
-fsanitize=float-divide-by-zero -fsanitize=float-cast-overflow -ftrapv
-grecord-gcc-switches -O0 -ggdb3 "

我迄今为止最好的异步信号安全尝试

如果不安全请告诉我。我还没有找到显示行号的方法。

#include <execinfo.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>


#define TRACE_MAX 1024


void handler(int sig) {
(void)sig;
void *array[TRACE_MAX];
size_t size;
const char msg[] = "failed with a signal\n";


size = backtrace(array, TRACE_MAX);
write(STDERR_FILENO, msg, sizeof(msg));
backtrace_symbols_fd(array, size, STDERR_FILENO);
_Exit(1);
}


void my_func_2(void) {
*((int*)0) = 1;
}


void my_func_1(double f) {
(void)f;
my_func_2();
}


void my_func_1(int i) {
(void)i;
my_func_2();
}


int main() {
/* Make a dummy call to `backtrace` to load libgcc because man backrace says:
*    *  backtrace() and backtrace_symbols_fd() don't call malloc() explicitly, but they are part of libgcc, which gets loaded dynamically when first used.  Dynamic loading usually triggers a call to mal‐
*       loc(3).  If you need certain calls to these two functions to not allocate memory (in signal handlers, for example), you need to make sure libgcc is loaded beforehand.
*/
void *dummy[1];
backtrace(dummy, 1);
signal(SIGSEGV, handler);


my_func_1(1);
}

编译并运行:

g++ -ggdb3 -O2 -std=c++11 -Wall -Wextra -pedantic -rdynamic -o stacktrace_on_signal_safe.out stacktrace_on_signal_safe.cpp
./stacktrace_on_signal_safe.out

需要使用-rdynamic来获取函数名:

failed with a signal
./stacktrace_on_signal_safe.out(_Z7handleri+0x6e)[0x56239398928e]
/lib/x86_64-linux-gnu/libc.so.6(+0x42520)[0x7f04b1459520]
./stacktrace_on_signal_safe.out(main+0x38)[0x562393989118]
/lib/x86_64-linux-gnu/libc.so.6(+0x29d90)[0x7f04b1440d90]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x80)[0x7f04b1440e40]
./stacktrace_on_signal_safe.out(_start+0x25)[0x562393989155]

然后我们可以将它管道到c++filt来要求:

./stacktrace_on_signal_safe.out |& c++filt

给:

failed with a signal
/stacktrace_on_signal_safe.out(handler(int)+0x6e)[0x55b6df43f28e]
/lib/x86_64-linux-gnu/libc.so.6(+0x42520)[0x7f40d4167520]
./stacktrace_on_signal_safe.out(main+0x38)[0x55b6df43f118]
/lib/x86_64-linux-gnu/libc.so.6(+0x29d90)[0x7f40d414ed90]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x80)[0x7f40d414ee40]
./stacktrace_on_signal_safe.out(_start+0x25)[0x55b6df43f155]

由于优化,几个级别都丢失了,使用-O0我们得到了一个更完整的:

/stacktrace_on_signal_safe.out(handler(int)+0x76)[0x55d39b68325f]
/lib/x86_64-linux-gnu/libc.so.6(+0x42520)[0x7f4d8ffdd520]
./stacktrace_on_signal_safe.out(my_func_2()+0xd)[0x55d39b6832bb]
./stacktrace_on_signal_safe.out(my_func_1(int)+0x14)[0x55d39b6832f1]
./stacktrace_on_signal_safe.out(main+0x4a)[0x55d39b68333e]
/lib/x86_64-linux-gnu/libc.so.6(+0x29d90)[0x7f4d8ffc4d90]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x80)[0x7f4d8ffc4e40]
./stacktrace_on_signal_safe.out(_start+0x25)[0x55d39b683125]

行号不存在,但我们可以使用addr2line获取它们。这需要在没有-rdynamic的情况下构建:

g++ -ggdb3 -O0 -std=c++23 -Wall -Wextra -pedantic -o stacktrace_on_signal_safe.out stacktrace_on_signal_safe.cpp
./stacktrace_on_signal_safe.out |& sed -r 's/.*\(//;s/\).*//' | addr2line -C -e stacktrace_on_signal_safe.out -f

生产:

??
??:0
handler(int)
/home/ciro/stacktrace_on_signal_safe.cpp:14
??
??:0
my_func_2()
/home/ciro/stacktrace_on_signal_safe.cpp:22
my_func_1(i
/home/ciro/stacktrace_on_signal_safe.cpp:33
main
/home/ciro/stacktrace_on_signal_safe.cpp:45
??
??:0
??
??:0
_start
??:?

awk解析非-rdynamic输出的+<addr>数字:

./stacktrace_on_signal_safe.out(+0x125f)[0x55984828825f]
/lib/x86_64-linux-gnu/libc.so.6(+0x42520)[0x7f8644a1e520]
./stacktrace_on_signal_safe.out(+0x12bb)[0x5598482882bb]
./stacktrace_on_signal_safe.out(+0x12f1)[0x5598482882f1]
./stacktrace_on_signal_safe.out(+0x133e)[0x55984828833e]
/lib/x86_64-linux-gnu/libc.so.6(+0x29d90)[0x7f8644a05d90]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x80)[0x7f8644a05e40]
./stacktrace_on_signal_safe.out(+0x1125)[0x559848288125]

如果你还想打印实际的信号数到stdout,这里有一个异步信号安全实现int到字符串:使用写或异步安全函数从信号处理程序打印int,因为printf不是。

在Ubuntu 22.04上测试。

c++ 23 # EYZ0

与许多其他答案一样,本节忽略了问题的异步信号安全方面,这可能导致代码在崩溃时死锁,这可能会很严重。我们只希望有一天c++标准会添加一个类似# eyz0的函数来一劳永逸地解决这个问题。

这将是一般更好的c++堆栈跟踪选项向前移动:在C或c++中打印调用堆栈,因为它显示行号,并自动为我们做需求。

stacktrace_on_signal.cpp

#include <stacktrace>
#include <iostream>


#include <signal.h>
#include <stdlib.h>
#include <unistd.h>


void handler(int sig) {
(void)sig;
/* De-register this signal in the hope of avoiding infinite loops
* if asyns signal unsafe things fail later on. But can likely still deadlock. */
signal(sig, SIG_DFL);
// std::stacktrace::current
std::cout << std::stacktrace::current();
// C99 async signal safe version of exit().
_Exit(1);
}


void my_func_2(void) {
*((int*)0) = 1;
}


void my_func_1(double f) {
(void)f;
my_func_2();
}


void my_func_1(int i) {
(void)i;
my_func_2();
}


int main() {
signal(SIGSEGV, handler);
my_func_1(1);
}

编译并运行:

g++ -ggdb3 -O2 -std=c++23 -Wall -Wextra -pedantic -o stacktrace_on_signal.out stacktrace_on_signal.cpp -lstdc++_libbacktrace
./stacktrace_on_signal.out

在GCC 12.1上从源代码编译的输出,Ubuntu 22.04:

   0# handler(int) at /home/ciro/stacktrace_on_signal.cpp:11
1#      at :0
2# my_func_2() at /home/ciro/stacktrace_on_signal.cpp:16
3#      at :0
4#      at :0
5#      at :0
6#

我认为它错过了my_func_1,因为优化被打开了,总的来说我们对此无能为力。使用-O0会更好:

   0# handler(int) at /home/ciro/stacktrace_on_signal.cpp:11
1#      at :0
2# my_func_2() at /home/ciro/stacktrace_on_signal.cpp:16
3# my_func_1(int) at /home/ciro/stacktrace_on_signal.cpp:26
4#      at /home/ciro/stacktrace_on_signal.cpp:31
5#      at :0
6#      at :0
7#      at :0
8#

但不确定为什么main没有出现在那里。

# EYZ0

https://github.com/gcc-mirror/gcc/blob/releases/gcc-12.1.0/libstdc%2B%2B-v3/src/libbacktrace/backtrace-supported.h.in#L45提到backtrace_simple是安全的:

/* BACKTRACE_USES_MALLOC will be #define'd as 1 if the backtrace
library will call malloc as it works, 0 if it will call mmap
instead.  This may be used to determine whether it is safe to call
the backtrace functions from a signal handler.  In general this
only applies to calls like backtrace and backtrace_pcinfo.  It does
not apply to backtrace_simple, which never calls malloc.  It does
not apply to backtrace_print, which always calls fprintf and
therefore malloc.  */

但它使用起来似乎不太方便,主要是一个内部工具。

# EYZ0

这就是std::stacktrace的基础:https://en.cppreference.com/w/cpp/utility/basic_stacktrace

它有一个分配器参数,cppreference将其描述为:

为在热路径或嵌入式环境中使用basic_stacktrace提供了自定义分配器的支持。用户可以在堆栈上或其他适当的地方分配stacktrace_entry对象。

所以我想知道basic_stacktrace本身是否是异步信号安全的,如果它不可能使一个版本的std::stacktrace也是一个自定义分配器,例如:

  • 写入磁盘上的文件,如boost::stacktrace::safe_dump_to
  • 或者写入某个预先分配的具有最大大小的堆栈缓冲区

https://apolukhin.github.io/papers/stacktrace_r1.html可能是提案,提到:

关于信号安全的注意:本建议并不试图为捕获和解码堆栈跟踪提供信号安全的解决方案。这种功能目前还不能在一些流行的平台上实现。然而,本文试图提供一个可扩展的解决方案,通过提供一个信号安全分配器和改变堆栈跟踪实现细节,有可能使信号安全。

只是得到核心转储吗?

核心转储允许您使用GDB: 我如何分析一个程序的核心转储文件与GDB时,它有命令行参数?检查内存,因此它比只有跟踪更强大。

只要确保你正确地启用了它,特别是在Ubuntu 22.04上,你需要:

echo 'core' | sudo tee /proc/sys/kernel/core_pattern

或学习使用apport,请参见:https://askubuntu.com/questions/1349047/where-do-i-find-core-dump-files-and-how-do-i-view-and-analyze-the-backtrace-st/1442665#1442665