WinDbg: Debugger de gente grande (parte 2)
Rebooting
No primeiro artigo da série descobrimos o que é o WinDbg, suas semelhanças e diferenças com o Visual Studio, comandos básicos para controlar o programa e para visualizar as variáveis. Agora continuaremos com o uso do WinDbg, para depois entrarmos na parte mais interessante: escarafunchar a memória.
Além de tudo que já vimos e veremos sobre as funcionalidades do WinDbg, eu gostaria de mencionar uma grande vantagem do WinDbg em relação ao debugger integrado do Visual Studio: o WinDbg é um debugger independente, que pode ser usado para verificar um problema na máquina de um usuário. Você pode gravar a pasta de instalação do WinDbg e os fontes do seu programa em um CD e levá-lo para o cliente. Chegando lá, configure o WinDbg dizendo que os fontes e os PDBs estão no CD, e pronto. Depois é só levar o CD embora (NÃO ESQUEÇA DISSO), sem ter que instalar milhares de ferramentas na máquina do cliente.
Ah, e não se esqueça: o WinDbg não está restrito somente a programas desenvolvidos em Visual C++. Ele pode fazer debug de programas gerados por qualquer ferramenta que gere informações de debug em PDB. Isso inclui o Visual Basic 6, Borland C++ Builder e o Delphi.
Breakpoints visuais
Vamos começar nossa sessão de debug, usando nosso programa de testes.
- Usando o menu Open >> Open Executable, abra o executável que criamos.
- Agora, abra o arquivo .CPP do programa, usando File >> Open Source File
- Coloque um breakpoint (usando F9) na linha que tem a chamada para SHGetFolderPath
- Pressione F5 e espere o programa parar nessa função
O WinDbg faz breakpoint diretamente nos fontes, como o Visual Studio. Não se esqueça que esse recurso não funciona para executáveis compilados em versão RELEASE, já que eles não tem informação de debug. Para efetuar o debug de um executável RELEASE, é necessário configurar o compilador para gerar os symbols (Debug Information no VS.NET).
Breakpoints em funções que não são suas
Uma das grandes vantagens do WinDbg é que ele possibilita que você coloque breakpoints em funções da Win32 API e de DLLs com a mesma facilidade que você coloca um breakpoint nas funções do seu executável.
No nosso programa de testes, nós usamos a função SHGetFolderPath para descobrir em qual pasta o Windows está instalado e encontrar o JPG que será colocado como fundo de tela. Se você conhece ao menos um pouco da arquitetura do Windows, você deve imaginar onde isso fica gravado. Se você falou no registro, é claro, ponto para você. A chamada dessa API garante que teremos ao menos uma chamada para as APIs de acesso ao registro.
(Na realidade, só o fato de chamar CoInitialize já faz que com que o programa acesse o registro zilhares de vezes, já que toda a configuração do COM também está no registro. E as funções do Shell também criam vários objetos COM)
O raciocínio do breakpoint
Checando a MSDN, encontramos duas funções que podem ser usadas para abrir uma chave de registro: RegOpenKeyEx e RegOpenKey. Qual será a função que o programa usa? Na dúvida podemos colocar um breakpoint nas duas, mas em 99% das vezes, a função correta é a com final Ex.
Quando uma função da API precisa ser modificada para receber mais parâmetros, o pessoal da Microsoft implementa a função com mais parâmetros e coloca Ex no final do nome da função. Depois disso, eles criam uma função stub com a assinatura da função antiga, que somente repassa os parâmetros para a função Ex, fornecendo os parâmetros que não existiam na função original. Ou seja: no final, a função Ex sempre será chamada, já que a função sem o Ex repassa a chamada para a Ex.
Sendo assim, colocaremos o breakpoint na função RegOpenKeyEx.
Já sei qual a função, mas em que DLL ela está?
Agora precisamos descobrir em que DLL está a função RegOpenKeyEx. Como eu já disse no artigo anterior, é possível colocar o breakpoint com o comando BP RegOpenKeyEx, mas isso faria com que o WinDbg procurasse esse função em todas as DLLs. Além disso, nós preferimos o modo elegante... :-)
Para procurar as funções e variáveis de um módulo (executável ou DLL), usamos o comando X, no seguinte formato:
x [Options] Module!Symbol
Sendo que em Module e em Symbol podemos usar o curinga *. Segue aqui a saída de comando da nossa busca pela função RegOpenKeyEx:
0:000> x *!RegOpenKeyEx
Não encontramos nada... Será que a função não existe? Vamos tentar colocar um coringa no final:
0:000> x *!RegOpenKeyEx* 77dd1a8b ADVAPI32!RegOpenKeyExW 77dd229a ADVAPI32!RegOpenKeyExA
(Além disso, você pode ver mensagens de erro como "Symbol file could not be found". Por enquanto vamos ignorar esse erro. Rodando o comando de procura pela segunda vez faz com que essa mensagem não apareça mais. Arrumaremos o problema dos symbols mais tarde)
Encontramos a função com a letra A e a letra W no final. Mais uma vez devemos escolher qual a função certa.
Unicode e ANSI
A letra A é adicionada no nome das funções que suportam caracteres ANSI, e a letra W nas funções que aceitam caracteres UNICODE (o W vem de wide char). Como estamos usando Windows 2000/XP/2003 (você está, não está?), a função correta é a que tem W no final.
Os caracteres UNICODE ocupam 2 bytes, o que permite que um caractere UNICODE possa representar até 65536 letras ou símbolos diferentes (em oposição ao ANSI/ASCII, que suportam somente 255), o que permite conter confortavelmente todos os caracteres e símbolos de todas as línguas e dialetos existentes no nosso planeta. Isso acaba com aquela história de MODE CON CODEPAGE PREPARE (lembra?) para ficarmos mudando a página de caracteres dependendo do idioma escolhido.
Quando o Windows NT foi projetado (nos ido de 1989), foi feita a decisão pelo UNICODE, para facilitar a internacionalização do Windows e dos softwares que nele rodam. O mesmo não aconteceu com o Windows 95/98/Me, que herda muita coisa do Windows 3.1, baseado em ANSI.
Como estamos no Windows NT, colocaremos o breakpoint na função ADVAPI32!RegOpenKeyExW. Lembrando que caso um programa use as versões ANSI das funções e chame ADVAPI32!RegOpenKeyExA, essa função é um stub que converte as strings para UNICODE e repassa a chamada para ADVAPI32!RegOpenKeyExW.
Coloquemos então o breakpoint:
0:000> bp ADVAPI32!RegOpenKeyExW
Agora é só pressionar F5 (ou usar o comando G) e esperar que o programa pare.
Chegando lá
Logo depois de mandar o programa seguir, teremos uma saída parecida com essa:
Breakpoint 1 hit eax=0012ee18 ebx=00000057 ecx=0012edf0 edx=00000002 esi=00000000 edi=775a8afc eip=77dd1a8b esp=0012e990 ebp=0012ebbc iopl=0 nv up ei pl nz na pe nc cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000202 ADVAPI32!RegOpenKeyExW: 77dd1a8b 55 push ebp
Como não temos os fontes do Windows, temos que nos contentar com o assembly, menu View >> Disassembly. Você pode perguntar do que nos adianta ver o assembly de uma função do Windows. Dou-lhe então dois motivos: descobrir como a função funciona e verificar os parâmetros que foram passados para a função. Para fazer as duas coisas você precisa conhecer assembly. Não é necessário sem mestre em assembly, mas todo programador que se preze deve saber ao menos ler código assembly.
Futuramente faremos um log das chamadas da API usando o WinDbg, onde o disassembly será muito útil.
Completando
Meu intuito com essa série não é escrever um tratado detalhado sobre WinDbg, isso me levaria a escrever até a parte 50. Minha intenção foi mostrar essa ferramenta poderosa e desconhecida da maioria do programadores. Caso você precise de mais detalhes e informações, você pode seguir esses caminhos:
- Existe um newsgroup da Microsoft somente para WinDbg. Você acessá-lo via web ou usando seu cliente de news preferido (em nntp://msnews.microsoft.com). Em inglês, é claro.
- Leia a documentação que vem junto com o WinDbg, ela é bem interessante e explica vários conceitos. Existe uma parte do help que explica como identificar código assembly gerado por um compilador C/C++. Usando o comando ".hh <algum comando>", o help se abre com o <algum comando> já digitado no Índice do HTML Help, o que facilita a procura por tópicos específicos
- Google (precisa falar?)
E leia os próximos artigos da série!




