Marco de pila dañado GDB - ¿Cómo depurar?

113

Tengo el siguiente rastro de pila. ¿Es posible distinguir algo útil de esto para depurar?

Program received signal SIGSEGV, Segmentation fault.
0x00000002 in ?? ()
(gdb) bt
#0  0x00000002 in ?? ()
#1  0x00000001 in ?? ()
#2  0xbffff284 in ?? ()
Backtrace stopped: previous frame inner to this frame (corrupt stack?)
(gdb) 

¿Dónde empezar a mirar el código cuando obtenemos un Segmentation fault, y el seguimiento de la pila no es tan útil?

NOTA: Si publico el código, los expertos de SO me darán la respuesta. Quiero seguir la guía de SO y encontrar la respuesta yo mismo, así que no voy a publicar el código aquí. Disculpas

Sangeeth Saravanaraj
fuente
Probablemente su programa saltó a la maleza: ¿puede recuperar algo del puntero de la pila?
Carl Norum
1
Otra cosa a considerar es si el puntero del marco está configurado correctamente. ¿Está construyendo sin optimizaciones o pasando una bandera como -fno-omit-frame-pointer? Además, para la corrupción de la memoria, valgrindpodría ser una herramienta más apropiada, si es una opción para usted.
FatalError

Respuestas:

155

Esas direcciones falsas (0x00000002 y similares) son en realidad valores de PC, no valores de SP. Ahora, cuando obtiene este tipo de SEGV, con una dirección de PC falsa (muy pequeña), el 99% de las veces se debe a una llamada a través de un puntero de función falso. Tenga en cuenta que las llamadas virtuales en C ++ se implementan mediante punteros de función, por lo que cualquier problema con una llamada virtual puede manifestarse de la misma manera.

Una instrucción de llamada indirecta simplemente empuja la PC después de la llamada a la pila y luego establece la PC en el valor objetivo (falso en este caso), por lo que si esto es lo que sucedió, puede deshacerlo fácilmente sacando manualmente la PC de la pila. . En código x86 de 32 bits, simplemente haga lo siguiente:

(gdb) set $pc = *(void **)$esp
(gdb) set $esp = $esp + 4

Con código x86 de 64 bits que necesita

(gdb) set $pc = *(void **)$rsp
(gdb) set $rsp = $rsp + 8

Entonces, debería poder hacer una bty averiguar dónde está realmente el código.

El otro 1% de las veces, el error se debe a que se sobrescribe la pila, generalmente al desbordar una matriz almacenada en la pila. En este caso, es posible que pueda obtener más claridad sobre la situación utilizando una herramienta como valgrind

Chris Dodd
fuente
5
@George: gdb executable corefileabrirá gdb con el archivo ejecutable y principal, en cuyo punto puede hacer bt(o los comandos anteriores seguidos de bt) ...
Chris Dodd
2
@mk .. ARM no usa la pila para direcciones de retorno, sino que usa el registro de enlace. Por lo tanto, generalmente no tiene este problema, o si lo tiene, generalmente se debe a alguna otra corrupción de la pila.
Chris Dodd
2
Incluso en ARM, creo, todos los registros de propósito general y LR se almacenan en la pila antes de que la función llamada comience a ejecutarse. Una vez que finaliza la función, el valor de LR se introduce en la PC y, por lo tanto, la función regresa. Entonces, si la pila está dañada, podemos ver que un valor incorrecto es PC, ¿verdad? En este caso, el ajuste del puntero de pila conducirá a la pila adecuada y ayudará a depurar el problema. ¿Qué piensas? por favor déjeme saber sus pensamientos. Gracias.
mk ..
1
¿Qué significa falso?
Danny Lo
5
ARM no es x86: su puntero de pila se llama sp, no espo rsp, y su instrucción de llamada almacena la dirección de retorno en el lrregistro, no en la pila. Entonces, para ARM, todo lo que realmente necesita para deshacer la llamada es set $pc = $lr. Si $lrno es válido, tiene un problema mucho más difícil de resolver.
Chris Dodd
44

Si la situación es bastante simple, la respuesta de Chris Dodd es la mejor. Parece que saltó a través de un puntero NULL.

Sin embargo, es posible que el programa se haya disparado en el pie, la rodilla, el cuello y el ojo antes de estrellarse: sobrescribió la pila, estropeó el puntero del cuadro y otros males. Si es así, no es probable que desenredar el hachís le muestre patatas y carne.

La solución más eficiente será ejecutar el programa bajo el depurador y pasar por alto las funciones hasta que el programa se bloquee. Una vez que se identifica una función que falla, comience de nuevo, ingrese a esa función y determine qué función llama la causa. Repita hasta que encuentre la única línea de código infractora. El 75% de las veces, la solución será obvia.

En el otro 25% de las situaciones, la línea de código infractora es una pista falsa. Estará reaccionando a condiciones (inválidas) configuradas muchas líneas antes, tal vez miles de líneas antes. Si ese es el caso, el mejor curso elegido depende de muchos factores: principalmente su comprensión del código y su experiencia con él:

  • Quizás el establecimiento de un punto de observación del depurador o la inserción de diagnósticos printfen variables críticas conduzca a la necesaria A ha!
  • Quizás cambiar las condiciones de prueba con diferentes entradas proporcionará más información que la depuración.
  • Tal vez un segundo par de ojos lo obligue a verificar sus suposiciones o recopilar evidencia pasada por alto.
  • A veces, todo lo que se necesita es ir a cenar y pensar en las pruebas reunidas.

¡Buena suerte!

Wallyk
fuente
13
Si no se dispone de un segundo par de ojos, los patitos de goma están bien probados como alternativas.
Matt
2
Eliminar el final de un búfer también puede hacerlo. Puede que no se bloquee cuando cancele el final del búfer, pero cuando salga de la función, muere.
phyatt
Puede ser útil: GDB: "Siguiente" automático
user202729
28

Suponiendo que el puntero de la pila es válido ...

Puede ser imposible saber exactamente dónde ocurre la SEGV a partir del backtrace; creo que los dos primeros marcos de pila se sobrescriben por completo. 0xbffff284 parece una dirección válida, pero las dos siguientes no lo son. Para ver más de cerca la pila, puede intentar lo siguiente:

gdb $ x / 32ga $ rsp

o una variante (reemplace el 32 con otro número). Eso imprimirá un número de palabras (32) a partir del puntero de pila de tamaño gigante (g), formateado como direcciones (a). Escriba 'ayuda x' para obtener más información sobre el formato.

Instrumentar su código con algunos 'printf' centinelas puede no ser una mala idea, en este caso.

manabear
fuente
Increíblemente útil, gracias. Tenía una pila que solo retrocedía tres fotogramas y luego presioné "Trazado anterior detenido: fotograma anterior idéntico a este fotograma (¿pila corrupta?)"; He hecho algo exactamente como esto en código en un controlador de excepciones de CPU antes, pero no pude recordar más que info symbolcómo hacerlo en gdb.
leander
22
FWIW en dispositivos ARM de 32 bits: x/256wa $sp =)
leander
2
@leander ¿Podrías decirme qué es X / 256wa? Lo necesito para ARM de 64 bits. En general, será útil si puede explicar qué es.
mk ..
5
Según la respuesta, 'x' = examinar la ubicación de la memoria; imprime un número de 'w' = palabras (en este caso, 256) y las interpreta como 'a' = direcciones. Hay más información en el manual de GDB en sourceware.org/gdb/current/onlinedocs/gdb/Memory.html#Memory .
leander
7

Mire algunos de sus otros registros para ver si uno de ellos tiene el puntero de pila almacenado en caché. A partir de ahí, es posible que pueda recuperar una pila. Además, si está incrustado, con frecuencia la pila se define en una dirección muy particular. Usando eso, a veces también puedes obtener una pila decente. Todo esto supone que cuando saltó al hiperespacio, su programa no vomitó toda la memoria en el camino ...

Michael Dorgan
fuente
3

Si se trata de una sobrescritura de pila, los valores pueden corresponder a algo reconocible del programa.

Por ejemplo, me encontré mirando la pila

(gdb) bt
#0  0x0000000000000000 in ?? ()
#1  0x000000000000342d in ?? ()
#2  0x0000000000000000 in ?? ()

y 0x342des 13357, que resultó ser un ID de nodo cuando hice grep en los registros de la aplicación. Eso ayudó inmediatamente a reducir los sitios candidatos donde podría haberse producido la sobrescritura de la pila.

Craig Ringer
fuente