O programa só trava como versão de compilação - como depurar?

95

Eu tenho um problema do tipo "Gato de Schroedinger" aqui - meu programa (na verdade, o conjunto de testes do meu programa, mas um programa mesmo assim) está travando, mas apenas quando construído no modo de lançamento e apenas quando iniciado a partir da linha de comando . Por meio da depuração do homem das cavernas (ou seja, mensagens de printf () desagradáveis ​​em todo o lugar), determinei o método de teste em que o código está travando, embora, infelizmente, a falha real pareça acontecer em algum destruidor, já que as últimas mensagens de rastreamento que vejo estão em outros destruidores que são executados de forma limpa.

Quando tento executar este programa dentro do Visual Studio, ele não falha. O mesmo acontece ao iniciar a partir do WinDbg.exe. O travamento ocorre apenas ao iniciar a partir da linha de comando. A propósito, isso está acontecendo no Windows Vista e, infelizmente, não tenho acesso a uma máquina XP agora para testar.

Seria muito bom se eu pudesse fazer com que o Windows imprimisse um rastreamento de pilha ou algo diferente do que simplesmente encerrar o programa como se ele tivesse sido encerrado corretamente. Alguém tem algum conselho sobre como eu poderia obter informações mais significativas aqui e, com sorte, corrigir esse bug?

Edit: O problema foi realmente causado por uma matriz out-of-bounds, que descrevo mais neste post . Obrigado a todos pela ajuda para encontrar este problema!

Nik Reiman
fonte
Você pode dar uma amostra desse método de teste?
akalenuk,
Não desculpe, o código é muito complexo para colar aqui facilmente e, como mencionei, isso não está acontecendo no método de teste em si, mas sim em um destruidor depois. No entanto, não há ponteiros não inicializados ou algo parecido neste método.
Nik Reiman,
3
A maioria das respostas são pouco mais do que suposições. Existem algumas técnicas comuns para analisar compilações de lançamento travadas sem anexar um depurador: stackoverflow.com/a/18513077/214777?stw=2
Sebastian
Talvez não seja sua culpa: o nível de otimização -O3 é perigoso no g ++?
Brent Bradburn,

Respostas:

127

Em 100% dos casos que vi ou ouvi falar, em que um programa C ou C ++ funciona bem no depurador, mas falha quando executado externamente, a causa foi a escrita além do final de um array local de função. (O depurador coloca mais na pilha, então é menos provável que você substitua algo importante.)

James Curran
fonte
31
Alguém dê um charuto a este homem! No meu caso, estava passando um StringBuilder que não tinha capacidade grande o suficiente para uma função P / Invoke. Eu acho que é como alguém escrevendo no seu rosto com um marcador mágico quando você está dormindo: sob o depurador, eles acabam rabiscando na sua testa, então você não percebe, mas sem o depurador, eles acabam esfaqueando você no olho ... algo assim. Obrigado por esta dica!
Nicholas Piasecki de
1
No meu caso, acabou sendo um problema de alinhamento em um processador ARM usando Obj-C.
Almo
1
11 anos depois e isso ainda soa verdadeiro ... não se esqueça de reservar seus vetores.
David Tran
1
ok, então como se muda o comportamento do modo de depuração para que se possa realmente depurar.
Paul Childs
1
"Agora sabendo onde procurar", mas como tudo que funciona na depuração informa onde está o problema. Embora eu ache que sua resposta esteja correta na maioria dos casos, e saber o que procurar é um bom começo, navegar por uma grande base de código para localizar exatamente onde está o problema pode ser proibitivamente caro.
Paul Childs
54

Quando eu encontrei problemas como esse antes, geralmente era devido à inicialização de variável. No modo de depuração, variáveis ​​e ponteiros são inicializados para zero automaticamente, mas não no modo de liberação. Portanto, se você tiver um código como este

int* p;
....
if (p == 0) { // do stuff }

No modo de depuração, o código em if não é executado, mas no modo de liberação p contém um valor indefinido, que é improvável que seja 0, portanto, o código é executado frequentemente causando um travamento.

Gostaria de verificar seu código para variáveis ​​não inicializadas. Isso também pode se aplicar ao conteúdo de matrizes.

David Dibben
fonte
Casos típicos são esquecer de colocar uma variável de membro em (uma) a lista de inicialização de membros de construtores. Tem o mesmo efeito, mas é mais difícil de encontrar se você não souber que também deve procurar a inicialização de membro adequada.
steffenj,
1
No modo de depuração, as variáveis ​​são geralmente inicializadas para alguma 'constante definida pelo compilador' que pode ser usada na depuração para indicar em que estado a variável está. Por exemplo: ponteiros NULL ou 0xDeadBeef são populares.
Martin York,
Os tempos de execução de depuração normalmente inicializam a memória com algum valor diferente de zero, especificamente para que os testes de ponteiro NULL façam com que o código aja como se o ponteiro não fosse NULL. Caso contrário, você tem um código que é executado corretamente no modo de depuração que trava o modo de liberação.
Michael Burr,
1
Não, as variáveis ​​não são inicializadas de forma alguma e ainda é UB "usá-las" até que sejam atribuídas. No entanto, o conteúdo da memória subjacente geralmente é preenchido com 0x0000000 ou 0xDEADBEEF ou outros padrões reconhecíveis.
Lightness Races in Orbit
26

Nenhuma resposta até agora tentou dar uma visão geral séria sobre as técnicas disponíveis para depurar aplicativos de lançamento:

  1. As compilações Release e Debug se comportam de maneira diferente por vários motivos. Aqui está uma excelente visão geral. Cada uma dessas diferenças pode causar um bug na versão Release que não existe na versão Debug.

  2. A presença de um depurador também pode alterar o comportamento de um programa , tanto para versões de lançamento quanto para depuração. Veja esta resposta. Resumindo, pelo menos o Visual Studio Debugger usa o Debug Heap automaticamente quando anexado a um programa. Você pode desativar o heap de depuração usando a variável de ambiente _NO_DEBUG_HEAP. Você pode especificar isso nas propriedades do computador ou nas Configurações do Projeto no Visual Studio. Isso pode tornar a falha reproduzível com o depurador anexado.

    Mais informações sobre depuração de corrupção de heap aqui.

  3. Se a solução anterior não funcionar, você precisa capturar a exceção não tratada e anexar um depurador post-mortem à instância em que a falha ocorre. Você pode usar, por exemplo, WinDbg para isso, detalhes sobre os depuradores post-mortem disponíveis e sua instalação no MSDN

  4. Você pode melhorar seu código de tratamento de exceções e, se este for um aplicativo de produção, você deve:

    uma. Instale um manipulador de encerramento personalizado usandostd::set_terminate

    Se você quiser depurar esse problema localmente, poderá executar um loop infinito dentro do manipulador de terminação e enviar algum texto para o console para notificá-lo de que std::terminatefoi chamado. Em seguida, anexe o depurador e verifique a pilha de chamadas. Ou você imprime o rastreamento de pilha conforme descrito nesta resposta.

    Em um aplicativo de produção, você pode enviar um relatório de erro de volta para casa, de preferência junto com um pequeno despejo de memória que permite analisar o problema conforme descrito aqui.

    b. Use o mecanismo estruturado de tratamento de exceções da Microsoft, que permite capturar as exceções de hardware e software. Veja MSDN . Você pode proteger partes do seu código usando SEH e usar a mesma abordagem de a) para depurar o problema. SEH fornece mais informações sobre a exceção ocorrida que você pode usar ao enviar um relatório de erro de um aplicativo de produção.

Sebastian
fonte
16

Coisas a serem observadas:

Array overruns - o depurador do Visual Studio insere preenchimento que pode interromper travamentos.

Condições de corrida - você tem vários encadeamentos envolvidos? Se for assim, uma condição de corrida só aparece quando um aplicativo é executado diretamente.

Linking - é a sua versão de lançamento puxando as bibliotecas corretas.

Coisas para tentar:

Minidump - realmente fácil de usar (basta procurar no msdn) fornecerá um crash dump completo para cada thread. Você apenas carrega a saída no Visual Studio e é como se você estivesse depurando no momento da falha.

morechilli
fonte
1
Olá, fiz um voto negativo anônimo para esta resposta. Eu gostaria de entender por quê?
morechilli
12

Você pode definir o WinDbg como seu depurador post-mortem. Isso iniciará o depurador e o anexará ao processo quando a falha ocorrer. Para instalar o WinDbg para depuração post-mortem, use a opção / I (observe que está maiúsculo ):

windbg /I

Mais detalhes aqui .

Quanto à causa, é muito provavelmente uma variável unitializada, como sugerem as outras respostas.

Franci Penov
fonte
2
E não se esqueça de que você pode fazer com que o compilador gere arquivos PDB até mesmo para versões de lançamento, embora não seja o padrão.
Michael Burr,
A única resposta real para a pergunta realmente.
Sebastian
10

Depois de muitas horas de depuração, finalmente encontrei a causa do problema, que na verdade foi causado por um estouro de buffer, causando uma única diferença de byte:

char *end = static_cast<char*>(attr->data) + attr->dataSize;

Este é um erro de cerca (erro off-by-one) e foi corrigido por:

char *end = static_cast<char*>(attr->data) + attr->dataSize - 1;

O estranho foi que coloquei várias chamadas para _CrtCheckMemory () em várias partes do meu código e elas sempre retornaram 1. Consegui encontrar a origem do problema colocando "return false;" chamadas no caso de teste e, em seguida, determinar por tentativa e erro onde estava a falha.

Obrigado a todos por seus comentários - aprendi muito sobre windbg.exe hoje! :)

Nik Reiman
fonte
8
Hoje estive depurando um problema semelhante e _CrtCheckMemory () sempre retornava 1. Mas então percebi o porquê: no modo Release, _CrtCheckMemory é #definido como ((int) 1).
Brian Morearty de
7

Mesmo que você tenha construído seu exe como um release, você ainda pode gerar arquivos PDB (banco de dados do programa) que permitirão que você empilhe rastreio e faça uma quantidade limitada de inspeção de variáveis. Em suas configurações de construção, há uma opção para criar os arquivos PDB. Ligue e vincule novamente. Em seguida, tente primeiro executar a partir do IDE para ver se consegue travar. Se sim, ótimo - você está pronto para ver as coisas. Caso contrário, ao executar a partir da linha de comando, você pode fazer uma das duas coisas:

  1. Execute EXE e, antes de travar, faça um Attach To Process (menu Tools no Visual Studio).
  2. Após a falha, selecione a opção para iniciar o depurador.

Quando solicitado a apontar para arquivos PDB, navegue para encontrá-los. Se os PDBs foram colocados na mesma pasta de saída do EXE ou DLLs, provavelmente serão selecionados automaticamente.

Os PDBs fornecem um link para a fonte com informações de símbolo suficientes para possibilitar a visualização de rastreamentos de pilha, variáveis ​​etc. Você pode inspecionar os valores normalmente, mas esteja ciente de que pode obter leituras falsas, pois a passagem de otimização pode significar apenas coisas aparecem nos registros ou as coisas acontecem em uma ordem diferente da esperada.

NB: Estou assumindo um ambiente Windows / Visual Studio aqui.

Greg Whitfield
fonte
3

Falhas como essa quase sempre são causadas porque um IDE geralmente define o conteúdo de uma variável não inicializada como zeros, nulo ou algum outro valor 'sensível', ao passo que, ao executar nativamente, você obterá qualquer lixo aleatório que o sistema coletar.

Portanto, seu erro é quase certo que você está usando algo como se estivesse usando um ponteiro antes de ter sido inicializado corretamente e você está fugindo com ele no IDE porque ele não aponta para nenhum lugar perigoso - ou o valor é manipulado por seu verificação de erros - mas no modo de liberação, ele faz algo desagradável.

Cruachan
fonte
3

Para ter um despejo de memória que você possa analisar:

  1. Gere arquivos PDB para seu código.
  2. Você rebase para ter seu exe e dlls carregados no mesmo endereço.
  3. Habilite o depurador post mortem, como Dr. Watson
  4. Verifique o endereço de falhas de travamento usando uma ferramenta como localizador de travamento .

Você também deve verificar as ferramentas em Ferramentas de depuração para janelas . Você pode monitorar o aplicativo e ver todas as exceções de primeira chance anteriores à sua exceção de segunda chance.

Espero que ajude...

Yuval Peled
fonte
3

Uma ótima maneira de depurar um erro como esse é habilitar otimizações para sua compilação de depuração.

Mgill404
fonte
2

Uma vez eu tive um problema quando o aplicativo se comportou de forma semelhante ao seu. Acabou sendo um estouro de buffer desagradável no sprintf. Naturalmente, funcionou quando executado com um depurador conectado. O que fiz, foi instalar um filtro de exceção não tratada ( SetUnhandledExceptionFilter ) no qual eu simplesmente bloqueei infinitamente (usando WaitForSingleObject em um identificador falso com um valor de tempo limite de INFINITO).

Então, você poderia algo como:

long __stdcall MyFilter (EXCEPTION_POINTERS *)
{
    HANDLE hEvt = :: CreateEventW (0,1,0,0);
    if (hEvt)
    {
        if (WAIT_FAILED == :: WaitForSingleObject (hEvt, INFINITE))
        {
            // log de falha
        }
    }

}
// em algum lugar em seu wmain / WinMain:
SetUnhandledExceptionFilter (MyFilter);

Em seguida, anexei o depurador depois que o bug se manifestou (o programa gui parou de responder).

Então você pode fazer um despejo e trabalhar com isso mais tarde:

.dump / ma path_to_dump_file

Ou depure-o imediatamente. A maneira mais simples é rastrear onde o contexto do processador foi salvo pelo mecanismo de tratamento de exceções do tempo de execução:

sd esp Alcance 1003f

O comando pesquisará o espaço de endereço da pilha para registro (s) CONTEXTO, desde o comprimento da pesquisa. Eu geralmente uso algo como 'l? 10000' . Observe, não use números extraordinariamente grandes como o registro que você está procurando, geralmente próximo ao quadro de filtro de exceção não manipulado. 1003f é a combinação de sinalizadores (acredito que corresponda a CONTEXT_FULL) usada para capturar o estado do processador. Sua pesquisa seria semelhante a esta:

0: 000> sd esp l1000 1003f
0012c160 0001003f 00000000 00000000 00000000? ...............

Depois de obter os resultados de volta, use o endereço no comando cxr:

.cxr 0012c160

Isso o levará a este novo CONTEXTO, exatamente no momento do travamento (você obterá exatamente o rastreamento de pilha no momento em que seu aplicativo travou). Além disso, use:

.exr -1

para descobrir exatamente qual exceção ocorreu.

Espero que ajude.

considerou
fonte
2

Às vezes, isso acontece porque você envolveu uma operação importante dentro da macro "assert". Como você deve saber, "assert" avalia expressões apenas no modo de depuração.

Mohamad mehdi Kharatizadeh
fonte
1

Com relação aos problemas para obter informações de diagnóstico, você tentou usar adplus.vbs como alternativa ao WinDbg.exe? Para anexar a um processo em execução, use

adplus.vbs -crash -p <process_id>

Ou para iniciar o aplicativo caso a falha aconteça rapidamente:

adplus.vbs -crash -sc your_app.exe

Informações completas sobre adplus.vbs podem ser encontradas em: http://support.microsoft.com/kb/286350

DocMax
fonte
1

Ntdll.dll com depurador anexado

Uma pequena diferença conhecida entre iniciar um programa a partir do IDE ou WinDbg em oposição a iniciá-lo a partir da linha de comando / área de trabalho é que, ao iniciar com um depurador conectado (ou seja, IDE ou WinDbg), o ntdll.dll usa uma implementação de heap diferente que executa alguma validação. na alocação / liberação de memória.

Você pode ler algumas informações relevantes no ponto de interrupção inesperado do usuário em ntdll.dll . Uma ferramenta que pode ajudá-lo a identificar o problema é o PageHeap.exe .

Análise de falhas

Você não escreveu qual é a "falha" que está ocorrendo. Assim que o programa travar e oferecer a você o envio das informações de erro para a Microsoft, você deverá ser capaz de clicar nas informações técnicas e verificar pelo menos o código de exceção e, com algum esforço, poderá até realizar análises post mortem (ver Heisenbug : O programa WinApi trava em alguns computadores) para obter instruções)

Suma
fonte
1

O Vista SP1 tem um gerador de despejo de memória muito bom embutido no sistema. Infelizmente, ele não é ativado por padrão!

Consulte este artigo: http://msdn.microsoft.com/en-us/library/bb787181(VS.85).aspx

O benefício dessa abordagem é que nenhum software extra precisa ser instalado no sistema afetado. Agarre e rasgue, baby!


fonte
1

De acordo com minha experiência, esses são problemas de corrupção de memória.

Por exemplo :

char a[8];
memset(&a[0], 0, 16);

: /*use array a doing some thing */

é muito possível ser normal no modo de depuração quando se executa o código.

Mas no lançamento, isso seria / poderia ser um crash.

Para mim, vasculhar onde a memória está fora dos limites é muito trabalhoso.

Use algumas ferramentas como Visual Leak Detector (windows) ou valgrind (linux) são escolhas mais sábias.

Gaiger Chen
fonte
1

Eu vi muitas respostas certas. No entanto, não há ninguém que me ajudou. No meu caso, houve um uso incorreto das instruções SSE com a memória não alinhada . Dê uma olhada em sua biblioteca matemática (se você usar uma) e tente desabilitar o suporte SIMD, recompilar e reproduzir a falha.

Exemplo:

Um projeto inclui mathfu e usa as classes com STL vector: std :: vector <mathfu :: vec2> . Tal uso provavelmente causará um travamento no momento da construção do item mathfu :: vec2, uma vez que o alocador padrão STL não garante o alinhamento de 16 bytes necessário. Neste caso para comprovar a ideia, pode-se definir #define MATHFU_COMPILE_WITHOUT_SIMD_SUPPORT 1antes de cada inclusão do mathfu , recompilar na configuração do Release e verificar novamente.

As configurações Debug e RelWithDebInfo funcionaram bem para meu projeto, mas não para o Release . A razão por trás desse comportamento é provavelmente porque o depurador processa solicitações de alocação / desalocação e faz alguma contabilidade de memória para verificar e verificar os acessos à memória.

Eu experimentei a situação nos ambientes Visual Studio 2015 e 2017.

Vlad Serhiienko
fonte
0

Algo semelhante aconteceu comigo uma vez com o GCC. Acabou sendo uma otimização muito agressiva que foi habilitada apenas na criação da versão final e não durante o processo de desenvolvimento.

Bem, para falar a verdade, a culpa foi minha, não do gcc, pois não percebi que meu código estava contando com o fato de que aquela otimização em particular não teria sido feita.

Levei muito tempo para rastreá-lo e só cheguei a ele porque perguntei em um newsgroup e alguém me fez pensar sobre isso. Portanto, deixe-me retribuir o favor para o caso de isso também estar acontecendo com você.

Remo.D
fonte
0

Achei este artigo útil para o seu cenário. As opções do compilador ISTR estavam um pouco desatualizadas. Dê uma olhada nas opções de projeto do Visual Studio para ver como gerar arquivos PDB para sua versão de lançamento, etc.

fizzer
fonte
0

É suspeito que isso aconteceria fora do depurador e não dentro; a execução no depurador normalmente não altera o comportamento do aplicativo. Gostaria de verificar as diferenças de ambiente entre o console e o IDE. Além disso, obviamente, compile a versão sem otimizações e com informações de depuração e veja se isso afeta o comportamento. Finalmente, verifique as ferramentas de depuração post-mortem que outras pessoas sugeriram aqui, geralmente você pode obter alguma pista delas.

usuario
fonte
0

Depurar compilações de versão pode ser uma dor devido às otimizações que alteram a ordem em que as linhas de seu código parecem ser executadas. Isso pode realmente ficar confuso!

Uma técnica para pelo menos reduzir o problema é usar MessageBox () para exibir instruções rápidas informando a que parte do programa seu código deve ser ("Iniciando Foo ()", "Iniciando Foo2 ()"); comece a colocá-los no topo das funções na área de seu código que você suspeita (o que você estava fazendo no momento em que ele travou?). Quando você souber qual função, altere as caixas de mensagem para blocos de código ou até mesmo linhas individuais dentro dessa função até reduzi-la a algumas linhas. Então você pode começar a imprimir o valor das variáveis ​​para ver em que estado elas estão no ponto de travamento.


fonte
Ele já tentou polvilhar com printfs, então as caixas de mensagens não trouxeram nada de novo para a festa.
Greg Whitfield,
0

Tente usar _CrtCheckMemory () para ver em que estado está a memória alocada. Se tudo correr bem, _CrtCheckMemory retorna TRUE , senão FALSE .

Vhaerun
fonte
0

Você pode executar seu software com Global Flags habilitado (procure em Debugging Tools for Windows). Muitas vezes, ajuda a resolver o problema.

Marcin Gil
fonte
0

Faça seu programa gerar um mini dump quando a exceção ocorrer e, em seguida, abra-o em um depurador (por exemplo, no WinDbg). As principais funções a serem observadas: MiniDumpWriteDump, SetUnhandledExceptionFilter

Mikhailitsky
fonte
0

Aqui está um caso que alguém pode achar instrutivo. Ele só travou no lançamento no Qt Creator - não na depuração. Eu estava usando arquivos .ini (porque prefiro aplicativos que possam ser copiados para outras unidades do que aqueles que perdem suas configurações se o Registro for corrompido). Isso se aplica a todos os aplicativos que armazenam suas configurações na árvore de diretórios dos aplicativos. Se as compilações de depuração e liberação estiverem em diretórios diferentes, você também pode ter uma configuração diferente entre elas. Eu tinha uma preferência marcada em um que não foi marcada no outro. Acabou sendo a fonte do meu acidente. Ainda bem que encontrei.

Odeio dizer isso, mas só diagnostiquei a falha no MS Visual Studio Community Edition; após ter instalado o VS, deixando meu aplicativo travar no Qt Creator e escolhendo abri-lo no depurador do Visual Studio . Embora meu aplicativo Qt não tivesse informações de símbolo, descobri que as bibliotecas Qt tinham algumas. Isso me levou à linha ofensiva; pois pude ver qual método estava sendo chamado. (Mesmo assim, acho que Qt é uma estrutura LGPL conveniente, poderosa e de plataforma cruzada.)

CodeLurker
fonte
-3

Eu tive esse erro e travou mesmo ao tentar! Limpar! meu projeto. Portanto, apaguei os arquivos obj manualmente do diretório Release e, depois disso, ele foi compilado perfeitamente.

Chris89
fonte
-6

Eu concordo com Rolf. Como a reprodutibilidade é tão importante, você não deve ter um modo sem depuração. Todas as suas compilações devem ser depuráveis. Ter dois destinos para depurar mais do que duplica sua carga de depuração. Apenas envie a versão do "modo de depuração", a menos que seja inutilizável. Nesse caso, torne-o utilizável.

wnoise
fonte
Isso pode funcionar para 10% dos aplicativos, mas certamente não para todos eles. Você gostaria de jogar jogos lançados como compilações de DEBUG? Distribuir seu código de segurança secreto de marca registrada no modo amigável para desmontagem, talvez até junto com os PDBs? Eu acho que não.
steffenj,
Steffenj: Quero que os desenvolvedores de jogos encontrem bugs. Idealmente, antes de serem enviados, mas se for depois, quero que eles consigam obter informações suficientes para reproduzi-las e rastreá-las. se for um código secreto, a marca registrada não se aplica. PDBs? Banco de dados de proteínas? depurador python?
wnoise,
IMHO, isso é uma má ideia. Os executáveis ​​são maiores, não são otimizados e funcionam muito mais lentamente. Esses casos são realmente muito raros; mesmo sendo especialmente enlouquecedores quando acontecem. Você não deve entregar produtos consistentemente inferiores, preocupando-se com a depuração de pior caso extremamente rara. (O meu não foi um dos muitos votos negativos.) Eu fiz alguma programação para a NASA; e dissemos que, no mínimo, cada linha de código deve ser testada uma vez. O teste de unidade também pode ajudar.
CodeLurker