Blog

  • RtlGetModuleBase & RtlGetProcAddress

    No post ExAllocatePool(WithoutTag) falei um pouco sobre os conflitos de utilizar APIs novas em drivers e acabar tornando-os incompatíveis com sistemas legados. Nossa proposta inicial era ter um único binário que pudesse ser executado tando em Windows NT como em sistemas mais recentes. Chegamos a uma solução que pode não ser considerada a ideal, onde utilizamos sempre as funções antigas mesmo em sistemas que suporte APIs mais novas.

    Pensando em resolver este tipo de limitação, o DDK nos trouxe a MmGetSystemRoutineAddress. Analogamente à GetProcAddress exportada pela Kernel32.dll, a MmGetSystemRoutineAddress obtém o endereço de uma função dinamicamente a partir de seu nome de exportação. Mas o mundo ainda não estará a salvo enquanto o Windows NT sobreviver. Essa nova função é implementada somente a partir do Windows 2000.

    Já que não temos uma alternativa para Windows NT, vamos fazer uma. Com um ou dois conceitos sobre PE, implementamos uma versão desta função. A estrutura do PE carrega alguns quilos de regras, mas para nossa necessidade podemos implementar uma versão light. Se você quer ter detalhes das regras adotadas pelo PE, você pode dar uma olhada no artigo An In-Depth Look into the Win32 Portable Executable File Format por Matt Pietrek.

    Bom, agora que já lemos todo o artigo e já sabemos tudo sobre PE, segue protótipo do algoritmo básico de como caminhar pela estrutura do PE procurando por uma determinada função exportada pelo nome. Esta função não é fornecida pelo DDK do Windows NT e está definida no código fonte exemplo disponível para download.

    NTSTATUS RtlGetProcAddress(IN PVOID     pBaseAddress,
                               IN LPCSTR    pszFunctionName,
                               IN NTPROC    *pProcedure);

    Repare que os parâmetros de entrada são o endereço base, o nome da função desejada e por fim o endereço onde será armazenado o endereço desta função. Mas onde é que vou conseguir esse tal de endereço base? Lembre-se de que as funções que estamos procurando são exportadas pela ntoskrnl.lib e que são implementadas no módulo ntoskrnl.exe. Para nos certificar de que a função que procuramos é realmente exportada por este módulo, utilize o “Dependecy Walker” para visualizar a tabela de exportação deste módulo. Como já comentei antes, algumas funções são implementadas como macros, desta forma, sua definição estará em um arquivo de header e não na tabela de exportação.

    Para obtermos o endereço base do módulo que exporta estas funções, vamos utilizar a ZwQuerySystemInformation, que embora não seja documentada pela Microsoft, já existem várias publicações que comentam a respeito dela. Desta forma definiremos a RtlGetModuleBase que terá o comportamento similar à bem conhecida GetModuleHandle. Esta função também está definida no exemplo para download.

    NTSTATUS
    NTAPI
    ZwQuerySystemInformation(IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
                             IN OUT PVOID SystemInformation,
                             IN ULONG SystemInformationLength,
                             OUT PULONG ReturnLength OPTIONAL)

    Existem muitas funções e definições não documentadas que podem ser de extrema utilidade. O livro Windows NT/2000 Native Api Reference por Gary Nebbett é excelente para fazer uso destas funções. Ele traz protótipos, enums, estruturas utilizadas em chamadas, descrições do que cada função faz e de cada parâmetro. Aqui em nosso exemplo, vamos utilizar apenas as declarações abaixo para obter informações sobre os módulos carregados no address space do sistema.

    typedef struct _SYSTEM_MODULE_INFORMATION   // Information Class 11
    {
        ULONG Reserved[2];
        PVOID Base;
        ULONG Size;
        ULONG Flags;
        USHORT Index;
        USHORT Unknown;
        USHORT LoadCount;
        USHORT ModuleNameOffset;
        CHAR ImageName[256];
     
    } SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;

    O membro “Base” da estrutura acima nos traz o endereço base do módulo que é onde o PE está. No código de exemplo está a definição da nossa rotina RtlGetModuleBase que tem o seguinte protótipo.

    NTSTATUS RtlGetModuleBase(IN LPCSTR     pszModuleName,
                              OUT PVOID*    ppBaseAddress);

    Com a união dos seus poderes, agora poderemos saber se uma determinada API é implementada no sistema corrente e obter seu endereço. Assim é perfeitamente possível ter um único binário que possa rodar tanto em Windows NT utilizando o ExFreePool, como em sistemas posteriores utilizando o ExFreePoolWithTag. Abaixo segue um exemplo bem básico como sempre. É claro que podemos criar uma única rotina de alocação que faria todo o trabalho sujo.

    #include "GetProcAddr.h"
     
    //-f--> Tipo para o ponteiro de função ExFreePoolWithTag
    typedef VOID (NTAPI* PF_EX_FREE_POOL_WITH_TAG)
    (
        IN PVOID  P,
        IN ULONG  Tag 
    );
     
     
    VOID OnDriverUnload(PDRIVER_OBJECT     pDriverObj)
    {
        //-f--> Esta rotina está aqui apenas para permitir
        //      que o driver seja terminado, mesmo vazia.
    }
     
     
    /****
    ***
    **           Era uma vez um Driver...
    **
    */
    NTSTATUS DriverEntry(PDRIVER_OBJECT     pDriverObj,
                         PUNICODE_STRING    pusRegistryPath)
    {
        NTSTATUS                    nts;
        PVOID                       pBaseAddress, pTemp;
        PF_EX_FREE_POOL_WITH_TAG    pfExFreePoolWithTag;
     
        //-f--> Seta rotina de finalização
        pDriverObj->DriverUnload = OnDriverUnload;
     
        //-f--> Obtem o endereço base
        nts = RtlGetModuleBase("ntoskrnl.exe",
                               &pBaseAddress);
     
        //-f--> Testar retorno não mata ninguem, mas a falta
        //      pode matar seu sistema
        if (!NT_SUCCESS(nts))
            return nts;
     
        //-f--> Alocando memória para teste do Free
        pTemp = ExAllocatePoolWithTag(NonPagedPool,
                                      10,
                                      'tseT');
     
        //-f--> Obtem o endereço da API
        nts = RtlGetProcAddress(pBaseAddress,
                                "ExFreePoolWithTag",
                                (NTPROC*)&pfExFreePoolWithTag);
     
        if (NT_SUCCESS(nts))
        {
            //-f--> Se o sistema implementa esta API, então
            //      obteremos sucesso e poderemos chama-la
            pfExFreePoolWithTag(pTemp,
                                'tseT');
        }
        else
        {
            //-f--> Quem não tem cão, caça com gato.
            ExFreePool(pTemp);
        }
     
        return STATUS_SUCCESS;
    }

    Have fun! 🙂

    ExGetProc.zip

  • Legacy Drivers, WDM, WDF, KMDF, UMDF… WTF?!

    Estes dias me perguntaram qual a relação entre o sistema operacional e essa sopa de letrinhas que vem se acumulando com o passar dos builds. Neste post vou tentar resumir o que cada um destes modelos traz de novo e qual sua relação com o sistema operacional.

    No inicio Deus criou a Terra, logo em seguida veio o Windows NT que introduziu o que chamamos de “Legacy Drivers”. Este modelo foi utilizado em todos os drivers do Windows NT e ainda existe muito dos conceitos dele nos novos modelos. Aqui os conceitos clássicos de IRP e I/O Manager foram adotados.

    Com a chegada do Plug-and-Play, os sistemas Windows 98 e Windows 2000 foram os primeiros da família a implementar o WDM (Windows Driver Model). Este modelo tinha a pretensão de unificar os conceitos de desenvolvimento para drivers em ambas as plataformas (9x e NT). Quem já teve contato com VXDs sabe o quanto são diferentes dos conceitos dos drivers do Windows NT. Programadores de VXDs tiveram que aprender uma tecnologia completamente diferente. Com o WDM, além de poder utilizar os mesmos fontes para gerar drivers para ambas as plataformas, os mesmos binários podem ser utilizados em ambas plataformas. Ou seja, eu posso gerar um único binário que irá rodar tanto em Windows 98 como em Windows 2000. Além disso, o WDM é capaz de identificar e classificar dispositivos pela classe e sub-classe do Plug-and-Play, e assim, associar drivers e filtros a eles. Os Legacy Drivers ainda são suportados em Windows 2000 assim como os VXDs ainda são suportados em Windows 98. Na prática, para a plataforma NT, drivers WDM são Legacy Drivers que compartilham de novas regras para classificar e associar drivers aos dispositivos. Já com relação à plataforma 9x, a mudança foi drástica. As chamadas WDM são encaminhadas a um VXD que faz a tradução de IRP para IOP.

    Com o fim decretado da plataforma 9x, a grande necessidade agora era tornar o desenvolvimento de drivers uma coisa menos dolorosa de se fazer. Isso foi prioridade no desenvolvimento do WDF (Windows Driver Foundation). Um comentário que não esqueço foi do vídeo sobre o KMFD (Kernel Mode Driver Framework) no Channel9 que diz: “É difícil escrever driver de Kernel Mode. Realmente difícil. De fato, é difícil acreditar o quão dificil é. Bem, o pessoal de Windows Driver tem trabalhado intensamente para tornar um pouco menos difícil (não fácil) escrever drivers de Kernel Mode que não derrubem seu sistema. Você sabe, telas azuis e coisas assim.”. O WDF traz muita coisa pronta, detalhes que eram repetitivos mesmo em WDM agora têm um comportamento padrão no WDF, mas que podem ser alterados à medida da necessidade. Aplicando uma abstração muito maior que permite, por exemplo, registrar rotinas de callback para manipular somente os eventos do seu interesse e deixar que o framework tome conta do gerenciamento de energia, que diga-se de passagem, né brinquedo não. Maior abstração não significa necessariamente menor controle. Ainda se pode ter acesso a todos os membros de uma IRP caso você pense que é fácil e queira fazer com suas próprias mãos. Eu li num destes artigos da NT Insider que um desenvolvedor ainda tem saber muito mesmo para fazer pouco em Kernel Mode e a Microsoft quer mudar isso. Desenvolvedores poderiam saber menos sobre tantas coisas envolvidas no desenvolvimento de drivers para fazer algo simples. O WDF é composto pelo KMDF e UMDF (User Mode Driver Framework). Isso mesmo!!! User Mode. Fazer com que drivers sejam menos nocivos ao sistema e conseqüentemente impedir que uma falha em um driver que não seja crítico ao sistema (como o seu MP3 player) cause uma tela azul e derrube todo o seu sistema. É realmente interessante ver como isso funciona. Seu driver roda como um serviço COM, utilizando uma conta de sistema e que utiliza um framework de comunicação que é baseado em COM para ter uma interação com o sistema como se estivesse em Kernel Mode. Existe um .ppt muito interessante que foi utilizado na WinHEC 2006 que demonstra simplificadamente como isso acontece. Ainda não é possível desenvolver drivers .Net por causa do impacto de performance no sistema, mas que isso é desejável, não há sombra de dúvidas. É claro que apenas alguns tipos de dispositivos poderão trabalhar em User Mode, mas para a crescente linha de dispositivos USB já é tudibão. O WDF virá junto com o DDK do Windows Vista, muitos (para não mencionar todos) dos drivers do novo sistema foram migrados para WDF, mas este não é um luxo apenas do Windows Vista. O KMDF pode ser instalado mesmo em Windows 2000 SP4 e o UMDF beta pode ser instalado no Windows XP SP2.

  • Getting Started…

    Vamos deixar de papo e vamos meter logo a mão na massa. Atendendo a alguns pedidos, neste post vou dar os passos para o desenvolvimento mínimo de um driver. Ao final deste post, teremos um módulo que será instalado e carregado no Kernel sem nenhum objetivo funcional. Ainda não vou falar sobre contexto de processo, Devices, Symbolic Links, IOCTL, IRP, DPC, ISR, FGTS, IR, IPVA e muito menos de IPTU. Falarei um pouco de cada coisa com o passar do tempo. Hoje vamos apenas compilar do zero um módulo vazio que poderá futuramente servir como ponto de partida para futuras experiências.

    Para compilar um driver para Windows, vamos precisar minimamente do Windows Device Driver Kit (DDK). O DDK pode der obtido no site da Microsoft. Existem algumas alternativas para se obter o DDK, mas a mais econômica é optar pelo download do arquivo ISO. Quando este post foi criado, a versão disponibilizada no site era a “Windows Server 2003 SP1 DDK” com 230 MB.

    Creio que não será necessário dar os mínimos detalhes da instalação do DDK, algo como “Leve o mouse até o botão cujo texto diz NEXT e pressione o botão esquerdo do mouse”. As opções defaults são mais que suficientes para nossas experiências. Caso você esteja com pouco espaço em disco, mude as opções do “Build Environments” para que sejam instalados apenas os ambientes do Windows 2000 como é exibido abaixo. Isso fará com que o espaço necessário caia de 628 MB para 246 MB. Se ainda assim você tiver problemas para instalar o DDK, talvez você deva considerar a ajuda profissional.

    Terminada a instalação do DDK, já temos tudo que precisamos para criar drivers. Obviamente existem varias outras ferramentas que tornariam nosso trabalho mais confortável, mas neste post ficaremos apenas a pão e água.

    Para nosso teste, crie uma pasta onde vamos colocar o fonte do driver (Ex: C:\Projects\Useless). Dentro desta pasta crie o arquivo texto de nome “Useless.c” e utilize o seu editor de texto do coração para digitar o fonte abaixo. Não vale utilizar o “Microsoft Word”, mas se você pensou nisso, utilize o “Notepad” e procure ajuda profissional.

    #include <ntddk.h>
     
    VOID OnDriverUnload(IN PDRIVER_OBJECT   pDriverObject);
     
    /****
    ***     DriverEntry 
    **
    **      Ponto de entrada do nosso driver, tudo começa aqui,
    **      depois vai enrolando, enrolando, ...
    */
     
    NTSTATUS DriverEntry(IN PDRIVER_OBJECT  pDriverObject,
                         IN PUNICODE_STRING pusRegistryPath)
    {
        //-f--> Se houver um depurador atachado ao nosso sistema
        //      poderemos ver a mensagem abaixo.
        DbgPrint("Cagamba, não é que funciona mesmo?\n");
     
        //-f--> Aqui informamos ao sistema que nosso driver é
        //      capaz de ser descarregado dinamicamente e que a
        //      rotina de CallBack que vai tratar a finalização
        //       de tudo é a OnDriverUnload.
        pDriverObject->DriverUnload = OnDriverUnload;
     
        //-f--> Ufa, conseguimos chegar até aqui, isso merece
        //      um retorno de sucesso para o sistema.
        return STATUS_SUCCESS;
    }
     
     
    /****
    ***     OnDriverUnload
    **
    **      Função de CallBack que trata das finalizações
    **      necessárias para a descarga do driver.
    */
     
    VOID OnDriverUnload(IN PDRIVER_OBJECT   pDriverObject)
    {
        //-f--> Se houver um depurador atachado ao nosso sistema
        //      poderemos ver a mensagem abaixo.
        DbgPrint("Mas já? Eu nem fiz nada...\n");
    }

    Depois de fazer o “Copy” e “Paste” do fonte acima, vamos notar alguns pontos. Neste exemplo, lembre-se de salvar seu arquivo fonte com extensão “.c”, caso contrário teremos alguns problemas em exportar o nosso ponto de entrada. Daqui a alguns posts, pretendo demonstrar como ter arquivos “.cpp” e como utilizar a orientação a objeto na construção de drivers. Outro ponto importante a notar é que a atribuição feita para a função de DriverUnload é opcional. Caso você não queira prover uma função de callback para a descarga do seu driver, basta não fazer esta atribuição. Entretanto, nestas condições o driver não será apto de ser descarregado. Você precisará desta atribuição mesmo que seja para uma função vazia a fim de que seu driver possa ser desligado. Vamos falar disso com mais detalhes posteriormente (detalhes no DDK).

    Ainda é necessário criar os arquivos que definem o projeto. No mesmo diretório do fonte, crie o arquivo de nome “makefile” e copie o texto abaixo para ele. Este arquivo simplesmente implementa o verdadeiro “makefile” que é utilizado para compilar diversos componentes do DDK.

    !INCLUDE $(NTMAKEENV)\makefile.def

    O arquivo de makefile nunca deve ser editado. Todas as configurações sobre qual o tipo de driver será compilado, quais os fontes irão compor o driver, quais bibliotecas serão utilizadas e outras tantas definições são especificadas no arquivo de nome “sources” que deve existir também no mesmo diretório com o conteúdo que segue abaixo. Em conjunto com o “makefile.def” do DDK, estes arquivos criam uma série de macros que facilitam muito a criação do arquivo de projeto tal como um “.vcproj” do Visual Studio. Posts sobre como utilizar o Visual Studio 2005 para compilar drivers também estão em minha lista de tarefas.

    TARGETNAME=Useless
    TARGETPATH=obj
    TARGETTYPE=DRIVER
     
    SOURCES=Useless.c

    Existe uma infinidade de detalhes a serem comentados sobre um arquivo “sources”, por enquanto vamos nos limitar a saber que o nome do seu binário final é definido pela variável “TARGETNAME” e que a lista de fontes que compõem seu driver deve estar na variável “SOURCES” separados por espaço. (Muito mais detalhes no DDK).

    Por fim, criados os arquivos, vamos compilá-los. Para isso devemos configurar as variáveis de ambiente para definir onde estão as libs, os headers e os binários que vão compilar seus fontes. A instalação do DDK disponibiliza um atalho em seu menu “Iniciar” que ponta para o arquivo “setenv.bat”. Este atalho cria uma janela de Prompt e configura o ambiente para a compilação do driver. Você encontra o atalho em “Build Environments” que foi instalado no seu menu iniciar. Deve haver uma versão “Checked” e uma versão “Free” deste atalho. Enquanto a versão “Checked” configura o ambiente para gerar drivers com informações de Debug, a versão “Free” gera os drivers para produção, que é o equivalente ao “Release”. Selecionando o “Checked Build” do “Windows 2000”, teremos uma janela de Prompt de comando com o título como mostra a figura abaixo, siga os mesmos passos da imagem abaixo para ter os mesmos resultados.

    Chamando o comando “build” deveríamos ter os seguintes resultados.

    Tudo pronto, agora só precisamos de uma vítima, ou melhor, um voluntário. Pode ser o micro de um priminho, um irmão caçula, um estagiário, contanto que não seja uma pessoa maior que você ou que possa fazer com que você perca seu emprego. Não aconselho utilizar sua própria máquina, podemos precisar dela mais tarde. Normalmente eu utilizo máquinas virtuais, são vítimas perfeitas. Em seguida, vamos instalar nosso driver da maneira mais simples que eu conheço. Vamos editar algumas linhas no registro e reiniciar nosso sistema. Existem APIs e ferramentas que registram o driver e o colocam para trabalhar na mesma hora sem reiniciar o sistema, mas lembrem-se, estamos a pão e água.

    Primeiro vamos copiar o arquivo “Useless.sys” que foi gerado na subpasta “objchk_w2k_x86\i386” do nosso projeto para a pasta “System32\drivers”. Em seguida, utilizando o editor de registro, precisaremos criar a chave “Useless” dentro da chave “Services” do sistema e colocar os valores como demonstrado na figura abaixo.

    Estas são as linhas mínimas para um driver no registro. O valor “Type” especifica um driver de Kernel, o valor “Start” indica que este terá seu inicio manualmente e finalmente o valor “ErrorControl” informa ao sistema como ele deve se comportar caso este driver falhe em sua carga (detalhes, detalhes e detalhes).

    Neste ponto temos que reiniciar o sistema para que o sistema tome conhecimento deste novo módulo. Nosso driver não irá iniciar automaticamente. Depois de reiniciada, vá ao Prompt de inicie o driver com o comando “net start Useless”. Não é magnífico como não aconteceu nada? Isso significa que o driver está fazendo exatamente o que se propunha fazer, “Nada”. Você pode interromper o driver com o comando “net stop Useless”. Se tivéssemos depurador atachado ao sistema, teríamos as seguintes saídas.

    Para os que nunca criaram um driver antes e não podem ver as saídas no WinDbg, fica muito frustrante, pois não aconteceu nada que comprovasse que nosso driver estivesse realmente carregado e rodando em Kernel. Para dar um pouco mais de ação a essa monotonia, modifique a função DriverEntry como mostra a figura abaixo. Recompile o driver, o substitua no diretório “System32\drivers” e finalmente reinicie a máquina.

    NTSTATUS DriverEntry(IN PDRIVER_OBJECT  pDriverObject,
                         IN PUNICODE_STRING pusRegistryPath)
    {
        //-f--> Diga olá à BSOD e vá se acostumando com ela...
        *(PVOID*)0x00000000 = 0;
     
        //-f--> Não vamos viver para ver isso.
        return STATUS_SUCCESS;
    }

    Neste caso, quando o driver for iniciado, este vai tentar escrever no endereço zero, uma exceção será lançada e aqui teremos nossa primeira Tela Azul da Morte (Blue Screen of Dead). Divirta-se!!!

    Vou tentar intercalar posts para iniciantes com posts mais avançados a fim de tentar atrair a atenção de ambos os grupos. Neste post não temos nada muito útil, mas já temos a base para que pessoas que nunca tiveram contato com este tipo de desenvolvimento possam dar o primeito passo. Todo cidadão tem o direito de gerar uma Tela Azul. 😉

  • ExAllocatePool(WithoutTag)

    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

    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!!!”));

    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.