imprimir pila de llamadas en C o C ++


120

¿Hay alguna forma de volcar la pila de llamadas en un proceso en ejecución en C o C ++ cada vez que se llama a una determinada función? Lo que tengo en mente es algo como esto:

void foo()
{
   print_stack_trace();

   // foo's body

   return
}

Donde print_stack_tracefunciona de manera similar a calleren Perl.

O algo como esto:

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

    // etc...
}

where register_stack_trace_functioncoloca algún tipo de punto de interrupción interno que hará que se imprima un seguimiento de pila cada vez que foose llame.

¿Existe algo como esto en alguna biblioteca C estándar?

Estoy trabajando en Linux, usando GCC.


Antecedentes

Tengo una ejecución de prueba que se comporta de manera diferente en función de algunos interruptores de línea de comando que no deberían afectar este comportamiento. Mi código tiene un generador de números pseudoaleatorios que supongo que se llama de manera diferente en función de estos interruptores. Quiero poder ejecutar la prueba con cada conjunto de interruptores y ver si el generador de números aleatorios se llama de manera diferente para cada uno.


1
@Armen, ¿estás familiarizado con alguno de estos?
Nathan Fellman

1
@Nathan: Si su depurador es gdb, puede manejar ese caso . No puedo hablarte de otros, pero supongo que gdb no es el único que tiene esta funcionalidad. Aparte: Acabo de ver mi comentario anterior. :: mordaza :: s/easier/either/¿cómo diablos pasó eso?
dmckee --- gatito ex-moderador

2
@dmckee: De hecho, debería serlo s/either/easier. Lo que tendría que hacer con gdb es escribir un script que se interrumpa en esa función e imprima el seguimiento de la pila, luego continúa. Ahora que lo pienso, tal vez sea hora de que aprenda sobre las secuencias de comandos de gdb.
Nathan Fellman

1
¡Gah! Voy a dormir un poco.
Muy

Respuestas:


79

Para una solución solo para Linux, puede usar backtrace (3) que simplemente devuelve una matriz de void *(de hecho, cada uno de estos apunta a la dirección de retorno del marco de pila correspondiente). Para traducir esto en algo útil, hay backtrace_symbols (3) .

Preste atención a la sección de notas en backtrace (3) :

Es posible que los nombres de los símbolos no estén disponibles sin el uso de opciones especiales de enlace. Para los sistemas que utilizan el enlazador GNU, es necesario utilizar la opción -rdynamic enlazador. Tenga en cuenta que los nombres de las funciones "estáticas" no se exponen y no estarán disponibles en el backtrace.


10
FWIW, esta funcionalidad también existe en Mac OS X: developer.apple.com/library/mac/#documentation/Darwin/Reference/…
EmeryBerger

8
Windows tiene CaptureStackBackTrace
bobobobo


En Linux con glibc, desafortunadamente, las backtrace_symbolsfunciones no proporcionan el nombre de la función, el nombre del archivo fuente y el número de línea.
Maxim Egorushkin

Además de usar -rdynamic, también verifique que su sistema de compilación no agregue -fvisibility=hiddenopciones. (ya que descartará por completo el efecto de -rdynamic)
Dima Litvinov

36

Impulsar stacktrace

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

Esta es la opción más conveniente que he visto hasta ahora, porque:

  • realmente puede imprimir los números de línea.

    Sin embargo , solo hace llamadas aaddr2line , lo cual es feo y podría ser lento si está tomando demasiados rastreos.

  • demanda por defecto

  • El impulso es solo el encabezado, por lo que probablemente no sea necesario modificar su sistema de compilación

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

Desafortunadamente, parece ser una adición más reciente y el paquete libboost-stacktrace-devno está presente en 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

Tenemos que agregar -ldlal final o la compilación falla.

Salida:

 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

La salida y se explica con más detalle en la sección "backtrace glibc" a continuación, que es análoga.

Tenga en cuenta cómo my_func_1(int)y my_func_1(float), que están destrozados debido a la sobrecarga de funciones , fueron muy bien demandados para nosotros.

Tenga en cuenta que la primera intllamada está desactivada en una línea (28 en lugar de 27 y la segunda está desactivada en dos líneas (27 en lugar de 29). En los comentarios se sugirió que esto se debe a que se está considerando la siguiente dirección de instrucción, que hace que 27 se convierta en 28, y 29 salte del círculo y se convierta en 27.

Luego observamos que con -O3, la salida está completamente mutilada:

 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

En general, los retrocesos son mutilados irreparablemente por optimizaciones. La optimización de llamadas finales es un ejemplo notable de eso: ¿Qué es la optimización de llamadas finales?

Evaluación comparativa ejecutada en -O3:

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

Salida:

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

Entonces, como era de esperar, vemos que este método es extremadamente lento, probablemente para llamadas externas addr2liney solo será factible si se realiza una cantidad limitada de llamadas.

Cada impresión de backtrace parece tardar cientos de milisegundos, así que tenga en cuenta que si ocurre un backtrace con mucha frecuencia, el rendimiento del programa se verá afectado significativamente.

Probado en Ubuntu 19.10, GCC 9.2.1, boost 1.67.0.

glibc backtrace

Documentado en: https://www.gnu.org/software/libc/manual/html_node/Backtraces.html

C Principal

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

Compilar:

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

-rdynamic es la opción clave requerida.

Correr:

./main.out

Salidas:

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

Así que inmediatamente vemos que ocurrió una optimización en línea y que algunas funciones se perdieron del seguimiento.

Si intentamos obtener las direcciones:

addr2line -e main.out 0x4008f9 0x4008fe

obtenemos:

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

que está completamente apagado.

Si hacemos lo mismo con en su -O0lugar, ./main.outda el seguimiento completo correcto:

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

y entonces:

addr2line -e main.out 0x400a74 0x400a79

da:

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

entonces las líneas están apagadas por solo una, TODO ¿por qué? Pero esto aún podría ser utilizable.

Conclusión: los backtraces solo pueden mostrarse perfectamente con -O0. Con las optimizaciones, el backtrace original se modifica fundamentalmente en el código compilado.

No pude encontrar una manera simple de exigir automáticamente los símbolos de C ++ con esto, sin embargo, aquí hay algunos trucos:

Probado en Ubuntu 16.04, GCC 6.4.0, libc 2.23.

glibc backtrace_symbols_fd

Este ayudante es un poco más conveniente backtrace_symbolsy produce resultados básicamente idénticos:

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

Probado en Ubuntu 16.04, GCC 6.4.0, libc 2.23.

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

Adaptado de: https://gist.github.com/fmela/591333/c64f4eb86037bb237862a8283df70cdfc25f01d3

Este es un "truco" porque requiere cambiar el 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 y ejecuta:

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 

salida:

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]

Probado en Ubuntu 18.04.

glibc backtracecon C ++ exigiendo hack 2: analizar la salida del backtrace

Se muestra en: https://panthema.net/2008/0901-stacktrace-demangled/

Este es un truco porque requiere análisis.

TODO consígalo para compilarlo y mostrarlo aquí.

libunwind

TODO, ¿tiene esto alguna ventaja sobre el backtrace de glibc? Salida muy similar, también requiere modificar el comando de compilación, pero no forma parte de glibc, por lo que requiere una instalación de paquete adicional.

Código adaptado de: https://eli.thegreenplace.net/2015/programmatic-access-to-the-call-stack-in-c/

C Principal

/* 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 y ejecuta:

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 700debe estar en la parte superior o debemos usar -std=gnu99:

Correr:

./main.out

Salida:

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

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

y:

addr2line -e main.out 0x4007db 0x4007e2

da:

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

y:

addr2line -e main.out 0x4009f3 0x4009f8

da:

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

Probado en Ubuntu 16.04, GCC 6.4.0, libunwind 1.1.

libunwind con demanda de nombre C ++

Código adaptado de: https://eli.thegreenplace.net/2015/programmatic-access-to-the-call-stack-in-c/

relajarse.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 y ejecuta:

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

Salida:

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)

y luego podemos encontrar las líneas de my_func_2y my_func_1(int)con:

addr2line -e unwind.out 0x400c80 0x400cb7

lo que da:

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

TODO: ¿por qué las líneas están desviadas en uno?

Probado en Ubuntu 18.04, GCC 7.4.0, libunwind 1.2.1.

Automatización de GDB

También podemos hacer esto con GDB sin volver a compilar usando: ¿Cómo realizar una acción específica cuando se alcanza un determinado punto de interrupción en GDB?

Aunque si va a imprimir mucho el backtrace, esto probablemente será menos rápido que las otras opciones, pero tal vez podamos alcanzar velocidades nativas con compile code, pero me da pereza probarlo ahora: ¿Cómo llamar al ensamblaje en 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 y ejecuta:

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

Salida:

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 Quería hacer esto solo -exdesde la línea de comando para no tener que crear, main.gdbpero no pude hacer commandsque funcionara allí.

Probado en Ubuntu 19.04, GDB 8.2.

Kernel de Linux

¿Cómo imprimir el seguimiento de la pila de hilos actual dentro del kernel de Linux?

libdwfl

Esto se mencionó originalmente en: https://stackoverflow.com/a/60713161/895245 y podría ser el mejor método, pero tengo que comparar un poco más, pero vota esa respuesta.

TODO: Traté de minimizar el código en esa respuesta, que estaba funcionando, a una sola función, pero está fallando, avíseme si alguien puede encontrar por qué.

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 y ejecuta:

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

Salida:

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

Ejecución de referencia:

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

Salida:

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

Entonces vemos que este método es 10 veces más rápido que el seguimiento de pila de Boost y, por lo tanto, podría ser aplicable a más casos de uso.

Probado en Ubuntu 19.10 amd64, libdw-dev 0.176-1.1.

Ver también


1
Todos los "TODO: líneas de una en una" se deben a que el número de línea se toma desde el comienzo de la siguiente expresión.
SS Anne

6

No existe una forma estandarizada de hacerlo. Para Windows, la funcionalidad se proporciona en la biblioteca DbgHelp


6

¿Hay alguna forma de volcar la pila de llamadas en un proceso en ejecución en C o C ++ cada vez que se llama a una determinada función?

Puede utilizar una función de macro en lugar de una declaración de retorno en la función específica.

Por ejemplo, en lugar de usar return,

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

    ... do something ...

    return 0
}

Puede utilizar una función macro.

#include "c-callstack.h"

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

    ... do something ...

    NL_RETURN(0);
}

Siempre que ocurra un error en una función, verá la pila de llamadas de estilo Java como se muestra a continuación.

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)

El código fuente completo está disponible aquí.

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


6

Otra respuesta a un viejo hilo.

Cuando necesito hacer esto, usualmente solo uso system()ypstack

Entonces algo como esto:

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

Esto salidas

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

Esto debería funcionar en Linux, FreeBSD y Solaris. No creo que macOS tenga pstack o un equivalente simple, pero este hilo parece tener una alternativa .

Si lo está utilizando C, necesitará utilizar Cfunciones de cadena.

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

He usado 7 para el número máximo de dígitos en el PID, según esta publicación .


Buen punto, ya que el sujeto pregunta por C. No, necesitaría adaptarse, ya que std :: string es solo C ++. Actualizaré mi respuesta con una versión C.
Paul Floyd

5

Específico de Linux, TLDR:

  1. backtracein glibcproduce trazas de pila precisas solo cuando -lunwindestá vinculado (característica específica de la plataforma no documentada).
  2. Para generar el nombre de la función , el archivo fuente y el uso del número de línea#include <elfutils/libdwfl.h> (esta biblioteca está documentada solo en su archivo de encabezado). backtrace_symbolsy backtrace_symbolsd_fdson menos informativos.

En Linux moderno, puede obtener las direcciones de seguimiento de pila usando la función backtrace. La forma indocumentada de backtraceproducir direcciones más precisas en plataformas populares es vincular con -lunwind( libunwind-deven Ubuntu 18.04) (consulte el resultado de ejemplo a continuación). backtraceusa la función _Unwind_Backtracey, por defecto, esta última proviene libgcc_s.so.1y esa implementación es la más portátil. Cuando -lunwindestá vinculado, proporciona una versión más precisa de, _Unwind_Backtracepero esta biblioteca es menos portátil (consulte las arquitecturas compatibles en libunwind/src).

Desafortunadamente, el complemento backtrace_symbolsdy las backtrace_symbols_fdfunciones no han podido resolver las direcciones de seguimiento de pila en nombres de funciones con el nombre del archivo de origen y el número de línea durante probablemente una década (consulte el resultado de ejemplo a continuación).

Sin embargo, existe otro método para resolver direcciones en símbolos y produce los rastros más útiles con el nombre de la función , el archivo fuente y el número de línea . El método es ay #include <elfutils/libdwfl.h>enlazar con -ldw( libdw-deven Ubuntu 18.04).

Ejemplo de trabajo en C ++ ( 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");
}

Compilado en 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

Salidas:

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

Cuando no -lunwindestá vinculado, produce un seguimiento de pila menos preciso:

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

A modo de comparación, la backtrace_symbols_fdsalida para el mismo stacktrace es menos informativa:

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

En una versión de producción (así como la versión en lenguaje C) que le gusten a hacer de este código extra robusta mediante la sustitución boost::core::demangle, std::stringy std::coutcon sus llamadas subyacentes.

También puede anular __cxa_throwpara capturar el seguimiento de pila cuando se lanza una excepción e imprimirlo cuando se detecta la excepción. En el momento en que ingresa al catchbloque, la pila se ha desenrollado, por lo que es demasiado tarde para llamar backtrace, y es por eso que se debe capturar la pila en la throwque se implementa la función __cxa_throw. Tenga en cuenta que en un programa de subprocesos múltiples __cxa_throwse pueden llamar simultáneamente varios subprocesos, por lo que si captura el seguimiento de la pila en una matriz global, debe ser thread_local.


1
¡Buena respuesta! Bien investigado también.
SS Anne

@SSAnne Muy amable, gracias. Ese -lunwindproblema se descubrió al hacer esta publicación, anteriormente lo usé libunwinddirectamente para obtener el seguimiento de la pila e iba a publicarlo, pero lo backtracehace por mí cuando -lunwindestá vinculado.
Maxim Egorushkin

1
@SSAnne Puede deberse a que el autor original de la biblioteca, David Mosberger, se centró inicialmente en IA-64, pero luego la biblioteca obtuvo más tracción nongnu.org/libunwind/people.html . gccno expone la API, ¿verdad?
Maxim Egorushkin

3

Puede implementar la funcionalidad usted mismo:

Use una pila global (cadena) y al comienzo de cada función inserte el nombre de la función y otros valores (por ejemplo, parámetros) en esta pila; al salir de la función, vuelva a abrirla.

Escriba una función que imprima el contenido de la pila cuando se la llame y utilícela en la función en la que desea ver la pila de llamadas.

Esto puede parecer mucho trabajo, pero es bastante útil.


2
Yo no haría eso. Más bien, crearía un contenedor que utiliza las API específicas de la plataforma subyacente (ver más abajo). La cantidad de trabajo probablemente sería la misma, pero la inversión debería amortizar más rápido.
Paul Michalik

3
@paul: su respuesta se refiere a Windows cuando el OP especifica claramente Linux ... pero podría ser útil para los usuarios de Windows que aparecen aquí.
slashmais

Correcto, pasé por alto eso ... Hm, es la última oración de la pregunta, así que tal vez el cartel debería modificar su solicitud para mencionar su plataforma de destino en un lugar más destacado.
Paul Michalik

1
Esta sería una buena idea, excepto que mi base de código incluye unas pocas docenas de archivos que contienen algunos cientos (si no algunos miles) de archivos, por lo que esto es inviable.
Nathan Fellman

tal vez no si piratea un script sed / perl para agregar después de cada declaración de función call_registror MY_SUPERSECRETNAME(__FUNCTION__);que empuja el argumento en su constructor y aparece en su destructor FUNCTION siempre representa el nombre de la función actual.
fluyó el

2

Por supuesto, la siguiente pregunta es: ¿será suficiente?

La principal desventaja de los stack-traces es que por qué se llama a la función precisa, no tiene nada más, como el valor de sus argumentos, lo cual es muy útil para depurar.

Si tiene acceso a gcc y gdb, sugeriría usarlo assertpara verificar una condición específica y producir un volcado de memoria si no se cumple. Por supuesto, esto significa que el proceso se detendrá, pero tendrá un informe completo en lugar de un simple seguimiento de pila.

Si desea una forma menos molesta, siempre puede usar el registro. Existen instalaciones de tala muy eficientes, como Pantheios, por ejemplo. Lo que una vez más podría darle una imagen mucho más precisa de lo que está sucediendo.


1
Por supuesto, puede que no sea suficiente, pero si puedo ver que la función se llama en su lugar con una configuración y no con la otra, entonces ese es un buen lugar para comenzar.
Nathan Fellman

2

Puedes usar Poppy para esto. Normalmente se usa para recopilar el seguimiento de la pila durante un bloqueo, pero también puede generarlo para un programa en ejecución.

Ahora, aquí está la parte buena: puede generar los valores de los parámetros reales para cada función en la pila, e incluso las variables locales, contadores de bucle, etc.


2

Sé que este hilo es antiguo, pero creo que puede ser útil para otras personas. Si está usando gcc, puede usar sus características de instrumento (opción -finstrument-functions) para registrar cualquier llamada de función (entrada y salida). Eche un vistazo a esto para obtener más información: http://hacktalks.blogspot.fr/2013/08/gcc-instrument-functions.html

Por lo tanto, puede, por ejemplo, insertar y colocar todas las llamadas en una pila, y cuando desee imprimirlas, solo mire lo que tiene en su pila.

Lo he probado, funciona perfectamente y es muy útil.

ACTUALIZACIÓN: también puede encontrar información sobre la opción de compilación -finstrument-functions en el documento de GCC sobre las opciones de instrumentación: https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html


También debe vincular a los documentos de GCC en caso de que el artículo desaparezca.
HolyBlackCat

Gracias, tienes razón. Por lo tanto, agregué una ACTUALIZACIÓN en mi publicación con un enlace al documento de gcc
François

2

Puede utilizar las bibliotecas de Boost para imprimir la pila de llamadas actual.

#include <boost/stacktrace.hpp>

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

Hombre aquí: https://www.boost.org/doc/libs/1_65_1/doc/html/stacktrace.html


Recibí un error cannot locate SymEnumSymbolsExW at C:\Windows\SYSTEM32\dbgeng.dllen Win10.
zwcloud

0

Puede utilizar el generador de perfiles de GNU. ¡También muestra el gráfico de llamadas! el comando es gprofy necesita compilar su código con alguna opción.


-6

¿Hay alguna forma de volcar la pila de llamadas en un proceso en ejecución en C o C ++ cada vez que se llama a una determinada función?

No, no lo hay, aunque pueden existir soluciones dependientes de la plataforma.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.