ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

我希望我的程序能够在崩溃时生成堆栈跟踪,并且下次用户运行它时,它会询问他们是否可以将堆栈跟踪发送给我,以便我可以追踪问题。我可以处理向我发送信息,但我不知道如何生成跟踪字符串。有任何想法吗?

backtrace 和 backtrace_symbols_fd 不是异步信号安全的。你不应该在信号处理程序中使用这些函数
backtrace_symbols 调用 malloc,因此不能在信号处理程序中使用。其他两个函数(backtrace 和 backtrace_symbols_fd)则没有这个问题,通常用于信号处理程序中。
@cmccabe 不正确的 backtrace_symbols_fd 通常不会调用 malloc 但如果其 catch_error 块中出现问题则可能
它“可能”在没有针对 backtrace_symbols_fd(或任何回溯)的 POSIX 规范的意义上说;但是,根据 linux.die.net/man/3/backtrace_symbols_fd,GNU/Linux 的 backtrace_symbols_fd 被指定为从不调用 malloc。因此,可以安全地假设它永远不会在 Linux 上调用 malloc。
2021年有没有更好的办法解决这个问题?我只想像在 Java 或 Python 中一样打印堆栈跟踪。

V
Violet Giraffe

对于 Linux,我相信 Mac OS X,如果您使用 gcc 或任何使用 glibc 的编译器,您可以使用 execinfo.h 中的 backtrace() 函数打印堆栈跟踪,并在遇到分段错误时正常退出。可以在 in the libc manual 中找到文档。

这是一个安装 SIGSEGV 处理程序并在出现段错误时将堆栈跟踪打印到 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 可以使用这些信息来制作漂亮的堆栈跟踪:

$ 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


还有 /lib/libSegFault.so 可以与 LD_PRELOAD 一起使用。
看起来您的回溯输出中的前两个条目在信号处理程序中包含一个返回地址,并且可能在 libc 的 sigaction() 中包含一个返回地址。虽然您的回溯似乎是正确的,但我有时发现需要额外的步骤来确保故障的实际位置出现在回溯中,因为它可以被内核用 sigaction() 覆盖。
如果崩溃来自 malloc 内部会发生什么?难道你不会持有一个锁,然后因为“回溯”试图分配内存而被卡住吗?
catchsegv 不是 OP 所需要的,但对于捕获分段错误和获取所有信息来说非常棒。
对于 ARM,我还必须使用 -funwind-tables 进行编译。否则我的堆栈深度始终为 1(空)。
d
dequis

它甚至比“man backtrace”更容易,有一个与 glibc 一起作为 libSegFault.so 分发的小文档库(GNU 特定),我相信它是由 Ulrich Drepper 编写的,用于支持程序 catchsegv(参见“man catchsegv”)。

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

在 catchsegv 中运行: $ catchsegv program -o hai 在运行时与 libSegFault 链接: $ LD_PRELOAD=/lib/libSegFault.so program -o hai 在编译时与 libSegFault 链接: $ gcc -g1 -lSegFault -o program program.cc $ program -o hai

在所有 3 种情况下,您将获得更清晰的回溯,而优化更少(gcc -O0 或 -O1)和调试符号(gcc -g)。否则,你可能会得到一堆内存地址。

您还可以通过以下方式捕获更多堆栈跟踪信号:

$ 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


“可能性 3. 在编译时与 libSegFault 链接”不起作用。
@crafter:你是什么意思“不起作用”。你试过什么,在什么语言/编译器/工具链/发行版/硬件上?它编译失败了吗?捕捉错误?完全生产输出?产生难以使用的输出?谢谢你的详细信息,它将帮助每个人。
'最好的来源是不幸的来源'......希望有一天,catchsegv 的手册页实际上会提到 SEGFAULT_SIGNALS。在那之前,有这个答案可以参考。
我不敢相信我已经编写 C 5 年了,但从未听说过这个:/
@StéphaneGourichon @HansKratz 要与 libSegFault 链接,您必须将 -Wl,--no-as-needed 添加到编译器标志。否则,ld 将确实链接到 libSegFault,因为它识别出二进制文件不使用它的任何符号。
É
Étienne

Linux

虽然使用 execinfo.h 中的 backtrace() 函数来打印堆栈跟踪并在遇到分段错误时正常退出有 already been suggested,但我没有提到确保生成的回溯指向实际位置所需的复杂性错误(至少对于某些架构 - x86 & ARM)。

当您进入信号处理程序时,堆栈帧链中的前两个条目包含信号处理程序内部的返回地址和 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() 函数的所有危险仍然存在,不应被忽视,但我发现我在这里描述的功能对调试崩溃很有帮助。

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

以下是我了解此实现详细信息的文章的链接:http://www.linuxjournal.com/article/6391


在使用 GNU ld 的系统上,请记住使用 -rdynamic 进行编译以指示链接器将所有符号(不仅是使用的符号)添加到动态符号表中。这允许 backtrace_symbols() 将地址转换为函数名称
此外,您需要在 GCC 的命令行中添加“-mapcs-frame”选项以在 ARM 平台上生成堆栈帧
这可能为时已晚,但我们可以使用 addr2line 命令以某种方式获取发生崩溃的确切行吗?
glibc 的较新版本中,uc_mcontext 不包含名为 eip 的字段。现在有一个需要索引的数组,uc_mcontext.gregs[REG_EIP] 是等价的。
对于 ARM,在我向编译器添加 -funwind-tables 选项之前,我的回溯始终具有深度 1。
C
Community

尽管提供了描述如何使用 GNU libc backtrace() 函数1correct answer,并且我提供了描述如何确保从信号处理程序指向实际的回溯的 my own answer故障的位置2,我没有看到任何提及从回溯输出的 demangling C++ 符号。

从 C++ 程序获取回溯时,可以通过 c++filt1 运行输出以解开符号或直接使用 abi::__cxa_demangle1

1 Linux & OS X 请注意,c++filt 和 __cxa_demangle 是 GCC 特定的

Linux

以下 C++ Linux 示例使用与我的 other answer 相同的信号处理程序,并演示了如何使用 c++filt 来对符号进行解码。

代码:

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;
}

输出 (./test):

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]

分解输出 (./test 2>&1 | c++filt):

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]

以下是基于我的 original answer 的信号处理程序构建的,可以替换上面示例中的信号处理程序,以演示如何使用 abi::__cxa_demangle 来对符号进行反编码。此信号处理程序产生与上述示例相同的解组输出。

代码:

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);
}

谢谢你,jschmier。我创建了一个小 bash 脚本来将其输出提供给 addr2line 实用程序。请参阅:stackoverflow.com/a/15801966/1797414
不要忘记#include
良好的文档,自 2008 年以来已在此处发布了一个简单的头文件... panthema.net/2008/0901-stacktrace-demangled 与您的方法非常相似 :)
abi::__cxa_demangle 似乎不是异步信号安全的,因此信号处理程序可能会在 malloc 中的某处死锁。
std::cerrfree()exit() 的使用都违反了在 POSIX 系统上调用非异步信号安全调用的限制。 如果您的进程在 free()malloc()newdetete 等任何调用中失败,则此代码将死锁。
F
Fabio says Reinstate Monica

可能值得一看 Google Breakpad,这是一个跨平台的故障转储生成器和处理转储的工具。


它报告诸如分段错误之类的东西,但它不报告有关未处理的 C++ 异常的任何信息。
u
user

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

GCC 还有两个内置函数可以帮助您,但它们可能会或可能不会完全在您的架构上实现,它们是 __builtin_frame_address__builtin_return_address。两者都需要立即整数级别(立即,我的意思是它不能是变量)。如果给定级别的 __builtin_frame_address 不为零,则获取同一级别的返回地址应该是安全的。


C
Community

感谢热心的极客让我注意到 addr2line 实用程序。

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

该脚本接受一个参数:包含 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 

B
Benson

需要注意的是,一旦你生成了一个核心文件,你就需要使用 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 ...

我希望这有帮助。


您也可以直接从崩溃程序中调用 gdb。将调用 gdb 的 SIGSEGV、SEGILL、SIGBUS、SIGFPE 的设置处理程序。详细信息:stackoverflow.com/questions/3151779/… 优点是您可以像 bt full 一样获得漂亮的带注释的回溯,还可以获得所有线程的堆栈跟踪。
您还可以获得比答案更容易的回溯: gdb -silent ./prog core --eval-command=backtrace --batch -it will show backtrace and close debugger
u
user

ulimit -c <value> 设置 unix 上的核心文件大小限制。默认情况下,核心文件大小限制为 0。您可以使用 ulimit -a 查看您的 ulimit 值。

此外,如果您从 gdb 中运行程序,它会在“违反分段”(SIGSEGV,通常当您访问一块尚未分配的内存时)停止您的程序,或者您可以设置断点。

ddd 和 nemiver 是 gdb 的前端,这使得新手更容易使用它。


核心转储比堆栈跟踪更有用,因为您可以在调试器中加载核心转储并查看整个程序的状态及其在崩溃点的数据。
其他人建议的回溯工具可能总比没有好,但它非常基本——它甚至没有给出行号。另一方面,使用核心转储,您可以追溯查看应用程序崩溃时的整个状态(包括详细的堆栈跟踪)。尝试将其用于现场调试可能存在实际问题,但它绝对是一个更强大的工具,用于分析开发过程中的崩溃和断言(至少在 Linux 上)。
b
baziorek

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

#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

boost documentation 复制的示例回溯:

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

G
Gregory

我一直在研究这个问题一段时间。

并深埋在 Google 性能工具自述文件中

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

谈 libunwind

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

很想听听这个图书馆的意见。

-rdynamic 的问题在于它在某些情况下会相对显着地增加二进制文件的大小


在 x86/64 上,我没有看到 -rdynamic 增加二进制大小。添加 -g 会导致更大的增长。
我注意到 libunwind 没有获取行号的功能,我猜(没有测试) unw_get_proc_name 返回函数符号(对于重载等进行了混淆)而不是原始名称。
这是正确的。正确执行此操作非常棘手,但我在 gaddr2line 方面取得了巨大成功,这里有很多实用信息blog.bigpixel.ro/2010/09/stack-unwinding-stack-trace-with-gcc
R
Roy

镇上的新国王来了https://github.com/bombela/backward-cpp

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);
}

哇!这就是它应该怎么做的!我刚刚放弃了自己的解决方案,转而支持这个解决方案。
我不明白这是如何解决问题的。您必须在抛出异常的同一位置调用它,方法是在使用此库后捕获它并再次抛出它(正如他们的示例所阐明的那样)。如果我错了,请纠正我,但这在程序崩溃的情况下没有用
@MazenAk 你可以安装一个事件处理程序来捕获 SIGSEV 和 SIGABRT 签出 github.com/bombela/backward-cpp#signalhandling
谢谢,我已经阅读 README 文件好几天了,我没有注意到这部分,今天试一试。
S
Stephen Deken

某些版本的 libc 包含处理堆栈跟踪的函数;你也许可以使用它们:

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

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


m
markhor

您可以使用 DeathHandler - 小型 C++ 类,它为您完成所有工作,可靠。


不幸的是,它使用 execlp() 来执行 addr2line 调用...完全留在自己的程序中会很好(这可以通过以某种形式包含 addr2line 代码)
l
loopzilla

忘记更改您的来源并使用 backtrace() 函数或宏进行一些黑客攻击 - 这些只是糟糕的解决方案。

作为一个正常工作的解决方案,我建议:

使用“-g”标志编译您的程序,以便将调试符号嵌入二进制文件(不要担心这不会影响您的性能)。在 linux 上运行下一个命令:“ulimit -c unlimited” - 允许系统进行大型故障转储。当您的程序崩溃时,您将在工作目录中看到文件“core”。运行下一个命令以将回溯打印到标准输出: gdb -batch -ex "backtrace" ./your_program_exe ./core

这将以人类可读的方式打印程序的正确可读回溯(带有源文件名和行号)。此外,这种方法将使您可以自由地自动化您的系统:有一个简短的脚本来检查进程是否创建了核心转储,然后通过电子邮件将回溯发送给开发人员,或者将其记录到某个日志记录系统中。


它给出了错误的行号。可以改进吗?
m
mana
ulimit -c unlimited

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

问候


用户不要求核心转储。他要求堆栈跟踪。请参阅delorie.com/gnu/docs/glibc/libc_665.html
核心转储将包含崩溃时的调用堆栈,不是吗?
你假设他在 Unix 上,并使用 Bash。
如果您使用 tcsh,则必须执行 limit coredumpsize unlimited
S
Stéphane

看着:

男人 3 回溯

和:

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

这些是 GNU 扩展。


在我不久前创建的这个页面上可能有其他示例可以提供帮助:charette.no-ip.com:81/programming/2010-01-25_Backtrace
A
Adam Mitz

请参阅 ACE(自适应通信环境)中的堆栈跟踪工具。它已经编写为涵盖所有主要平台(以及更多)。该库是 BSD 风格的许可,因此如果您不想使用 ACE,甚至可以复制/粘贴代码。


链接似乎已失效。
C
Community

作为仅限 Windows 的解决方案,您可以使用 Windows Error Reporting 获得等效的堆栈跟踪(包含更多信息)。只需几个注册表项,它就可以设置为 collect user-mode dumps

从 Windows Server 2008 和带有 Service Pack 1 (SP1) 的 Windows Vista 开始,可以配置 Windows 错误报告 (WER),以便在用户模式应用程序崩溃后收集并在本地存储完整的用户模式转储。 [...] 默认情况下不启用此功能。启用该功能需要管理员权限。要启用和配置该功能,请使用 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps 项下的以下注册表值。

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

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

它已经在系统中实现。如果您需要对要转储的信息量进行更细粒度的控制,您可以使用上述 WER,也可以自己调用 MiniDumpWriteDump。 (确保从不同的进程调用它。)

比堆栈跟踪更完整。其中,它可以包含局部变量、函数参数、其他线程的堆栈、加载的模块等等。数据量(以及随之而来的大小)是高度可定制的。

无需发布调试符号。这既大大减少了部署的规模,也使逆向工程变得更加困难。

很大程度上独立于您使用的编译器。使用 WER 甚至不需要任何代码。无论哪种方式,获得符号数据库 (PDB) 的方法对于离线分析都非常有用。我相信 GCC 可以生成 PDB,或者有工具可以将符号数据库转换为 PDB 格式。

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

必读,如果您想评估小型转储的适用性:

有效的小型转储

有效的小型转储(第 2 部分)


t
terminus

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


I
INS

*nix:您可以拦截 SIGSEGV(通常在崩溃之前引发此信号)并将信息保存到文件中。 (例如,您可以使用 gdb 进行调试的核心文件除外)。

win:从 msdn 检查 this

您还可以查看 google 的 chrome 代码,了解它是如何处理崩溃的。它有一个很好的异常处理机制。


SEH 对生成堆栈跟踪没有帮助。虽然它可能是解决方案的一部分,但该解决方案更难实现,并且提供的信息更少,但以披露更多关于您的应用程序的信息为代价,而不是真正的解决方案:编写一个小型转储。并设置 Windows 为您自动执行此操作。
j
jard18

我在这里看到了很多执行信号处理程序然后退出的答案。这是要走的路,但请记住一个非常重要的事实:如果要获取生成的错误的核心转储,则不能调用 exit(status)。请改为调用 abort()


D
Daniil Iaitskov

我发现@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;
} 

J
Jim Buck

我将使用在 Visual Leak Detector 中为泄漏的内存生成堆栈跟踪的代码。不过,这只适用于 Win32。


并要求您随代码一起提供调试符号。一般情况下是不可取的。编写一个小型转储并设置 Windows 在未处理的异常情况下自动为您执行此操作。
t
twodayslate

如果您仍然想像我一样单独行动,您可以链接到 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]

e
enthusiasticgeek

除了上述答案之外,这里还有如何让 Debian Linux OS 生成核心转储

在用户的主文件夹中创建一个“coredumps”文件夹 转到 /etc/security/limits.conf。在 ' ' 行下方,键入“soft core unlimited”,如果为 root 启用核心转储,则键入“root soft core unlimited”,以便为核心转储提供无限空间。注意:“* soft core unlimited”不包括root,这就是为什么必须在自己的行中指定root。要检查这些值,请注销,重新登录,然后键入“ulimit -a”。 “核心文件大小”应设置为无限制。检查 .bashrc 文件(用户和 root,如果适用)以确保未在此处设置 ulimit。否则,上面的值将在启动时被覆盖。打开 /etc/sysctl.conf。在底部输入以下内容:“kernel.core_pattern = /home//coredumps/%e_%t.dump”。 (%e 将是进程名称,%t 将是系统时间)退出并输入“sysctl -p”以加载新配置检查 /proc/sys/kernel/core_pattern 并验证这与您刚刚输入的内容是否匹配. 核心转储可以通过在命令行(“&”)上运行一个进程,然后用“kill -11”杀死它来测试。如果核心转储成功,您将在分段错误指示后看到“(核心转储)”。


K
Kasprzol

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


O
Oleksandr Kozlov
gdb -ex 'set confirm off' -ex r -ex bt -ex q <my-program>

佚名

我忘记了“apport”的 GNOME 技术,但我对使用它知之甚少。它用于生成堆栈跟踪和其他诊断以进行处理,并且可以自动归档错误。这当然值得一试。


G
Graham Toal

你可能不会喜欢这个 - 我只能说它对我有用,而且我有类似但不完全相同的要求:我正在为 1970 年代类似 Algol 的语言编写一个编译器/转译器,它使用 C 作为它是输出然后编译 C,因此就用户而言,他们通常不知道涉及 C,因此尽管您可能将其称为转译器,但它实际上是一个使用 C 作为中间代码的编译器。被编译的语言具有在原始本机编译器中提供良好诊断和完整回溯的历史。我已经能够找到 gcc 编译器标志和库等,它们允许我捕获原始编译器所做的大部分运行时错误(尽管有一个明显的异常 - 未分配的变量捕获)。当发生运行时错误(例如算术溢出、除以零、数组索引越界等)时,原始编译器会向控制台输出一个回溯,列出每个活动过程调用的堆栈帧中的所有变量。我努力在 C 中获得这种效果,但最终只能用 hack 来描述......当程序被调用时,提供 C“main”的包装器会查看它的 argv,如果有特殊选项不存在,它在 gdb 下重新启动自身,并使用包含 gdb 选项和程序本身的“魔术”选项字符串的更改 argv。然后,这个重新启动的版本通过在调用用我们语言编写的代码的主块之前恢复原始参数来隐藏用户代码中的这些字符串。当错误发生时(只要它不是由用户代码显式捕获在程序中的错误),它会退出到 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 语法打印,而不是用户编写的语言的语法。并且(直到我在 C 的每个生成的行上添加合适的#line 指令 :-() 回溯列出了 C 源文件和行号。

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 "