Archive for August, 2006

ExAllocatePool(WithoutTag)

30 de August de 2006

Como a maioria dos desenvolvedores de drivers, eu também tenho que construir versões de drivers para várias versões do Windows. Ainda não vou comentar sobre VXDs neste post, mas já posso comentar sobre ter um único arquivo .SYS que possa rodar tanto em Windows Server 2003 como em Windows NT. Sei que este sistema operacional não é mais vendido e nem conta mais com o suporte da Microsoft, mas é impressionante ver como ainda encontramos Windows 95 e Windows NT tanto em ambientes corporativos como nas casas de usuários finais. Assim temos sempre que ficar nos esquivando das limitações de cada sistema operacional quando estamos planejando desenvolver um sistema.

Uma função que recebeu uma nova versão foi a ExFreePool, sua nova versão é a ExFreePoolWithTag, que é implementada a partir Windows 2000. Para termos um único binário que seja executado em ambas as versões, temos que utilizar a mais velha que ainda é suportada pelos sistemas recentes.

Vamos considerar o fonte abaixo para um driver que realiza a incrível tarefa de alocar memória dinamicamente.

#include 
 
NTSTATUS DriverEntry(PDRIVER_OBJECT  pDriverObject,
                     PUNICODE_STRING pusRegistryPath)
{
   PVOID pTemp;
 
   //-f--> Tô pensando que é fácil alocar memória.
    if (!(pTemp = ExAllocatePool(NonPagedPool, 10))
       return STATUS_NO_MEMORY;
 
   //-f--> Memory leak é falta de educação.
    ExFreePool(pTemp);
   return STATUS_SUCCESS;
}

Utilizando o ambiente de build para Windows XP para compilar este driver, podemos ver que o mesmo binário funciona em Windows Server 2003, Windows XP, Windows 2000 e Windows NT. Ou pelo menos deveria. Quando tentamos iniciar nosso maravilhoso driver em Windows NT, obtemos a seguinte mensagem:

Mas como? Se dermos uma olhada mais de perto em nosso driver, vamos descobrir que ele é um traidor miserável. Como uma DLL, drivers são módulos executáveis que utilizam a clássica estrutura PE para declarar suas dependências. Assim, podemos abrir nosso driver no “Dependency Walker” e ver o que ele espera da vida para poder ser carregado. Por encreça que parível, ele realmente depende estaticamente das funções novas de alocação de memória. Traidor maldito!

Isso acontece porque dentro do arquivo ntddk.h existe um conjunto de defines que acabam mudando as chamadas ExFreePool para ExFreePoolWithTag. Assim podemos pegar todos aqueles 7 quilos de código escritos para NT e utilizá-los em builds para sistemas mais novos sem mudar uma só linha, ou ainda podemos ter código fonte compartilhado entre drivers para Windows NT e para sistemas mais novos.

#define POOL_TAGGING 1
 
...
 
#ifdef POOL_TAGGING
#define ExAllocatePool(a,b) ExAllocatePoolWithTag(a,b,' kdD')
#define ExAllocatePoolWithQuota(a,b) ExAllocatePoolWithQuotaTag(a,b,' kdD')
#endif

Então você me faz as seguintes perguntas: Mas qual a real diferença entre versão velha e a nova? Meu driver pode ter problemas se tentar utilizar a versão velha rodando em um sistema mais novo? Existe vida inteligente fora da terra?

A versão nova veio para ajudar a detectar memory leaks. Cada alocação de memória fica associada a uma Tag. Estas Tags podem ser visualizadas utilizando ferramentas de depuração tais como o WinDbg. Por exemplo: Se toda a sua biblioteca de configuração utilizar uma Tag, e a sua biblioteca de comunicação utilizar outra, fica fácil saber quais delas está deixando memória alocada. Desta forma fica mais fácil saber qual programador do seu time você vai mandar pra rua. As macros no ntddk.h apenas utilizam uma Tag default para as alocações de memória que não tem uma Tag associada.

Mas o que acontece com os drivers que foram compilados com o DDK do Windows NT e que rodam sobre o sistemas posteriores? Estes drivers de fato utilizam as funções velhas. Se utilizarmos o Depends neles poderemos ver isso. Por uma questão de compatibilidade, as funções velhas ainda são exportadas em sistemas novos para dar suporte aos drivers antigos. Vamos dar uma olhada na implementação da ExAllocatePool do Windows 2000.

A implementação da função antiga simplesmente encaminha a alocação de memória para a função nova e utiliza a Tag ‘None’. Isso nos assegura que não morreremos de câncer se continuarmos utilizando as funções antigas em um sistema novo.

Bom, o que estávamos querendo mesmo? Ah sim, construir um driver novo que possa rodar tanto em Windows NT como em sistemas posteriores. Poderíamos simplesmente utilizar o DDK do Windows NT e contar com a compatibilidade reversa para rodar em sistemas mais novos. Isso é possível, mas algumas das funções que vemos no DDK são definidas como macros. A função IoSetCompletionRoutine é um exemplo clássico disso. Utilizar um DDK antigo significa não ter algumas destas funções definidas e teríamos que ficar remendando definições do DDK se quiséssemos desfrutar delas. Existem ainda aqueles casos de pessoas perdidas em ilhas desertas apenas com o DDK mais novo.

#include 
 
#undef ExAllocatePool
#undef ExFreePool
 
NTSTATUS DriverEntry(PDRIVER_OBJECT  pDriverObject,
                     PUNICODE_STRING pusRegistryPath)
{
 
...

Para resolver isso, logo depois do include para ntddk.h, vamos dar um #undef nas macros que redirecionam as chamadas do ExAllocatePool para a nova versão e prontio, seus pobremas se acabaram-se. Recompilando teremos um driver novo que utiliza as funções antigas de alocação. Desta forma ele pode ser utilizado em sistemas desde o Windows NT.

A desvantagem deste método é que seus drivers não poderão utilizar as Tags quando rodando em um sistema que as suporte. Na verdade é possível ter um único binário que se rodando em Windows NT utilize ExFreePool e se rodando em um sistema mais novo utilize ExFreePoolWithTag, mas isso é outro Post.

Debug or Not Debug

25 de August de 2006

Aqui está uma coisa muito simples de se fazer. Gerar compilação condicional utilizando o pre-processador juntamente com o símbolo DBG tem que ser uma tarefa simples. Assim como o símbolo _DEBUG é definido pelos projetos escritos em C/C++ no Visual Studio, o símbolo DBG é definido pelo DDK para informar que o build corrente está em Checked ou Free. Assim podemos exemplificar com um trecho muito simples de código como segue abaixo:

//-f--> Tô achando que é fácil...
#ifdef DBG
   DbgPrintf("Versão Checked\n");
#else
   DbgPrintf("Versão Free\n");
#endif

O código acima exibirá a mensagem “Versão Checked” no quando o build for feito em Checked e “Versão Free” quando o build for feito em Free, certo?

ERRADO!!! Isso mesmo, errado! O símbolo DBG é definido como 1 quando em Checked e definido como 0 quando Free. Assim temos DBG=1 quando Checked e DBG=0 quando Free. Da maneira que está, o código acima ainda exibirá a mensagem “Versão Checked” mesmo quando compilado como Free. Isso porque o símbolo DBG ainda é definido, definido como 0, mas é definido. Assim para testar adequadamente esta condição devemos fazê-la com #if no lugar de #ifdef como segue abaixo:

//-f--> Agora sim, exibe a configuração atual do build
#if DBG
   DbgPrintf("Versão Checked\n");
#else
   DbgPrintf("Versão Free\n");
#endif
 
//-f--> Testando apenas o build Free
#if !DBG
   DbgPrintf("Versão Free\n");
#endif

Para facilitar a depuração, não é difícil ver em alguns drivers o código que quando compilado em Checked interrompe a execução utilizando o seguinte código.

//-f--> Breakpoint quando em Checked
#if DBG
   __debugbreak();
#endif

Se o teste do build estivesse incorreto aqui, teríamos um bela tela azul caso o código acima fosse para produção. Como vocês devem saber, a chamada a breakpoints em código lançam exceções que seriam tratadas pelo debugger, mas se o debugger não estiver atachado ao sistema, COISAS HORRIVEIS ACONTECEM.

No arquivo ntddk.h existe a macro abaixo que ajuda a testar condições de build em Checked.

#if DBG
    #define IF_DEBUG if (TRUE)
#else
    #define IF_DEBUG if (FALSE)
#endif

Assim, fica bem clara a sintaxe abaixo:

IF_DEBUG
{
   __debugbreak();
}

Já que estamos falando de builds condicionais, podemos também podem ter especializações de build no arquivo SOURCES. A variável de ambiente DDKBUILDENV é definida como “che” quando o build é feito em Checked, ou definida como “fre” quando em Free. Assim podemos, por exemplo, utilizar paths para libs diferentes de acordo com a configuração do build atual.

!if "$(DDKBUILDENV)" == "fre"
BUILD_CONFIG=Release
!else
BUILD_CONFIG=Debug
!endif
 
TARGETLIBS=.\Lib\$(BUILD_CONFIG)\HtsCpp.lib

KdPrint((“Hello World!!!”));

16 de August de 2006

Meu primeiro computadorNeste primeiro post, vou aproveitar que ninguém vai ler mesmo para falar sobre mim. Afinal, as pessoas que se interessarem em ler este blog podem querer conhecer meu perfil com um pouco mais de detalhes. Nascido em 1976, eu nem imaginava o que seria da vida quando aos 13 anos ví um computador pela primeira vez. Meu primo tinha um CP200 e eu nem sabia que tipo de video game era aquele. Quando meu primo fez um upgrade para um poderoso MSX, eu comprei o CP200 dele. Eu simplesmente não sabia nada a respeito. Na época os jogos e aplicativos eram gravados em fita K7 e eu não tinha o gravador para desfrutar de toda essa tecnologia. Resultado, eramos eu e o manual de BASIC e foi assim que aprendi a programar.

Não precisou de muito tempo pra eu decidir que queria ser programador, mas tinha que ser para programar uma tal de linguagem C. Eu queria fazer o que os outros achavam difícil, talvez tenha sido por esse motivo que deixei de fazer o tradicional curso de Processamento de Dados para fazer Informática Industrial na ETE Jorge Street em São Caetano do Sul.

Apesar de programar Visual Basic, ainda não sabia nada de C para Windows quando fui fazer meu estágio na Provectus. Eu diria que não pude caí em um lugar melhor. Eles trabalhavam com um hardware próprio que era equipado com um processador V40. Utilizando uma biblioteca de Runtime própria, podimamos fazer com que um programa feito para DOS pudesse rodar naquele painel de coleta de dados. Com o tempo veio a primeira DLL, o primeiro serviço, o primeiro aplicativo, o primeiro driver e a tão esperada primeira tela azul. A placa de rede que recebia as mensagens da rede de coletores também era hardware próprio e começei a dar manutenção no código que já estava pronto. Tivemos que refazer todo o protocolo da rede dos coletores que era todo feito por nós. Assim, enquanto meus amigos faziam formulários, querys e relatórios, o fato de depurar um protocolo de rede, parte em assembly e parte em C, tanto o driver como o firmware utilizando um osciloscópio me dava a certeza de que era aquilo que eu queria fazer.

Depois de quatro anos trabalhando com coletores de dados, a vontade de programar exclusivamente para Windows me fez trabalhar em um site financeiro. Construir componentes MTS e COM+ foi bom para ter uma outra visão do negócio. Mas como era de se esperar, o trabalho em C/C++ para estes fins era pouco e logo acabou, assim foram me colocando em projetos de ASP com SQL, e graças a isso fui trabalhar em uma empresa de segurança da informação. Eu simplesmente não aguentava mais chegar no trabalho e ter que abrir o Query analyzer.

Scua me abriu as portas para a programação de baixo nível para Windows. Hooks de janela, DLLs de autenticação (GINA), logon no windows com smartcard e biometria, filtros de File Systems, criptografia de arquivos e partições em tempo real, controle de acesso, SoftICE e WinDBG. Atualmente trabalho na Open Communications Security e sou responsável pelo desenvolvimento de baixo nível de softwares contra fraudes eletrônicas. Detectar, combater e despistar root-kits, trojans e key-loggers.

Pretendo neste blog dar algumas dicas de desenvolvimento C/C++ para Windows, incluindo algumas coisas em User-Mode e outras em Kernel-Mode. Espero poder ajudar oferecendo algumas dicas e facilidades para este pequeno grupo de pessoas que desenvolvem drivers para Windows no Brasil.