Cách tự động tạo stacktrace khi chương trình của tôi gặp sự cố


590

Tôi đang làm việc trên Linux với trình biên dịch GCC. Khi chương trình C ++ của tôi gặp sự cố, tôi muốn nó tự động tạo một stacktrace.

Chương trình của tôi đang được điều hành bởi nhiều người dùng khác nhau và nó cũng chạy trên Linux, Windows và Macintosh (tất cả các phiên bản được biên dịch bằng cách sử dụng gcc).

Tôi muốn chương trình của tôi có thể tạo ra dấu vết ngăn xếp khi nó gặp sự cố và lần sau khi người dùng chạy nó, nó sẽ hỏi họ xem có thể gửi dấu vết ngăn xếp cho tôi để tôi có thể theo dõi vấn đề không. Tôi có thể xử lý việc gửi thông tin cho tôi nhưng tôi không biết cách tạo chuỗi theo dõi. Có ý kiến ​​gì không?


4
backtrace và backtrace_symbols_fd không an toàn async-signal. bạn không nên sử dụng các chức năng này trong trình xử lý tín hiệu
Parag Bafna

10
backtrace_symbols gọi malloc, và do đó không được sử dụng trong trình xử lý tín hiệu. Hai hàm khác (backtrace và backtrace_symbols_fd) không có vấn đề này và thường được sử dụng trong các trình xử lý tín hiệu.
cmccabe

3
@cmccabe đó là backtrace_symbols_fd không chính xác thường không gọi malloc nhưng có thể nếu họ gặp khó khăn trong khối catch_error của nó
Sam Saffron

6
Nó "có thể" theo nghĩa là không có thông số POSIX cho backtrace_symbols_fd (hoặc bất kỳ backtrace nào); tuy nhiên, backtrace_symbols_fd của GNU / Linux được chỉ định để không bao giờ gọi malloc, theo linux.die.net/man/3/backtrace_symbols_fd . Do đó, thật an toàn khi cho rằng nó sẽ không bao giờ gọi malloc trên Linux.
codetaku

Làm thế nào để nó sụp đổ?
Ciro Santilli 郝海东 冠状 病 事件 法轮功

Câu trả lời:


509

Đối với Linux và tôi tin rằng Mac OS X, nếu bạn đang sử dụng gcc hoặc bất kỳ trình biên dịch nào sử dụng glibc, bạn có thể sử dụng các hàm backtrace () trong execinfo.h để in stacktrace và thoát một cách duyên dáng khi bạn gặp lỗi phân đoạn. Tài liệu có thể được tìm thấy trong hướng dẫn libc .

Đây là một chương trình ví dụ cài đặt một SIGSEGVtrình xử lý và in một stacktrace đến stderrkhi nó phân tách. Các baz()chức năng ở đây làm cho segfault kích hoạt xử lý:

#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.
}

Biên dịch với -g -rdynamicthông tin biểu tượng cho bạn trong đầu ra của bạn, mà glibc có thể sử dụng để tạo một stacktrace đẹp:

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

Thực hiện điều này giúp bạn có đầu ra này:

$ ./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]

Điều này cho thấy mô-đun tải, bù và chức năng mà mỗi khung trong ngăn xếp đến từ. Ở đây bạn sẽ nhìn thấy xử lý tín hiệu trên đỉnh của ngăn xếp, và các chức năng libc trước khi mainthêm vào main, foo, bar, và baz.


53
Ngoài ra còn có /lib/libSegFault.so mà bạn có thể sử dụng với LD_PRELOAD.
CesarB

6
Có vẻ như hai mục đầu tiên trong đầu ra backtrace của bạn chứa một địa chỉ trả về bên trong trình xử lý tín hiệu và có thể là một bên sigaction()trong libc. Mặc dù backtrace của bạn có vẻ đúng, đôi khi tôi thấy rằng các bước bổ sung là cần thiết để đảm bảo vị trí thực sự của lỗi xuất hiện trong backtrace vì nó có thể được ghi đè sigaction()bằng kernel.
jschmier

9
Điều gì sẽ xảy ra nếu vụ tai nạn đến từ bên trong malloc? Sau đó, bạn sẽ không giữ một khóa và sau đó bị mắc kẹt khi "backtrace" cố gắng phân bổ bộ nhớ?
Mattias Nilsson

7
catchsegvkhông phải là những gì OP cần nhưng là tuyệt vời để nắm bắt các lỗi phân khúc và nhận được tất cả thông tin.
Matt Clarkson

8
Đối với ARM, tôi cũng phải biên dịch với các bảng -funwind. Nếu không, độ sâu ngăn xếp của tôi luôn là 1 (trống).
jfritz42

128

Nó thậm chí còn dễ dàng hơn "man backtrace", có một thư viện tài liệu nhỏ (cụ thể là GNU) được phân phối với glibc là libSegFault.so, mà tôi tin là do Ulrich Drepper viết để hỗ trợ chương trình Catchsegv (xem "man Catchsegv").

Điều này cho chúng ta 3 khả năng. Thay vì chạy "chương trình -o hai":

  1. Chạy trong Catchsegv:

    $ catchsegv program -o hai
  2. Liên kết với libSegFault khi chạy:

    $ LD_PRELOAD=/lib/libSegFault.so program -o hai
  3. Liên kết với libSegFault tại thời gian biên dịch:

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

Trong cả 3 trường hợp, bạn sẽ nhận được các backtraces rõ ràng hơn với ít tối ưu hóa hơn (gcc -O0 hoặc -O1) và các biểu tượng gỡ lỗi (gcc -g). Nếu không, bạn chỉ có thể kết thúc với một đống địa chỉ bộ nhớ.

Bạn cũng có thể bắt được nhiều tín hiệu hơn cho dấu vết ngăn xếp với thứ gì đó như:

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

Đầu ra sẽ trông giống như thế này (chú ý backtrace ở phía dưới):

*** 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]

Nếu bạn muốn biết thông tin chi tiết, nguồn tốt nhất không may là nguồn: Xem http://sourceware.org/git/?p=glibc.git;a=blob;f=debug/segfault.c và thư mục mẹ của nó http://sourceware.org/git/?p=glibc.git;a=tree;f=debug


1
"Khả năng 3. Liên kết với libSegFault tại thời gian biên dịch" không hoạt động.
HHK

5
@crafter: Ý bạn là "không hoạt động". Bạn đã thử những gì, trên ngôn ngữ / trình biên dịch / toolchain / phân phối / phần cứng? Nó đã thất bại trong việc biên dịch? Để bắt lỗi? Để sản xuất đầu ra ở tất cả? Để sản xuất đầu ra khó sử dụng? Cảm ơn bạn đã chi tiết nó sẽ giúp tất cả mọi người.
Stéphane Gourichon

1
'Nguồn tốt nhất không may là nguồn' ... Hy vọng, một ngày nào đó, trang man cho Catchsegv sẽ thực sự đề cập đến SEGFAULT_SIGNALS. Cho đến lúc đó, có câu trả lời này để tham khảo.
greggo

Tôi không thể tin rằng tôi đã lập trình C được 5 năm và chưa bao giờ nghe về điều này: /
DavidMFrey

6
@ StéphaneGourichon @HansKratz Để liên kết với libSegFault, bạn sẽ phải thêm -Wl,--no-as-neededvào các cờ biên dịch. Mặt khác, ldthực sự sẽ không liên kết với nhau libSegFault, bởi vì nó nhận ra rằng nhị phân không sử dụng bất kỳ ký hiệu nào của nó.
Phillip

122

Linux

Mặc dù việc sử dụng các hàm backtrace () trong execinfo.h để in stacktrace và thoát một cách duyên dáng khi bạn gặp lỗi phân đoạn đã được đề xuất , tôi không thấy đề cập đến những điều phức tạp cần thiết để đảm bảo các điểm backtrace dẫn đến vị trí thực tế của lỗi (ít nhất là đối với một số kiến ​​trúc - x86 & ARM).

Hai mục đầu tiên trong chuỗi khung ngăn xếp khi bạn vào trình xử lý tín hiệu chứa địa chỉ trả về bên trong trình xử lý tín hiệu và một mục bên trong sigaction () trong libc. Khung ngăn xếp của chức năng cuối cùng được gọi trước khi tín hiệu (là vị trí của lỗi) bị mất.

#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;
 struct ucontext   *uc_link;
 stack_t           uc_stack;
 struct sigcontext 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);
}

Đầu ra

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]

Tất cả các mối nguy hiểm khi gọi các hàm backtrace () trong trình xử lý tín hiệu vẫn tồn tại và không nên bỏ qua, nhưng tôi thấy chức năng tôi mô tả ở đây khá hữu ích trong việc gỡ lỗi.

Điều quan trọng cần lưu ý là ví dụ tôi cung cấp được phát triển / thử nghiệm trên Linux cho x86. Tôi cũng đã thực hiện thành công điều này trên ARM bằng cách sử dụng uc_mcontext.arm_pcthay vìuc_mcontext.eip .

Đây là một liên kết đến bài viết mà tôi đã tìm hiểu chi tiết cho việc triển khai này: http://www.linuxjournal.com/article/6391


11
Trên các hệ thống sử dụng GNU ld, hãy nhớ biên dịch -rdynamicđể hướng dẫn trình liên kết thêm tất cả các ký hiệu, không chỉ các ký hiệu được sử dụng vào bảng ký hiệu động. Điều này cho phép backtrace_symbols()chuyển đổi địa chỉ thành tên hàm
jschmier

1
Ngoài ra, bạn cần thêm tùy chọn "-mapcs-frame" vào dòng lệnh của GCC để tạo khung ngăn xếp trên nền tảng ARM
qehgt

3
Điều này có thể là quá muộn nhưng chúng ta có thể sử dụng addr2linelệnh bằng cách nào đó để có được dòng chính xác nơi xảy ra sự cố?
nhiệt tình

4
Trên các bản dựng gần đây hơn glibc uc_mcontextkhông chứa một trường có tên eip. Bây giờ có một mảng cần được lập chỉ mục, uc_mcontext.gregs[REG_EIP]là tương đương.
mmlb

6
Đối với ARM, backtraces của tôi luôn có độ sâu 1 cho đến khi tôi thêm tùy chọn -funwind-bảng vào trình biên dịch.
jfritz42

84

Mặc dù một câu trả lời đúng đã được cung cấp mô tả làm thế nào để sử dụng GNU libc backtrace()chức năng 1 và tôi cung cấp câu trả lời của riêng tôi mô tả làm thế nào để đảm bảo một vết lùi từ một điểm xử lý tín hiệu đến vị trí thực tế của lỗi 2 , tôi không thấy bất kỳ đề cập nào về việc sắp xếp các biểu tượng C ++ xuất phát từ backtrace.

Khi nhận được backtraces từ chương trình C ++, đầu ra có thể được chạy qua c++filt1 để giải mã các ký hiệu hoặc bằng cách sử dụng trực tiếp 1 .abi::__cxa_demangle

  • 1 Linux & OS X Lưu ý rằng c++filt__cxa_demanglecụ thể là GCC
  • 2 Linux

Ví dụ C ++ Linux sau đây sử dụng trình xử lý tín hiệu tương tự như câu trả lời khác của tôi và giải thích cách c++filtsử dụng để giải mã các ký hiệu.

Mã số :

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

Đầu ra ( ./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]

Đầu ra có dấu ( ./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]

Phần sau đây dựa trên trình xử lý tín hiệu từ câu trả lời ban đầu của tôi và có thể thay thế trình xử lý tín hiệu trong ví dụ trên để giải thích cách abi::__cxa_demanglesử dụng để giải mã các ký hiệu. Trình xử lý tín hiệu này tạo ra đầu ra được khử giống như ví dụ trên.

Mã số :

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

1
Cảm ơn bạn vì điều này, jschmier. Tôi đã tạo một tập lệnh bash nhỏ để cung cấp đầu ra của điều này vào tiện ích addr2line. Xem: stackoverflow.com/a/15801966/1797414
Array_sea

4
Đừng quên #include <cxxabi.h>
Bamaco

1
Tài liệu tốt và một tệp tiêu đề đơn giản đã được đăng ở đây kể từ năm 2008 ... panthema.net/2008/0901-stacktrace-demangled rất giống với cách tiếp cận của bạn :)
kevinf

abi :: __ cxa_demangle dường như không phải là tín hiệu an toàn không đồng bộ, vì vậy trình xử lý tín hiệu có thể bế tắc ở đâu đó trong malloc.
orcy

Việc sử dụng std::cerr, free()exit()tất cả các vi phạm hạn chế đối với các cuộc gọi gọi phi async-tín hiệu an toàn trên hệ thống POSIX. Mã này sẽ bế tắc nếu quá trình của bạn thất bại trong bất kỳ cuộc gọi như free(), malloc() newhoặc detete.
Andrew Henle

31

Có thể đáng để xem Google Breakpad , một công cụ tạo bãi đổ đa nền tảng và các công cụ để xử lý các bãi chứa.


Nó báo cáo về những thứ như lỗi phân đoạn, nhưng nó không báo cáo bất kỳ thông tin nào về các ngoại lệ C ++ chưa được xử lý.
DBedrenko

21

Bạn đã không chỉ định hệ điều hành của mình, vì vậy điều này rất khó trả lời. Nếu bạn đang sử dụng một hệ thống dựa trên gnu libc, bạn có thể sử dụng chức năng libc backtrace().

GCC cũng có hai nội dung có thể hỗ trợ bạn, nhưng có thể hoặc không thể triển khai đầy đủ trên kiến ​​trúc của bạn và đó là __builtin_frame_address__builtin_return_address. Cả hai đều muốn một mức nguyên ngay lập tức (bởi ngay lập tức, ý tôi là nó không thể là một biến). Nếu __builtin_frame_addressvới một mức nhất định là khác không, sẽ an toàn khi lấy địa chỉ trả về cùng cấp.


13

Cảm ơn bạn nhiệt tình đã thu hút sự chú ý của tôi đến tiện ích addr2line.

Tôi đã viết một kịch bản nhanh và bẩn để xử lý đầu ra của câu trả lời được cung cấp ở đây : (cảm ơn jschmier!) Bằng cách sử dụng tiện ích addr2line.

Kịch bản chấp nhận một đối số duy nhất: Tên của tệp chứa đầu ra từ tiện ích của jschmier.

Đầu ra sẽ in một cái gì đó như sau cho mỗi cấp độ của dấu vết:

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;

Mã số:

#!/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 

12

ulimit -c <value>đặt giới hạn kích thước tệp lõi trên unix. Theo mặc định, giới hạn kích thước tệp lõi là 0. Bạn có thể thấy các ulimitgiá trị của mình với ulimit -a.

đồng thời, nếu bạn chạy chương trình của mình từ bên trong gdb, nó sẽ tạm dừng chương trình của bạn về "vi phạm phân khúc" ( SIGSEGVnói chung là khi bạn truy cập vào một phần bộ nhớ mà bạn đã phân bổ) hoặc bạn có thể đặt điểm dừng.

ddd và nemiver là giao diện người dùng cho gdb, giúp làm việc với người mới dễ dàng hơn nhiều.


6
Các bãi chứa lõi vô cùng hữu ích hơn so với dấu vết ngăn xếp vì bạn có thể tải kết xuất lõi trong trình gỡ lỗi và xem trạng thái của toàn bộ chương trình và dữ liệu của nó tại điểm xảy ra sự cố.
Adam Hawes

1
Tiện ích backtrace mà những người khác đã đề xuất có lẽ tốt hơn không có gì, nhưng nó rất cơ bản - thậm chí nó không cung cấp số dòng. Mặt khác, sử dụng các bãi chứa lõi, cho phép bạn xem lại toàn bộ trạng thái ứng dụng của mình tại thời điểm ứng dụng bị hỏng (bao gồm cả dấu vết ngăn xếp chi tiết). Có thể có những vấn đề thực tế khi cố gắng sử dụng điều này để gỡ lỗi trường, nhưng nó chắc chắn là một công cụ mạnh hơn để phân tích các sự cố và xác nhận trong quá trình phát triển (ít nhất là trên Linux).
tộc

10

Điều quan trọng cần lưu ý là một khi bạn tạo một tệp cốt lõi, bạn sẽ cần sử dụng công cụ gdb để xem xét nó. Để gdb hiểu ý nghĩa của tệp cốt lõi của bạn, bạn phải nói với gcc để tạo công cụ nhị phân bằng các ký hiệu gỡ lỗi: để thực hiện việc này, bạn biên dịch bằng cờ -g:

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

Sau đó, bạn có thể đặt "ulimit -c không giới hạn" để cho phép kết xuất lõi hoặc chỉ chạy chương trình của bạn bên trong gdb. Tôi thích cách tiếp cận thứ hai hơn:

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

Tôi hi vọng cái này giúp được.


4
Bạn cũng có thể gọi gdbngay từ chương trình bị rơi của bạn. Trình xử lý thiết lập cho SIGSEGV, SEGILL, SIGBUS, SIGFPE sẽ gọi gdb. Chi tiết: stackoverflow.com/questions/3151779/ Mạnh Ưu điểm là bạn có được backtrace đẹp, có chú thích như trong bt full, bạn cũng có thể có được dấu vết ngăn xếp của tất cả các luồng.
Vi.

Bạn cũng có thể nhận được backtrace dễ dàng hơn trong câu trả lời: gdb -silent ./prog core --eval-command = backtrace --batch -it sẽ hiển thị backtrace và close debugger
baziorek

10

Tôi đã xem xét vấn đề này một thời gian.

Và chôn sâu trong Công cụ hiệu suất Google README

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

nói về libunwind

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

Rất thích nghe ý kiến ​​của thư viện này.

Vấn đề với -rdynamic là nó có thể tăng kích thước của nhị phân tương đối đáng kể trong một số trường hợp


2
Trên x86 / 64, tôi chưa thấy kích thước nhị phân tăng nhiều. Thêm -g làm cho một sự gia tăng lớn hơn nhiều.
Dan

1
Tôi nhận thấy rằng libunwind không có chức năng để lấy số dòng và tôi đoán (không kiểm tra) yet_get_proc_name trả về ký hiệu hàm (bị che khuất vì quá tải và như vậy) thay vì tên ban đầu.
Herbert

1
Đúng rồi. Làm điều này rất khó để làm điều này một cách chính xác, nhưng tôi đã thành công tuyệt vời với gaddr2line, có rất nhiều thông tin thực tế ở đây blog.bigpixel.ro/2010/09/stack-unwinding-stack-trace-with-gcc
Gregory


9

Bạn có thể sử dụng DeathHandler - lớp C ++ nhỏ, mọi thứ cho bạn, đáng tin cậy.


1
thật không may, nó sử dụng execlp()để thực hiện các cuộc gọi addr2line ... sẽ rất tuyệt nếu bạn hoàn toàn ở trong chương trình riêng (có thể bằng cách bao gồm mã addr2line ở một số dạng)
ví dụ

9

Hãy quên việc thay đổi nguồn của bạn và thực hiện một số hack với hàm backtrace () hoặc macro - đây chỉ là những giải pháp kém.

Là một giải pháp làm việc đúng đắn, tôi sẽ tư vấn:

  1. Biên dịch chương trình của bạn với cờ "-g" để nhúng các biểu tượng gỡ lỗi thành nhị phân (đừng lo điều này sẽ không ảnh hưởng đến hiệu suất của bạn).
  2. Trên linux chạy lệnh tiếp theo: "ulimit -c không giới hạn" - để cho phép hệ thống tạo ra các sự cố lớn.
  3. Khi chương trình của bạn bị sập, trong thư mục làm việc, bạn sẽ thấy tệp "lõi".
  4. Chạy lệnh tiếp theo để in backtrace sang stdout: gdb -batch -ex "backtrace" ./your_program_exe ./core

Điều này sẽ in backtrace thích hợp của chương trình của bạn theo cách có thể đọc được của con người (với tên tệp nguồn và số dòng). Ngoài ra, cách tiếp cận này sẽ cho phép bạn tự động tự động hóa hệ thống của mình: có một đoạn mã ngắn kiểm tra xem quy trình có tạo ra kết xuất lõi hay không, sau đó gửi backtraces qua email cho nhà phát triển hoặc đăng nhập vào hệ thống ghi nhật ký.


Nó cho số dòng sai. Nó có thể được cải thiện?
HeyJude

7
ulimit -c unlimited

là một biến hệ thống, wich sẽ cho phép tạo kết xuất lõi sau khi ứng dụng của bạn gặp sự cố. Trong trường hợp này một số lượng không giới hạn. Hãy tìm một tập tin gọi là core trong cùng thư mục. Hãy chắc chắn rằng bạn đã biên dịch mã của mình với thông tin gỡ lỗi được kích hoạt!

Trân trọng


5
Người dùng không yêu cầu một bãi chứa lõi. Anh ta yêu cầu một dấu vết ngăn xếp. Xem delorie.com/gnu/docs/glibc/libc_665.html
Todd Gamblin

1
một bãi chứa lõi sẽ chứa ngăn xếp cuộc gọi tại thời điểm xảy ra sự cố, phải không?

3
Bạn đang giả sử anh ta trên Unix và sử dụng Bash.
Paul Tomblin

2
Nếu bạn đang sử dụng tcsh, bạn phải làmlimit coredumpsize unlimited
sivabudh


6

Xem tiện ích Stack Trace trong ACE (Môi trường giao tiếp ADAPTIVE). Nó đã được viết để bao gồm tất cả các nền tảng chính (và nhiều hơn nữa). Thư viện được cấp phép theo kiểu BSD để bạn thậm chí có thể sao chép / dán mã nếu bạn không muốn sử dụng ACE.


Các liên kết dường như đã chết.
tglas

5

Tôi có thể giúp với phiên bản Linux: có thể sử dụng chức năng backtrace, backtrace_symbols và backtrace_symbols_fd. Xem các trang hướng dẫn tương ứng.


5

Có vẻ như trong một trong những phiên bản thư viện c ++ boost xuất hiện cuối cùng để cung cấp chính xác những gì bạn muốn, có lẽ mã sẽ là đa nền tảng. Đó là boost :: stacktrace , mà bạn có thể sử dụng như trong mẫu 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
}

Trong Linux Bạn biên dịch mã ở trên:

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

Ví dụ backtrace được sao chép từ tài liệu boost :

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

4

* nix: bạn có thể chặn SIGSEGV (thông thường tín hiệu này được đưa ra trước khi gặp sự cố) và giữ thông tin vào một tệp. (bên cạnh tệp lõi mà bạn có thể sử dụng để gỡ lỗi bằng gdb chẳng hạn).

giành chiến thắng: Kiểm tra điều này từ msd.

Bạn cũng có thể xem mã chrome của google để xem cách xử lý sự cố. Nó có một cơ chế xử lý ngoại lệ tốt đẹp.


SEH không giúp đỡ trong việc tạo ra một dấu vết ngăn xếp. Mặc dù nó có thể là một phần của giải pháp, nhưng giải pháp đó khó thực hiện hơn và cung cấp ít thông tin hơn với chi phí tiết lộ nhiều thông tin về ứng dụng của bạn hơn giải pháp thực sự : Viết một kết xuất nhỏ. Và thiết lập Windows để làm điều này tự động cho bạn.
IInspectable

4

Tôi thấy rằng giải pháp @tgamblin chưa hoàn thành. Nó không thể xử lý với stackoverflow. Tôi nghĩ bởi vì theo mặc định, trình xử lý tín hiệu được gọi với cùng một ngăn xếp và SIGSEGV được ném hai lần. Để bảo vệ bạn cần đăng ký một ngăn xếp độc lập cho trình xử lý tín hiệu.

Bạn có thể kiểm tra điều này với mã dưới đây. Theo mặc định, trình xử lý không thành công. Với macro được xác định STACK_OVERFLOW, mọi thứ đều ổn.

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

4

Vị vua mới trong thị trấn đã đến https://github.com/bombela/backward-cpp

1 tiêu đề để đặt trong mã của bạn và 1 thư viện để cài đặt.

Cá nhân tôi gọi nó bằng chức năng này

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

Ồ Cuối cùng đó là cách nó nên được thực hiện! Tôi vừa bỏ qua giải pháp riêng ủng hộ cái này.
tglas

3

Tôi sẽ sử dụng mã tạo ra dấu vết ngăn xếp cho bộ nhớ bị rò rỉ trong Visual Leak dò . Điều này chỉ hoạt động trên Win32, mặc dù.


Và yêu cầu bạn gửi các biểu tượng gỡ lỗi với mã của bạn. Nói chung là không mong muốn. Viết một kết xuất nhỏ và thiết lập Windows để tự động thực hiện cho bạn trong các trường hợp ngoại lệ chưa được xử lý.
IInspectable

3

Tôi đã thấy rất nhiều câu trả lời ở đây thực hiện một trình xử lý tín hiệu và sau đó thoát ra. Đó là cách để đi, nhưng hãy nhớ một thực tế rất quan trọng: Nếu bạn muốn nhận kết xuất lõi cho lỗi được tạo, bạn không thể gọi exit(status). Gọi abort()thay!


3

Là một giải pháp chỉ dành cho Windows, bạn có thể nhận được tương đương với theo dõi ngăn xếp (với nhiều thông tin hơn, nhiều hơn nữa) bằng Báo cáo Lỗi Windows . Chỉ với một vài mục đăng ký, nó có thể được thiết lập để thu thập các kết xuất chế độ người dùng :

Bắt đầu với Windows Server 2008 và Windows Vista với Gói dịch vụ 1 (SP1), Báo cáo lỗi Windows (WER) có thể được cấu hình để các bãi chứa chế độ người dùng đầy đủ được thu thập và lưu trữ cục bộ sau khi ứng dụng ở chế độ người dùng gặp sự cố. [...]

Tính năng này không được bật theo mặc định. Kích hoạt tính năng yêu cầu đặc quyền của quản trị viên. Để bật và định cấu hình tính năng, hãy sử dụng các giá trị đăng ký sau trong khóa HKEY_LOCAL_MACHINE \ SOFTWARE \ Microsoft \ Windows \ Windows Error Reporting \ LocalDumps .

Bạn có thể đặt các mục đăng ký từ trình cài đặt của mình, có các đặc quyền bắt buộc.

Tạo kết xuất chế độ người dùng có các ưu điểm sau so với việc tạo theo dõi ngăn xếp trên máy khách:

  • Nó đã được thực hiện trong hệ thống. Bạn có thể sử dụng WER như đã nêu ở trên hoặc tự gọi MiniDumpWriteDump , nếu bạn cần kiểm soát chi tiết hơn đối với lượng thông tin cần kết xuất. (Đảm bảo gọi nó từ một quy trình khác.)
  • Cách đầy đủ hơn một dấu vết ngăn xếp. Trong số những thứ khác, nó có thể chứa các biến cục bộ, đối số hàm, ngăn xếp cho các luồng khác, các mô-đun được tải, v.v. Lượng dữ liệu (và do đó kích thước) có khả năng tùy biến cao.
  • Không cần phải gửi biểu tượng gỡ lỗi. Điều này vừa làm giảm đáng kể quy mô triển khai của bạn, vừa khiến việc thiết kế ngược ứng dụng của bạn trở nên khó khăn hơn.
  • Phần lớn độc lập với trình biên dịch bạn sử dụng. Sử dụng WER thậm chí không yêu cầu bất kỳ mã. Dù bằng cách nào, có một cách để có được cơ sở dữ liệu ký hiệu (PDB) rất hữu ích cho phân tích ngoại tuyến. Tôi tin rằng GCC có thể tạo PDB hoặc có các công cụ để chuyển đổi cơ sở dữ liệu ký hiệu sang định dạng PDB.

Xin lưu ý rằng WER chỉ có thể được kích hoạt bởi sự cố ứng dụng (tức là hệ thống chấm dứt một quá trình do một ngoại lệ chưa được xử lý). MiniDumpWriteDumpcó thể được gọi bất cứ lúc nào Điều này có thể hữu ích nếu bạn cần bỏ trạng thái hiện tại để chẩn đoán các vấn đề khác ngoài sự cố.

Đọc bắt buộc, nếu bạn muốn đánh giá khả năng áp dụng của các bãi nhỏ:


2

Ngoài các câu trả lời ở trên, đây là cách bạn tạo hệ điều hành Debian Linux tạo kết xuất lõi

  1. Tạo một thư mục Coredumps vào trong thư mục nhà của người dùng
  2. Truy cập /etc/security/limits.conf. Bên dưới dòng '', gõ lõi mềm không giới hạn, không giới hạn, và lõi mềm không giới hạn, nếu cho phép kết xuất lõi cho root, để cho phép không gian không giới hạn cho các bãi lõi.
  3. LƯU Ý: Lõi * không giới hạn lõi mềm không bao gồm root, đó là lý do tại sao root phải được chỉ định trong dòng riêng của nó.
  4. Để kiểm tra các giá trị này, hãy đăng xuất, đăng nhập lại và nhập vào ul ulit -a Kiếm. Kích thước tập tin lõi Core nên được đặt thành không giới hạn.
  5. Kiểm tra các tệp .bashrc (người dùng và root nếu có) để đảm bảo rằng ulimit không được đặt ở đó. Nếu không, giá trị trên sẽ được ghi đè khi khởi động.
  6. Mở /etc/sysctl.conf. Nhập thông tin sau ở dưới cùng: Hạt nhân.core_potype = /home//coredumps/%e_%t.dump tựa. (% e sẽ là tên quy trình và% t sẽ là thời gian hệ thống)
  7. Thoát và gõ vào sysctl -pv để tải cấu hình mới Kiểm tra / Proc / sys / kernel / core_potype và xác minh rằng điều này phù hợp với những gì bạn vừa nhập.
  8. Việc bán phá giá cốt lõi có thể được kiểm tra bằng cách chạy một quy trình trên dòng lệnh (Nhận & Trực), và sau đó giết nó bằng cách giết chết -11. Nếu việc bán phá giá lõi thành công, bạn sẽ thấy Lọ (lõi bị đổ) sau dấu hiệu lỗi phân đoạn.

2

Nếu bạn vẫn muốn đi một mình như tôi đã làm, bạn có thể liên kết bfdvà tránh sử dụng addr2linenhư tôi đã làm ở đây:

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

Điều này tạo ra đầu ra:

[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]

1

Trên Linux / unix / MacOSX sử dụng các tệp lõi (bạn có thể kích hoạt chúng bằng lệnh gọi hệ thống ulimit hoặc tương thích ). Trên Windows sử dụng báo cáo lỗi của Microsoft (bạn có thể trở thành đối tác và có quyền truy cập vào dữ liệu sự cố ứng dụng của mình).


0

Tôi đã quên công nghệ "apport" của Gnome, nhưng tôi không biết nhiều về việc sử dụng nó. Nó được sử dụng để tạo stacktraces và các chẩn đoán khác để xử lý và có thể tự động báo lỗi. Đó chắc chắn là giá trị kiểm tra.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.