¿Cómo se puede obtener un seguimiento de pila en C?

Respuestas:

81

glibc proporciona la función backtrace ().

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

sanxiyn
fuente
6
glibc FTW ... de nuevo. (Esta es otra razón por la que considero que glibc es el estándar de oro absoluto cuando se trata de programación en C (eso y el compilador que lo acompaña)).
Trevor Boyd Smith
4
¡Pero espera hay mas! La función backtrace () solo proporciona una matriz de punteros void * que representan las funciones de la pila de llamadas. "Eso no es muy útil. Arg." ¡No temáis! glibc proporciona una función que convierte todas las direcciones vacías * (las direcciones de la función de pila de llamadas) en símbolos de cadena legibles por humanos. char ** backtrace_symbols (void *const *buffer, int size)
Trevor Boyd Smith
1
Advertencia: solo funciona para funciones C, creo. @Trevor: Solo busca syms por dirección en la tabla ELF.
Conrad Meyer
2
También hay void backtrace_symbols_fd(void *const *buffer, int size, int fd)que enviar la salida directamente a stdout / err, por ejemplo.
sem.
2
backtrace_symbols()apesta. Requiere exportar todos los símbolos y no admite símbolos DWARF (depuración). libbacktrace es una opción mucho mejor en muchos (la mayoría) de los casos.
Erwan Legrand
29

Hay backtrace () y backtrace_symbols ():

Desde la página del manual:

#include <execinfo.h>
#include <stdio.h>
...
void* callstack[128];
int i, frames = backtrace(callstack, 128);
char** strs = backtrace_symbols(callstack, frames);
for (i = 0; i < frames; ++i) {
    printf("%s\n", strs[i]);
}
free(strs);
...

Una forma de usar esto de una manera / OOP más conveniente es guardar el resultado de backtrace_symbols () en un constructor de clase de excepción. Por lo tanto, cada vez que lanza ese tipo de excepción, tiene el seguimiento de la pila. Luego, solo proporcione una función para imprimirlo. Por ejemplo:

class MyException : public std::exception {

    char ** strs;
    MyException( const std::string & message ) {
         int i, frames = backtrace(callstack, 128);
         strs = backtrace_symbols(callstack, frames);
    }

    void printStackTrace() {
        for (i = 0; i < frames; ++i) {
            printf("%s\n", strs[i]);
        }
        free(strs);
    }
};

...

try {
   throw MyException("Oops!");
} catch ( MyException e ) {
    e.printStackTrace();
}

Ta da!

Nota: habilitar los indicadores de optimización puede hacer que el seguimiento de la pila resultante sea inexacto. Idealmente, se utilizaría esta capacidad con los indicadores de depuración activados y los indicadores de optimización desactivados.

Tom
fuente
@shuckc solo para convertir la dirección en una cadena de símbolos, lo que se puede hacer externamente usando otras herramientas si es necesario.
Woodrow Barlow
22

Para Windows, consulte la API StackWalk64 () (también en Windows de 32 bits). Para UNIX, debe usar la forma nativa del sistema operativo para hacerlo, o recurrir al backtrace () de glibc, si está disponible.

Sin embargo, tenga en cuenta que tomar un Stacktrace en código nativo rara vez es una buena idea, no porque no sea posible, sino porque generalmente está tratando de lograr lo incorrecto.

La mayoría de las veces, las personas intentan obtener un seguimiento de pila en, digamos, una circunstancia excepcional, como cuando se detecta una excepción, una afirmación falla o, lo peor y lo más incorrecto de todos, cuando obtiene una "excepción" fatal o una señal como violación de la segmentación.

Teniendo en cuenta el último problema, la mayoría de las API requerirán que asigne memoria explícitamente o puede que lo haga internamente. Si lo hace en el estado frágil en el que se encuentra actualmente su programa, puede empeorar aún más las cosas. Por ejemplo, el informe de fallos (o coredump) no reflejará la causa real del problema, sino su intento fallido de solucionarlo).

Supongo que está tratando de lograr esa cosa del manejo de errores fatales, ya que la mayoría de la gente parece intentar eso cuando se trata de obtener un seguimiento de pila. Si es así, confiaría en el depurador (durante el desarrollo) y dejaría que el proceso se volcara en producción (o mini volcado en Windows). Junto con una gestión adecuada de los símbolos, no debería tener problemas para determinar la instrucción que la causa después de la autopsia.

Christian.K
fuente
2
Tiene razón en que es frágil intentar la asignación de memoria en un manejador de señales o excepciones. Una posible salida es asignar una cantidad fija de espacio de "emergencia" al inicio del programa o utilizar un búfer estático.
j_random_hacker
Otra salida es crear un servicio coredump, que se ejecuta de forma independiente
Kobor42
5

Debería utilizar la biblioteca de desenrollado .

unw_cursor_t cursor; unw_context_t uc;
unw_word_t ip, sp;
unw_getcontext(&uc);
unw_init_local(&cursor, &uc);
unsigned long a[100];
int ctr = 0;

while (unw_step(&cursor) > 0) {
  unw_get_reg(&cursor, UNW_REG_IP, &ip);
  unw_get_reg(&cursor, UNW_REG_SP, &sp);
  if (ctr >= 10) break;
  a[ctr++] = ip;
}

Su enfoque también funcionaría bien a menos que realice una llamada desde una biblioteca compartida.

Puede usar el addr2linecomando en Linux para obtener la función fuente / número de línea de la PC correspondiente.

user262802
fuente
"función de origen / número de línea"? ¿Qué pasa si la vinculación está optimizada para un tamaño de código reducido? Sin embargo, diré que este parece un proyecto útil. Lástima que no haya forma de obtener los registros. Definitivamente investigaré esto. ¿Sabes que es absolutamente independiente del procesador? ¿Solo funciona en cualquier cosa que tenga un compilador de C?
Mawg dice reinstalar a Monica el
1
Ok, este comentario valió la pena, ¡aunque solo sea por la mención del útil comando addr2line!
Ogre Psalm33
addr2line falla para el código reubicable en sistemas con ASLR (es decir, la mayoría de lo que la gente ha estado usando durante la última década).
Erwan Legrand
4

No existe una forma independiente de la plataforma para hacerlo.

Lo más cercano que puede hacer es ejecutar el código sin optimizaciones. De esa manera, puede adjuntar al proceso (utilizando el depurador visual de c ++ o GDB) y obtener un seguimiento de pila utilizable.

Nils Pipenbrinck
fuente
Eso no me ayuda cuando ocurre un accidente en una computadora integrada en el campo. :(
Kevin
@Kevin: Incluso en las máquinas integradas, suele haber una forma de obtener un código auxiliar del depurador remoto o al menos un volcado del núcleo. Tal vez no una vez que se implementa en el campo, aunque ...
ephemient
si ejecuta gcc-glibc en la plataforma de su elección windows / linux / mac ... entonces backtrace () y backtrace_symbols () funcionarán en las tres plataformas. Dada esa declaración, usaría las palabras "no hay una forma [portátil] de hacerlo".
Trevor Boyd Smith
4

Para Windows, CaptureStackBackTrace()también es una opción, que requiere menos código de preparación por parte del usuario StackWalk64(). (Además, para un escenario similar que tuve, CaptureStackBackTrace()terminó funcionando mejor (más confiablemente) que StackWalk64()).

Quasidart
fuente
2

Solaris tiene el comando pstack , que también se copió en Linux.

ruido
fuente
1
Útil, pero no realmente C (es una utilidad externa).
efímero
1
también, de la descripción (sección: restricciones) ": pstack actualmente funciona solo en Linux, solo en una máquina x86 que ejecuta binarios ELF de 32 bits (64 bits no son compatibles)"
Ciro Costa
0

Puede hacerlo caminando la pila hacia atrás. En realidad, sin embargo, con frecuencia es más fácil agregar un identificador en una pila de llamadas al comienzo de cada función y colocarlo al final, luego simplemente caminar imprimiendo el contenido. Es un poco PITA, pero funciona bien y al final te ahorrará tiempo.

Serafina Brocious
fuente
1
¿Podrías explicar más detalladamente "caminar la pila hacia atrás"?
Spidey
@Spidey En los sistemas integrados, a veces esto es todo lo que tienes; supongo que se rechazó porque la plataforma es WinXP. Pero sin una biblioteca que admita caminar sobre la pila, básicamente tienes que "caminar" sobre la pila. Empiece con el puntero base actual (en x86, este es el contenido del registro RBP). Eso te lleva a apuntar en la pila con 1. el RBP anterior guardado (así es como se mantiene el recorrido de la pila), y 2. la dirección de retorno de llamada / rama (llamando al registro RIP guardado de la función), que te dice cuál era la función. Luego, si tiene acceso a la tabla de símbolos, puede buscar la dirección de la función.
Ted Middleton
0

Durante los últimos años he estado usando libbacktrace de Ian Lance Taylor. Es mucho más limpio que las funciones de la biblioteca GNU C que requieren exportar todos los símbolos. Proporciona más utilidad para la generación de backtraces que libunwind. Y por último, pero no menos importante, ASLR no lo derrota, ya que los enfoques que requieren herramientas externas como addr2line.

Libbacktrace fue inicialmente parte de la distribución de GCC, pero ahora el autor lo pone a disposición como una biblioteca independiente bajo una licencia BSD:

https://github.com/ianlancetaylor/libbacktrace

En el momento de escribir este artículo, no usaría nada más a menos que necesite generar backtraces en una plataforma que no es compatible con libbacktrace.

Erwan Legrand
fuente