logo
Contato | Sobre...        
rebarba rebarba

WinDbg: Debugger de gente grande (parte 3)

Plano de decolagem

Nessa terceira parte veremos a funcionalidade do WinDbg responsável pelo nosso querido "source level debug": os symbols. "Source level debug" nada mais é do que o debug que estamos acostumados a fazer na IDE do Visual C++, o debug usando os fontes como referência ao invés do assembly nu e cru.

Symbols: para que servem?

Os symbols são as informações de debug e referência de uma imagem executável. Quando um executável C/C++ é compilado, ele transforma as instruções da linguagem em assembly x86, e nessa tradução, as informações de tipo de dados que são usadas na linguagem se perdem. Exemplo: A instrução x86 MOV move um inteiro 32 bits entre registradores/memória. O processador não faz a menor idéia se o int que está sendo movido provém de uma variável, de um struct, se é um ponteiro ou um cast de algum outro tipo de dados. Vamos usar um programa simples para exemplificar:

typedef struct _TEST_STRUCT
{
   int d;
   int e;
} TEST_STRUCT;

int Aumenta(int i)
{
   return i + 2;
}

int _tmain(int argc, _TCHAR* argv[])
{
   int a;
   int b;
   int c;
   TEST_STRUCT test;

   a = 10;
   b = 20;
   c = 30;
   a = b;
   
   test.d = a;
   test.e = Aumenta(c);

   return 0;
}

Esse programa declara alguns inteiros e um struct, e atribui diversos valores a eles. Vamos ver como fica o assembly disso (sem otimização, claro):

;
; inicialização da pilha debug
;
push    ebp
mov     ebp,esp
sub     esp,0xf4
push    ebx
push    esi
push    edi
lea     edi,[ebp-0xf4]
mov     ecx,0x3d
mov     eax,0xcccccccc
rep     stosd
;
; aqui começam as atribuições
;
mov     dword ptr [ebp-0x8],0xa    ; atribui 10 à variável [a]
mov     dword ptr [ebp-0x14],0x14  ; atribui 20 à variável [b]
mov     dword ptr [ebp-0x20],0x1e  ; atribui 30 à variável [c]

mov     eax,[ebp-0x14]             ; move o valor de [b] para EAX
mov     [ebp-0x8],eax              ; move o valor de EAX para [a]

mov     eax,[ebp-0x8]              ; move o valor de [a] para EAX
mov     [ebp-0x30],eax             ; move o valor de EAX para [test.d]

mov     eax,[ebp-0x20]             ; move o valor de [c] para EAX
push    eax                        ; coloca o valor de EAX na pilha
call    0x0041150a                 ; chama a função "Aumenta" 
add     esp,0x4                    ; como a função é __cdecl, volta a pilha
mov     [ebp-0x2c],eax             ; atribui o retorno de "Aumenta" à [test.e]

Podemos ver claramente a atribuição das variáveis aqui. Mas isso só acontece porque temos os fontes e sabemos que as variáveis existem. Note que todos os MOVs são feitos em relação a endereços de memória e registradores. Note também que não existe diferença alguma entre a atribuição de variáveis simples e as atribuições para membros de uma estrutura. O processador é instruído a mover determinados valores para determinados endereços de memória, mas ele não sabe que naquele endereço existe uma variável. Em C/C++, essa informação das variáveis se perde durante a compilação, porque ela não é necessária para que o programa seja executado.

Apesar dessa informação ser desnecessária para que o sistema operacional e o processador executem o programa, ela é muito útil na hora de fazer o debug do executável. Caso não tivéssemos o fonte desse programa seria muito difícil descobrir que as últimas atribuições são para membros de uma estrutura.

Aí que entram os symbols. Eles contém as associações entre os endereços de memória e as variáveis e funções. Assim é possível saber durante uma sessão de debug, que call 0x0041150a é na verdade um call [Aumenta].

Vendo os symbols funcionando

Quando um programa C/C++ é compilado em configuração DEBUG, os symbols são incluídos dentro do arquivo executável. Assim, não é necessário ter um arquivo de symbols separado. Como o Windows é compilado em RELEASE, todos os EXEs e DLLs que fazem parte dele não contém informações de debug. Mas a Microsoft disponibiliza os arquivos de symbols separadamente (somente com os nomes de funções, sem as variáveis).

No WinDbg, é necessário informar a localização dos arquivos com o symbols, bem como a localização dos fontes. O Visual C++ coloca a localização dos fontes junto com os symbols dentro de um executável DEBUG, mas ele usa o caminho absoluto (que pode não ser o mesmo no momento que você estiver fazendo debug). Para informar os caminhos dos symbols e dos fontes, use os menus "File" >> "Symbol File Path..." e "File" >> "Source File Path" respectivamente.

Vamos fazer um teste com o nosso programinha do primeiro artigo da série, para ver qual a grande diferença entre o "com symbols" e o "olhe mamãe, sem os symbols" durante o debug de um executável Windows. Siga os seguintes passos:

  • Compile o programa, se você ainda não fez. No primeiro artigo você acha as instruções para isso;
  • Abra o CPP do programa (usando o menu "File" >> "Open Source File") e coloque um breakpoint na linha onde o objeto do ActiveDesktop é criado;
  • Pressione F5 e deixe o programa parar no breakpoint. Quando ele chegar coloque o breakpoint na função RegOpenKeyExW, usando o comando "bp ADVAPI32!RegOpenKeyExW" (como explicado na parte 2);
  • Pressione F5 para que o programa siga até o breakpoint em RegOpenKeyExW.

Agora que chegamos até aqui, vamos ver o que temos. O comando "k" mostra as funções que estão na pilha:

0:000> k
ChildEBP RetAddr  
WARNING: Stack unwind information not available. Following frames may be wrong.
0012fba0 7751c837 ADVAPI32!RegOpenKeyExW
0012fbc0 7751c549 ole32!CoGetComCatalog+0x1f0
0012fbd8 7751c65c ole32!CoRevokeClassObject+0xf4f
0012fbec 7751c5e1 ole32!CoGetComCatalog+0x15
0012fc08 77501aed ole32!CoRevokeClassObject+0xfe7
0012fc10 77501b29 ole32!CoCreateInstance+0x17c7
0012fc30 77500200 ole32!CoCreateInstance+0x1803
0012fd14 7750031d ole32!OleInitialize+0xd6e
0012fd3c 775002ec ole32!CoCreateInstanceEx+0x4f
0012fd60 7750035a ole32!CoCreateInstanceEx+0x1e
0012fd90 00412b32 ole32!CoCreateInstance+0x34
0012fedc 0041f4df 1bit_windbg!wmain+0xf2 [c:\temp\code\1bit_windbg\1bit_windbg.cpp @ 47]
0012ffc0 7c816d4f 1bit_windbg!wmainCRTStartup+0x16f [f:\vs70builds\3077\vc\crtbld\crt\src\crt0.c @ 256]
0012fff0 00000000 kernel32!RegisterWaitForInputIdle+0x49

Podemos notar que existem diversas chamadas dentro da OLE32.DLL antes de chamar advapi32!RegOpenKeyExW. Note também que a pilha da OLE32 mostra que aparentemente temos algumas funções recursivas (que chamam a si mesmas), como a CoCreateInstanceEx, mas não é esse o caso. Na verdade, a instrução que está em ole32!CoCreateInstance+0x1803 chamou uma função, e a instrução dessa função que está em ole32!CoCreateInstance+0x17c7 fez outra chamada. O que acontece nesse caso é que, como o WinDbg não possui os symbols dos endereços citados, ele usa notação "symbol mais próximo conhecido" + offset. CoCreateInstance é conhecido porque é uma função exportada da OLE32.DLL, e o WinDbg consegue saber seu endereço sem ter os symbols (podemos ver isso também usando o Depends.exe, que vem com o Visual C++ e com o Platform SDK).

Vamos fazer a mágica dos symbols funcionar: primeiro crie a pasta "C:\WebSymbols" (ou outro nome de preferência) no seu computador. Depois abra "File" >> "Symbols File Path", adicione "srv*C:\WebSymbols*http://msdl.microsoft.com/download/symbols" em "Symbol Path". Selecione "Reload" e clique em OK (é necessário estar conectado à Internet). Vamos ver a pilha novamente:

ChildEBP RetAddr  
0012fb58 7751c6bd ADVAPI32!RegOpenKeyExW
0012fba0 7751c837 ole32!CComCatalog::TryToLoadCLB+0x66
0012fbc0 7751c549 ole32!CComCatalog::AddRef+0x25
0012fbd8 7751c65c ole32!CComCatalog::QueryInterface+0x106
0012fbec 7751c5e1 ole32!GetCatalogHelper+0x15
0012fc08 77501aed ole32!InitializeCatalogIfNecessary+0x32
0012fc10 77501b29 ole32!GetClassInfoFromClsid+0xa
0012fc30 77500200 ole32!LookForConfiguredClsid+0x19
0012fd14 7750031d ole32!ICoCreateInstanceEx+0x106
0012fd3c 775002ec ole32!CComActivator::DoCreateInstance+0x28
0012fd60 7750035a ole32!CoCreateInstanceEx+0x1e
0012fd90 00412b32 ole32!CoCreateInstance+0x37
0012fedc 0041f4df 1bit_windbg!wmain+0xf2 [c:\temp\code\1bit_windbg\1bit_windbg.cpp @ 47]
0012ffc0 7c816d4f 1bit_windbg!wmainCRTStartup+0x16f [f:\vs70builds\3077\vc\crtbld\crt\src\crt0.c @ 256]
0012fff0 00000000 kernel32!BaseProcessStart+0x23

Agora temos os nomes das funções internas da OLE32.DLL. Isso é MUITO útil na hora de diagnosticar problemas que acontecem em chamadas à Win32 API. Essa string que colocamos no "Symbol Path" instrui o WinDbg a procurar no servidor de symbols da Microsoft os symbols não encontrados localmente. Veja depois o conteúdo de "c:\WebSymbols" para ver como o WinDbg organiza os symbols baixados. Os symbols para a maioria dos EXEs e DLLs que fazem parte do Windows estão disponíveis no Symbol Server da Microsoft.

Gerando seus próprios symbols

Os arquivos de symbols são extremamente úteis quando é necessário fazer debug de uma versão RELEASE do seu software, mas no computador de um cliente. Isso pode ser necessário para diagnosticar um erro que só acontece em um cliente específico, e não é possível simular em outro lugar.

Uma boa idéia é ter no controle de versões (SourceSafe, CVS, Subversion, etc) os executáveis que são enviados para os clientes, assim como os respectivos symbols. Assim, quando for necessário diagnosticar o problema in loco, é só gravar um CD com a pasta do WinDbg (não precisa instalar, grave a pasta "Program Files\Debugging Tools for Windows" e execute de lá mesmo), os symbols da versão RELEASE e os fontes usados para compilar essa versão.

Para gerar os symbols no Visual C++ 6.0, entre nas propriedades do projeto, aba "Link", escolha a categoria "Debug" e selecione a opção "Debug Info"

Para gerar os symbols no Visual C++ 7.x (VS.NET 2002/2003), entre nas propriedades do projeto, "Linker" >> "Debugging" no TreeView do lado esquerdo e configure "Generate Debug Info" como "YES (/DEBUG)".

Em ambos os casos o Visual C++ gerará um arquivo PDB na pasta onde o programa está sendo compilado.


  
 
 
Comentários
Evandro Nascimento | em 24/03/2005 | #
Parabéns, fantasticas as materias gostei muito!

Continue escrevendo sobre este assunto, pois o conhecimento de tal ferramenta é fundamental aos desenvolvedores. Gostaria de obter mais informações sobre debugger de crash-dump. Pois venho usando o windbg para tal finalidade e quero obter mais informaçõe de tecnicas sobre esta finalidade. Quanto debugar processos, gostaria de obter informações sobre como fazer break-point condicional.

[ ]s,
Evandro Nascimento
Ronivaldo Jorge | em 03/01/2007 | #
Evandro, gostaria de saber se existe alguma forma de chamar e janela de impressao do windows de forma manual, por exemplo de tiver um arquivo .doc e for palo DOS e digitar "teste.doc" automaticamente abre o word com o arquivo teste.doc. existe alguma forma de chamar a jenela que seleciona a impressora no windows da mesma forma?

Sei que isso nao é comentario, mas nao sei quem pode me ajudar e vi que tens conhecimento em manipulacao de funções internas no windows.


Desde ja agradeço
Algo a dizer?
Nome:


Site:


E-mail:


Escreva o número vinte e seis:


 Não mostre meu e-mail no site, não serve pra nada mesmo...

Comentário





Os comentários devem ser sobre assuntos relativos ao post, eu provavelmente apagarei comentários totalmente offtopic. Se quiser me enviar uma mensagem, use o formulário de contato. E não esqueça: isso é um site pessoal e eu me reservo o direito de apagar qualquer comentário ofensivo ou inapropriado.
rebarba rebarba
  ::::