cetak tumpukan panggilan dalam C atau C ++


120

Apakah ada cara untuk membuang tumpukan panggilan dalam proses yang sedang berjalan di C atau C ++ setiap kali fungsi tertentu dipanggil? Yang ada dalam pikiran saya adalah seperti ini:

void foo()
{
   print_stack_trace();

   // foo's body

   return
}

Dimana print_stack_tracecara kerjanya mirip dengan callerdi Perl.

Atau sesuatu seperti ini:

int main (void)
{
    // will print out debug info every time foo() is called
    register_stack_trace_function(foo); 

    // etc...
}

di mana register_stack_trace_functionmenempatkan semacam breakpoint internal yang akan menyebabkan jejak tumpukan dicetak setiap kali foodipanggil.

Apakah yang seperti ini ada di beberapa pustaka C standar?

Saya bekerja di Linux, menggunakan GCC.


Latar Belakang

Saya memiliki uji coba yang berperilaku berbeda berdasarkan beberapa sakelar baris perintah yang seharusnya tidak memengaruhi perilaku ini. Kode saya memiliki generator nomor pseudo-acak yang saya asumsikan dipanggil berbeda berdasarkan sakelar ini. Saya ingin dapat menjalankan pengujian dengan setiap set sakelar dan melihat apakah generator nomor acak dipanggil berbeda untuk masing-masing.


1
@Armen, apakah Anda akrab dengan semua ini?
Nathan Fellman

1
@Nathan: Jika debugger Anda adalah gdb, kasus tersebut dapat ditangani . Saya tidak dapat memberi tahu Anda tentang orang lain, tetapi saya berasumsi bahwa gdb tidak sendirian dalam memiliki fungsi ini. Selain: Saya baru saja melihat komentar saya sebelumnya. :: gag :: s/easier/either/bagaimana sih itu bisa terjadi?
dmckee --- kucing mantan moderator

2
@dmckee: Sebenarnya, seharusnya begitu s/either/easier. Yang perlu saya lakukan dengan gdb adalah menulis skrip yang melanggar fungsi itu dan mencetak jejak tumpukan, lalu melanjutkan. Sekarang setelah dipikir-pikir, mungkin sudah waktunya saya belajar tentang gdb scripting.
Nathan Fellman

1
Gah! Akan tidur. Segera nyata sekarang ...
dmckee --- kucing mantan moderator

Jawaban:


79

Untuk solusi khusus linux, Anda dapat menggunakan lacak balik (3) yang hanya mengembalikan larik void *(sebenarnya masing-masing titik ini mengarah ke alamat pengirim dari bingkai tumpukan yang sesuai). Untuk menerjemahkan ini menjadi sesuatu yang berguna, ada backtrace_symbols (3) .

Perhatikan bagian catatan di lacak balik (3) :

Nama simbol mungkin tidak tersedia tanpa menggunakan opsi linker khusus. Untuk sistem yang menggunakan GNU linker, perlu menggunakan opsi -rdynamic linker. Perhatikan bahwa nama fungsi "statis" tidak ditampilkan, dan tidak akan tersedia di lacak balik.



8
Windows memiliki CaptureStackBackTrace
bobobobo


glibcSayangnya, di Linux dengan backtrace_symbolsfungsi tidak menyediakan nama fungsi, nama file sumber, dan nomor baris.
Maxim Egorushkin

Selain menggunakan -rdynamic, periksa juga apakah sistem build Anda tidak menambahkan -fvisibility=hiddenopsi! (karena itu akan benar-benar membuang efek -rdynamic)
Dima Litvinov

38

Tingkatkan stacktrace

Didokumentasikan di: https://www.boost.org/doc/libs/1_66_0/doc/html/stacktrace/getting_started.html#stacktrace.getting_started.how_to_print_current_call_stack

Ini adalah opsi paling nyaman yang pernah saya lihat sejauh ini, karena:

  • benar-benar dapat mencetak nomor baris.

    Itu hanya membuat panggilan ke addr2lineNamun , yang jelek dan mungkin lambat jika Anda mengambil terlalu banyak jejak.

  • demangles secara default

  • Boost hanya untuk tajuk, jadi kemungkinan besar tidak perlu mengubah sistem build Anda

boost_stacktrace.cpp

#include <iostream>

#define BOOST_STACKTRACE_USE_ADDR2LINE
#include <boost/stacktrace.hpp>

void my_func_2(void) {
    std::cout << boost::stacktrace::stacktrace() << std::endl;
}

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

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

int main(int argc, char **argv) {
    long long unsigned int n;
    if (argc > 1) {
        n = strtoul(argv[1], NULL, 0);
    } else {
        n = 1;
    }
    for (long long unsigned int i = 0; i < n; ++i) {
        my_func_1(1);   // line 28
        my_func_1(2.0); // line 29
    }
}

Sayangnya, ini tampaknya merupakan tambahan yang lebih baru, dan paket libboost-stacktrace-devtersebut tidak ada di Ubuntu 16.04, hanya 18.04:

sudo apt-get install libboost-stacktrace-dev
g++ -fno-pie -ggdb3 -O0 -no-pie -o boost_stacktrace.out -std=c++11 \
  -Wall -Wextra -pedantic-errors boost_stacktrace.cpp -ldl
./boost_stacktrace.out

Kami harus menambahkan -ldldi akhir atau kompilasi gagal.

Keluaran:

 0# boost::stacktrace::basic_stacktrace<std::allocator<boost::stacktrace::frame> >::basic_stacktrace() at /usr/include/boost/stacktrace/stacktrace.hpp:129
 1# my_func_1(int) at /home/ciro/test/boost_stacktrace.cpp:18
 2# main at /home/ciro/test/boost_stacktrace.cpp:29 (discriminator 2)
 3# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 4# _start in ./boost_stacktrace.out

 0# boost::stacktrace::basic_stacktrace<std::allocator<boost::stacktrace::frame> >::basic_stacktrace() at /usr/include/boost/stacktrace/stacktrace.hpp:129
 1# my_func_1(double) at /home/ciro/test/boost_stacktrace.cpp:13
 2# main at /home/ciro/test/boost_stacktrace.cpp:27 (discriminator 2)
 3# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 4# _start in ./boost_stacktrace.out

Keluaran dan selanjutnya dijelaskan pada bagian "lacak balik glibc" di bawah ini, yang analog.

Perhatikan bagaimana my_func_1(int)dan my_func_1(float), yang rusak karena kelebihan beban fungsi , telah dirusak dengan baik untuk kami.

Perhatikan bahwa intpanggilan pertama dimatikan oleh satu baris (28 bukannya 27 dan yang kedua terputus oleh dua baris (27 bukannya 29). Disarankan dalam komentar bahwa ini karena alamat instruksi berikut sedang dipertimbangkan, yang mana membuat 27 menjadi 28, dan 29 melompat keluar lingkaran dan menjadi 27.

Kami kemudian mengamati bahwa dengan -O3, output sepenuhnya dimutilasi:

 0# boost::stacktrace::basic_stacktrace<std::allocator<boost::stacktrace::frame> >::size() const at /usr/include/boost/stacktrace/stacktrace.hpp:215
 1# my_func_1(double) at /home/ciro/test/boost_stacktrace.cpp:12
 2# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 3# _start in ./boost_stacktrace.out

 0# boost::stacktrace::basic_stacktrace<std::allocator<boost::stacktrace::frame> >::size() const at /usr/include/boost/stacktrace/stacktrace.hpp:215
 1# main at /home/ciro/test/boost_stacktrace.cpp:31
 2# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 3# _start in ./boost_stacktrace.out

Backtrack pada umumnya tidak dapat diperbaiki lagi oleh pengoptimalan. Pengoptimalan tail call adalah contoh penting dari itu: Apa itu pengoptimalan panggilan ekor?

Tolok ukur berjalan pada -O3:

time  ./boost_stacktrace.out 1000 >/dev/null

Keluaran:

real    0m43.573s
user    0m30.799s
sys     0m13.665s

Jadi seperti yang diharapkan, kami melihat bahwa metode ini kemungkinan besar sangat lambat untuk panggilan eksternal ke addr2line, dan hanya dapat dilakukan jika sejumlah panggilan sedang dibuat.

Setiap pencetakan lacak balik tampaknya memakan waktu ratusan milidetik, jadi berhati-hatilah bahwa jika lacak balik sangat sering terjadi, kinerja program akan sangat terganggu.

Diuji pada Ubuntu 19.10, GCC 9.2.1, boost 1.67.0.

glibc backtrace

Didokumentasikan di: https://www.gnu.org/software/libc/manual/html_node/Backtraces.html

main.c

#include <stdio.h>
#include <stdlib.h>

/* Paste this on the file you want to debug. */
#include <stdio.h>
#include <execinfo.h>
void print_trace(void) {
    char **strings;
    size_t i, size;
    enum Constexpr { MAX_SIZE = 1024 };
    void *array[MAX_SIZE];
    size = backtrace(array, MAX_SIZE);
    strings = backtrace_symbols(array, size);
    for (i = 0; i < size; i++)
        printf("%s\n", strings[i]);
    puts("");
    free(strings);
}

void my_func_3(void) {
    print_trace();
}

void my_func_2(void) {
    my_func_3();
}

void my_func_1(void) {
    my_func_3();
}

int main(void) {
    my_func_1(); /* line 33 */
    my_func_2(); /* line 34 */
    return 0;
}

Menyusun:

gcc -fno-pie -ggdb3 -O3 -no-pie -o main.out -rdynamic -std=c99 \
  -Wall -Wextra -pedantic-errors main.c

-rdynamic adalah pilihan kunci yang diperlukan.

Lari:

./main.out

Keluaran:

./main.out(print_trace+0x2d) [0x400a3d]
./main.out(main+0x9) [0x4008f9]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f35a5aad830]
./main.out(_start+0x29) [0x400939]

./main.out(print_trace+0x2d) [0x400a3d]
./main.out(main+0xe) [0x4008fe]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f35a5aad830]
./main.out(_start+0x29) [0x400939]

Jadi kami segera melihat bahwa pengoptimalan sebaris terjadi, dan beberapa fungsi hilang dari pelacakan.

Jika kami mencoba mendapatkan alamat:

addr2line -e main.out 0x4008f9 0x4008fe

kami memperoleh:

/home/ciro/main.c:21
/home/ciro/main.c:36

yang benar-benar mati.

Jika kita melakukan hal yang sama -O0sebagai gantinya, ./main.outberikan jejak lengkap yang benar:

./main.out(print_trace+0x2e) [0x4009a4]
./main.out(my_func_3+0x9) [0x400a50]
./main.out(my_func_1+0x9) [0x400a68]
./main.out(main+0x9) [0x400a74]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f4711677830]
./main.out(_start+0x29) [0x4008a9]

./main.out(print_trace+0x2e) [0x4009a4]
./main.out(my_func_3+0x9) [0x400a50]
./main.out(my_func_2+0x9) [0x400a5c]
./main.out(main+0xe) [0x400a79]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f4711677830]
./main.out(_start+0x29) [0x4008a9]

lalu:

addr2line -e main.out 0x400a74 0x400a79

memberikan:

/home/cirsan01/test/main.c:34
/home/cirsan01/test/main.c:35

jadi garisnya putus hanya satu, TODO kenapa? Tapi ini mungkin masih bisa digunakan.

Kesimpulan: backtraces hanya mungkin dapat ditampilkan dengan sempurna -O0. Dengan pengoptimalan, lacak balik asli secara fundamental diubah dalam kode yang dikompilasi.

Saya tidak dapat menemukan cara sederhana untuk secara otomatis menghapus simbol C ++ dengan ini, namun berikut beberapa retasan:

Diuji pada Ubuntu 16.04, GCC 6.4.0, libc 2.23.

glibc backtrace_symbols_fd

Helper ini sedikit lebih nyaman daripada backtrace_symbols, dan menghasilkan keluaran yang pada dasarnya identik:

/* Paste this on the file you want to debug. */
#include <execinfo.h>
#include <stdio.h>
#include <unistd.h>
void print_trace(void) {
    size_t i, size;
    enum Constexpr { MAX_SIZE = 1024 };
    void *array[MAX_SIZE];
    size = backtrace(array, MAX_SIZE);
    backtrace_symbols_fd(array, size, STDOUT_FILENO);
    puts("");
}

Diuji pada Ubuntu 16.04, GCC 6.4.0, libc 2.23.

glibc backtracedengan C ++ demangling hack 1: -export-dynamic+dladdr

Diadaptasi dari: https://gist.github.com/fmela/591333/c64f4eb86037bb237862a8283df70cdfc25f01d3

Ini adalah "retasan" karena memerlukan penggantian ELF dengan -export-dynamic.

glibc_ldl.cpp

#include <dlfcn.h>     // for dladdr
#include <cxxabi.h>    // for __cxa_demangle

#include <cstdio>
#include <string>
#include <sstream>
#include <iostream>

// This function produces a stack backtrace with demangled function & method names.
std::string backtrace(int skip = 1)
{
    void *callstack[128];
    const int nMaxFrames = sizeof(callstack) / sizeof(callstack[0]);
    char buf[1024];
    int nFrames = backtrace(callstack, nMaxFrames);
    char **symbols = backtrace_symbols(callstack, nFrames);

    std::ostringstream trace_buf;
    for (int i = skip; i < nFrames; i++) {
        Dl_info info;
        if (dladdr(callstack[i], &info)) {
            char *demangled = NULL;
            int status;
            demangled = abi::__cxa_demangle(info.dli_sname, NULL, 0, &status);
            std::snprintf(
                buf,
                sizeof(buf),
                "%-3d %*p %s + %zd\n",
                i,
                (int)(2 + sizeof(void*) * 2),
                callstack[i],
                status == 0 ? demangled : info.dli_sname,
                (char *)callstack[i] - (char *)info.dli_saddr
            );
            free(demangled);
        } else {
            std::snprintf(buf, sizeof(buf), "%-3d %*p\n",
                i, (int)(2 + sizeof(void*) * 2), callstack[i]);
        }
        trace_buf << buf;
        std::snprintf(buf, sizeof(buf), "%s\n", symbols[i]);
        trace_buf << buf;
    }
    free(symbols);
    if (nFrames == nMaxFrames)
        trace_buf << "[truncated]\n";
    return trace_buf.str();
}

void my_func_2(void) {
    std::cout << backtrace() << std::endl;
}

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

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

int main() {
    my_func_1(1);
    my_func_1(2.0);
}

Kompilasi dan jalankan:

g++ -fno-pie -ggdb3 -O0 -no-pie -o glibc_ldl.out -std=c++11 -Wall -Wextra \
  -pedantic-errors -fpic glibc_ldl.cpp -export-dynamic -ldl
./glibc_ldl.out 

keluaran:

1             0x40130a my_func_2() + 41
./glibc_ldl.out(_Z9my_func_2v+0x29) [0x40130a]
2             0x40139e my_func_1(int) + 16
./glibc_ldl.out(_Z9my_func_1i+0x10) [0x40139e]
3             0x4013b3 main + 18
./glibc_ldl.out(main+0x12) [0x4013b3]
4       0x7f7594552b97 __libc_start_main + 231
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7) [0x7f7594552b97]
5             0x400f3a _start + 42
./glibc_ldl.out(_start+0x2a) [0x400f3a]

1             0x40130a my_func_2() + 41
./glibc_ldl.out(_Z9my_func_2v+0x29) [0x40130a]
2             0x40138b my_func_1(double) + 18
./glibc_ldl.out(_Z9my_func_1d+0x12) [0x40138b]
3             0x4013c8 main + 39
./glibc_ldl.out(main+0x27) [0x4013c8]
4       0x7f7594552b97 __libc_start_main + 231
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7) [0x7f7594552b97]
5             0x400f3a _start + 42
./glibc_ldl.out(_start+0x2a) [0x400f3a]

Diuji di Ubuntu 18.04.

glibc backtracewith C ++ demangling hack 2: parse backtrace output

Ditampilkan di: https://panthema.net/2008/0901-stacktrace-demangled/

Ini adalah peretasan karena membutuhkan penguraian.

TODO mendapatkannya untuk dikompilasi dan menunjukkannya di sini.

libunwind

TODO, apakah ini memiliki keunggulan dibandingkan glibc backtrace? Keluaran yang sangat mirip, juga membutuhkan modifikasi perintah build, tetapi bukan bagian dari glibc sehingga membutuhkan instalasi paket tambahan.

Kode diadaptasi dari: https://eli.thegreenplace.net/2015/programmatic-access-to-the-call-stack-in-c/

main.c

/* This must be on top. */
#define _XOPEN_SOURCE 700

#include <stdio.h>
#include <stdlib.h>

/* Paste this on the file you want to debug. */
#define UNW_LOCAL_ONLY
#include <libunwind.h>
#include <stdio.h>
void print_trace() {
    char sym[256];
    unw_context_t context;
    unw_cursor_t cursor;
    unw_getcontext(&context);
    unw_init_local(&cursor, &context);
    while (unw_step(&cursor) > 0) {
        unw_word_t offset, pc;
        unw_get_reg(&cursor, UNW_REG_IP, &pc);
        if (pc == 0) {
            break;
        }
        printf("0x%lx:", pc);
        if (unw_get_proc_name(&cursor, sym, sizeof(sym), &offset) == 0) {
            printf(" (%s+0x%lx)\n", sym, offset);
        } else {
            printf(" -- error: unable to obtain symbol name for this frame\n");
        }
    }
    puts("");
}

void my_func_3(void) {
    print_trace();
}

void my_func_2(void) {
    my_func_3();
}

void my_func_1(void) {
    my_func_3();
}

int main(void) {
    my_func_1(); /* line 46 */
    my_func_2(); /* line 47 */
    return 0;
}

Kompilasi dan jalankan:

sudo apt-get install libunwind-dev
gcc -fno-pie -ggdb3 -O3 -no-pie -o main.out -std=c99 \
  -Wall -Wextra -pedantic-errors main.c -lunwind

Entah #define _XOPEN_SOURCE 700harus berada di atas, atau kita harus menggunakan -std=gnu99:

Lari:

./main.out

Keluaran:

0x4007db: (main+0xb)
0x7f4ff50aa830: (__libc_start_main+0xf0)
0x400819: (_start+0x29)

0x4007e2: (main+0x12)
0x7f4ff50aa830: (__libc_start_main+0xf0)
0x400819: (_start+0x29)

dan:

addr2line -e main.out 0x4007db 0x4007e2

memberikan:

/home/ciro/main.c:34
/home/ciro/main.c:49

Dengan -O0:

0x4009cf: (my_func_3+0xe)
0x4009e7: (my_func_1+0x9)
0x4009f3: (main+0x9)
0x7f7b84ad7830: (__libc_start_main+0xf0)
0x4007d9: (_start+0x29)

0x4009cf: (my_func_3+0xe)
0x4009db: (my_func_2+0x9)
0x4009f8: (main+0xe)
0x7f7b84ad7830: (__libc_start_main+0xf0)
0x4007d9: (_start+0x29)

dan:

addr2line -e main.out 0x4009f3 0x4009f8

memberikan:

/home/ciro/main.c:47
/home/ciro/main.c:48

Diuji pada Ubuntu 16.04, GCC 6.4.0, libunwind 1.1.

libunwind dengan nama C ++ demangling

Kode diadaptasi dari: https://eli.thegreenplace.net/2015/programmatic-access-to-the-call-stack-in-c/

unwind.cpp

#define UNW_LOCAL_ONLY
#include <cxxabi.h>
#include <libunwind.h>
#include <cstdio>
#include <cstdlib>
#include <iostream>

void backtrace() {
  unw_cursor_t cursor;
  unw_context_t context;

  // Initialize cursor to current frame for local unwinding.
  unw_getcontext(&context);
  unw_init_local(&cursor, &context);

  // Unwind frames one by one, going up the frame stack.
  while (unw_step(&cursor) > 0) {
    unw_word_t offset, pc;
    unw_get_reg(&cursor, UNW_REG_IP, &pc);
    if (pc == 0) {
      break;
    }
    std::printf("0x%lx:", pc);

    char sym[256];
    if (unw_get_proc_name(&cursor, sym, sizeof(sym), &offset) == 0) {
      char* nameptr = sym;
      int status;
      char* demangled = abi::__cxa_demangle(sym, nullptr, nullptr, &status);
      if (status == 0) {
        nameptr = demangled;
      }
      std::printf(" (%s+0x%lx)\n", nameptr, offset);
      std::free(demangled);
    } else {
      std::printf(" -- error: unable to obtain symbol name for this frame\n");
    }
  }
}

void my_func_2(void) {
    backtrace();
    std::cout << std::endl; // line 43
}

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

void my_func_1(int i) {
    (void)i;
    my_func_2();
}  // line 54

int main() {
    my_func_1(1);
    my_func_1(2.0);
}

Kompilasi dan jalankan:

sudo apt-get install libunwind-dev
g++ -fno-pie -ggdb3 -O0 -no-pie -o unwind.out -std=c++11 \
  -Wall -Wextra -pedantic-errors unwind.cpp -lunwind -pthread
./unwind.out

Keluaran:

0x400c80: (my_func_2()+0x9)
0x400cb7: (my_func_1(int)+0x10)
0x400ccc: (main+0x12)
0x7f4c68926b97: (__libc_start_main+0xe7)
0x400a3a: (_start+0x2a)

0x400c80: (my_func_2()+0x9)
0x400ca4: (my_func_1(double)+0x12)
0x400ce1: (main+0x27)
0x7f4c68926b97: (__libc_start_main+0xe7)
0x400a3a: (_start+0x2a)

dan kemudian kita dapat menemukan baris dari my_func_2dan my_func_1(int)dengan:

addr2line -e unwind.out 0x400c80 0x400cb7

pemberian yang mana:

/home/ciro/test/unwind.cpp:43
/home/ciro/test/unwind.cpp:54

TODO: mengapa garis terputus satu per satu?

Diuji pada Ubuntu 18.04, GCC 7.4.0, libunwind 1.2.1.

Otomatisasi GDB

Kami juga dapat melakukan ini dengan GDB tanpa kompilasi ulang dengan menggunakan: Bagaimana melakukan tindakan tertentu ketika breakpoint tertentu tercapai di GDB?

Meskipun jika Anda akan banyak mencetak backtrace, ini kemungkinan akan kurang cepat daripada opsi lain, tetapi mungkin kami dapat mencapai kecepatan asli dengan compile code, tetapi saya malas untuk mengujinya sekarang: Bagaimana memanggil assembly di gdb?

main.cpp

void my_func_2(void) {}

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

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

int main() {
    my_func_1(1);
    my_func_1(2.0);
}

main.gdb

start
break my_func_2
commands
  silent
  backtrace
  printf "\n"
  continue
end
continue

Kompilasi dan jalankan:

g++ -ggdb3 -o main.out main.cpp
gdb -nh -batch -x main.gdb main.out

Keluaran:

Temporary breakpoint 1 at 0x1158: file main.cpp, line 12.

Temporary breakpoint 1, main () at main.cpp:12
12          my_func_1(1);
Breakpoint 2 at 0x555555555129: file main.cpp, line 1.
#0  my_func_2 () at main.cpp:1
#1  0x0000555555555151 in my_func_1 (i=1) at main.cpp:8
#2  0x0000555555555162 in main () at main.cpp:12

#0  my_func_2 () at main.cpp:1
#1  0x000055555555513e in my_func_1 (f=2) at main.cpp:4
#2  0x000055555555516f in main () at main.cpp:13

[Inferior 1 (process 14193) exited normally]

TODO Saya ingin melakukan ini dengan hanya -exdari baris perintah agar tidak perlu membuat main.gdbtetapi saya tidak commandsdapat mengerjakannya di sana.

Diuji di Ubuntu 19.04, GDB 8.2.

Kernel Linux

Bagaimana cara mencetak jejak tumpukan benang saat ini di dalam kernel Linux?

libdwfl

Ini awalnya disebutkan di: https://stackoverflow.com/a/60713161/895245 dan itu mungkin metode terbaik, tetapi saya harus melakukan tolok ukur lebih banyak, tetapi tolong naikkan jawaban itu.

TODO: Saya mencoba meminimalkan kode dalam jawaban itu, yang berfungsi, menjadi satu fungsi, tetapi itu segfault, beri tahu saya jika ada yang dapat menemukan alasannya.

dwfl.cpp

#include <cassert>
#include <iostream>
#include <memory>
#include <sstream>
#include <string>

#include <cxxabi.h> // __cxa_demangle
#include <elfutils/libdwfl.h> // Dwfl*
#include <execinfo.h> // backtrace
#include <unistd.h> // getpid

// /programming/281818/unmangling-the-result-of-stdtype-infoname
std::string demangle(const char* name) {
    int status = -4;
    std::unique_ptr<char, void(*)(void*)> res {
        abi::__cxa_demangle(name, NULL, NULL, &status),
        std::free
    };
    return (status==0) ? res.get() : name ;
}

std::string debug_info(Dwfl* dwfl, void* ip) {
    std::string function;
    int line = -1;
    char const* file;
    uintptr_t ip2 = reinterpret_cast<uintptr_t>(ip);
    Dwfl_Module* module = dwfl_addrmodule(dwfl, ip2);
    char const* name = dwfl_module_addrname(module, ip2);
    function = name ? demangle(name) : "<unknown>";
    if (Dwfl_Line* dwfl_line = dwfl_module_getsrc(module, ip2)) {
        Dwarf_Addr addr;
        file = dwfl_lineinfo(dwfl_line, &addr, &line, nullptr, nullptr, nullptr);
    }
    std::stringstream ss;
    ss << ip << ' ' << function;
    if (file)
        ss << " at " << file << ':' << line;
    ss << std::endl;
    return ss.str();
}

std::string stacktrace() {
    // Initialize Dwfl.
    Dwfl* dwfl = nullptr;
    {
        Dwfl_Callbacks callbacks = {};
        char* debuginfo_path = nullptr;
        callbacks.find_elf = dwfl_linux_proc_find_elf;
        callbacks.find_debuginfo = dwfl_standard_find_debuginfo;
        callbacks.debuginfo_path = &debuginfo_path;
        dwfl = dwfl_begin(&callbacks);
        assert(dwfl);
        int r;
        r = dwfl_linux_proc_report(dwfl, getpid());
        assert(!r);
        r = dwfl_report_end(dwfl, nullptr, nullptr);
        assert(!r);
        static_cast<void>(r);
    }

    // Loop over stack frames.
    std::stringstream ss;
    {
        void* stack[512];
        int stack_size = ::backtrace(stack, sizeof stack / sizeof *stack);
        for (int i = 0; i < stack_size; ++i) {
            ss << i << ": ";

            // Works.
            ss << debug_info(dwfl, stack[i]);

#if 0
            // TODO intended to do the same as above, but segfaults,
            // so possibly UB In above function that does not blow up by chance?
            void *ip = stack[i];
            std::string function;
            int line = -1;
            char const* file;
            uintptr_t ip2 = reinterpret_cast<uintptr_t>(ip);
            Dwfl_Module* module = dwfl_addrmodule(dwfl, ip2);
            char const* name = dwfl_module_addrname(module, ip2);
            function = name ? demangle(name) : "<unknown>";
            // TODO if I comment out this line it does not blow up anymore.
            if (Dwfl_Line* dwfl_line = dwfl_module_getsrc(module, ip2)) {
              Dwarf_Addr addr;
              file = dwfl_lineinfo(dwfl_line, &addr, &line, nullptr, nullptr, nullptr);
            }
            ss << ip << ' ' << function;
            if (file)
                ss << " at " << file << ':' << line;
            ss << std::endl;
#endif
        }
    }
    dwfl_end(dwfl);
    return ss.str();
}

void my_func_2() {
    std::cout << stacktrace() << std::endl;
    std::cout.flush();
}

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

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

int main(int argc, char **argv) {
    long long unsigned int n;
    if (argc > 1) {
        n = strtoul(argv[1], NULL, 0);
    } else {
        n = 1;
    }
    for (long long unsigned int i = 0; i < n; ++i) {
        my_func_1(1);
        my_func_1(2.0);
    }
}

Kompilasi dan jalankan:

sudo apt install libdw-dev
g++ -fno-pie -ggdb3 -O0 -no-pie -o dwfl.out -std=c++11 -Wall -Wextra -pedantic-errors dwfl.cpp -ldw
./dwfl.out

Keluaran:

0: 0x402b74 stacktrace[abi:cxx11]() at /home/ciro/test/dwfl.cpp:65
1: 0x402ce0 my_func_2() at /home/ciro/test/dwfl.cpp:100
2: 0x402d7d my_func_1(int) at /home/ciro/test/dwfl.cpp:112
3: 0x402de0 main at /home/ciro/test/dwfl.cpp:123
4: 0x7f7efabbe1e3 __libc_start_main at ../csu/libc-start.c:342
5: 0x40253e _start at ../csu/libc-start.c:-1

0: 0x402b74 stacktrace[abi:cxx11]() at /home/ciro/test/dwfl.cpp:65
1: 0x402ce0 my_func_2() at /home/ciro/test/dwfl.cpp:100
2: 0x402d66 my_func_1(double) at /home/ciro/test/dwfl.cpp:107
3: 0x402df1 main at /home/ciro/test/dwfl.cpp:121
4: 0x7f7efabbe1e3 __libc_start_main at ../csu/libc-start.c:342
5: 0x40253e _start at ../csu/libc-start.c:-1

Menjalankan benchmark:

g++ -fno-pie -ggdb3 -O3 -no-pie -o dwfl.out -std=c++11 -Wall -Wextra -pedantic-errors dwfl.cpp -ldw
time ./dwfl.out 1000 >/dev/null

Keluaran:

real    0m3.751s
user    0m2.822s
sys     0m0.928s

Jadi kami melihat bahwa metode ini 10x lebih cepat daripada stacktrace Boost, dan oleh karena itu mungkin berlaku untuk lebih banyak kasus penggunaan.

Diuji di Ubuntu 19.10 amd64, libdw-dev 0.176-1.1.

Lihat juga


1
Semua "TODO: baris off oleh satu" adalah karena nomor baris diambil dari awal ekspresi berikutnya.
SS Anne


6

Apakah ada cara untuk membuang tumpukan panggilan dalam proses yang sedang berjalan di C atau C ++ setiap kali fungsi tertentu dipanggil?

Anda dapat menggunakan fungsi makro sebagai ganti pernyataan pengembalian dalam fungsi tertentu.

Misalnya, daripada menggunakan return,

int foo(...)
{
    if (error happened)
        return -1;

    ... do something ...

    return 0
}

Anda dapat menggunakan fungsi makro.

#include "c-callstack.h"

int foo(...)
{
    if (error happened)
        NL_RETURN(-1);

    ... do something ...

    NL_RETURN(0);
}

Setiap kali terjadi kesalahan dalam suatu fungsi, Anda akan melihat tumpukan panggilan gaya Java seperti yang ditunjukkan di bawah ini.

Error(code:-1) at : so_topless_ranking_server (sample.c:23)
Error(code:-1) at : nanolat_database (sample.c:31)
Error(code:-1) at : nanolat_message_queue (sample.c:39)
Error(code:-1) at : main (sample.c:47)

Kode sumber lengkap tersedia di sini.

c-callstack di https://github.com/Nanolat


6

Jawaban lain untuk utas lama.

Ketika saya perlu melakukan ini, saya biasanya hanya menggunakan system()danpstack

Jadi seperti ini:

#include <sys/types.h>
#include <unistd.h>
#include <string>
#include <sstream>
#include <cstdlib>

void f()
{
    pid_t myPid = getpid();
    std::string pstackCommand = "pstack ";
    std::stringstream ss;
    ss << myPid;
    pstackCommand += ss.str();
    system(pstackCommand.c_str());
}

void g()
{
   f();
}


void h()
{
   g();
}

int main()
{
   h();
}

Output ini

#0  0x00002aaaab62d61e in waitpid () from /lib64/libc.so.6
#1  0x00002aaaab5bf609 in do_system () from /lib64/libc.so.6
#2  0x0000000000400c3c in f() ()
#3  0x0000000000400cc5 in g() ()
#4  0x0000000000400cd1 in h() ()
#5  0x0000000000400cdd in main ()

Ini harus bekerja di Linux, FreeBSD dan Solaris. Saya tidak berpikir bahwa macOS memiliki pstack atau padanan sederhana, tetapi utas ini tampaknya memiliki alternatif .

Jika Anda menggunakan C, maka Anda perlu menggunakan Cfungsi string.

#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

void f()
{
    pid_t myPid = getpid();
    /*
      length of command 7 for 'pstack ', 7 for the PID, 1 for nul
    */
    char pstackCommand[7+7+1];
    sprintf(pstackCommand, "pstack %d", (int)myPid);
    system(pstackCommand);
}

Saya telah menggunakan 7 untuk jumlah digit maksimal di PID, berdasarkan posting ini .


Poin yang bagus, karena subjek memang meminta C. Tidak, itu perlu diadaptasi, karena std :: string hanya C ++. Saya akan memperbarui jawaban saya dengan versi C.
Paul Floyd

6

Khusus Linux, TLDR:

  1. backtracein glibcmenghasilkan stacktraces yang akurat hanya jika -lunwindditautkan (fitur khusus platform yang tidak berdokumen).
  2. Untuk menghasilkan nama fungsi , file sumber dan penggunaan nomor baris#include <elfutils/libdwfl.h> (pustaka ini hanya didokumentasikan di file header). backtrace_symbolsdan backtrace_symbolsd_fdpaling tidak informatif.

Di Linux modern, Anda bisa mendapatkan alamat stacktrace menggunakan fungsi backtrace. Cara tidak terdokumentasi untuk backtracemenghasilkan alamat yang lebih akurat pada platform populer adalah dengan menautkan -lunwind( libunwind-devdi Ubuntu 18.04) (lihat contoh keluaran di bawah). backtracemenggunakan fungsi _Unwind_Backtracedan secara default yang terakhir berasal libgcc_s.so.1dan implementasinya paling portabel. Ketika -lunwindditautkan, ia memberikan versi yang lebih akurat _Unwind_Backtracetetapi pustaka ini kurang portabel (lihat arsitektur yang didukung di libunwind/src).

Sayangnya, pendamping backtrace_symbolsddan backtrace_symbols_fdfungsi belum dapat menyelesaikan alamat stacktrace ke nama fungsi dengan nama file sumber dan nomor baris mungkin selama satu dekade sekarang (lihat contoh keluaran di bawah).

Namun, ada metode lain untuk menyelesaikan alamat ke simbol dan menghasilkan jejak yang paling berguna dengan nama fungsi , file sumber dan nomor baris . Metodenya adalah #include <elfutils/libdwfl.h>dan ditautkan dengan -ldw( libdw-devdi Ubuntu 18.04).

Bekerja C ++ contoh ( test.cc):

#include <stdexcept>
#include <iostream>
#include <cassert>
#include <cstdlib>
#include <string>

#include <boost/core/demangle.hpp>

#include <execinfo.h>
#include <elfutils/libdwfl.h>

struct DebugInfoSession {
    Dwfl_Callbacks callbacks = {};
    char* debuginfo_path = nullptr;
    Dwfl* dwfl = nullptr;

    DebugInfoSession() {
        callbacks.find_elf = dwfl_linux_proc_find_elf;
        callbacks.find_debuginfo = dwfl_standard_find_debuginfo;
        callbacks.debuginfo_path = &debuginfo_path;

        dwfl = dwfl_begin(&callbacks);
        assert(dwfl);

        int r;
        r = dwfl_linux_proc_report(dwfl, getpid());
        assert(!r);
        r = dwfl_report_end(dwfl, nullptr, nullptr);
        assert(!r);
        static_cast<void>(r);
    }

    ~DebugInfoSession() {
        dwfl_end(dwfl);
    }

    DebugInfoSession(DebugInfoSession const&) = delete;
    DebugInfoSession& operator=(DebugInfoSession const&) = delete;
};

struct DebugInfo {
    void* ip;
    std::string function;
    char const* file;
    int line;

    DebugInfo(DebugInfoSession const& dis, void* ip)
        : ip(ip)
        , file()
        , line(-1)
    {
        // Get function name.
        uintptr_t ip2 = reinterpret_cast<uintptr_t>(ip);
        Dwfl_Module* module = dwfl_addrmodule(dis.dwfl, ip2);
        char const* name = dwfl_module_addrname(module, ip2);
        function = name ? boost::core::demangle(name) : "<unknown>";

        // Get source filename and line number.
        if(Dwfl_Line* dwfl_line = dwfl_module_getsrc(module, ip2)) {
            Dwarf_Addr addr;
            file = dwfl_lineinfo(dwfl_line, &addr, &line, nullptr, nullptr, nullptr);
        }
    }
};

std::ostream& operator<<(std::ostream& s, DebugInfo const& di) {
    s << di.ip << ' ' << di.function;
    if(di.file)
        s << " at " << di.file << ':' << di.line;
    return s;
}

void terminate_with_stacktrace() {
    void* stack[512];
    int stack_size = ::backtrace(stack, sizeof stack / sizeof *stack);

    // Print the exception info, if any.
    if(auto ex = std::current_exception()) {
        try {
            std::rethrow_exception(ex);
        }
        catch(std::exception& e) {
            std::cerr << "Fatal exception " << boost::core::demangle(typeid(e).name()) << ": " << e.what() << ".\n";
        }
        catch(...) {
            std::cerr << "Fatal unknown exception.\n";
        }
    }

    DebugInfoSession dis;
    std::cerr << "Stacktrace of " << stack_size << " frames:\n";
    for(int i = 0; i < stack_size; ++i) {
        std::cerr << i << ": " << DebugInfo(dis, stack[i]) << '\n';
    }
    std::cerr.flush();

    std::_Exit(EXIT_FAILURE);
}

int main() {
    std::set_terminate(terminate_with_stacktrace);
    throw std::runtime_error("test exception");
}

Dikompilasi pada Ubuntu 18.04.4 LTS dengan gcc-8.3:

g++ -o test.o -c -m{arch,tune}=native -std=gnu++17 -W{all,extra,error} -g -Og -fstack-protector-all test.cc
g++ -o test -g test.o -ldw -lunwind

Keluaran:

Fatal exception std::runtime_error: test exception.
Stacktrace of 7 frames:
0: 0x55f3837c1a8c terminate_with_stacktrace() at /home/max/src/test/test.cc:76
1: 0x7fbc1c845ae5 <unknown>
2: 0x7fbc1c845b20 std::terminate()
3: 0x7fbc1c845d53 __cxa_throw
4: 0x55f3837c1a43 main at /home/max/src/test/test.cc:103
5: 0x7fbc1c3e3b96 __libc_start_main at ../csu/libc-start.c:310
6: 0x55f3837c17e9 _start

Jika tidak ada -lunwindyang ditautkan, ini menghasilkan pelacakan tumpukan yang kurang akurat:

0: 0x5591dd9d1a4d terminate_with_stacktrace() at /home/max/src/test/test.cc:76
1: 0x7f3c18ad6ae6 <unknown>
2: 0x7f3c18ad6b21 <unknown>
3: 0x7f3c18ad6d54 <unknown>
4: 0x5591dd9d1a04 main at /home/max/src/test/test.cc:103
5: 0x7f3c1845cb97 __libc_start_main at ../csu/libc-start.c:344
6: 0x5591dd9d17aa _start

Sebagai perbandingan, backtrace_symbols_fdkeluaran untuk stacktrace yang sama paling tidak informatif:

/home/max/src/test/debug/gcc/test(+0x192f)[0x5601c5a2092f]
/usr/lib/x86_64-linux-gnu/libstdc++.so.6(+0x92ae5)[0x7f95184f5ae5]
/usr/lib/x86_64-linux-gnu/libstdc++.so.6(_ZSt9terminatev+0x10)[0x7f95184f5b20]
/usr/lib/x86_64-linux-gnu/libstdc++.so.6(__cxa_throw+0x43)[0x7f95184f5d53]
/home/max/src/test/debug/gcc/test(+0x1ae7)[0x5601c5a20ae7]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe6)[0x7f9518093b96]
/home/max/src/test/debug/gcc/test(+0x1849)[0x5601c5a20849]

Dalam versi produksi (serta versi bahasa C) Anda mungkin ingin membuat kode ini lebih kuat dengan mengganti boost::core::demangle, std::stringdan std::coutdengan panggilan dasarnya.

Anda juga dapat mengganti __cxa_throwuntuk menangkap stacktrace saat pengecualian dilemparkan dan mencetaknya saat pengecualian tertangkap. Pada saat memasuki catchblok, tumpukan telah dibatalkan, jadi sudah terlambat untuk dipanggil backtrace, dan inilah mengapa tumpukan harus ditangkap throwyang diimplementasikan oleh fungsi __cxa_throw. Perhatikan bahwa dalam program multi-threaded __cxa_throwdapat dipanggil secara bersamaan oleh beberapa thread, sehingga jika ia menangkap stacktrace ke dalam array global yang harus thread_local.


1
Jawaban bagus! Diteliti dengan baik juga.
SS Anne

@SSAnne Sangat baik, terima kasih. Itu -lunwindmasalah mendapat ditemukan saat membuat posting ini, saya sebelumnya digunakan libunwindlangsung untuk mendapatkan stacktrace dan akan posting, tapi backtracetidak untuk saya ketika -lunwindterhubung.
Maxim Egorushkin

1
@SSAnne Mungkin karena penulis asli perpustakaan David Mosberger awalnya berfokus pada IA-64, tetapi kemudian perpustakaan tersebut mendapat daya tarik lebih nongnu.org/libunwind/people.html . gcctidak mengekspos API, bukan?
Maxim Egorushkin

3

Anda dapat mengimplementasikan sendiri fungsionalitas tersebut:

Gunakan tumpukan global (string) dan di awal setiap fungsi, dorong nama fungsi dan nilai lainnya (misalnya parameter) ke tumpukan ini; saat keluar dari fungsi pop lagi.

Tulis fungsi yang akan mencetak konten tumpukan saat dipanggil, dan gunakan ini di fungsi tempat Anda ingin melihat tumpukan panggilan.

Ini mungkin terdengar seperti pekerjaan yang berat, tetapi cukup berguna.


2
Saya tidak akan melakukan itu. Sebaliknya, saya akan membuat pembungkus yang menggunakan API khusus platform yang mendasarinya (lihat di bawah). Jumlah pekerjaan mungkin sama, tetapi investasi akan terbayar lebih cepat.
Paul Michalik

3
@paul: jawaban Anda merujuk ke windows ketika OP dengan jelas menentukan linux ... tetapi bisa berguna untuk windows-guys yang muncul di sini.
slashmais

Benar, saya melewatkannya..Hm, itu kalimat terakhir dari pertanyaan, jadi mungkin pengepos harus memodifikasi permintaannya untuk menyebutkan platform targetnya di tempat yang lebih menonjol.
Paul Michalik

1
Ini akan menjadi ide yang bagus, kecuali basis kode saya menyertakan beberapa lusin file yang berisi beberapa ratus (jika bukan beberapa ribu) file, jadi ini tidak layak.
Nathan Fellman

mungkin tidak jika Anda meretas skrip sed / perl untuk ditambahkan setelah setiap deklarasi fungsi call_registror MY_SUPERSECRETNAME(__FUNCTION__);yang mendorong argumen dalam konstruktornya dan muncul di destruktornya. FUNCTION selalu mewakili nama fungsi saat ini.
diterbangkan

2

Tentu pertanyaan selanjutnya adalah: apakah ini cukup?

Kerugian utama dari stack-traces adalah mengapa Anda memiliki fungsi yang tepat yang dipanggil, Anda tidak memiliki yang lain, seperti nilai argumennya, yang sangat berguna untuk debugging.

Jika Anda memiliki akses ke gcc dan gdb, saya sarankan untuk menggunakan assert untuk memeriksa kondisi tertentu, dan menghasilkan dump memori jika tidak terpenuhi. Tentu saja ini berarti prosesnya akan berhenti, tetapi Anda akan memiliki laporan lengkap dan bukan hanya jejak tumpukan.

Jika Anda menginginkan cara yang tidak terlalu mencolok, Anda selalu dapat menggunakan logging. Ada fasilitas logging yang sangat efisien di luar sana, seperti Pantheios misalnya. Yang sekali lagi bisa memberi Anda gambaran yang jauh lebih akurat tentang apa yang sedang terjadi.


1
Tentu saja ini mungkin tidak cukup, tetapi jika saya dapat melihat bahwa fungsi dipanggil dengan satu konfigurasi dan tidak dengan yang lain, maka itu adalah tempat yang bagus untuk memulai.
Nathan Fellman

2

Anda dapat menggunakan Poppy untuk ini. Ini biasanya digunakan untuk mengumpulkan jejak tumpukan selama crash tetapi juga dapat mengeluarkannya untuk program yang sedang berjalan.

Sekarang, inilah bagian baiknya: ia dapat menampilkan nilai parameter aktual untuk setiap fungsi pada stack, dan bahkan variabel lokal, penghitung loop, dll.


2

Saya tahu utas ini sudah lama, tetapi menurut saya utas ini bisa berguna bagi orang lain. Jika Anda menggunakan gcc, Anda dapat menggunakan fitur instrumennya (opsi -finstrument-functions) untuk mencatat panggilan fungsi apa pun (masuk dan keluar). Lihat ini untuk informasi lebih lanjut: http://hacktalks.blogspot.fr/2013/08/gcc-instrument-functions.html

Dengan demikian, Anda dapat, misalnya, mendorong dan memunculkan setiap panggilan ke dalam tumpukan, dan ketika Anda ingin mencetaknya, Anda cukup melihat apa yang ada di tumpukan Anda.

Saya sudah mengujinya, ini bekerja dengan sempurna dan sangat berguna

UPDATE: Anda juga dapat menemukan informasi tentang opsi kompilasi -finstrument-functions di dokumen GCC mengenai opsi Instrumentasi: https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html


Anda juga harus menautkan ke dokumen GCC jika artikel tersebut turun.
HolyBlackCat

Terima kasih kamu benar Saya telah menambahkan UPDATE di posting saya dengan tautan ke dokumen gcc
François

2

Anda dapat menggunakan pustaka Boost untuk mencetak callstack saat ini.

#include <boost/stacktrace.hpp>

// ... somewhere inside the `bar(int)` function that is called recursively:
std::cout << boost::stacktrace::stacktrace();

Laki-laki di sini: https://www.boost.org/doc/libs/1_65_1/doc/html/stacktrace.html


Saya mendapat kesalahan cannot locate SymEnumSymbolsExW at C:\Windows\SYSTEM32\dbgeng.dllpada Win10.
zwcloud

0

Anda dapat menggunakan profiler GNU. Ini menunjukkan grafik panggilan juga! perintahnya adalah gprofdan Anda perlu mengkompilasi kode Anda dengan beberapa opsi.


-6

Apakah ada cara untuk membuang tumpukan panggilan dalam proses yang sedang berjalan di C atau C ++ setiap kali fungsi tertentu dipanggil?

Tidak, tidak ada, meskipun solusi yang bergantung pada platform mungkin ada.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.