Blog

  • Começar de novo

    Alguns drivers precisam iniciar logo que o sistema carrega, ou melhor, enquanto o sistema carrega. Quando configuramos nosso driver com Start = 0 (Boot), nosso driver carrega junto com drivers bem básicos, tais como File Systems, Bus Drivers e por aí vai. Certa vez, precisei que um driver de Boot abrisse um arquivo para obter algumas informações do sistema. Infelizmente coisas como partições, volumes e File Systems ainda não estavam trabalhando, assim, tive que adiar um pouco este processo. Neste post vou comentar sobre como dar continuidade à inicialização do seu driver depois que a função DriverEntry terminou.

    Para exemplificar o caso, escrevi um driver que ilustra claramente essa situação. O projeto completo está em um link para download no final deste post. Olhando para a implementação da nossa DriverEntry temos:

    /****
    ***     DriverEntry
    **
    **      Ponto de entrada do driver, que será executado enquanto
    **      o sistema Boota (do verbo "acabei de ligar a máquina").
    */
    extern "C" 
    NTSTATUS DriverEntry(IN PDRIVER_OBJECT  pDriverObj,
                         IN PUNICODE_STRING pusRegistryPath)
    {
        NTSTATUS    nts;
     
        //-f--> Ói nóis aqui traveis...
        KdPrint(("Starting DrvReinit...\n"));
     
        //-f--> Registra rotina de descarga do driver
        pDriverObj->DriverUnload = OnDriverUnload;
     
        //-f--> Tenta abrir o arquivo o quanto antes, afinal
        //      eu nasci de sete meses mesmo.
        nts = TryOpenFile();
     
        //-f--> E como vocês podem ver...
        if (!NT_SUCCESS(nts))
        {
            //-f--> O arquivo não foi aberto
            KdPrint(("Scheduling reinitialization.\n"));
     
            //-f--> E eu até imagino qual seja o erro.
            ASSERT(nts == STATUS_OBJECT_PATH_NOT_FOUND);
     
            //-f--> Registra OnReinitialize para ser chamada mais tarde.
            //      Mais tarde significa quando todos os drivers de boot
            //      foram carregados.
            IoRegisterBootDriverReinitialization(pDriverObj,
                                                 OnReinitialize,
                                                 NULL);
        }
        else
        {
            //-f--> Er... Tem certeza de que você instalou esse driver
            //      direito? Bom, então o sistema não está iniciando
            //      agora e você iniciou este driver na mão.
        }
     
        //-f--> Se a função DriverEntry não retornar STATUS_SUCCESS,
        //      a rotina agendada para reiniciar o driver não será chamada.
        return STATUS_SUCCESS;
    }

    A função IoRegisterBootDriverReinitialization (Ufa! Algumas destas funções exigem fôlego) registra uma rotina de callback que será chamada ao final da carga de todos os drivers de Boot. Isso vai nos dar uma segunda oportunidade de tentar fazer o que nos foi proposto. Esta rotina é normalmente utilizada por filtros que se atacham sobre dispositivos não Plug-and-Play, e assim, não podem contar com a chamada da função AddDevice para serem notificados de que um novo device foi criado.

    Em nosso exemplo, o objetivo é simplesmente ler um arquivo. Nestas condições, o erro que recebemos é o STATUS_OBJECT_PATH_NOT_FOUND. Para garantir que o arquivo que estamos querendo ler realmente existe, vamos utilizar o próprio arquivo onde o driver é implementado. Logo, se nosso código está rodando, o arquivo está lá.

    /****
    ***     TryOpenFile
    **
    **      Tenta abrir o arquivo onde este driver está implementado.
    **      Se este código está rodando, então este arquivo tem que estar lá.
    */
    NTSTATUS TryOpenFile(VOID)
    {
        NTSTATUS            nts;
        IO_STATUS_BLOCK     IoStatusBlock;
        HANDLE              hFile;
        OBJECT_ATTRIBUTES   ObjAttributes;
        UNICODE_STRING      usFilePath =
            RTL_CONSTANT_STRING(L"\\??\\C:\\Windows\\System32\\drivers\\DrvReinit.sys");
     
        //-f--> Olá depurador...
        KdPrint(("Trying open the file...\n"));
     
        //-f--> Preenchendo a estrutura OBJECT_ATTRIBUTES,
        //      fazê o quê, faz parte.
        InitializeObjectAttributes(&ObjAttributes,
                                   &usFilePath,
                                   OBJ_CASE_INSENSITIVE,
                                   NULL,
                                   NULL);
     
        //-f--> Solicita a abertura do arquivo
        nts = ZwCreateFile(&hFile,
                           GENERIC_READ,
                           &ObjAttributes,
                           &IoStatusBlock,
                           NULL,
                           0,
                           FILE_SHARE_READ | FILE_SHARE_WRITE,
                           FILE_OPEN,
                           0,
                           NULL,
                           0);
     
        //-f--> Abriu ou não?
        if (!NT_SUCCESS(nts))
        {
            //-f--> Sinto muito, não foi dessa vez
            KdPrint(("Error 0x%08x opening the file.\n", nts));
        }
        else
        {
            //-f--> Até que não foi tão difícil assim
            KdPrint(("File opened OK. Er... So, let's close it now.\n"));
     
            //-f--> Fecha o handle do arquivo. Não queremos nada com ele mesmo.
            ZwClose(hFile);
        }
        return nts;
    }

    Inicialmente quando tentamos abrir este arquivo à partir da função DriverEntry e nos é retornado o erro indicando que o mesmo não existe, podemos cair em uma crise existencial. Afinal, se o arquivo não existe, como o driver foi carregado? Será que o driver também não existe? Será que eu também não existo? Outras crises poderiam ser mencionadas como a tão conhecida:

    “Deus é amor.
    O amor é cego.
    Steve Wonder é cego.
    Logo, Steve Wonder é Deus.

    Disseram-me que eu sou ninguém.
    Ninguém é perfeito.
    Logo, eu sou perfeito.
    Mas só Deus é perfeito.
    Portanto, eu sou Deus.

    Se Steve Wonder é Deus, eu sou Steve Wonder!
    Meu Deus, eu sou cego!”

    Mas voltando ao assunto, caso a rotina de reinicialização for executada e o serviço que o driver precisa ainda não estiver disponível, então podemos re-agendar a chamada desta rotina mais uma vez (ou quantas vezes forem necessárias). Um dos parâmetros que a rotina de reinicialização recebe, é o que informa quantas vezes ela foi chamada pelo sistema. Isso poderia ser utilizado para desistir de procurar o recurso que nunca aparece quando a milésima tentativa falhasse. Acompanhe nossa rotina de exemplo.

    /****
    ***     OnReinitialize
    **
    **      Rotina que é registrada pela funçao 
    **      IoRegisterBootDriverReinitialization para ser chamada mais tarde.
    */
    VOID OnReinitialize(IN PDRIVER_OBJECT     pDriverObj,
                        IN PVOID              pContext,
                        IN ULONG              ulCount)
    {
        NTSTATUS    nts;
     
        //-f--> Esta pode ser reagenda quantas vezes forem necessárias.
        //      Vamos mostrar quantas foram.
        KdPrint(("OnReinitialize was called %d times...\n", ulCount));
     
        //-f--> Se não houvesse este comentário, vocês nunca descobririam
        //      que a função abaixo TentaAbrirArquivo
        nts = TryOpenFile();
     
        //-f--> E aí? Deu certo?
        if (!NT_SUCCESS(nts))
        {
            //-f--> Se o erro for diferente deste abaixo, então
            //      não sei de nada. A culpa não é minha. Eu não te conheço.
            ASSERT(nts == STATUS_OBJECT_PATH_NOT_FOUND);
     
            //-f--> Não pode ser, isso não está acontecendo de verdade.
            //      Vamos tentar um cadim mais tarde então.
            IoRegisterBootDriverReinitialization(pDriverObj,
                                                 OnReinitialize,
                                                 NULL);
        }
    }

    Instalando um driver de Boot

    Para assegurar que teremos a necessidade de atrasar a inicialização do nosso driver, vamos fazer com que nosso driver de exemplo seja um dos primeiros a ser carregado. Para isso, vamos utilizar o já mencionado DriverLoader para instalá-lo em um grupo específico.

    Depois de compilar o código de exemplo, copie o driver para o diretório System32\drivers da máquina vítima. Execute o DriverLoader, preencha os campos como demostrado abaixo e clique em RegisterService.

    Para completar a experiência, vamos reiniciar o sistema e conectar o depurador de Kernel para acompanhar. Deveriamos ter a seguinte saída.

    Mas não poderiamos utilizar um work Item para isso? Na verdade sim, mas existem sutis diferenças entre essas alternativas. Utilizar um Work Item não garante que a rotina não será executada antes da sua DriverEntry terminar, e obviamente, também não garante que ela seja executada somente depois que todos os drivers de Boot forem carregados.

    Até a proxima… 😉

    DrvReinit.zip

  • Nós queremos exemplos

    Alguns meses após minha entrada na Open, pediram me que eu fizesse uma participação em uma palestra sobre Código Seguro. Minha parte falava sobre o estouro de pilha e como tirar proveito desse descuido do programador para invadir o programa. O grande ponto a notar foi que além dos programadores, o público era composto por engenheiros e arquitetos de software, o pessoal do comercial, que tinha mais contato com os clientes, também estava lá, havia uma ou duas pessoas do administrativo lá também, ou seja, um público que poucos já ouviram falar em pilha. Teve um que disse: “Eu lembro mais ou menos disso na minha certificação de .Net, que dizia que estruturas são criadas na pilha enquanto objetos são criados no heap, ou vice-versa”. Enfim, as coisas começaram a complicar quando comecei a destrinchar o código de exemplo que forneci com PUSHs, POPs e MOVs. Resultado, alguns só dormiam enquanto outros babavam e falavam naquela língua alienígena que só falamos enquanto dormimos. Conheço bem essa língua porque minha esposa sempre troca a maior idéia comigo enquanto ela dorme e eu estou sentado na cama com o notebook. Apesar dela dizer palavras completamente incompreensíveis, ela sempre responde quando faço alguma pergunta à respeito. Ela introduz o assunto dizendo: “Admivoza bumizav”, então eu pergunto: “Por que você acha isso?”, e ela responde: “Zumirag abmish mua”. Mas voltando ao assunto, ficou claro que a quantidade de detalhes técnicos ficou incompatível com o público. Por isso, há pouco tempo atrás, em uma palestra sobre drivers para Windows, tentei passar uma visão pouco detalhada, apenas dar a idéia do que eles são e como eles contribuem para funcionamemto do sistema. Afinal, todo o departamento de desenvolvimento estava lá, incluindo arquitetos, engenheiros e .Net coders (nada contra). Para a minha surpresa, ao final da palestra, todos ficaram com aquela cara de “Ué, e o resto? Não tem nem um exemplinho?”. Então tá bom… Neste post vou escrever um driver mínimo, mas que já tenha alguma interação com uma aplicação de teste.

    E no início…

    Aqui vou partir do princípio que você já sabe como criar um projeto de driver do zero e como utilizar o Visual Studio para codificar drivers, indo direto ao ponto onde escrevemos o driver. O exemplo de hoje será um driver de eco bem básico que receberá comandos de escrita e leitura. Então seria assim, você inicialmente escreve buffers, que em nosso exemplo serão strings, utilizando a função WriteFile e tudo será armazenado no driver. As leituras subsequentes a partir da função ReadFile trarão os mesmos dados que foram escritos. Este exemplo vai nos ser muito útil em outros posts, e também vai servir como ponto de partida para os que estão querendo escrever seu primeiro driver.

    Sabendo que vamos armazenar os dados enviados em uma lista, então criaremos uma lista de buffers formada pelos nós definidos como mostra abaixo.

    //-f--> Definição para o tipo para ser armazenado
    //      em nossa lista de buffers.
    typedef struct _BUFFER_ENTRY
    {
        PVOID       pBuffer;    //-f--> Buffer enviado
        ULONG       ulSize;     //      Tamanho do buffer
        LIST_ENTRY  Entry;      //      Nó para a lista
     
    } BUFFER_ENTRY, *PBUFFER_ENTRY;
     
     
    //-f--> Ponta da nossa lista e mutex de proteção
    LIST_ENTRY      g_BufferList;
    KMUTEX          g_Mutex;

    Depois de definida a estrutura, criamos uma variável global que será a ponta de nossa lista e também um mutex para proteger nossa lista de possíveis acessos em paralelo. Isso aconteceria se tivéssemos duas aplicações de teste rodando ao mesmo tempo. Se quiser mais detalhes sobre listas ligadas do DDK, existe um post que fala sobre isso também.

    Escrevendo a DriverEntry

    O primeiro ponto a notar aqui é que como nosso exemplo foi codificado em um arquivo .CPP, por isso precisaremos colocar um sonoro extern “C” à definicão da função DriverEntry. Caso contrário, o linker não encontrará o ponto de entrada do driver. Logo no início da implementação, temos a mensagem que será lançada ao depurador via KdPrint. Em seguida vou inicializando a ponta da nossa lista ligada bem como o mutex que a protege. Agora vamos setar alguns membros na estrutura DriverObject, e o primeiro deles será o membro DriverUnload, que recebe um ponteiro de função de callback que informará ao driver que este está sendo descarregado. Os próximos membros são as rotinas que serão chamadas quando o driver receber as solicitações de Create/Open, Close, Read e Write. Falaremos destas rotinas com mais detalhes um pouco adiante. Depois disso, vamos criar o DeviceObject, que vai ser nosso meio de comunicação com o driver. Conforme eu disse na palestra, todas as solicitações que um driver recebe, são por meio de um device. A função IoCreateDevice faz isso para nós. Criado o device vamos configurá-lo de maneira que este utilize buffers intermediários. Para isso devemos setar o bit DO_BUFFERED_IO no campo Flags do DeviceObject que acabou de ser criado. Buffers intermediários? Falaremos sobre BufferedIo versus DirectIo numa próxima oportunidade. Existem muitos conceitos novos neste post e não vamos nos ater a todos eles, caso contrário, ninguém vai ler isso com medo de nunca acabar.

    Ter um DeviceObject é legal, mas não é tudo na vida de um driver, para que uma aplicação User Mode possa se comunicar com um driver, é necessário criar um Symbolic Link. Isso é feito logo em seguida com a função IoCreateSymbolicLink. O restante do código desta função não deve causar grandes surpresas à grande maioria de vocês, mas em caso de dúvidas, é só mandar um e-mail que a gente resolve na porrada.

    /****
    ***     DriverEntry
    **
    **      Ponto de entrada do nosso driver.
    **      Faca nos dentes e sangue nos olhos.
    */
     
    extern "C"
    NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObj,
                         IN PUNICODE_STRING pusRegistryPath)
    {
        NTSTATUS        nts;
        PDEVICE_OBJECT  pDeviceObj = NULL;
     
        __try
        {
            //-f--> Dizendo olá para o Kernel Debugger
            KdPrint(("Starting KernelEcho driver...\n"));
     
            //-f--> Inicializando a lista de buffers e mutex
            InitializeListHead(&g_BufferList);
            KeInitializeMutex(&g_Mutex, 0);
     
            //-f--> Setando nossa função de descarga do driver
            pDriverObj->DriverUnload = OnDriverUnload;
     
            //-f--> Setando as rotinas que meu driver vai dar
            //      suporte.
            pDriverObj->MajorFunction[IRP_MJ_CREATE] = OnCreate;
            pDriverObj->MajorFunction[IRP_MJ_CLOSE] = OnClose;
            pDriverObj->MajorFunction[IRP_MJ_WRITE] = OnWrite;
            pDriverObj->MajorFunction[IRP_MJ_READ] = OnRead;
     
            //-f--> Criando device de controle
            nts = IoCreateDevice(pDriverObj,
                                 0,
                                 &g_usDeviceName,
                                 FILE_DEVICE_UNKNOWN,
                                 0,
                                 FALSE,
                                 &pDeviceObj);
            if (!NT_SUCCESS(nts))
                ExRaiseStatus(nts);
     
            //-f--> Vamos fazer I/O com buffer intermediário
            pDeviceObj->Flags |= DO_BUFFERED_IO;
     
            //-f--> Criando symbolic link para que aplicações
            //      possam ver este device.
            nts = IoCreateSymbolicLink(&g_usSymbolicLink,
                                       &g_usDeviceName);
            if (!NT_SUCCESS(nts))
                ExRaiseStatus(nts);
     
        }
        __except(EXCEPTION_EXECUTE_HANDLER)
        {
            //-f--> Obtendo código da cagada
            nts = GetExceptionCode();
     
            //-f--> Isso vai fazer o depurador parar aqui.
            //      Mas só se compilado em Checked
            ASSERT(FALSE);
            KdPrint(("An exception occurred at " __FUNCTION__ "\n"));
     
            //-f--> Como tivemos problemas na inicialização, vamos
            //      desfazer o que foi feito.
            if (pDeviceObj)
                IoDeleteDevice(pDeviceObj);
        }
     
        return nts;
    }

    Escrevendo Dispatch Functions

    Também vou partir do princípio de que você já tem uma idéia do que é uma IRP. Agora vamos escrever as funções que às manipulam. São as chamadas Dispatch Functions. Estas funções são setadas na inicialização do driver como vocês puderam observar no código acima. Na estrutura DriverObject, o membro MajorFunction é um array de ponteiros de função indexado pelas macros do tipo IRP_MJ_READ. O protótipo da função é o mesmo para todas as funções e será exibido logo abaixo. Uma Dispatch Function tem que manipular IRPs seguindo algumas regras, como tudo no DDK. Uma função mínima poderia ser escrita como segue abaixo.

    /****
    ***     OnDispatch
    **
    **      Exemplo de uma Dispatch Function mínima
    */
     
    NTSTATUS
    OnDispatch(IN PDEVICE_OBJECT  pDeviceObj,
               IN PIRP            pIrp)
    {
        //-f--> Aqui preencho o status desta IRP para a aplicação.
        pIrp->IoStatus.Status = STATUS_SUCCESS;
        pIrp->IoStatus.Information = 0;
     
        //-f--> Completo a IRP. Depois disso, é terminantemente
        //      proibido tocar na estrutura pIRP. Isso não te pertence mais.
        IoCompleteRequest(pIrp, IO_NO_INCREMENT);
     
        //-f--> Retorna o status para o IoManager.
        return STATUS_SUCCESS;
    }

    Mas o que aconteceria se chamássemos a função ReadFile com um handle para o nosso device quando não preenchemos a posição IRP_MJ_READ? Queimariamos eternamente no mármore do inferno? Na verdade, se dermos uma olhada na tabela MajorFunction antes de preenchê-la, veremos que existe o mesmo endereço em todas as posições da tabela. Vamos colocar um break-point logo na entrada da DriverEntry, dar uma fuçada na tabela antes de ser preenchida e ver o que tem por lá.


    Como vemos, essa tabela é toda inicializada com um ponteiro de função que na sua implementação, considerando que as IRPs de gerenciamento de energia têm um tratamento especial, completa a IRP com o status de STATUS_INVALID_DEVICE_REQUEST.

    Uma Dispatch Function basicamente segue uma das três alternativas para tratar uma IRP. Em casos de filtros, nosso driver poderia repassar a IRP para o driver o qual estivesse atachado. Tudo bem, já está anotado aqui… “Fazer um post dando um exemplo de filtro”. A segunda alternativa seria reter a IRP para fazer um tratamento assíncrono, e por último e não menos importante, simplesmente completar a IRP. Notem que em nosso exemplo, tudo o que fazemos é dizer à aplicação que a IRP foi executada com sucesso e completamos a mesma. Para completar a IRP, utilizamos a função IoCompleteRequest, que recebe a IRP a ser completada e o Boost de Prioridade. Ah? Supondo que sua IRP tivesse alguma interação com hardware, isso iria consumir algum tempo da thread em Kernel Mode, esse tempo iria gerar um atraso na thread atual e que seria compensado por este Boost. Como esse não é o nosso caso, vamos utilizar a macro de define nenhum Boost. O DDK tem uma lista de constantes que determina qual o Boost que a thread deveria receber para cada tipo de dispositivo. Veja um deles extraído do meu wdm.h (essa definição pode estar no ntddk.h dependendo da sua versão de DDK).

    //
    // Priority increment for completing CD-ROM I/O.  This is used by CD-ROM device
    // and file system drivers when completing an IRP (IoCompleteRequest)
    //
     
    #define IO_CD_ROM_INCREMENT             1

    Ao final deste post , haverá um link para baixar todos os arquivos necessários para gerar a aplicação e o driver. Reparem nos fontes de exemplo que nossas Dispath Functions OnCreate e OnClose se parecem muito com o exemplo acima. Isso porque não tomamos nenhuma ação quando se abre ou se fecha um handle para o device que criamos.

    Obtendo parâmetros da IRP

    Já nas outras Dispatch Functions OnRead e OnWrite, temos que obter os dados que o driver precisa para executar a IRP, tais como buffer enviado pelo usuário e tamanho do mesmo, tanto na escrita como na leitura. Estes parâmetros estão em uma Stack Location dentro da IRP. Nossa, quanto mais eu rezo, mais nominho esquisito me aparece… Stack Locations são estruturas de parâmetros que são alocados junto com a IRP. Existe uma Stack Location para cada device na pilha de dispositivos que foi chamada. Essa conversa pode ficar bem divertida, mas temos um post para terminar. Vamos deixar para falar sobre Stack Locations em nosso exemplo de filtro. Lá esse assunto fará mais sentido. Mas se você não se agüenta de curiosidade e quiser saber mais sobre o assunto, veja o que a referência diz a respeito de Stack Locations. Por agora vamos apenas considerar que estes parâmetros estão lá e que para ter acesso a esta estrutura devemos utilizar a macro IoGetCurrentIrpStackLocation. Para se ter uma idéia mais prática de todo esse blablablá, segue todo o código da função OnWrite com comentários a dar com pau.

    /****
    ***     OnWrite
    **
    **      Esta rotina é chamana quando a API WriteFile é chamada
    **      utilizando o handle do nosso device.
    */
     
    NTSTATUS
    OnWrite(IN PDEVICE_OBJECT  pDeviceObj,
            IN PIRP            pIrp)
    {
        PIO_STACK_LOCATION  pStack;
        PVOID               pUserBuffer;
        ULONG               ulSize;
        PBUFFER_ENTRY       pBufferEntry = NULL;
        NTSTATUS            nts;
        BOOLEAN             bMutexAcquired = FALSE;
     
        __try
        {
            //-f--> Um olá para o depurador
            KdPrint(("Writing into EchoDevice...\n"));
     
            //-f--> O buffer é um dos parâmetros que vêm
            //      na própria IRP
            pUserBuffer = (PCHAR)pIrp->AssociatedIrp.SystemBuffer;
            ASSERT(pUserBuffer != NULL);
     
            //-f-->Obtém endereço da Stack Location corrente
            pStack = IoGetCurrentIrpStackLocation(pIrp);
     
            //-f--> Obtém o tamanho do buffer
            ulSize = pStack->Parameters.Write.Length;
     
            //-f--> Aloca o nó junto com o buffer que será ocupado
            //      pelo buffer enviado pelo usuário.
            pBufferEntry = (PBUFFER_ENTRY) ExAllocatePoolWithTag(
                PagedPool,
                sizeof(BUFFER_ENTRY) + ulSize,
                ECHO_TAG);
     
            //-f--> Se não tem memória já viu...
            if (!pBufferEntry)
                ExRaiseStatus(STATUS_NO_MEMORY);
     
            //-f--> Inicializando a estrutura
            pBufferEntry->pBuffer = (pBufferEntry + 1);
            pBufferEntry->ulSize = ulSize;
     
            //-f--> Copia o buffer enviado pelo usuário
            //      para o buffer alocado aqui.
            RtlCopyMemory(pBufferEntry->pBuffer,
                          pUserBuffer,
                          ulSize);
     
            //-f--> Obtém o mutex que protege a lista
            //      de acessos em paralelo.
            nts = KeWaitForMutexObject(&g_Mutex,
                                       UserRequest,
                                       KernelMode,
                                       FALSE,
                                       NULL);
            if (!NT_SUCCESS(nts))
                ExRaiseStatus(nts);
     
            //-f--> Temos que lembrar disso caso algo muito
            //      ruim aconteça
            bMutexAcquired = TRUE;
     
            //-f--> Insere novo elemento no final da lista
            InsertTailList(&g_BufferList,
                           &pBufferEntry->Entry);
     
            //-f--> Informa ao IoManager que todos os dados enviados
            //      ao driver foram lidos com sucesso.
            pIrp->IoStatus.Information = ulSize;
            nts = STATUS_SUCCESS;
        }
        __except(EXCEPTION_EXECUTE_HANDLER)
        {
            //-f--> Obtém código da cagada
            nts = GetExceptionCode();
     
            //-f--> Isso vai fazer o depurador parar aqui.
            //      Mas só se compilado em Checked
            ASSERT(FALSE);
            KdPrint(("An exception occurred at " __FUNCTION__ "\n"));
     
            //-f--> Se deu algo errado e já alocamos
            //      este buffer, então vamos desalocar
            if (pBufferEntry)
                ExFreePool(pBufferEntry);
     
            //-f--> Informa ao IoManager que nada foi transferido.
            pIrp->IoStatus.Information = 0;
        }
     
        //-f--> Libera mutex
        if (bMutexAcquired)
            KeReleaseMutex(&g_Mutex,
                           FALSE);
     
        //-f--> Completando a IRP.
        pIrp->IoStatus.Status = nts;
        IoCompleteRequest(pIrp, IO_NO_INCREMENT);
        return nts;
    }

    Outro ponto importante a se notar aqui é o preenchimento do campo Information da estrutura IoStatus que fica na IRP. Nestas funções de transferência de dados, este campo informa ao IoManager a quantidade de dados que foi transferida da aplicação para o driver e vice-versa. Este campo se reflete diretamente sobre o quarto parâmetro da API WriteFile, que tem exatamente a mesma função. Depois de recebidos e validados os parâmetros, alocamos o nó que vai receber o buffer. Reparem que estamos alocando em memória paginada, afinal de contas, todas as nossas funções serão executadas em PASSIVE_LEVEL. Apesar desta função ser uma Dispatch Function, isso não significa que ela seja executada em DISPATCH_LEVEL. Vamos com calma, essas são coisas bem diferentes. Anotando… “Post sobre IRQLs e POOL_TYPEs”. A função OnRead é similar à OnWrite, assim vou poupá-los de colocar todo o código aqui.

    Quando meu driver for descarregado

    A função OnDriverUnload será chamada quando o driver estiver sendo terminado. Aqui, além de esvaziar a lista de buffers que podem ter ficado esquecidos no driver, vamos apagar o Symbolic Link e o DeviceObject que foi criado na inicialização. Simples assim…

    /****
    ***     OnDriverUnload
    **
    **      A festa acabou, vai pra casa, um abraço pra muié,
    **      e bejo nas criança.
    */
     
    VOID OnDriverUnload(IN PDRIVER_OBJECT   pDriverObj)
    {
        PLIST_ENTRY     pEntry;
        PBUFFER_ENTRY   pBufferEntry;
     
        //-f--> Diga boa noite
        KdPrint(("Terminating KernelEcho driver...\n"));
     
        //-f--> Aqui removemos todos os nós que ainda não foram lidos
        //      pela aplicação. Isso aconteceria se a aplicação chamasse
        //      WriteFile e não chamasse ReadFile.
        while(!IsListEmpty(&g_BufferList))
        {
            //-f--> Pega o primeiro nó da lista
            pEntry = RemoveHeadList(&g_BufferList);
     
            //-f--> Obtém o endereço a partir do nó
            pBufferEntry = CONTAINING_RECORD(pEntry, BUFFER_ENTRY, Entry);
     
            //-f--> Finalmente libera a memória utilizada por
            //      este nó
            ExFreePool(pBufferEntry);
        }
     
        //-f--> Apagando DeviceObject e SymbolicLink
        //      criados na inicialização.
        IoDeleteSymbolicLink(&g_usSymbolicLink);
        IoDeleteDevice(pDriverObj->DeviceObject);
    }

    Nossa, que erro horrível! E se o driver for terminado enquanto alguma leitura ou escrita estiver sendo realizada? Será que um futuro negro nos aguarda e nossas almas serão amaldiçoadas pelo resto da eternidade? Será que a Pequena Sereia tem algo a ver com isso?

    Bom, melhor deixar nossas crenças de lado e focar no DDK. Um driver não pode ser terminado enquanto houverem referências para algum device deste driver. Note que esta rotina não tem retorno para que possamos informar ao sistema que o driver pode ou não ser descarregado. Se alguma aplicação ainda tiver um handle aberto para algum device enquanto você solicita a parada do mesmo, o sistema responderá que o driver não poderá ser terminado. Nessa condição, a rotina OnDriverUnload nem será chamada. Mas caso contrário, se nada impedir do driver ser descarregado e nossa rotina for chamada, já era… Seu driver já está a caminho do céu dos drivers.

    O mundo encantado de User Mode

    Não vou colocar todo o código fonte da aplicação aqui no post, mas todos os fontes estão no arquivo disponível para download. Creio que uma coisa que vale a pena mostrar aqui é a sintaxe de como obter o handle para o device que criamos em nosso driver de exemplo.

        //-f--> Aqui abrimos um handle para o nosso device que
        //      foi criado pelo driver. Vale lembrar que nosso
        //      driver de exemplo tem que ser instalado e iniciado
        //      para que a chamada abaixo funcione corretamente.
        hDevice = CreateFile("\\\\.\\EchoDevice",
                             GENERIC_ALL,
                             0,
                             NULL,
                             OPEN_EXISTING,
                             0,
                             NULL);

    Depois que o handle para o device foi obtido, as operações de Read, Write e Close seguem exatamente como se estivéssemos realizando as mesmas operações com arquivos. Não precisa ser nenhum mestre Jedi para conseguir utilizar estas funções.

        //-f--> Envia a string recebida ao driver via
        //      WriteFile.
        if (!WriteFile(hDevice,
                       szBuffer,
                       dwBytes,
                       &dwBytes,
                       NULL))

    Instalando e testando

    Já mostrei em um outro post como instalar um driver na mão. Entretanto, existem meios mais civilizados de instalar um driver. Um deles é utilizando o OSR Driver Loader, uma ferramenta que oferecida pela OSR que instala seu driver sem a necessidade de reiniciar a máquina. Na verdade, este é um procedimento bem simples de se fazer, mas não simples o suficiente para comentar sobre isso ainda neste post, então vamos utilizar a ferramenta mesmo.

    Depois de compilar o driver, coloque uma cópia dele no System32\drivers da máquina vítima. Em seguida execute o DriverLoader e preencha os campos como exibido na figura abaixo.


    Depois é só clicar em Register Service para instalar o novo driver e em seguida clicar em Start Service para iniciar o driver. Pronto, agora você já poderá utilizar a aplicação de teste. A aplicação é muito simples de utilizar. Depois de iniciada, digite as strings que deveriam ser enviadas ao driver. Uma string vazia indica o fim das strings e então começa a leitura das mesmas strings enfiladas no driver.

    Ufa! Como vimos, mesmo um driver que faça algo muito simples requer uma quantidade considerável de código e muitos conceitos diferentes. Sei que ficaram algumas lacunas durante o post, mas espero ter ajudado. Caso tenham dúvidas em algum ponto do driver ou mesmo da aplicação de teste, não hesitem em perguntar ou mandar seus comentários. Os contatos ajudam muito a definir os próximos posts.
    Have fun!

    KernelEcho.zip

  • Step into Kernel (VmWare+WinDbg)

    Agora chega de papinho furado e vamos ao que realmente interessa. Falando sobre Kernel Debugging, já escrevi um post descrevendo os passos necessários para fazer Kernel Debugging com o WinDbg utilizando duas máquinas e um cabo serial. Mas ter duas máquinas dedicadas a essa prática é um luxo que nem todos têm. Assim, em outro post, comentei sobre como instalar e utilizar o SoftIce para depurar drivers em uma única máquina dedicada para o debug. Digo máquina dedicada para debug porquê utilizar sua máquina de desenvolvimento para depurar drivers pode não ser uma das suas melhores idéias. Os mais corajosos e confiantes em seu próprio código ainda se arriscam nessa prática. Bom, eu não posso falar muito, já fiz isso em tempos de vacas magras. De qualquer forma, ainda não fugimos da necessidade de ter duas máquinas para termos um ambiente mínimo de desenvolvimento e teste de drivers. Uma alternativa a esses cenários é a utilização de uma única máquina, mas que tenha memória e CPU suficientes para rodar uma máquina virtual a fim de fazer o debug de Kernel. Neste post vou dar os passos para utilizar uma máquina virtual como cobaia para testar e depurar drivers.

    Configurando a máquina virtual

    O meio de comunicação natural entre máquinas reais que utilizam o WinDbg para fazer o Kernel Debugging é uma porta serial. Para seguir os mesmos passos, precisamos fazer com que a sua máquina virtual ganhe uma porta serial para fazer esta comunicação possível. Com sua máquina virtual desligada, clique sobre a opção Edit vitual machine settings.


    Aparecerá uma janela com a lista dos dispositivos já instalados em sua máquina no tab Hardware. Clique em Add… de forma a adicionar um novo dispositivo e selecione Serial Port na lista de dispositivos que será apresentada na tela a seguir.


    Depois de clicar em Next, selecione a opção Output named pipe. Isso fará com que a porta serial na máquina virtual se comunique com a máquina real através de um pipe nomeado. Clicando em OK, serão apresentadas as configurações específicas do pipe nomeado. O nome do pipe será utilizado mais tarde em uma das configurações do WinDbg, então caso você queira utilizar um outro nome que não o sugerido aqui, trate de se lembrar deste mesmo nome mais tarde. Em seguida, mude o valor do segundo combo de forma a indicar que o a outra ponta desta comunicação será uma aplicação, no caso o WinDbg. Depois disso, é só clicar em Finish.


    Ao final destas configurações, marque a opção Yield CPU on poll conforme mostra a figura abaixo.

    Configurando a máquina TARGET

    Feito todo esse “Ziriguidum”, agora teremos que configurar o sistema da máquina TARGET para que ela esteja apta a fazer o Kernel Debugging. Lembre-se que para o Windows que roda dentro da máquina virtual, não existe pipe nomeado, apenas uma porta serial. Se você ainda não sabe o que é uma máquina TARGET nem como configurar uma, então dê um pulo neste post e siga os passos descritos na parte onde uma máquina TARGET é configurada.

    Configurando a máquina HOST

    Admitindo que sua máquina TARGET já foi configurada, agora teremos que configurar o WinDbg de forma que ele se conecte a uma máquina utilizando o pipe nomeado no lugar de uma porta serial. Para isso, costumo criar um arquivo de batch que contenha a seguinte linha de comando:

    start C:\Arquiv~1\Debugg~1\windbg.exe -b -k com:pipe,port=\\.\pipe\com_1,resets=0

    Criar um arquivo de batch não é um passo obrigatório, talvez você prefira redigitar tudo na janela Run… a cada vez que o debug for iniciado, mas é tudo uma questão de gosto. Repare que o nome do pipe aparece aqui. Espero que você ainda se lembre daquele nominho que você escolheu.

    Conectando…

    Agora que temos tudo já configurado, é só conectar e depurar. Nessa história, a máquina virtual é quem cria o pipe nomeado que será aberto pelo WinDbg. Sendo assim se você iniciar o WinDbg com os parâmetros demodntrados antes de iniciar a máquina virtual, você verá a janela abaixo informando que o pipe não foi encontrado.


    Por isso a seqüência é primeiro ligar a máquina virtual, selecionar a opção debug no Boot como mostrado abaixo, e somente depois disso é que você deveria inciar o WinDbg com os parâmetros acima descritos.


    Quando o WinDbg finalmente conecta na máquina virtual via o pipe nomeado, temos as seguintes mensagens em nosso Command Window dentro do WinDbg.


    Daí em diante você já sabe… É pegar os paus mesmo (no bom sentido).

    Mais uma vez espero ter ajudado.
    Até a próxima. 🙂

  • Agora ele só fala disso…

    As inscrições para o curso de drivers para Windows já começaram a operar nesse final de semana. Por causa de alguns atrasos, o início do curso foi adiado e também já foi confirmado para o dia 23 de junho.

    Para ver a lista completa de cursos de extensão oferecidos pela universidade, selecione o ítem “Tecnologia da informação” do menu “Extensão” no site da Universidade.

    Para quem curte a criação de Pingüins, o hard coder e amigo William, estará dando um curso de desenvolvimento de drivers para Linux na mesma universidade. Ele também utilizará um dos hardwares de treinamentos da OSR, que aparece na foto acima, para dar a oportunidade de pôr a mão na massa. No caso do curso de Linux, o hardware utilizado será o de USB.

    Até mais…

  • Curso de drivers para Windows

    Como alguns de vocês puderam ler em um outro post, fui convidado pela Universidade Gama Filho a ministrar um curso de desenvolvimento em Kernel Mode para Windows. Já foram fechados os detalhes do curso que darei e já estariam produzindo o material para a divulgação, mas enquanto este post era escrito, fui notificado de que ocorreu um atraso na produção deste material e que estaria disponível a partir do dia 21 de maio. Já dei uma olhada no Folder que eles estão produzindo, e pelo que vi, resolveram resumir a descrição do curso que darei. Por esta razão, vou colocar a versão completa da descrição aqui. Segue o arquivo que enviei para a universidade.

    Objetivo:
    ==========
    Este curso é destinado aos desenvolvedores ou estudantes que precisam entender
    os conceitos fundamentais sobre implementação de drivers para Windows. Este
    curso não abordará implementações específicas de drivers, tais como impressoras,
    vídeo, SCSI, NDIS, USB, 1394 ou UMDF. O objetivo deste curso é preparar os
    alunos que querem entender, testar, complementar ou construir drivers para
    Windows empregando os conceitos gerais envolvidos durante o processo.
     
     
    Pré-requisitos:
    ==================
    Conhecimento da linguagem C
    Básico de API Windows
    Básico de Sistemas Operacionais
     
     
    Tópicos abordados:
    =====================
    Visão geral da arquitetura do Sistema
            Processos e Threads     
            Memória Virtual e Paginação
            Kernel Mode x User Mode
            Subsistemas e API nativa
            IoManager
            Pilha de Drivers e Plug-and-Play
            Object Manager
                    Terminal Server
            Camada de abstração de Hardware (HAL)
     
    Ambiente (obtenção, instalação e utilização)
            Windows Device Driver Kit
            Microsoft Visual Studio Express
            Microsoft Windows Debugging Tools
            Símbolos
     
    Escrevendo um Driver
            Escrevendo DriverEntry e DriverUnload
            Compilando o Driver
            Instalando o Driver (Legacy)
                    Dependências
                    Grupos
                    Load Order
            Depurando o Driver
                    Instalações Checked Build
                    Driver Verifier
                    Mapeando imagem para depuração
                    Utilizando Máquinas Virtuais
                    SoftIce
            Criando DeviceObject
            Symbolic Links
            I/O Request Packets
            IOCTLs e DeviceIoControl
            Implementando Dispatch Routines
                    Buffered I/O
                    Direct I/O
                    Neither I/O
            Objetos, Handles e Ponteiros
            Contexto Arbitrário
            IRQL's, APC's, DPC's e WorkItems
            Sincronismo
                    Mutex
                    FastMutex
                    ERESOURCE
                    Spin Lock
            Eventos e Timers
            Filas Personalizadas
     
    Interações com Hardware
            Port I/O
            Interrupções e ISR's
            DMA
     
    Escrevendo Filtros
            Escrevendo a rotina AddDevice
            Filtros para drivers Legacy
            Repassando IRPs
            Stack Locations
            Completion Routines
            Tratamento de IRPs Pendentes
            Cancelamento de IRPs
            Criando IRPs para outros Drivers
     
    Tipos de Drivers
            Legacy drivers
            WDM Drivers
            Minidrivers
            Miniports
            Miniclass
     
    Instalações
            Criando um arquivo .INF
            O uso da SetupApi
     
    Referências
            Web sites
            Grupos de discussão
            Livros

    Até o momento em que este post foi escrito, o atendimento da universidade não estava apto a fornecer detalhes sobre o curso, tais como data de inscrição, conteúdo ou mesmo calendário. Mas posso lhes adiantar o que estou sabendo até agora.

    O curso terá duração de 40 horas, sendo distribuídas em 10 aulas de 4 horas cada. As aulas serão dadas aos sábados das 13:00 às 17:00.

    O curso está marcado para ter sua primeira turma iniciando dia 26 de maio. Serão necessários no mínimo 5 alunos para formar uma turma, mas pela quantidade de pessoas que já entraram em contato buscando detalhes (mesmo sem divulgação), minha preocupação passa a ser o limite superior de 10 alunos. Resolvemos limitar a turma em 10 alunos para que houvesse um melhor aproveitamento do conteúdo.

    Concordo que 40 horas é pouco para aprender tudo que é necessário para desenvolver drivers, mas já será um excelente ponto de partida para ter seu primeiro contato. Se você é do tipo de pessoa que precisa aprender absolutamente tudo para começar a desenvolver, então já vou lhe adiantando que este curso não é para você. Os maiores desenvolvedores de Kernel que eu conheço (de Blog, mas conheço), e que trabalham com isso a anos e anos, dizem que nunca sabem tudo. Para se ter uma idéia, alguns desenvolvedores focam apenas determinados assuntos dentro do Kernel. Um trabalha a 10 anos só com drivers de disco, um outro trabalha a 7 anos somente com drivers de rede. Tony Mason, por exemplo, trabalha a 18 anos só com File System Drivers. Agora pergunta pra eles se eles sabem tudo. E eu que, por uma época, me sentia mal por tentar me focar somente em trabalhos de Kernel e recusava trabalhos em User Mode. As vezes, em rodas de amigos, ouvia comentários de como fazer isso ou aquilo em .Net, e eu viajando na maionese. Eu mal sabia que dentro do assunto Kernel ainda existia uma infinidade de detalhes e especializações. Será possível aprender tudo?

    Estou preparando o conteúdo de forma que os alunos possam ter pequenas experiências práticas, tais como escrever e compilar um driver “Hello World”, instalá-los em máquinas virtuais e depurá-los. Não tem jeito, tem que colocar a mão na massa. Conectar duas máquinas reais com um cabo serial e fazer Debug de Kernel. Gerar e analizar Crash Dumps. Penso que temos que tirar o máximo proveito do fato de estarmos juntos em uma sala aula, e realizar algumas experiências que os livros tentam descrever somente com palavras e figuras. É esta vontade de pôr as coisas em prática que contribuíram para que eu adquirisse o kit de treinamento à venda na OSR Online. O kit que estarei levando para o curso, e que aparece na foto acima, é basicamente uma placa PCI de I/O Digital. Meu objetivo é fazer com que drivers de alunos possam controlar este dispositivo.

    Bom, à medida que eu for recebendo notícias, irei repassando a vocês.
    Até mais.

  • E aí? Deu Boston?

    Como comentei em meu último post, participei do seminário de desenvolvimento de drivers File System para Windows promovido pela OSR em Boston. Neste post vou comentar um pouco sobre como foram as coisas por lá.

    Para começar, ninguém menos que Tony Mason para dar este seminário que foi muito, mas muito proveitoso. Para quem não conhece, Tony Mason é um dos quatro sócios da OSR. Ele trabalha com drivers a mais de 20 anos, e particularmente com File Systems a partir de 1989. Começou a trabalhar com File Systems para Windows NT em 1993, mas já desenvolvia File Systems para UNIX antes disso. Tony Mason também é a pessoa responsável por revisar e atualizar o conteúdo do livro Windows NT File System Internals escrito por Rajeev Nagar e publicado em 1997. O livro hoje é referência no assunto e sua edição revisada, que ainda não tem data prevista, trará assuntos novos que foram implementados no Windows 2000, XP e Vista. Também comentei um pouco sobre isso neste post.

    Podem me chamar de “paga pau”, mas é lindo ver como ele simplesmente sapateia nos detalhes de implementação de File Systems Drivers (FSD) e File Systems Filters (FSF). Adivinha se meu exemplar do livro foi autografada por ele. Agora vou poder guardar esse autógrafo em minha agenda ao lado da embalagem do meu primeiro absorvente. Eu adoraria poder mostrar aqui, mas como temos uma assinatura envolvida, não acho que seria uma boa idéia colocar isso na Internet.

    Na turma éramos em quinze alunos, onde doze eram norte-americanos, um romeno, um canadense e um brasileiro. Tive a oportunidade de ser o primeiro brasileiro a fazer este curso na OSR. Haviam pessoas de várias idades. O mais novo tinha aproximadamente 25 anos enquanto que o mais velho tinha os cabelos completamente brancos. Imagino que a idade média da classe ficava perto dos 40 anos. Me conforta a certeza de que nesta área de desenvolvimento, não corremos o risco de ser substituídos pela garotada que acabou de sair da universidade simplesmente por menores preços de mão de obra. Fala a verdade! Quantos programadores de 40 anos você conhece? Eu disse programadores, não gerentes. Pois é, felizmente o desenvolvimento de drivers ainda vai exigir muito arroz com feijão dessa galera.

    Pasmem, uma mulher (aproximadamente 43 anos e parecia mulher mesmo!) também fazia parte do grupo. Não é machismo. Vocês sabem o quanto é incomum ver uma mulher (que não parece homem) programando, e programando Kernel ainda por cima. Outro indivíduo desta espécie rara é Molly Brown, programadora responsável pelo NT Cache Manager. Confira aqui.

    Como era de se esperar, o ritmo do seminário foi acelerado. Afinal de contas o conteúdo é bem denso. Tivemos uma visão geral de IoManager, memória virtual, multi-thread, multi-processamento e Object Manager só para começar. Esses assuntos ainda fazem parte apenas do sistema operacional e não específicamente de FSD. Começamos a ver o conteúdo de File System própriamente dito lá pela metade do segundo dia. Todo o conteúdo foi muito bem distribuído, e para cada ponto explicado, recebíamos os comentários focando no desenvolvimento de FSD, FSF, e Redirectors. Interessante ver como cada ponto era discutido com direito aos comentários sobre as diferenças de implementação entre as diversas versões do Windows.

    Em nosso grupo havia pelo menos uma pessoa que iria desenvolver um Redirector, outra que iria desenvolver um File System, mas o público em geral aprimorava seus conhecimentos para desenvolver anti-virus, anti-malware, controle de acesso e até sistemas de replicação de dados. Todos implementados como FSF.

    Replicação de dados? Não seria melhor desenvolver replicação de dados com um filtros de disco? É, eu também acho. A interface de File System é muito mais complexa que a interface de discos, mas vai explicar isso pro fulano lá. Meus sinceros votos de boa sorte a ele.

    Vou dizer que o nível da turma era médio. Bom, eu já lí o livro do Rajeev algumas vezes e já tive a oportunidade de trabalhar com FSFs. Eu estava ciente que não ia desperdiçar esta oportunidade de estar lá aprendendo coisas que um livro poderia me ensinar, mas não foi bem assim que uma parte do grupo pensou. Talvez eles foram pensando que seria fácil. Era engraçado quando tínhamos nossos Coffe-Breaks e eles comentavam que o assunto era muito profundo e com muitos detalhes. Sem dúvida que é! Alguns deles nunca tiveram contato com FSD antes. Posso dizer que aproveitou bem o curso quem já teve contato com o assunto e pôde desfrutar dos detalhes de interação entre o FSD e outros componentes do sistema como o Cache Manager, Memory Manager ou mesmo o Lan Manager Server. Teve gente que ficou meio que travando nos comentários de IoMarkIrpPending e Completion Routines. Fala sério! Não que este assunto seja trivial, mas precisava deixar para aprender isso em um seminário de FSD?

    Tinha gente muito boa lá também. Perguntas e discussões faziam Tony abrir os fontes do FASTFAT, CDFS ou do RDR para mostrar como as coisas são implementadas no mundo real. E eu achei que era só a referência que considerava fonte de exemplo como parte da documentação. Você já procurou alguma informação sobre IRP_MN_MDL na referência? Bom, consegui encontrar uma pista na parte que fala sobre IRP_MJ_READ, onde eles listam esta entre outras opções e dizem:

    “For more information about handling this IRP, study the CDFS and FASTFAT samples that are included in the Windows Driver Kit (WDK).”

    Segundo Tony, desenvolver um FSD requer muito conhecimento e experiência, mas ao contrário que muitos pensam, desenvolver filtros é ainda pior. Todos começam com a falsa impressão de que é necessário alterar apenas uma pequena parte do filtro de exemplo do WDK (SFilter) que já estaria tudo bem. Mas um efeito colateral acaba exigindo mais uma alteração aqui, outra alí, e quando se percebe, temos uma complexidade maior que a de um projeto de FSD. Um argumento forte que ele usou foi que quando você implementa um FSD, você sabe de tudo que acontece, mas quando você desenvolve um filtro, você está na interface de duas caixas pretas, o Sistema Operacional e o FSD, e qualquer alteração de comportamento gerada pelo seu filtro pode gerar as mais estranhas, intrigantes e inesperadas situações a serem depuradas em ambas as caixas pretas.

    A OSR nos forneceu um fichário com todos os PPT’s utilizados no seminário, também nos deu uma maleta da OSR, uma edição da NT Insider e para a surpresa de alguns, uma mola maluca. Uma mola maluca? Bom, resumidamente é uma mola, só que maluca.

    Se quiser saber mais sobre File System Drivers, uma excelente fonte de informação são as listas de discussões da OSR. Vale a pena dar uma olhada. Obviamente vou comentar mais sobre este assunto por aqui.

    Até a próxima. 🙂

  • Quem disse que seria fácil?

    Pois é meninos e meninas, o tempo parece ficar cada vez mais escasso. O período de provas intermediárias do quarto ano de Engenharia da Computação faz o tempo simplesmente desaparecer. E como se não fosse suficiente, ainda insisto em pegar alguns trabalhos extras para fazer em casa. O que posso fazer? Alguém precisa fazer o trabalho sujo. Como alguns vocês já sabem, em meus períodos de prova, meus posts ficam bem pobres e chego até a comentar sobre coisas que não tem muito (ou quase nada) a ver com o desenvolvimento de drivers para Windows. Assim como foi meu último post Off-Topic. O mais engraçado é que este post, que fala sobre minhas aventuras programando uma calculadora HP 50G, é uma das páginas mais visitadas deste blog. O que não é de se espantar. Contando comigo, quantos brasileiros você conhece que programa drivers para Windows? Se você conseguiu utilizar todos os dedos de uma mão, parabéns e me apresente alguns dos seus amigos. Eu trabalho com isso a anos e quase não consigo encher uma das mãos (contando com as pessoas que não trabalham mais com isso).

    Neste post vou trazer um preview do que vêm por aí nos próximos posts. Minha semana de provas acaba nesta sexta-feira próxima, e depois disso, imagino poder respirar um pouco e poder atormentar suas vidas com esse assunto que ninguém dá a mínima.

    Mas hardware que é bom…

    Dos posts escritos até agora, ainda não comentei nada sobre escrever drivers que de fato façam alguma manipulação com hardware. Isso mesmo, interrupções, port I/O e por aí vai. Mesmo por quê o Brasil não é lá um grande produtor de hardware. Nas empresas que trabalhei até agora, somente na primeira delas eu tive que controlar um hardware proprietário que mantinha uma rede de coletores de dados. No restante das empresas, apenas utilizei os privilégios do Kernel Mode para complementar alguma solução em segurança. Filtros de File System para controle de acesso, fitros de rede, hooks de APIs nativas, filtros de teclado e mouse, filtros de disco, criptografia em tempo real e blá blá blá… Mas e o hardware? Talvez o maior obstáculo para desenvolver um driver para hardware seja a necessidade de ter de fato um hardware para controlar. Para quem não sabe, a OSR vende kits de treinamento focando nessa necessidade. Estes kits trazem placas, circuitos, fios, cabos, chips… Uhuuu!!! Enfim, hardware. Para os interessados, dêem uma olhada no site da OSR Online Store na sessão Hardware for Learning que oferece inicialmente dois kits. Bom, pelo menos oferecia. Enquanto eu escrevia este post, um dos kits foi retirado do site. Um kit é uma placa PCI com I/O Digital e outra é uma placa USB. Semana passada, finalmente chegaram os kits de treinamento que encomendei, e que baseados neles, pretendo construir alguns exemplos básicos a fim de compartilhar com vocês.

    Curso de drivers para Windows em São Paulo

    O principal objetivo destes kits de treinamento é servir de exemplo prático em um curso introdutório de desenvolvimento em Kernel Mode para Windows em uma universidade aqui de São Paulo. Neste último mês, fui convidado por esta universidade para dar um curso sobre este assunto tão pouco explorado aqui no Brasil (Ainda não encontrei outro site dedicado a desenvolvimento de drivers para Windows em português). O curso ainda está sendo preparado e ainda não tem data de início. Além do que se espera de um curso deste tipo, que normalmente é uma pessoa falando apoiada em kilos de PPTs, pretendo oferecer um diferencial relevante neste curso. Quero dar a oportunidade dos alunos poderem aprender o suficiente para poderem desenvolver seu próprio driver, que irá controlar uma placa de verdade. Sabe como é, um curso com tanto conteúdo sem a parte prática seria como aprender sobre física nuclear, onde vemos toneladas de teoria e as vezes não conseguimos imaginar como isso seria aplicado na vida real. Os exemplos e PPTs que forem criados para o curso serão disponibilizados aqui. Obviamente haverão situações e dúvidas interessantes que merecerão posts por aqui.

    Treinamento File System OSR

    E por falar em cursos e treinamentos, chegou a minha vez. No próximo sábado, dia 14 de abril, estarei embarcando para Boston para participar de um seminário de Desenvolvimento de File System para Windows promovido pela OSR. Serão quatro dias com toneladas de teoria sobre o assunto. Pretendo dar uma visão de como o treinamento é organizado e qual o rítmo que o conteúdo é apresentado. Afinal de contas, esse assunto né brinquedo não.

    Engenharia da Computação

    Por último e não menos importante está a influência do que estou vendo na faculdade sendo refletida aqui. Já comentei esse assunto com amigos e vejo que vai ser inevitável. O estudo de VHDL, microeletrônica, processadores e microcontroladores vai acabar se manifestando aqui na forma de posts Off-Topic de gelo. Talvez o assunto não seja completamente Off-Topic, já que software de baixo nível é um assunto que está intimamente ligado a hardware. De qualquer forma, prometo me controlar.

    Bom, depois de tanta coisa, acho que fica mais fácil para vocês compreenderem minha falta de tempo para dar as caras por aqui. Mas não se preocupem discípulos do DDK, esse blog já me trouxe muita coisa legal e não pretendo parar de publicar assim de uma hora para outra. Até a próxima…

  • Sincronismo x Performance

    Não é à toa que meus últimos posts estão trazendo assuntos referentes a listas ligadas e performance. Nas últimas semanas, fui contratado por uma empresa de segurança para dar uma olhada em um de seus filtros de File System, a fim de diminuir o atraso causado por eles. Neste post vou falar sobre sincronismo e contenção de CPU.

    Acho que não deve ser novidade para muitos aqui que uma lista ligada, ou qualquer outro recurso, quando compartilhado entre várias threads, deve implementar algum sincronismo de acesso, evitando assim, que uma thread leia dados inválidos em conseqüência de uma alteração realizada por outra thread.

    Mas eu não tenho dois processadores, para quê sincronismo?

    É verdade que computadores com um processador, em um dado instante, executam apenas uma thread. Assim, nunca teremos duas threads sendo executadas ao mesmo tempo. Mas é importante lembrar que threads são executadas em pequenas fatias de tempo que são determinadas pelo scheduler, levando em consideração o tamanho do quantum e a prioridade de cada thread. Existem meios de evitar que uma thread seja interrompida pelo scheduler do Windows, mas em condições normais de temperatura e pressão, não sabemos quando uma thread será interrompida para que outra passe a ser executada.

    Suponha que a thread A esteja varrendo uma lista à procura de um determinado nó. Esta thread chega ao registro R e é interrompida para que a thread B possa ser executada. A thread B remove o mesmo registro R da lista e o desaloca da RAM. Quando a thread A é retomada e consulta os campos do finado registro R, esta acessará dados inválidos e tornará os passos seguintes imprevisíveis. Se bem que é bem previsível que algo azul deva acontecer.

    Existem vários mecanismos de sincronismo que podemos utilizar, mas neste post, vou comentar especificamente sobre os envolvidos nestas semanas. Mas além destes, sou obrigado a falar sobre o mais exótico que já vi em anos de experiência, que foi por mim apelidado de “Mutex, pero no mutcho”. Esta era uma classe derivada da classe VMutex do VToolsD. O método enter() desta classe tentava insanamente adquirir o mutex algumas milhares de vezes em um loop. Se depois destas milhares de interações, o mutex ainda não fosse adquirido, então a thread se dava por satisfeita e acessava o recurso de qualquer maneira. Será que o sistema estava tendo algum problema de Dead Lock? De qualquer forma, foi um prazer corrigir isso anos atrás. Tem coisas que a gente só acredita vendo.

    Conforme eu já comentei num outro post, filtro de File System é uma camada posta sobre os drivers FASTFAT, NTFS, CDFS, Network Redirectors e quaisquer outros drivers que implementem a interface de sistema de arquivos. Ou seja, todas as operações referentes a arquivos, inclusive os acessos com File Mapping passam pelos filtros de File System.

    Se você não está só interessado em saber a respeito de filtros de File System, e sim interessado em trabalhar com eles, então você tem a obrigação de ler o Windows NT File System Internals de Rajeev Nagar. Este livro é a única referência reconhecidamente abrangente o suficiente sobre este assunto. Publicado inicialmente em 1997 pela O’Reilly, este livro ainda é a ferramenta obrigatória para o desenvolvimento referente a File Systems mesmo para o Windows Vista. Sua publicação foi interrompida durante alguns anos, e durante essa época, eu mesmo cheguei a ver o preço em mais de U$ 200.00 por único um exemplar usado na Amazon. Hoje a OSR detém os direitos do livro e atualmente está trabalhando em uma edição atualizada, que deve trazer assuntos tais como o Shadow Copy, o NTFS transacional, Filter Manager, Mini-Redirectors e a desmontagem forçada de volumes. Entretanto este é um trabalho que ainda levará algum tempo, então em 2005, a versão original foi reimpressa para suprir essa necessidade enquanto a nova edição é composta.

    O filtro que tenho trabalhado mantem várias listas que devem ser consultadas a cada acesso interceptado. O mecanismo utilizado para sincronizar o acesso a estas listas era o Spin Lock. Uma escolha óbvia, mas inadequada para este cenário, onde a atividade é muito intensa. O grupo de listas que é consultado na IRP_MJ_READ é o mesmo utilizado na IRP_MJ_WRITE e em outros eventos. O resultado disso é a contenção de CPU. Ou seja, quando uma thread adquire um Spin Lock para realizar uma consulta na lista, todas as outras threads que precisam consultar a mesma lista precisam sentar e esperar até que o Spin Lock seja liberado (mesmo em casos onde temos mais de um processador). Sabendo que estas listas não são pequenas, adivinha se ficava lento…

    Assim como a fome mundial, o câncer de colo do útero, o exame de próstata e outros males que atingem a humanidade, algo devia ser feito a respeito. Por isso, numa incansável luta contra as forças do mal foi criado o ERESOURCE. (Cantos angelicais e uma névoa de gelo seco se dissipa no chão)

    Utilizando o ERESOURCE

    ERESOURCE é o meio mais adequado e nativamente utilizado para sincronizar acessos às estruturas que fazem parte de sistemas de arquivos, tais como o FCB (File Control Block), que além de outras informações, mantém o tamanho atual do arquivo. Com o ERESOURCE, várias threads podem ter acesso à mesma lista ligada ao mesmo tempo para leitura. Admitindo que nenhuma das threads vai alterar dados da lista, então todos os acessos poderiam ser realizados simultaneamente. Em contrapartida, se necessário for, uma thread pode ganhar acesso exclusivo à lista a fim de fazer alguma alteração.

    Para utilizar o ERESOURSE, você precisa declarar uma variável do tipo ERESOURCE, que deve residir em memória não paginada e alinhada em 8 bytes, e inicializá-la utilizando a função ExInitializeResourceLite. Para ter acesso compartihado (somente leitura) às listas, você deve utilizar a função ExAcquireResourceSharedLite. Esta função verifica a existência de alguma thread com acesso exclusivo sobre o recurso controlado. Caso haja, a função pode, dependendo de um parâmetro, retornar falha ou aguardar até que o recurso seja liberado. Para ter acesso exclusivo, utilize a função ExAcquireResourceExclusiveLite, que análogamente verifica a existência de threads com acesso compartilhado sobre o recurso, e opcionalmente, aguarda até que todas as threads com acesso compartilhado liberem o recurso para que o acesso exclusivo seja dado. Finalmente, para liberar o acesso adquirido, seja exclusivo ou compartilhado, utilize a função ExReleaseResourceLite.

    Um ponto interessante a ser notado é que a entrega de Kernel APC precisa ser desabilitada para as threads que adquirirem o ERESOURCE. Não conheço o real motivo desta necessidade, mas no mínimo evita que a thread que detém o acesso ao recurso controlado seja terminada por uma outra thread, isso sabendo que o TerminateThread é implementado via Kernel APC. Desta forma, a maneira mais comum de utilizar essas funções é como demonstrado abaixo.

        //-f--> Obtem acesso compartilhado (read only)
        KeEnterCriticalRegion();
        ExAcquireResourceSharedLite(&m_Resource, TRUE);
     
        //-f--> Realiza consultas ao recurso
     
        //-f--> Libera o recurso
        ExReleaseResourceLite(&m_Resource);
        KeLeaveCriticalRegion();

    Um ponto negativo é que a maioria das funções que lidam com ERESOURCE, diferente dos Spin Locks, devem ser chamadas em IRQL < DISPATCH_LEVEL. Desta forma, talvez sejam necessárias algumas manobras com as IRPs e suas CompletionsRoutines para lidar com isso.

    Feitas as modificações, o sistema ganhou muito em performance nos testes que fiz. O ganho será ainda maior à medida que tivermos mais operações em paralelo, e obviamente, em micros com mais de um processador.

    E todos viveram felizes para sempre…

  • Listas ligadas no DDK

    Desde o início dos meus estudos, sempre optei por estudar algo que unisse informática com eletrônica, e isso me levou a escolher o finado curso de Informática Industrial na ETE Jorge Street. Uma excelente escola e aprendi muito por lá. Entretanto só fui aprender o que é lista ligada em um ambiente profissional durante meu estágio. Anos mais tarde, a universidade me apresentou estas listas em forma de classes escritas em Java, onde lidamos com referências e o Garbage Collector. Mesmo quem trabalha com Visual C/C++, lida com classes e templates oferecidas pela MFC, ATL, WTL e STL, que acabam abstraindo a real implementação da lista ligada. Neste post vou falar um pouco sobre os recursos oferecidos pelo DDK em relação a este assunto.

    Mas já sou grandinho e sei construir listas ligadas em C/C++. Por que eu deveria utilizar os recursos do DDK?

    Se você vai apenas armazenar dados particulares, tais como uma lista de buffers a serem enviados para o dispositivo, então tudo bem, mas para lidar com estruturas do DDK, seria no mínimo interessante saber como elas são armazenadas em listas. Algumas situações exigem que você saiba utilizar as listas do DDK. Um exemplo disso é: Se você implementar o controle personalizado da fila de IRPs do seu driver, o campo pIrp->Tail.Overlay.ListEntry estará livre para ser utilizado enquanto tal IRP é mantida pelo seu driver.

    O mais simples destes recursos é a estrutura SINGLE_LIST_ENTRY que é definida no ntdef.h como exibido abaixo:

    typedef struct _SINGLE_LIST_ENTRY {
      struct _SINGLE_LIST_ENTRY *Next;
    } SINGLE_LIST_ENTRY, *PSINGLE_LIST_ENTRY;

    Uma variável do tipo SINGLE_LIST_ENTRY deve ser definida para ser a ponta de nossa lista. Esta variável deveria ter seu membro Next incializado com NULL antes de ser utilizada. As funções PushEntryList e PopEntryList são utilizadas respectivamente para adicionar e remover elementos da ponta da lista. Como a maioria das listas ligadas, os nós são referenciados pelo membro Next até que este seja NULL, indicando assim, o fim da cadeia de nós.

    VOID 
      PushEntryList(
        IN PSINGLE_LIST_ENTRY  ListHead,
        IN PSINGLE_LIST_ENTRY  Entry
        );
     
    PSINGLE_LIST_ENTRY 
      PopEntryList(
        IN PSINGLE_LIST_ENTRY  ListHead
        );

    Mas espere um pouco. Tudo isso é lindo, mas não está faltando nada? Como em qualquer estrutura de lista ligada, entre os membros de cada nó é preciso haver um que aponte para uma estrutura de mesmo tipo, que será o elo para o próximo nó da lista. No exemplo abaixo, o membro pNext é o responsável por isso.

    //-f--> Estrutura que define o nó da lista
    //      que será utilizada como exemplo
    typedef struct _MY_NODE
    {
        //-f--> Dados do seu driver definidos
        //      por você.
        UNICODE_STRING      usSomeData;
        ULONG               ulAnotherData;
        struct _MY_NODE     *pNext;
        PVOID               pMoreData;
     
    } MY_NODE, *PMY_NODE;

    Mas pelo que pudemos ver na estrutura do DDK, não existem membros úteis, ou seja, membros que contenham as informações que nos interessa armazenar, como os campos usSomeData ou pMoreData. Na estrutura oferecida pelo DDK, temos apenas o endereço do nó seguinte.

    Qual o interesse em guardar somente os nós?

    Na verdade, a estrutura SINGLE_LIST_ENTRY, assim como outras estruturas de listas do DDK, devem ser utilizadas em conjunto com a macro CONTAINING_RECORD. Esta macro nos retorna o endereço base da estrutura que tem um campo conhecido em um endereço conhecido. Bom, eu tentei reescrever esta frase umas três vezes, mas acho que você só irá entender quando ver o exemplo abaixo. Suponha que estamos utilizando o SINGLE_LIST_ENTRY em nosso exemplo anterior. Desta forma teríamos a seguinte estrutura:

    //-f--> Estrutura que define o nó da lista
    //      que será utilizada como exemplo
    typedef struct _MY_NODE
    {
        //-f--> Dados do seu driver definidos
        //      por você.
        UNICODE_STRING      usSomeData;
        ULONG               ulAnotherData;
     
        //-f--> Este membro não precisa ser necessariamente
        //      o primeiro ou o ultimo, ele pode estar em
        //      qualquer posição da sua estrutura.
        SINGLE_LIST_ENTRY   Entry;
     
        //-f--> Mais dados
        PVOID               pMoreData;
     
    } MY_NODE, *PMY_NODE;

    Nossa estrutura é basicamente a mesma, com exceção do membro pNext que agora foi mudado para utilizar a estrutura SINGLE_LIST_ENTRY. Reparem que o campo Entry não precisa estar nem no início e nem no final dos membros da nossa estrutura. O exemplo abaixo demonstra como incluir nós em listas formadas com SINGLE_LIST_ENTRY.

    NTSTATUS PrepareDataAndPush(VOID)
    {
        PMY_NODE            pMyNode;
     
        //-f--> Aqui obtemos o nó já alocado e com
        //      os campos devidamente preenchidos
        pMyNode = AllocateAndFillNode();
     
        //-f--> Testar nunca é demais
        ASSERT(pMyNode != NULL);
     
        //-f--> Para colocar o nó na lista, é necessário passar
        //      o endereço do membro do tipo SINGLE_LIST_ENTRY
        PushEntryList(&m_ListHead, &pMyNode->Entry);
     
        return STATUS_SUCCESS;
    }

    Na hora de incluir o nó na lista, devemos passar o endereço do membro do tipo SINGLE_LIST_ENTRY, como sugere o protótipo da função PushEntryList. Acompanhe o exemplo abaixo que demonstra como obter este nó utilizando a função PopEntryList. Esta função retorna o mesmo endereço passado na função PushEntryList, que é um PSINGLE_LIST_ENTRY. A partir deste endereço, conforme eu já havia comentado, podemos obter o endereço base de nossa estrutura utilizando a macro CONTAINING_RECORD. Veja o exemplo abaixo.

    NTSTATUS PopAndProcessNode(VOID)
    {
        PSINGLE_LIST_ENTRY  pEntry;
        PMY_NODE            pMyNode;
     
        //-f--> Aqui obtemos o nó da lista
        pEntry = PopEntryList(&m_ListHead);
     
        //-f--> Vamos verificar se realmente havia algo
        //      armazenado na lista
        if (!pEntry)
            return STATUS_NO_MORE_ENTRIES;
     
        //-f--> Agora vamos utilizar a macro CONTAINING_RECORD
        //      para obter o endereço base que precisamos
        pMyNode = (PMY_NODE) CONTAINING_RECORD(pEntry, MY_NODE, Entry);
     
        //-f--> Agora temos acesso a toda a estrutura
        ProcessAndFreeNode(pMyNode);
     
        return STATUS_SUCCESS;
    }

    Este mesmo estilo também é utilizado com a estrutura LIST_ENTRY, que é a estrutura de lista mais utilizada em drivers. Esta estrutura permite criar listas duplamente ligadas, e assim como o SINGLE_LIST_ENTRY, uma variável do tipo LIST_ENTRY é definida para ser a ponta de nossa lista. Sua inicialização é feita utilizando a função InitializeListHead, que inicializa os membros Blink e Flink com o endereço da ponta da lista.

    typedef struct _LIST_ENTRY {
      struct _LIST_ENTRY *Flink;
      struct _LIST_ENTRY *Blink;
    } LIST_ENTRY, *PLIST_ENTRY;

    O membro Flink aponta para o nó seguinte, enquanto que o membro Blink aponta para o nó anterior. Diferente da maioria das listas ligadas que conhecemos, suas pontas não são marcadas por NULL nos membros Blink ou Flink. Ao invés disso, suas pontas apontam para o endereço do nó designado como ponta de nossa lista. O exemplo abaixo demonstra como poderiamos varrer este tipo de lista em procura de um certo nó. Antes de realizar qualquer operação com as listas formadas por LIST_ENTRY, utilizamos a função IsListEmpty.

    //-f--> Procura por um nó através de uma lista
    //      formada por estruturas LIST_ENTRY
    BOOLEAN SearchEntry(PUNICODE_STRING  pusLookFor)
    {
        PLIST_ENTRY pEntry;
        PMY_NODE    pMyNode;
     
        //-f--> Antes de qualquer operação, é necessário
        //      verificar se existem nós na lista. Caso
        if (IsListEmpty(&m_ListHead))
            return FALSE;
     
        //-f--> Obtem o endereço do primeiro nó
        pEntry = m_ListHead.Flink;
     
        //-f--> Enquando pEntry não apontar para a ponta
        //      da lista, significa que ainda existem nós
        while(pEntry != &m_ListHead)
        {
            //-f--> Obtem o endereço base da estrutura
            pMyNode = (PMY_NODE) CONTAINING_RECORD(pEntry, MY_NODE, Entry);
     
            //-f--> Verifica se é o nó que estamos procurando
            if (RtlEqualUnicodeString(pusLookFor,
                                      &pMyNode->usSomeData,
                                      TRUE))
            {
                //-f--> Retorna TRUE sinalizando que o nó foi encontrado
                return TRUE;
            }
     
            //-f--> Obtem o endereço do próximo nó
            pEntry = pEntry->Flink;
        }
     
        //-f--> Se chegamos aqui, significa que não encontramos
        //      o nó desejado.
        return FALSE;
    }

    NOTA: O exemplo acima serve apenas para ilustrar a busca por um nó. Para implementar uma busca como essa em um ambiente de produção, é necessário ter em mente questões de sincronismo de acesso por multiplas threads e controle de referências.

    Para manipular listas formadas com LIST_ENTRY são utilizadas as funções InsertHeadList, InsertTailList, RemoveHeadList e por aí vai. Não vou listar todas as funções aqui, tenho certeza que a referência do DDK faz isso melhor que eu.

    Para ambos os tipos de listas, existem as funções ExInterlockedXxxList que recebem um Spin Lock a fim de sincronizar as alterações da lista. Lembre-se que caso você sincronize o acesso às listas utilizando Spin Locks, todos os nós devem residir em memória não paginada. Como alguns de vocês já devem saber, ao adquirir um Spin Lock, a IRQL da thread vai para DISPATCH_LEVEL. Nesta IRQL, o Memory Manager é incapaz de recuperar uma página de dados que foi paginada para disco, resultando em um BugCheck.

    É importante lembrar também que não se deve misturar os uso das funções ExInterlockedXxxList com as não sincronizadas. Se uma lista é acessada por várias threads, todos os acessos à mesma devem ser controlados.

    typedef struct _SLIST_ENTRY {
      struct _SLIST_ENTRY *Next;
    } SLIST_ENTRY, *PSLIST_ENTRY;

    A estrutura SLIST_ENTRY é uma alternativa para as listas do tipo SINGLE_LIST_ENTRY com acesso sincronizado. Esta versão se propôe ser mais eficiente utilizando as funções ExInterlockedPopEntryList e ExInterlockedPushEntryList. Diferente das outras estruturas aqui apresentadas, a ponta da lista é formada por uma estrutura diferente da estrutura utilizada nos nós, Esta estrutura é a SLIST_HEADER, que é uma estrutura opaca e que deve ser inicializada pela função ExInitializeSListHead.

    Um outro diferencial deste tipo de lista é que o DDK oferece a função ExQueryDepthSList que retorna a quantidade de nós armazenados na lista.

    Se o seu principal problema é performance, então o ideal seria utilizar o grupo de funções que trabalham com as Generic Tables. A ponta da lista é definida por uma estrutura opaca chamada RTL_GENERIC_TABLE. Esta estrutura juntamente com as funções de manipulação, tais como RtlGetElementGenericTable, criam arvores binárias com auto balanceamento. Que chique hein?

    Mas Generic Tables é um assunto a parte e que merece um post dedicado. Até a próxima!

  • Legal, mas o que é uma IRP?

    Meu amigo Lesma, assim que leu o título do meu último post, deu uma risada e disse que a maioria das pessoas que lessem isso perguntariam: “O que é uma IRP?”. Bom, pensando no que ele disse e levando em consideração a explicação nada simplificada que a referência do DDK nos oferece, vou dar uma descrição superficial da IRP, livrar minha consciência deste peso e poder dormir novamente.


    Vamos deixar de lado os assustadores e horripilantes diagramas do DDK para tentar ver as coisas de uma maneira um pouco mais simples. Depois disso, você poderá recorrer à documentação sagrada para reforçar os conceitos e tirar eventuais dúvidas sobre algo que você já saiba o que é, ou que pelo menos imagina.

    Vamos nos basear em um exemplo bem prático. Os drivers de File Systems por exemplo. O NTFS e o FAT são implementados como drivers de Kernel que recebem o nome File Systems Drivers. Imagino que muitos de vocês já tiveram a oportunidade de abrir e escrever em um arquivo.

    Para começar a conversar com um driver, temos inicialmente que criar uma conexão com ele, e isso é feito utilizando a função CreateFile. Do contrário que parece, essa função não é restrita à criação de arquivos, na verdade, o fato de abrir um arquivo é uma maneira específica de se abrir a conexão com o driver de File System (Detalhes sobre isso em uma próxima vez). A função CreateFile vai nos retornar um handle que será utilizado para interagir com o driver através de funções tais como ReadFile, WriteFile e DeviceIoControl, que por sinal, também não são restritas à operações com arquivos. Quando uma aplicação chama a função ReadFile o subsystema Win32 encaminha esta solicitação para a API nativa NtReadFile, que por sua vez faz a transição para Kernel Mode e chama o IoManager, este vai empacotar os parâmetros desta solicicação em uma estrutura chamada IRP (I/O Request Packet).

    O IoManager envia este pacote para o driver responsável passando pelos filtros que houverem instalados sobre ele. Um driver de anti-vírus é um exemplo perfeito para o senário que estamos falando aqui. Drivers de anti-vírus são implementados como File System Filters. Quando alguma aplicação escreve em um arquivo, a IRP de escrita passa pelo anti-vírus antes de chegar ao driver de File System, dando a oportunidade que o anti-vírus precisa para verificar se o que está sendo gravado contém a assinatura de algum vírus conhecido. No caso de uma leitura, a IRP passa pelo anti-vírus que instala uma CompletionRoutine nela, e desta forma ganha acesso aos dados quando a leitura for finalizada pelo driver final.

    A IRP é basicamente dividida em duas partes, sendo elas o Header e as Stack Locations. No Header temos informações gerais da IRP, tais como status, ponteiro para a thread à qual esta IRP pertence, endereços dos buffers do usuário, endereço da rotina de cancelamento desta IRP e por aí vai. Nas Stack Locations estão os parâmetros específicos da solicitação. No caso de uma leitura de arquivo, teremos o offset, o tamanho da leitura e o alinhamento.

    Dentro de uma IRP podem haver várias Stack Locations. Uma para cada device pertencente à cadeia de camadas que segue até chegar ao driver destino. Em outras palavras, seria uma Stack Location para o driver destino mais uma para cada filtro que estiver instalado sobre ele. A medida que a IRP vai descendo as camadas, cada driver repassa os parâmetros recebidos em sua Stack Location para a próxima, utilizando a função IoCopyCurrentIrpStackLocationToNext. Depois de copiados, esta camada pode fazer as alterações desejadas. Se não houver nenhuma alteração a ser feita nos parâmetros de uma camada para a outra, então pode-se utilizar a função IoSkipCurrentIrpStackLocation.

    As IRPs vão de uma camada para outra quando o filtro que a recebeu a encaminha para o device o qual está atachado, utilizando a função IoCallDriver. Quando esta IRP chega ao driver destino, o driver tem basicamente duas opções. Se a solicitação puder ser atendida imediatamente, o driver toma a ação desejada e finaliza a IRP utilizando a função IoCompleteRequest. Mas se for necessário comunicar com um dispositivo ou mesmo com outro driver, a IRP é posta em uma fila, marcada como pendente e só será finalizada quando todo processamento for feito.

    Voltando ao nosso exemplo, quando um driver de File System recebe uma IRP de escrita e dependendo de outras tantas condições, este driver coloca a IRP atual como pendente e cria uma nova IRP para os devices de Volume, onde estão as partições, que por sua vêz repassam solicitações para os devices de Storage. Desta forma fica fácil entender que drivers de disco não entendem nada de NTFS ou FAT. Drivers de storage podem possuir outros filtros de storage, como um RAID para espelhamento de discos entre outras coisas. Estas novas IRPs também são criadas pelo IoManager e todo o ciclo é refeito para cada nova solicitação.

    Visualizando IRPs

    O IrpTracker é uma ferramenta capaz de monitorar a atividade de IRPs de um determinado driver ou device. Veja a figura abaixo onde estou selecionando todo os devices do driver Kbdclass, responsável pela leitura de teclado. Não repare, minha máquina tem mesmo muitos teclados. Estive trabalhando em soluções anti key loggers atualmente. Selecionando todos os estes devices ficará bem visível o uso das IRPs para buscar as teclas que você pressiona.


    Para cada tecla que você bate no teclado, são emitidas quatro linhas no IrpTracker. Sendo que cada tecla batida representa dois movimentos, o de descida da tecla e o de liberação da mesma. Cada movimento é tratado com uma IRP que vem acompanhada de sua Completion, ou seja, uma linha é identificada como Call e outra como Comp, que representam respectivamente o envio da IRP e seu retorno. Reparem que neste exemplo, a linha Comp sempre vem antes da linha Call.

    Isso significa que a IRP teve sua completion antes de ser enviada para o driver do teclado?

    Na verdade, a IRP de leitura de teclado é do tipo que fica pendente até que uma tecla seja recebida. Assim, o sistema envia uma IRP para o driver que fica aguardando eventos do teclado. Quando este evento acontece, o driver recebe a tecla, completa a IRP e o sistema recebe sua completion. Logo em seguida o sistema lança uma nova IRP que aguardará o próximo evento. Desta forma teremos sempre pares linhas, sendo elas a Comp da IRP anterior e a Call da próxima IRP.

    Se você der um duplo clique em uma destas linhas, será possível ver os detalhes da cada campo da IRP como é exibido na figura abaixo.


    Estou preparando uma evolução do driver Useless.sys citado como ponto de partida em um post anterior. Essa evolução vai permitir que ele não seja tão Useless e que exemplifique o tratamento de IRPs na prática. Mas isso vai ficar para uma outra vez.

    Até lá…