stampa stack di chiamate in C o C ++


120

C'è un modo per eseguire il dump dello stack di chiamate in un processo in esecuzione in C o C ++ ogni volta che viene chiamata una determinata funzione? Quello che ho in mente è qualcosa del genere:

void foo()
{
   print_stack_trace();

   // foo's body

   return
}

Where print_stack_tracefunziona in modo simile a callerPerl.

O qualcosa del genere:

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

    // etc...
}

dove register_stack_trace_functioninserisce una sorta di punto di interruzione interno che causerà la stampa di un'analisi dello stack ogni volta che fooviene chiamato.

Esiste qualcosa di simile in qualche libreria C standard?

Sto lavorando su Linux, utilizzando GCC.


sfondo

Ho un test eseguito che si comporta in modo diverso in base ad alcune opzioni della riga di comando che non dovrebbero influire su questo comportamento. Il mio codice ha un generatore di numeri pseudo-casuali che presumo venga chiamato in modo diverso in base a questi interruttori. Voglio essere in grado di eseguire il test con ogni set di interruttori e vedere se il generatore di numeri casuali viene chiamato in modo diverso per ciascuno.


1
@ Armen, conosci qualcuno di questi?
Nathan Fellman

1
@ Nathan: se il tuo debugger è gdb, può gestire quel caso . Non posso parlarti degli altri, ma presumo che gdb non sia l'unico ad avere questa funzionalità. A parte: ho appena guardato il mio commento precedente. :: gag :: s/easier/either/come diavolo è successo?
dmckee --- gattino ex moderatore

2
@dmckee: In effetti, dovrebbe essere s/either/easier. Quello che dovrei fare con gdb è scrivere uno script che interrompe quella funzione e stampa la traccia dello stack, quindi continua. Ora che ci penso, forse è ora che impari a conoscere lo scripting gdb.
Nathan Fellman

1
Gah! Sto andando a dormire un po '.
Molto

Risposte:


79

Per una soluzione solo per Linux è possibile utilizzare backtrace (3) che restituisce semplicemente un array di void *(in effetti ognuno di questi punta all'indirizzo di ritorno dallo stack frame corrispondente). Per tradurli in qualcosa di utile, c'è backtrace_symbols (3) .

Presta attenzione alla sezione delle note in backtrace (3) :

I nomi dei simboli potrebbero non essere disponibili senza l'utilizzo di opzioni di linker speciali. Per i sistemi che utilizzano il linker GNU, è necessario utilizzare l'opzione -rdynamic linker. Notare che i nomi delle funzioni "statiche" non sono esposti e non saranno disponibili nel backtrace.


10
FWIW, questa funzionalità esiste anche su Mac OS X: developer.apple.com/library/mac/#documentation/Darwin/Reference/…
EmeryBerger



Su Linux con glibc, sfortunatamente, le backtrace_symbolsfunzioni non forniscono il nome della funzione, il nome del file sorgente e il numero di riga.
Maxim Egorushkin il

Oltre a usare -rdynamic, controlla anche che il tuo sistema di build non aggiunga -fvisibility=hiddenopzioni! (poiché eliminerà completamente l'effetto di -rdynamic)
Dima Litvinov

38

Potenzia stacktrace

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

Questa è l'opzione più conveniente che ho visto finora, perché:

  • può effettivamente stampare i numeri di riga.

    Tuttavia , faaddr2line solo chiamate , il che è brutto e potrebbe essere lento se stai prendendo troppe tracce.

  • per impostazione predefinita si districa

  • Boost è solo intestazione, quindi molto probabilmente non è necessario modificare il sistema di compilazione

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

Sfortunatamente, sembra essere un'aggiunta più recente e il pacchetto libboost-stacktrace-devnon è presente in Ubuntu 16.04, solo 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

Dobbiamo aggiungere -ldlalla fine altrimenti la compilazione fallisce.

Produzione:

 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

L'output e viene ulteriormente spiegato nella sezione "glibc backtrace" di seguito, che è analoga.

Notate come my_func_1(int)e my_func_1(float), che sono danneggiati a causa del sovraccarico di funzioni , sono stati ben smembrati per noi.

Si noti che le prime intchiamate sono fuori linea di una linea (28 invece di 27 e la seconda è fuori linea di due linee (27 invece di 29). Nei commenti è stato suggerito che questo è dovuto al fatto che si sta prendendo in considerazione il seguente indirizzo di istruzione, che fa 27 diventare 28 e 29 salta fuori dal loop e diventa 27.

Osserviamo quindi che con -O3l'output viene completamente mutilato:

 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

I backtraces sono in genere irreparabilmente mutilati dalle ottimizzazioni. L'ottimizzazione della chiamata di coda è un notevole esempio di ciò: che cos'è l'ottimizzazione della chiamata di coda?

Benchmark eseguito su -O3:

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

Produzione:

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

Quindi, come previsto, vediamo che questo metodo è estremamente lento per le chiamate esterne addr2linee sarà fattibile solo se viene effettuato un numero limitato di chiamate.

Ogni stampa di backtrace sembra richiedere centinaia di millisecondi, quindi tieni presente che se un backtrace si verifica molto spesso, le prestazioni del programma ne risentiranno in modo significativo.

Testato su Ubuntu 19.10, GCC 9.2.1, boost 1.67.0.

glibc backtrace

Documentato su: 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;
}

Compilare:

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

-rdynamic è l'opzione chiave richiesta.

Correre:

./main.out

Uscite:

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

Quindi vediamo immediatamente che si è verificata un'ottimizzazione inlining e alcune funzioni sono state perse dalla traccia.

Se proviamo a ottenere gli indirizzi:

addr2line -e main.out 0x4008f9 0x4008fe

otteniamo:

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

che è completamente spento.

Se facciamo lo stesso con -O0invece, ./main.outfornisce la traccia completa corretta:

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

e poi:

addr2line -e main.out 0x400a74 0x400a79

dà:

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

quindi le linee sono sbagliate di una sola, TODO perché? Ma questo potrebbe essere ancora utilizzabile.

Conclusione: i backtrace possono essere mostrati perfettamente solo con -O0. Con le ottimizzazioni, il backtrace originale viene modificato fondamentalmente nel codice compilato.

Non sono riuscito a trovare un modo semplice per districare automaticamente i simboli C ++ con questo, tuttavia, ecco alcuni hack:

Testato su Ubuntu 16.04, GCC 6.4.0, libc 2.23.

glibc backtrace_symbols_fd

Questo helper è un po 'più conveniente di backtrace_symbolse produce un output sostanzialmente identico:

/* 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("");
}

Testato su Ubuntu 16.04, GCC 6.4.0, libc 2.23.

glibc backtracecon C ++ districante hack 1: -export-dynamic+dladdr

Adattato da: https://gist.github.com/fmela/591333/c64f4eb86037bb237862a8283df70cdfc25f01d3

Questo è un "trucco" perché richiede di cambiare l'ELF con -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);
}

Compila ed esegui:

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 

produzione:

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]

Testato su Ubuntu 18.04.

glibc backtracecon C ++ demangling hack 2: analizzare l'output di backtrace

Visibile su: https://panthema.net/2008/0901-stacktrace-demangled/

Questo è un trucco perché richiede l'analisi.

TODO farlo compilare e mostrarlo qui.

libunwind

TODO ha qualche vantaggio su glibc backtrace? Un output molto simile, richiede anche la modifica del comando build, ma non fa parte di glibc, quindi richiede un'installazione aggiuntiva del pacchetto.

Codice adattato da: 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;
}

Compila ed esegui:

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

O #define _XOPEN_SOURCE 700deve essere in cima o dobbiamo usare -std=gnu99:

Correre:

./main.out

Produzione:

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

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

e:

addr2line -e main.out 0x4007db 0x4007e2

dà:

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

Con -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)

e:

addr2line -e main.out 0x4009f3 0x4009f8

dà:

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

Testato su Ubuntu 16.04, GCC 6.4.0, libunwind 1.1.

libunwind con il nome C ++ districante

Codice adattato da: 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);
}

Compila ed esegui:

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

Produzione:

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)

e quindi possiamo trovare le righe di my_func_2e my_func_1(int)con:

addr2line -e unwind.out 0x400c80 0x400cb7

che dà:

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

TODO: perché le linee sono sbagliate di uno?

Testato su Ubuntu 18.04, GCC 7.4.0, libunwind 1.2.1.

Automazione GDB

Possiamo anche farlo con GDB senza ricompilare utilizzando: Come eseguire un'azione specifica quando viene raggiunto un certo punto di interruzione in GDB?

Sebbene stamperai molto il backtrace, questo sarà probabilmente meno veloce delle altre opzioni, ma forse possiamo raggiungere velocità native con compile code, ma sono pigro per provarlo ora: come chiamare assembly in 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

Compila ed esegui:

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

Produzione:

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 Volevo farlo solo -exdalla riga di comando per non dover creare main.gdbma non sono riuscito a farlo commandsfunzionare lì.

Testato in Ubuntu 19.04, GDB 8.2.

Kernel Linux

Come stampare la traccia dello stack di thread corrente all'interno del kernel Linux?

libdwfl

Questo è stato originariamente menzionato su: https://stackoverflow.com/a/60713161/895245 e potrebbe essere il metodo migliore, ma devo fare un benchmark un po 'di più, ma per favore vota la risposta.

TODO: Ho provato a ridurre al minimo il codice in quella risposta, che funzionava, a una singola funzione, ma è segfault, fammi sapere se qualcuno riesce a trovare il motivo.

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

Compila ed esegui:

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

Produzione:

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

Benchmark eseguito:

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

Produzione:

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

Quindi vediamo che questo metodo è 10 volte più veloce dello stacktrace di Boost e potrebbe quindi essere applicabile a più casi d'uso.

Testato in Ubuntu 19.10 amd64, libdw-dev 0.176-1.1.

Guarda anche


1
Tutti i "TODO: righe fuori di uno" sono perché il numero di riga è preso dall'inizio dell'espressione successiva.
SS Anne

6

Non esiste un modo standardizzato per farlo. Per Windows la funzionalità è fornita nella libreria DbgHelp


6

C'è un modo per eseguire il dump dello stack di chiamate in un processo in esecuzione in C o C ++ ogni volta che viene chiamata una determinata funzione?

È possibile utilizzare una funzione macro invece dell'istruzione return nella funzione specifica.

Ad esempio, invece di utilizzare return,

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

    ... do something ...

    return 0
}

Puoi usare una funzione macro.

#include "c-callstack.h"

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

    ... do something ...

    NL_RETURN(0);
}

Ogni volta che si verifica un errore in una funzione, vedrai lo stack di chiamate in stile Java come mostrato di seguito.

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)

Il codice sorgente completo è disponibile qui.

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


6

Un'altra risposta a un vecchio thread.

Quando ho bisogno di farlo, di solito uso solo system()epstack

Quindi qualcosa del genere:

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

Questo produce

#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 ()

Questo dovrebbe funzionare su Linux, FreeBSD e Solaris. Non penso che macOS abbia pstack o un semplice equivalente, ma questo thread sembra avere un'alternativa .

Se stai usando C, dovrai usare le Cfunzioni di stringa.

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

Ho usato 7 per il numero massimo di cifre nel PID, in base a questo post .


Buon punto, poiché il soggetto richiede C. No, sarebbe necessario adattarlo, poiché std :: string è solo C ++. Aggiornerò la mia risposta con una versione C.
Paul Floyd

6

Specifico per Linux, TLDR:

  1. backtracein glibcproduce stacktrace accurati solo quando -lunwindè collegato (funzionalità specifica della piattaforma non documentata).
  2. Per visualizzare il nome della funzione , il file di origine e il numero di riga, utilizzare #include <elfutils/libdwfl.h>(questa libreria è documentata solo nel file di intestazione). backtrace_symbolse backtrace_symbolsd_fdsono meno informativi.

Su Linux moderno puoi ottenere gli indirizzi stacktrace usando function backtrace. Il modo non documentato per backtraceprodurre indirizzi più accurati su piattaforme popolari è collegarsi a -lunwind( libunwind-devsu Ubuntu 18.04) (vedere l'output di esempio sotto). backtraceusa la funzione _Unwind_Backtracee per impostazione predefinita quest'ultima proviene libgcc_s.so.1e quell'implementazione è più portabile. Quando -lunwindè collegato, fornisce una versione più accurata di _Unwind_Backtracema questa libreria è meno portabile (vedi architetture supportate in libunwind/src).

Sfortunatamente, il compagno backtrace_symbolsde le backtrace_symbols_fdfunzioni non sono stati in grado di risolvere gli indirizzi di stacktrace in nomi di funzioni con nome file di origine e numero di riga probabilmente da un decennio (vedere l'output di esempio sotto).

Tuttavia, esiste un altro metodo per risolvere gli indirizzi in simboli e produce le tracce più utili con il nome della funzione , il file di origine e il numero di riga . Il metodo è quello #include <elfutils/libdwfl.h>e il collegamento con -ldw( libdw-devsu Ubuntu 18.04).

Esempio C ++ funzionante ( 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");
}

Compilato su Ubuntu 18.04.4 LTS con 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

Uscite:

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

Quando no -lunwindè collegato, produce uno stacktrace meno accurato:

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

Per confronto, l' backtrace_symbols_fdoutput per lo stesso stacktrace è meno informativo:

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

In una versione di produzione (come la versione linguaggio C) come si può rendere questo codice extra robusto sostituendo boost::core::demangle, std::stringe std::coutcon le loro chiamate sottostanti.

È anche possibile eseguire __cxa_throwl' override per acquisire lo stacktrace quando viene generata un'eccezione e stamparlo quando viene rilevata l'eccezione. Nel momento in cui entra nel catchblocco, lo stack è stato srotolato, quindi è troppo tardi per chiamare backtrace, ed è per questo che deve essere catturato lo stack su throwcui è implementato dalla funzione __cxa_throw. Si noti che in un programma multi-thread __cxa_throwpuò essere chiamato simultaneamente da più thread, in modo che se cattura lo stacktrace in un array globale, deve essere thread_local.


1
Bella risposta! Anche ben studiato.
SS Anne

@SSAnne Molto gentile, grazie. Questo -lunwindproblema è stato scoperto durante la creazione di questo post, in precedenza ho utilizzato libunwinddirettamente per ottenere lo stacktrace e stavo per pubblicarlo, ma lo backtracefa per me quando -lunwindè collegato.
Maxim Egorushkin

1
@SSAnne Potrebbe essere dovuto al fatto che l'autore originale della libreria David Mosberger era inizialmente concentrato su IA-64, ma poi la libreria ha ottenuto più trazione nongnu.org/libunwind/people.html . gccnon espone l'API, è vero?
Maxim Egorushkin

3

Puoi implementare la funzionalità da solo:

Utilizzare uno stack globale (stringa) e all'inizio di ogni funzione inserire il nome della funzione e altri valori (ad esempio parametri) in questo stack; all'uscita dalla funzione pop di nuovo.

Scrivi una funzione che stamperà il contenuto dello stack quando viene chiamato e usala nella funzione in cui vuoi vedere lo stack di chiamate.

Questo può sembrare un sacco di lavoro ma è abbastanza utile.


2
Non lo farei. Piuttosto, creerei un wrapper che utilizza le API specifiche della piattaforma sottostante (vedi sotto). La quantità di lavoro sarebbe probabilmente la stessa, ma l'investimento dovrebbe ripagare più velocemente.
Paul Michalik

3
@paul: la tua risposta si riferisce a Windows quando l'OP specifica chiaramente linux ... ma potrebbe essere utile per i ragazzi di Windows che si presentano qui.
slashmais

Giusto, l'ho trascurato ... Hm, è l'ultima frase della domanda, quindi forse il poster dovrebbe modificare la sua richiesta per menzionare la sua piattaforma di destinazione in un posto più prominente.
Paul Michalik

1
Questa sarebbe una buona idea, tranne per il fatto che la mia base di codice include alcune dozzine di file contenenti poche centinaia (se non poche migliaia) di file, quindi non è fattibile.
Nathan Fellman

forse no se si hackera uno script sed / perl da aggiungere dopo ogni dichiarazione di funzione call_registror MY_SUPERSECRETNAME(__FUNCTION__);che inserisce l'argomento nel suo costruttore e inserisce il suo distruttore FUNZIONE rappresenta sempre il nome della funzione corrente.
volato il

2

Ovviamente la prossima domanda è: sarà sufficiente?

Il principale svantaggio degli stack-trace è che, perché hai la funzione precisa che viene chiamata, non hai nient'altro, come il valore dei suoi argomenti, che è molto utile per il debug.

Se hai accesso a gcc e gdb, ti suggerirei di utilizzare assertper verificare una condizione specifica e produrre un dump della memoria se non viene soddisfatta. Ovviamente questo significa che il processo si interromperà, ma avrai un rapporto completo invece di una semplice traccia dello stack.

Se desideri un modo meno invadente, puoi sempre utilizzare la registrazione. Ci sono strutture di registrazione molto efficienti là fuori, come Pantheios per esempio. Che ancora una volta potrebbe darti un'immagine molto più accurata di ciò che sta accadendo.


1
Ovviamente potrebbe non essere sufficiente, ma se riesco a vedere che la funzione viene chiamata in posizione con una configurazione e non con l'altra, allora è un buon punto di partenza.
Nathan Fellman

2

Puoi usare Poppy per questo. Viene normalmente utilizzato per raccogliere la traccia dello stack durante un arresto anomalo, ma può anche emetterlo per un programma in esecuzione.

Ora ecco la parte buona: può produrre i valori dei parametri effettivi per ciascuna funzione nello stack e persino variabili locali, contatori di loop, ecc.


2

So che questo thread è vecchio, ma penso che possa essere utile per altre persone. Se stai usando gcc, puoi usare le sue caratteristiche dello strumento (opzione -finstrument-functions) per registrare qualsiasi chiamata di funzione (entrata e uscita). Dai un'occhiata a questo per ulteriori informazioni: http://hacktalks.blogspot.fr/2013/08/gcc-instrument-functions.html

Puoi quindi ad esempio spingere e inserire ogni chiamata in uno stack, e quando vuoi stamparlo, guardi solo quello che hai nel tuo stack.

L'ho provato, funziona perfettamente ed è molto maneggevole

AGGIORNAMENTO: puoi anche trovare informazioni sull'opzione di compilazione -finstrument-functions nel documento GCC relativo alle opzioni di strumentazione: https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html


Dovresti anche collegarti ai documenti di GCC nel caso in cui l'articolo non funzioni.
HolyBlackCat

Grazie, hai ragione. Ho quindi aggiunto un AGGIORNAMENTO nel mio post con un collegamento al documento gcc
François

2

È possibile utilizzare le librerie Boost per stampare il callstack corrente.

#include <boost/stacktrace.hpp>

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

Uomo qui: https://www.boost.org/doc/libs/1_65_1/doc/html/stacktrace.html


Ho ricevuto un errore cannot locate SymEnumSymbolsExW at C:\Windows\SYSTEM32\dbgeng.dllsu Win10.
zwcloud

0

Puoi usare il profiler GNU. Mostra anche il grafico delle chiamate! il comando è gprofed è necessario compilare il codice con qualche opzione.


-6

C'è un modo per eseguire il dump dello stack di chiamate in un processo in esecuzione in C o C ++ ogni volta che viene chiamata una determinata funzione?

No, non c'è, anche se potrebbero esistere soluzioni dipendenti dalla piattaforma.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.