GDB corrompido stack frame - como depurar?

113

Eu tenho o seguinte rastreamento de pilha. É possível extrair algo útil disso para depuração?

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) 

Por onde começar a olhar para o código quando obtivermos um Segmentation fault, e o rastreamento de pilha não for tão útil?

NOTA: Se eu postar o código, os especialistas em SO me darão a resposta. Quero seguir a orientação do SO e encontrar a resposta sozinho, então não estou postando o código aqui. Desculpas.

Sangeeth Saravanaraj
fonte
Provavelmente seu programa saltou para o meio do mato - você pode recuperar alguma coisa do ponteiro da pilha?
Carl Norum,
1
Outra coisa a considerar é se o ponteiro do quadro está definido corretamente. Você está construindo sem otimizações ou passando uma bandeira como -fno-omit-frame-pointer? Além disso, para corrupção de memória, valgrindpode ser uma ferramenta mais apropriada, se for uma opção para você.
FatalError

Respostas:

155

Esses endereços falsos (0x00000002 e semelhantes) são, na verdade, valores de PC, não valores de SP. Agora, quando você obtém esse tipo de SEGV, com um endereço de PC falso (muito pequeno), 99% das vezes é devido à chamada por meio de um ponteiro de função falso. Observe que as chamadas virtuais em C ++ são implementadas por meio de ponteiros de função, portanto, qualquer problema com uma chamada virtual pode se manifestar da mesma maneira.

Uma instrução chamada indireta apenas empurra o PC após a chamada para a pilha e, em seguida, define o PC ao valor-alvo (falsa, neste caso), então se isso é o que aconteceu, você pode facilmente desfazê-lo por avançar manualmente o PC da pilha . No código x86 de 32 bits, você apenas faz:

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

Com o código x86 de 64 bits, você precisa

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

Então, você deve ser capaz de fazer um bte descobrir onde o código é realmente.

No outro 1% das vezes, o erro será devido à substituição da pilha, geralmente pelo estouro de uma matriz armazenada na pilha. Nesse caso, você pode obter mais clareza sobre a situação usando uma ferramenta como o valgrind

Chris Dodd
fonte
5
@George: gdb executable corefileirá abrir o gdb com o executável e o arquivo principal, momento em que você pode fazer bt(ou os comandos acima seguidos de bt) ...
Chris Dodd
2
@mk .. ARM não usa a pilha para endereços de retorno - ele usa o registrador de link ao invés. Portanto, geralmente não há esse problema, ou se tiver, geralmente é devido a alguma outra corrupção de pilha.
Chris Dodd
2
Mesmo no ARM, eu acho, todos os registradores de uso geral e LR são armazenados na pilha antes que a função chamada comece a ser executada. Quando a função termina, o valor de LR é inserido no PC e, portanto, a função retorna. Portanto, se a pilha estiver corrompida, podemos ver um valor errado. O PC está certo? Neste caso, o ajuste do ponteiro da pilha levará à pilha apropriada e ajudará a depurar o problema. O que você acha? por favor, deixe-me saber seus pensamentos. Obrigado.
mk ..
1
O que significa falso?
Danny Lo
5
ARM não é x86 - seu ponteiro de pilha é chamado sp, não espou rsp, e sua instrução de chamada armazena o endereço de retorno no lrregistrador, não na pilha. Portanto, para ARM, tudo o que você realmente precisa para desfazer a chamada é set $pc = $lr. Se $lrfor inválido, você terá um problema muito mais difícil de resolver.
Chris Dodd,
44

Se a situação for bastante simples, a resposta de Chris Dodd é a melhor. Parece que saltou um ponteiro NULL.

No entanto, é possível que o programa tenha atingido o pé, o joelho, o pescoço e o olho antes de cair - sobrescreveu a pilha, bagunçou o ponteiro do quadro e outros males. Nesse caso, desfazer o haxixe provavelmente não mostrará batatas e carne.

A solução mais eficiente será executar o programa no depurador e passar por cima das funções até que o programa trave. Assim que uma função de travamento for identificada, comece novamente e entre nessa função e determine qual função ela chama que causa o travamento. Repita até encontrar a única linha de código ofensiva. 75% das vezes, a correção será óbvia.

Nos outros 25% das situações, a chamada linha de código ofensiva é uma pista falsa. Ele estará reagindo a condições (inválidas) configuradas muitas linhas antes - talvez milhares de linhas antes. Se for esse o caso, o melhor curso escolhido depende de muitos fatores: principalmente sua compreensão do código e experiência com ele:

  • Talvez definir um watchpoint do depurador ou inserir diagnósticos printfem variáveis ​​críticas leve ao A ha! Necessário !
  • Talvez alterar as condições de teste com entradas diferentes fornecerá mais informações do que depuração.
  • Talvez um segundo par de olhos o force a verificar suas suposições ou reunir evidências ignoradas.
  • Às vezes, basta ir jantar e pensar nas evidências reunidas.

Boa sorte!

Wallyk
fonte
13
Se um segundo par de olhos não estiver disponível, patos de borracha são comprovadamente alternativas.
Matt,
2
Escrever no final de um buffer também pode fazer isso. Pode não travar onde você cancela o final do buffer, mas quando você sai da função, ele morre.
phyatt
Pode ser útil: GDB: 'Próxima' automática
user202729
28

Supondo que o ponteiro da pilha seja válido ...

Pode ser impossível saber exatamente onde o SEGV ocorre a partir do backtrace - acho que os dois primeiros frames da pilha foram completamente substituídos. 0xbffff284 parece ser um endereço válido, mas os próximos dois não são. Para uma análise mais detalhada da pilha, você pode tentar o seguinte:

gdb $ x / 32ga $ rsp

ou uma variante (substitua o 32 por outro número). Isso imprimirá algum número de palavras (32) a partir do ponteiro da pilha de tamanho gigante (g), formatado como endereços (a). Digite 'help x' para obter mais informações sobre o formato.

Instrumentar seu código com alguns 'printf' de sentinela pode não ser uma má ideia, neste caso.

manabear
fonte
Incrivelmente útil, obrigado - eu tinha uma pilha que retrocedeu apenas três quadros e depois cliquei em "Backtrace interrompido: quadro anterior idêntico a este quadro (pilha corrompida?)"; Eu fiz algo exatamente assim no código em um manipulador de exceção da CPU antes, mas não conseguia me lembrar de outra coisa senão info symbolcomo fazer isso no gdb.
leander
22
FWIW em dispositivos ARM de 32 bits: x/256wa $sp =)
leander
2
@leander Você poderia me dizer o que é X / 256wa? Eu preciso disso para ARM de 64 bits. Em geral, será útil se você puder explicar o que é.
mk ..
5
De acordo com a resposta, 'x' = examine a localização da memória; ele imprime um número de 'w' = palavras (neste caso, 256) e as interpreta como 'a' = endereços. Há mais informações no manual do GDB em sourceware.org/gdb/current/onlinedocs/gdb/Memory.html#Memory .
leander
7

Observe alguns de seus outros registradores para ver se um deles tem o ponteiro da pilha armazenado em cache. A partir daí, você pode recuperar uma pilha. Além disso, se estiver embutido, muitas vezes a pilha é definida em um endereço muito particular. Usando isso, às vezes você também pode obter uma pilha decente. Tudo isso pressupõe que quando você saltou para o hiperespaço, seu programa não vomitou toda a memória ao longo do caminho ...

Michael Dorgan
fonte
3

Se for uma substituição de pilha, os valores podem muito bem corresponder a algo reconhecível do programa.

Por exemplo, acabei de olhar para a pilha

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

e 0x342dé 13357, que acabou sendo um id de nó quando eu executei o grep nos logs do aplicativo para ele. Isso ajudou imediatamente a restringir os sites candidatos onde a substituição da pilha poderia ter ocorrido.

Craig Ringer
fonte