Archive for the ‘Kernel Development’ Category

Lendo Arquivos

1 de April de 2009

Como vocês viram em meu último post, meu trabalho de graduação utilizará uma ferramenta chamada LabView para receber e tratar os dados de uma placa USB. Estes dados serão coletados de um dispositivo chamado giroscópio usando TTL 232, que é um RS 232 com tensões de 0 e 5 volts. Mas como uma prova de conceito, teríamos que fazer o driver simular a recepção de dados para enviar ao LabView. Utilizamos um circuito que transforma TTL 232 em RS 232 apenas para permitir que os dados pudessem ser lidos de uma porta serial convencional. Então fiz um programinha idiota que grava em um arquivo tudo que recebe pela porta serial. Como o firmware ainda não estava nem começado, resolvi fazer um driver que lesse esse arquivo e repassasse os dados para a camada de aplicação. Perguntas sobre como manipular arquivos são especialmente frequentes. Muitos leitores gostariam de saber como criar, ler, escrever e até mesmo apagar arquivos em Kernel Mode. Talvez eu possa desapontá-los um pouco ao dizer que não é assim tão diferente de User Mode, mas já que estamos aqui sem fazer nada, por que não demonstrar?

Acredito que a maior diferença esteja no passo onde obtemos o handle para o arquivo. Vamos começar dando uma olhada na rotina ZwCreateFile().

NTSTATUS  
  ZwCreateFile(
    OUT PHANDLE  FileHandle,
    IN ACCESS_MASK  DesiredAccess,
    IN POBJECT_ATTRIBUTES  ObjectAttributes,
    OUT PIO_STATUS_BLOCK  IoStatusBlock,
    IN PLARGE_INTEGER  AllocationSize  OPTIONAL,
    IN ULONG  FileAttributes,
    IN ULONG  ShareAccess,
    IN ULONG  CreateDisposition,
    IN ULONG  CreateOptions,
    IN PVOID  EaBuffer  OPTIONAL,
    IN ULONG  EaLength
    );

A parte interessante desse passo é que a rotina não tem o clássico parâmetro FileName que vimos na API equivalente CreateFile() para User Mode. O nome do arquivo é descrito na estrutura OBJECT_ATTRIBUTES descrita abaixo.

typedef struct _OBJECT_ATTRIBUTES {
    ULONG  Length;
    HANDLE  RootDirectory;
    PUNICODE_STRING  ObjectName;
    ULONG  Attributes;
    PVOID  SecurityDescriptor;
    PVOID  SecurityQualityOfService;
 
} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES;
 
typedef CONST OBJECT_ATTRIBUTES *PCOBJECT_ATTRIBUTES;

Para preencher esta estrutura utilizamos a macro InitializeObjectAttributes().

VOID 
  InitializeObjectAttributes(
    OUT POBJECT_ATTRIBUTES  InitializedAttributes,
    IN PUNICODE_STRING  ObjectName,
    IN ULONG  Attributes,
    IN HANDLE  RootDirectory,
    IN PSECURITY_DESCRIPTOR  SecurityDescriptor
    );

O caminho do arquivo é descrito no parâmetro ObjectName, que é um ponteiro para uma UNICODE_STRING. Em Kernel, o caminho completo para um arquivo seria descrito como “\Device\HarddiskVolume0\Diretorio\Arquivo.ext” por exemplo. Isso acontece porque a parte “C:”, que normalmente utilizamos no caminho de arquivo em User Mode, é um Symbolic Link. Symbolic quem? Um Symbolic Link seria como um atalho para o nome em Kernel Mode. Aplicações em User Mode não podem abrir qualquer objeto do Kernel assim de cara. Então cada driver cria Symbolic Links para os objetos que desejam torná-los disponíveis para User Mode. Quando uma aplicação quer abrir o arquivo “C:\Temp\Test.txt”, o sub-sistema Win32 prefixa este caminho com “\??\”, que é o diretório inicial desta busca, resultando em “\??\C:\Temp\Test.txt”. Quando este nome chega ao Object Manager, o prefixo indica que a busca deve iniciar no diretório “\DosDevices”, o mesmo que utilizamos na chamada à API IoCreateSymbolicLink(). Enfim, pulando alguns detalhes para terminar esse post ainda nessa vida, o prefixo vai nos levar ao diretório “\GLOBAL??\”. Depois que o prefixo “\??” foi processado, a próxima parte a ser processada é “C:”. Utilizando a ferramenta WinObj da Systernals ilustrada na figura abaixo, vemos que aqui em minha máquina “C:” será substituído por “\Device\HarddiskVolume3”, que neste caso é o caminho para o device que receberá o restante da string a ser processada. Depois de realizada esta substituição, a string agora é “\Device\HarddiskVolume3\Temp\Test.txt”. O Object Manager agora recomeça a processar a string e encontra o device nela descrito.


Depois disso, sabendo que se trata de um device de volume de dados, o sistema consulta uma estrutura chamada Volume Parameter Block (VPB). Ela cria um link que vai nos informar se o volume indicado foi montado por algum driver de File System. No meu caso, o NTFS seria este driver. O device que ele criou faria o restante do tratamento da string para encontrar o arquivo desejado. Você não vai ter que percorrer todo esse caminho para abrir o arquivo. Basta colocar o prefixo “\??\” no caminho do arquivo que desejar abrir e todos os seus problemas se acabaram-se. Se você quiser mais detalhes sobre as traduções de nomes que ocorrem durante a abertura de um arquivo, este artigo da OSR Online é ótimo. Este é o link para a referência que fala sobre isso.

Depois de aberto, ler o arquivo fica fácil com a rotina ZwReadFile().

NTSTATUS 
  ZwReadFile(
    IN HANDLE  FileHandle,
    IN HANDLE  Event  OPTIONAL,
    IN PIO_APC_ROUTINE  ApcRoutine  OPTIONAL,
    IN PVOID  ApcContext  OPTIONAL,
    OUT PIO_STATUS_BLOCK  IoStatusBlock,
    OUT PVOID  Buffer,
    IN ULONG  Length,
    IN PLARGE_INTEGER  ByteOffset  OPTIONAL,
    IN PULONG  Key  OPTIONAL
    )

Os passos necessários para se abrir e ler um arquivo podem ser resumidos neste pequeno exemplo a seguir.

/****
***     ReadTestFile
**
**      Rotina que demostra de maneira simples como abrir e
**      ler um arquivo.
*/
 
NTSTATUS ReadTestFile(PVOID     pBuffer,
                      ULONG     cbBuffer,
                      PULONG    pulBytesRead)
{
    UNICODE_STRING      usFileName;
    OBJECT_ATTRIBUTES   ObjAttributes;
    IO_STATUS_BLOCK     IoStatusBlock;
    HANDLE              hFile = NULL;
    NTSTATUS            nts = STATUS_SUCCESS;
 
    //-f--> Montamos o UNICODE_STRING contendo o caminho
    //      do arquivo que desejamos abrir
    RtlInitUnicodeString(&usFileName,
                         L"\\??\\C:\\Temp\\Test.txt");
 
    //-f--> Aqui a macro nos ajuda com a estrutura
    //      OBJECT_ATTRIBUTES
    InitializeObjectAttributes(&ObjAttributes,
                               &usFileName,
                               OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE,
                               NULL,
                               NULL);
 
    //-f--> Aqui abrimos o arquivo.
    nts = ZwCreateFile(&hFile,
                       GENERIC_READ | SYNCHRONIZE,
                       &ObjAttributes,
                       &IoStatusBlock,
                       NULL,
                       FILE_ATTRIBUTE_NORMAL,
                       FILE_SHARE_READ,
                       FILE_OPEN,
                       FILE_SYNCHRONOUS_IO_NONALERT,
                       NULL,
                       0);
 
    //-f--> Retorna o erro em caso de falha.
    if (!NT_SUCCESS(nts))
        return nts;
 
    //-f--> Uma simples leitura no arquivo.
    nts = ZwReadFile(hFile,
                     NULL,
                     NULL,
                     NULL,
                     &IoStatusBlock,
                     Buffer,
                     cbBuffer,
                     NULL,
                     NULL);
 
    //-f--> Em caso de falha, fecha o arquivo, retorna o
    //      erro e finge que não é com você.
    if (!NT_SUCCESS(nts))
    {
        ZwClose(hFile);
        return nts;
    }
 
    //-f--> Aqui obtemos a quantidade de bytes lidos
    *pulBytesRead = IoStatusBlock.Information;
 
    //-f--> Fecha o handle do arquivo
    ZwClose(hFile);
    return nts;
}

A prova de conceito

Agora que todos nós sabemos como ler um arquivo, fica mais fácil de explicar como fiz um driver que simularia as leituras de um giroscópio apenas lendo o conteúdo de um arquivo. Durante a existência deste blog, já vimos como criar um projetinho do zero, como compilar drivers utilizando o Visual Studio, já vimos também o que é uma IRP, como oferecer serviços de leitura e escrita, como utilizar o FsContext para manter o contexto entre diferentes operações, e como debug é parte do desenvolvimento, também vimos como depurar drivers mesmo em uma máquina virtual. Vamos utilizar toda essa tranqueirada para montar um driver que abra um arquivo, armazene seu handle em uma área de contexto, e que conforme realizamos leituras ao device criado e exportado por ele, este retorne os dados de um arquivo em disco. Isso vai servir direitinho para simular leituras contínuas que o LabView fará ao meu driver USB.

Abrindo o Device e Arquivo

O driver receberá uma chamada de IRP_MJ_CREATE quando um handle para o device for aberto. Vou aproveitar esse evento para já abrir o arquivo e guardar seu handle resultante no FsContext do FILE_OBJECT que receberei. Se você está boiando, dê uma olhada nos posts indicados anteriormente.

E se o arquivo não existir?

Bom, caso tais eventos infelizes ocorram, vou retornar o erro pela própria IRP recebida. Assim, caso o arquivo não exista ou você não tenha permissão para abri-lo, o código de erro poderá ser verificado através da rotina GetLastError() caso obtivermos INVALID_HANDLE_VALUE como retorno da abertura do handle do device.

Dêem uma olhada como ficou a abertura do handle do device, que na mesma operação, abre o handle para o arquivo. Atenção, não misture as coisas. A aplicação vai obter o handle para o device, e através dele, fará leituras ao device. O device por sua vez utilizará o handle do arquivo para fazer leituras e retornar os dados à aplicação.

/****
***     OnCreate
**
**      A aplicação está chamado CreateFile com o path
**      do nosso device.
*/
 
NTSTATUS OnCreate(PDEVICE_OBJECT    pDeviceObj,
                  PIRP              pIrp)
{
    UNICODE_STRING      usFileName;
    OBJECT_ATTRIBUTES   ObjAttributes;
    IO_STATUS_BLOCK     IoStatusBlock;
    PIO_STACK_LOCATION  pStack;
    NTSTATUS            nts = STATUS_SUCCESS;
    HANDLE              hFile = NULL;
 
    //-f--> Montamos o UNICODE_STRING contendo o caminho
    //      do arquivo que desejamos abrir
    RtlInitUnicodeString(&usFileName,
                         L"\\??\\C:\\Temp\\Test.txt");
 
    //-f--> Aqui a macro nos ajuda com a estrutura
    //      OBJECT_ATTRIBUTES
    InitializeObjectAttributes(&ObjAttributes,
                               &usFileName,
                               OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE,
                               NULL,
                               NULL);
 
    //-f--> Aqui abrimos o arquivo. Como vamos repassar qualquer
    //      erro para a aplicação, então podemos utilizar a estrutura
    //      IO_STATUS_BLOCK da nossa IRP. Caso contrário poderiamos
    //      utilizar uma criada como variavel local.
    nts = ZwCreateFile(&hFile,
                       GENERIC_READ | SYNCHRONIZE,
                       &ObjAttributes,
                       &pIrp->IoStatus,
                       NULL,
                       FILE_ATTRIBUTE_NORMAL,
                       FILE_SHARE_READ,
                       FILE_OPEN,
                       FILE_SYNCHRONOUS_IO_NONALERT,
                       NULL,
                       0);
 
    //-f--> Vamos guardar o handle do arquivo em nossa área de
    //      contexto. Isso permite que várias aplicações de teste
    //      possam ser executas ao mesmo tempo. Para isso teremos
    //      obter a Stack Location atual.
    pStack = IoGetCurrentIrpStackLocation(pIrp);
    pStack->FileObject->FsContext = (PVOID)hFile;
 
    //-f--> Agora é só ler o arquivo, mas vamos fazer isso na IRP
    //      de leitura, só pra...
    IoCompleteRequest(pIrp,
                      IO_NO_INCREMENT);
    return nts;
}

Repare que a API ZwCreateFile() pede um ponteiro para IO_STATUS_BLOCK. Utilizei a mesma estrutura que está contida na IRP que recebemos. Assim eu não tenho que repassar o status de uma operação para a outra. A aplicação continua obtendo o handle para o device da mesma maneira como sempre foi feito, mas lembre-se que se houver uma falha nessa obtenção, o erro pode ter sido gerado por um problema ao abrir o handle do arquivo. Confira como a aplicação vai utilizar o driver.

/****
***     main
**
**      Ponto de entrada da aplicação
**
*/
 
int __cdecl main(int argc,
                 char* argv[])
{
    char    szBuffer[4096];
    HANDLE  hDevice = NULL;
    DWORD   dwError = ERROR_SUCCESS,
            dwBytes,
            i;
 
    //-f--> Obtendo um handle para o device
    printf("Opening the device \"\\\\.\\FileReader\"...\n");
 
    hDevice = CreateFile("\\\\.\\FileReader",
                         GENERIC_ALL,
                         0,
                         NULL,
                         OPEN_EXISTING,
                         0,
                         NULL);
 
    //-f--> Verifica se o handle foi aberto.
    if (hDevice == INVALID_HANDLE_VALUE)
    {
        //-f--> Ops!
        dwError = GetLastError();
        printf("Error #%d opening device...\n",
               dwError);
        return dwError;
    }
 
    //-f--> Realiza as leituras no device
    while (ReadFile(hDevice,
                    szBuffer,
                    sizeof(szBuffer),
                    &dwBytes,
                    NULL))
    {
        //-f--> Exibe dados na tela
        //      Tá tá, eu sei que não é a maneira mais eficiente do mundo.
        for (i = 0; i < dwBytes; i++)
            printf("%c", szBuffer[i]);
    }
 
    //-f--> Qualquer falha da chamada à função ZwReadFile é repassada para
    //      a estrutura IO_STATUS_BLOCK da IRP. Por isso é que vemos este
    //      erro aqui.
    if ((dwError = GetLastError()) != ERROR_NO_MORE_ITEMS)
        printf("\n\n Error #%d reading device...\n");
 
    //-f--> Põe a casa em ordem.
    printf("Closing device...\n");
    CloseHandle(hDevice);
 
    //-f--> Fim de festa! Chega!
    return dwError;
}

Lendo o Device e o Arquivo

Faremos a leitura do arquivo de forma similar à abertura. Vamos obter o handle do arquivo a partir do FsContext. Este ponteiro foi originalmente disponibilizado para que o driver pudesse armazenar nele o endereço de uma estrutura definida pelo desenvolvedor. Este ponteiro sempre será o mesmo para todas as operações que utilizam o mesmo FILE_OBJECT até que a operação de IRP_MJ_CLOSE seja chamada. Como um handle é algo muito pequeno, podemos gravar seu valor ao invés de um ponteiro para uma estrutura alocada em memória que contenha o valor do handle.

Aqui também vamos utilizar a repassagem da estrutura IO_STATUS_BLOCK para transferir o status da operação de leitura do arquivo para a aplicação.

/****
***     OnRead
**
**      Rotina que realiza leitura do arquivo já
**      aberto.
*/
 
NTSTATUS OnRead(PDEVICE_OBJECT    pDeviceObj,
                PIRP              pIrp)
{
    NTSTATUS            nts = STATUS_SUCCESS;
    PIO_STACK_LOCATION  pStack;
    HANDLE              hFile;
 
    //-f--> Obtemos a stack location atual
    pStack = IoGetCurrentIrpStackLocation(pIrp);
 
    //-f--> Aqui recuperamos o handle do arquivo.
    ASSERT(pStack->FileObject->FsContext != NULL);
    hFile = (HANDLE)pStack->FileObject->FsContext;
 
    //-f--> Uma simples leitura no arquivo.
    nts = ZwReadFile(hFile,
                     NULL,
                     NULL,
                     NULL,
                     &pIrp->IoStatus,
                     pIrp->AssociatedIrp.SystemBuffer,
                     pStack->Parameters.Read.Length,
                     NULL,
                     NULL);
 
    //-f--> STATUS_END_OF_FILE não é repassado para a camada aplicação
    //      como uma falha de leitura, então vamos usar um erro mais
    //      fácil de detectar.
    if (pIrp->IoStatus.Status == STATUS_END_OF_FILE)
        nts = pIrp->IoStatus.Status = STATUS_NO_MORE_ENTRIES;
 
    //-f--> Completa a IRP.
    IoCompleteRequest(pIrp,
                      IO_NO_INCREMENT);
    return nts;
}

Se nosso driver retornar STATUS_END_OF_FILE para a aplicação, a API ReadFile() não sinalizará uma falha, mas apenas informará que zero bytes foram lidos. Para facilitar a detecção do fim de arquivo lá na aplicação, vou retornar um código de erro diferente, assim a rotina ReadFile() retornará FALSE e o loop de leitura será interrompido.

Fechando o handle do Device e do Arquivo

Agora fica muito fácil. Vamos fechar o handle do arquivo quando o handle do device for fechado. Sem muitas novidades por aqui.

/****
***     OnCleanup
**
**      O handle para nosso device foi fechado. Vamos
**      aproveitar e fechar o handle do arquivo também.
*/
 
NTSTATUS OnCleanup(PDEVICE_OBJECT    pDeviceObj,
                   PIRP              pIrp)
{
    PIO_STACK_LOCATION  pStack;
    HANDLE              hFile;
 
    //-f--> Obtemos a stack location atual
    pStack = IoGetCurrentIrpStackLocation(pIrp);
 
    //-f--> Aqui recuperamos o handle do arquivo.
    ASSERT(pStack->FileObject->FsContext != NULL);
    hFile = (HANDLE)pStack->FileObject->FsContext;
 
    //-f--> Fecha o handle
    ZwClose(hFile);
 
    //-f--> Completa a IRP normalmente
    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = 0;
    IoCompleteRequest(pIrp,
                      IO_NO_INCREMENT);
 
    return STATUS_SUCCESS;
}

Se vocês já são leitores deste blog há algum tempo, verão que o restante do driver contém código elementar e que já foi comentado em outros posts. De qualquer forma, tanto o fonte do driver quando o fonte da aplicação de teste estão disponíveis para download. Caso vocês tenham alguma dúvida é só mandar um e-mail. Normalmente eu digo que vocês só precisarão torcer para eu saber a resposta, mas ultimamente vocês terão que torcer para eu também ter tempo de responder.

Como sempre, espero ter ajudado.
Have fun!

FileReader.zip

Gerenciando paginação do driver

18 de November de 2008

Depois de tanto falar sobre memória virtual e paginação, recebi uma pergunta que coincidentemente tem tudo a ver com o assunto dos últimos posts. “Para que servem os pragma alloc_text que vemos nos exemplos do WDK?” (Thiago Cardoso, Recife-PE). Essa pergunta já deve ter passado pela cabeça de muitos que já deram uma olhada nos exemplos do WDK. Visto que todos os exemplos do WDK que conheço utilizam este pragma, até que demorou para alguém perguntar sobre isso. Mas enfim, vamos ao que interessa.

Como já vimos, páginas de memória podem estar tanto nos chips de RAM como em disco. Também já vimos que threads que estão em alta prioridade de execução não podem acessar dados que são pagináveis. Mas como saberiamos quais dados são pagináveis ou não?

Controlando Paginação de Dados

Quando alocamos memória dinamicamente, podemos escolher se a área de memória a ser alocada será paginável ou não. A função ExAllocatePool, e suas irmãs (ExAllocatePoolWithTag, ExAllocatePoolWithQuota, ExAllocatePoolWithQuotaTag e ExAllocatePoolWithTagPriority), recebem um parâmetro do tipo POOL_TYPE que define se a memória a ser alocada será paginável ou não.

typedef enum _POOL_TYPE {
  NonPagedPool,
  PagedPool,
  NonPagedPoolMustSucceed,
  DontUseThisType,
  NonPagedPoolCacheAligned,
  PagedPoolCacheAligned,
  NonPagedPoolCacheAlignedMustS
} POOL_TYPE;
 
 
PVOID 
  ExAllocatePool(
    IN POOL_TYPE  PoolType,
    IN SIZE_T  NumberOfBytes
    );

Você terá que gerenciar quais alocações serão acessadas por diferentes prioridades de execução. Por exemplo: Uma determinada lista ligada é consultada apenas por funções que rodam somente em IRQL baixa, logo, todos os seus elementos podem ser alocados em memória paginável (PagedPool). Por outro lado, uma lista ligada que é consultada por funções que rodam em IRQLs altas, deve ter seus elementos alocados em memória não paginável (NonPagedPool).

Legal Fernando, mas nem tudo é alocado dinamicamente. Como ficam as variáveis estáticas?

Por padrão, todas as variáveis globais são não pagináveis. Isso é ruim se seu driver possui muitas variáveis globais, o que exigiria mais memória não paginável para que seu driver pudesse ser carregado, e como também já foi visto, memória não paginável deve ser poupada. Felizmente, podemos definir que um conjunto de variáveis globais possa ser paginável, desde que estas sejam apenas acessadas por threads em baixa IRQL.

Dentro de um módulo, seja uma aplicação, uma DLL ou mesmo um driver, o controle de paginação da memória é aplicado nas sessões que os compõem. Para que um grupo de variáveis seja definido em uma sessão paginável, podemos utilizar o pragma data_seg. Porém, não é todo compilador que nos permite fazer isso. Para saber se o compilador que estamos utilizando suporta o uso deste pragma, contamos com os headers do WDK, que definem o símbolo ALLOC_DATA_PRAGMA quando o compilador oferece suporte a este recurso. Isso, obviamente, tem se tornado menos significante, já que o aconselhável é utilizar o compilador do próprio WDK, mas não custa nada prever que seu código possa ser compilado por algum outro compilador. Veja o exemplo abaixo que define tanto variáveis não pagináveis como pagináveis.

//-f--> Estas são variáveis definidas em uma sessão não paginável
PDEVICE_OBJECT  g_pControlDeviceObj;
PDRIVER_OBJECT  g_pDriverObj;
 
//-f--> Aqui eu verifico se o compilador que estou utilizando
//      suporta o uso do #pragma data_seg.
//      Se sim, eu abro a sessão PAGEDATA que é uma sessão de
//      dados paginável.
#ifdef ALLOC_DATA_PRAGMA
    #pragma data_seg("PAGEDATA")
#endif
 
//-f--> Todas as variáveis declaradas aqui serão pagináveis.
//      Assim, apenas threads que rodam em baixo nível de
//      prioridades poderão acessar tais variáveis
 
//-f--> Definimos um buffer gigante. Ainda bem que é paginável
UCHAR   g_Buffer[100000];
 
//-f--> Aqui marcamos o fim da sessão paginável. As variáveis
//      definidas após este #pragma serão não pagináveis.
#ifdef ALLOC_DATA_PRAGMA
    #pragma data_seg()
#endif

Controlando Paginação de Código

Memória é memória, seja para armazenar dados ou código. Podemos também controlar onde as funções que você escreve serão definidas. Assim, podemos colocar todas as funções que executam em baixa prioridade em sessões de código pagináveis. Aqui usaremos o pragma que originou a dúvida do Thiago, o pragma alloc_text.

Este pragma tem duas limitações. A primeira delas é que o pragma deve ser aplicado depois da declaração da função, mas antes da definição da mesma. A outra é que este pragma não é aplicável em funções C++, ou seja, métodos de classes ou funções com sobrecargas não terão esse luxo de ser pagináveis. Se você é como eu, que prefere utilizar a tipagem forte do C++ em drivers, mesmo que você só escreva simples funções, você deverá utilizar o modificador extern “C” nas declarações das funções.

Esse pragma não é obrigatoriamente suportado por todos os compiladores, e assim como o data_seg, os headers do WDK definem o símbolo ALLOC_PRAGMA para sinalizar que o compilador utilizado suporta esse recurso. Segue mais um exemplo.

//-f--> Normalmente esta declaração é feita em um arquivo de
//      header. Observe que se estivermos compilando em C++,
//      teremos que usar o extern "C" para poder definir a
//      sessão onde as funções aqui declaradas serão definidas.
#ifdef __cplusplus
extern "C"
{
#endif
    //-f--> Declara uma
    ULONG SumOne(IN ULONG ulParam);
 
    //-f--> Declara outra
    ULONG SumTwo(IN ULONG ulParam);
 
#ifdef __cplusplus
}
#endif
 
 
//-f--> A parte a seguir fica no mesmo módulo onde a função é
//      definida, e deve ficar antes da definição da função.
//      Repare que aqui testamos se este pragma é suportado, e
//      se sim, cada função deve receber seu pragma alloc_text
#ifdef ALLOC_PRAGMA
    #pragma alloc_text(PAGE, SumOne);
    #pragma alloc_text(PAGE, SumTwo);
#endif
 
 
//-f--> Depois disso, podemos definir as funções normalmente
 
ULONG SumOne(IN ULONG ulParam);
{
    //--f-> Função SumOne que foi escrita por alguém.
 
    //-f--> Esse comentário só tem graça em Inglês.
    //
    //      Function SumOne that has been written by someone
 
    PAGED_CODE();
 
    return ulParam + 1;
}
 
ULONG SumTwo(IN ULONG ulParam);
{
    //-f--> Função desgraçada. (que não tem graça nenhuma)
    PAGED_CODE();
 
    return SumOne(SomeOne(ulParam));
}

Fernando, para quê servem estas macros PAGED_CODE que você usou no exemplo?

Bom, lá vem histórinha. Uma coisa desconfortável é que problemas com paginação de memória vão ocorrer apenas quando a página que você está acessando estiver em disco. Isso significa que você pode escrever um driver com problema, testá-lo, e se por “sorte”, as páginas estiverem todas em RAM durante o teste, você não verá nenhum problema. Uma das coisas que ajuda bastante é essa macro PAGED_CODE. Em Checked Build, esta macro é traduzida para uma função que irá verificar se a prioridade corrente é baixa suficiente para executar código paginável. Caso não seja, uma exceção de breakpoint será lançada. Espero que você esteja com o depurador atachado para ver isso acontecer. Caso contrário, não se preocupe, uma linda tela azul irá aparecer e você vai acabar conectando o depurador mais cedo ou mais tarde. Quando compilado em Free Build, essa macro é traduzida para nada, evitando perda de performance. Concluindo, isso evita de você chamar uma função paginável em IRQL alta e tudo funcionar por “sorte”.

...
 
#elif DBG
 
#define PAGED_CODE() {                                                       \
    if (KeGetCurrentIrql() > APC_LEVEL) {                                    \
        KdPrint(("EX: Pageable code called at IRQL %d\n", KeGetCurrentIrql())); \
        NT_ASSERT(FALSE);                                                    \
    }                                                                        \
}
 
...
 
#else
 
#define PAGED_CODE()        NOP_FUNCTION;
 
...
 
#endif

Uma excelente maneira de pegar problemas de paginação de cógido é utilizar o Driver Verifier com a opção de Force IRQL Checking habilitada. Esta opção, além de verificar se você está chamando as funções da API nas prioridades corretas, também força a paginação de tudo que seja paginável em seu driver toda vez que a IRQL subir para DISPATCH_LEVEL ou superior. Isso acaba com aquela “sorte” de utilizar um recurso paginável em IRQL alta quando o recurso já estiver em RAM.

Sessão Descartável

Além da sessão de código paginável PAGE que vimos, uma outra sessão também é vista com grande frequência nos exemplos do WDK. A sessão INIT é descartada quando a chamada à função DriverEntry do seu driver retornar ao sistema, e se for o caso do seu driver, depois que qualquer função de reinicialização for terminada. Se você não sabe o que é uma função de reinicialização, então dê uma passada por este post. Assim, se você tem funções que são somente utilizadas durante a inicialização do seu driver (que neste caso significa: funções chamadas pela DriverEntry, ou funções chamadas por funções que foram chamadas pela DriverEntry, ou ainda funções chamadas por funções que foram chamadas por funções… Ah! Eu acho que você entendeu), vocé pode utilizar o mesmo procedimento para defini-las na sessão INIT da mesma maneira como foi feito anteriormente. Mas não custa deixar um exemplo.

//-f--> As funções aqui declaradas serão removidas da RAM quando
//      a chamada à função DriverEntry retornar. Não tente chamá-las
//      depois disso, porque estas já foram para o céu das funções
//      de inicialização.
 
#ifdef ALLOC_PRAGMA
    #pragma alloc_text(INIT, DriverEntry);
    #pragma alloc_text(INIT, FunctionCalledByDriverEntry);
    #pragma alloc_text(INIT, AnotherFunctionCalledByDriverEntry);
 
    #pragma alloc_text(PAGE, SumOne);
    #pragma alloc_text(PAGE, SumTwo);
#endif

Isso é especialmente útil em Legacy Drivers, que realizam muitos passos na inicialização. Legacy drivers, além de procurar o hardware que vão controlar, ainda precisam fazer a associação dos recursos disponíveis (portas, interrupções, canais de DMA e etc), e isso acaba consumido uma quantidade expressiva de código. Por outro lado, drivers de WDM recebem tudo mastigadinho do Plug-and-Play Manager. O hardware foi detectado e a associação de recursos já foi negociada. Coisa linda de Deus!

Paginável ou não paginável, eis a questão

Supondo que você tenha muitas funções que rodem em IRQL alta, e que por isso, devem estar em memória não paginável, isso faria com que uma grande quantidade de memória não paginável seja utilizada para manter tais funções, mesmo que ninguém esteja utilizando o driver. Também podemos definir nossas próprias sessões e assim torná-las pagináveis ou não pagináveis quando nos for conveniente. Isso permitiria que todas aquelas funções não pagináveis sejam pagináveis enquanto ninguém obtiver uma referência para nosso driver. Desta forma, quando recebermos um IRP_MJ_CREATE ou quando programarmos o hardware para disparar interrupções, podemos dizer ao sistema que agora precisaremos tornar a sessão onde tais funções foram definidas como não paginável.

Primeiro de tudo, teremos que criar nossas sessões do coração, e faremos isso utilizando o pragma alloc_text para definir sessões que devem ter seu nome no formato com PAGExxxx, onde xxxx seja um nome único no seu driver. Veja o exemplinho de desencargo de consciência.

//-f--> As funções aqui declaradas serão definidas em uma sessão
//      que pode ser não paginável quando nos for conveniente.
 
#ifdef ALLOC_PRAGMA
    #pragma alloc_text(PAGEABCD, FunctionOne);
    #pragma alloc_text(PAGEABCD, FunctionTwo);
#endif
 
 
//-f--> As funções aqui declaradas serão definidas em uma outra 
//      sessão que pode ser não paginável. Assim podemos definir
//      diferentes grupos de funções em diferentes sessões.
 
#ifdef ALLOC_PRAGMA
    #pragma alloc_text(PAGE1234, FunctionThree);
    #pragma alloc_text(PAGE1234, FunctionFour);
#endif

Agora que já definimos quais funções estarão definidas nessa sessão, podemos torná-la não paginável somente quando tais funções forem utilizadas. Para tal devemos utilizar a função MmLockPagableCodeSection.

PVOID 
  MmLockPagableCodeSection(
    IN PVOID  AddressWithinSection
    );

Para identificar qual sessão será marcada como não paginável, teremos que passar um endereço que esteja dentro da sessão. Um nome de função definida na sessão já resolve o problema, mas lembre-se que todas as funções dentro da mesma sessão serão marcadas como não paginável.

A função MmLockPagableCodeSection nos retorna um endereço opaco que pode ser utilizado como parâmetro para a chamada à função MmUnlockPagableImageSection, que marca a sessão novamente como paginável. Essa função é normalmente chamada antes do driver ser descarregado. Seguindo a linha do nosso exemplo, poderiamos chamar esta função ao receber um IRP_MJ_CLOSE. O mesmo endereço retornado por MmLockPagableCodeSection pode ser utilizado como parâmetro às chamadas à função MmLockPagableSectionByHandle para novamente tornar uma sessão não paginável, o que é muito mais rápido que a chamada à MmLockPagableCodeSection. Assim, devemos chamar MmLockPagableCodeSection pelo menos uma vez para obter o ponteiro opaco, e depois disso, podemos chamar MmLockPagableSectionByHandle e MmUnlockPagableImageSection.

VOID 
  MmLockPagableSectionByHandle(
    IN PVOID  ImageSectionHandle
    );
 
VOID 
  MmUnlockPagableImageSection(
    IN PVOID  ImageSectionHandle
    );

O mesmo pode ser feito em sessões customizadas que definem dados, mas devemos utilizar a função MmLockPagableDataSection para obter o ponteiro opaco que identifica a sessão.

Minha função pode ser paginável?

Fernando, se minha função é chamada em PASSIVE_LEVEL, então ela pode ser paginável?

Não é bem assim. Sua função, mesmo sendo chamada em PASSIVE_LEVEL, pode conter intervalos de código que precisam ser não pagináveis. Se você chamar funções que elevam a IRQL, tais como KeAcquireSpinLock, sua função não pode ser definida em sessão paginável.

Mas Fernando, acompanhe o meu raciocínio. Se a função foi chamada e está em execução no momento, não é obvio que a página à qual ela está contida está em RAM?

Você pode até não acreditar, mas uma função é composta por uma cadeia de bytes que podem estar nas fronteiras de páginas. Isso significa que o inicio da sua função pode estar no final de uma página, que de fato foi paginada para RAM quando a chamada foi feita, mas não sabemos onde uma nova página pode começar. Esta nova página pode estar em disco, e se for acessada em IRQL alta, a paginação causará uma tela azul. Veja o código abaixo para ter uma idéia do que estou falando e não esqueça de ler os comentários.

/****
***     FunctionCalledAtPassiveLevel
**
**      Rotina que é chamada em PASSIVE_LEVEL,
**      mas tem elevação de IRQL durante a chamada.
*/
 
PLIST_ENTRY
FunctionCalledAtPassiveLevel(VOID)
{
    KIRQL       Irql;
    PLIST_ENTRY pEntry = NULL;
 
    PAGED_CODE();
 
    //-f--> Aqui nossa IRQL vai para as alturas, se o codigo que
    //      vier depois desta chamada estiver na próxima página
    //      de memória que estiver em disco, então (CABUM !!!)
    KeAquireSpinLock(&g_SpinLock, &Irql);
 
 
    //-f-->    -------======= Fronteira de página =======-------
 
 
    //-f--> O código aqui é executado em DISPATCH_LEVEL, o que
    //      impede esta função de ser definida em uma sessão
    //      paginável.
 
    if (!IsListEmpty(&g_NonPagedList))
    {
        pEntry = RemoveHeadList(&g_NonPagedList);
    }
 
    //-f--> Tudo bem que você acesse apenas dados não pagináveis,
    //      mas o código que você usa para tal acesso também precisa
    //      estar em memória não paginável. Afinal, esse código está
    //      sendo executado em IRQL alta e recuperar uma página de
    //      código em disco resultaria em coisas horríveis.
 
    //-f--> Voltamos a PASSIVE_LEVEL
    KeReleaseSpinLock(Irql);
 
    return pEntry;
}

Se você possui funções que são grandes, mas que contém intervalos pontuais com elevação de IRQL, então separe tais intervalos em funções isoladas que podem ser definidas em sessões não pagináveis, permitindo assim que você defina sua grande e complexa funcão em uma sessão paginável.

Ufa! Eu ainda poderia escrever mais alguns comentários sobre este assunto, mas se eu já estou cansado de escrever, imagino como vocês estão. O assunto é tratado com todos detalhes na referência. Mas se tiverem qualquer dúvida a respeito, é só me enviar um e-mail e torcer pra eu saber responder.

Até mais!

Buffered, Direct ou Neither em IOCTLs

16 de October de 2008

Depois de uma pitada de memória virtual para entendermos os conceitos mais relevantes e darmos uma boa passeada nos métodos de transferências de dados entre aplicação e driver, hoje vamos fechar essa trilogia falando sobre os métodos de transferências de dados em IOCTLs. Se você não sabe criar ou utilizar IOCTLs, este outro post pode ajudar.

Flags não ajudam aqui

No post referente aos métodos de transferências de dados foi visto que definimos o método de transferência através de uma máscara de bits que está localizada no campo Flags em um DEVICE_OBJECT. O método escolhido aqui define como o I/O Manager vai manipular os dados nas operações de leitura (IRP_MJ_READ) e escrita (IRP_MJ_WRITE) do driver. O método escolhido é aplicado para ambas as operações. Não podemos ter escritas utilizando um método enquanto as leituras são realizadas utilizando outro. No caso das IOCTLs, a história é diferente. O método de transferência é escolhido quando se define o control code utilizando a macro CTL_CODE.

#define IOCTL_Device_Function CTL_CODE(DeviceType, Function, Method, Access)

Para se ter uma explicação mais detalhada sobre o uso desta macro, visite este post, ou dê uma olhada na referência. Aqui comentarei apenas sobre os métodos de transfêrencias de dados que é selecionado pelo parâmetro Method desta macro. A utilização desta macro para definir IOCTLs é normalmente feita em um arquivo de header que será compartilhado entre a aplicação e o driver. A definição desta macro é obtida a partir do header Windows.h para User-Mode e Ntddk.h para Kernel-Mode. Abaixo segue a definição dos IOCTLs que implementaremos neste post.

//-f--> Aqui definimos os IOCTLs de cópia utilizando os
//      deferentes métodos de transferência de dados entre
//      aplicação e driver.
 
//-f--> Utilizando cópia de sistema
#define IOCTL_COPY_BUFFERED CTL_CODE(FILE_DEVICE_UNKNOWN,   \
                                     0x800,                 \
                                     METHOD_BUFFERED,       \
                                     FILE_ANY_ACCESS)
 
//-f--> Travando as páginas da aplicação
#define IOCTL_COPY_DIRECT   CTL_CODE(FILE_DEVICE_UNKNOWN,   \
                                     0x801,                 \
                                     METHOD_OUT_DIRECT,     \
                                     FILE_ANY_ACCESS)
 
//-f--> Seja o que Deus quiser
#define IOCTL_COPY_NEITHER  CTL_CODE(FILE_DEVICE_UNKNOWN,   \
                                     0x802,                 \
                                     METHOD_NEITHER,        \
                                     FILE_ANY_ACCESS)

Como se pode observar, podemos ter diferentes métodos de transferência de dados para diferentes IOCTLs. Neste post vou criar um driver que ofereça três IOCTLs que simplesmente copiam os dados recebidos no buffer de entrada para buffer de saída. O que teremos que fazer inicialmente é utilizar a macro CTL_CODE para criar as IOCTLs dos serviços que nosso driver de exemplo irá oferecer. O código completo do driver de exemplo está disponível para download ao final deste post.

Pô Fernando, eu incluí o Windows.h em minha aplicação de teste, mas ainda está faltando a definição da macro CTL_CODE e recebo a mensagem de erro abaixo. Estou usando um Windows.h incompleto?

Z:\sources\testapp.cpp(45) : error C3861: 'CTL_CODE': identifier not found

O negócio é o seguinte: O Wizard das versões mais recentes do Visual Studio cria o arquivo StdAfx.h contendo, entre outras, as seguintes linhas:

#define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
// Windows Header Files:
#include 

Observe que o símbolo WIN32_LEAN_AND_MEAN é definido antes da inclusão do arquivo Windows.h. A fim de ganhar velocidade de compilação, este símbolo evita a declaração de algumas toneladas de definições que raramente são utilizadas por aplicações. O que está acontecendo é que a macro CTL_CODE é uma destas coisas raramente utilizadas. É rapaz, interagir com drivers não é pra qualquer um não. Enfim, para resolver este problema é só comentar a definição deste símbolo e todos viverão felizes para sempre.

Calma lá Fernando! Todos menos eu, que não usei o Wizard do Visual Studio. Estou utilizando o arquivo SOURCES para compilar minha aplicação. O fato é que em meu fonte não existe nenhuma definição desse tal de “Win32 Lemming“. Qual é a desculpinha agora?

Se você está utilizando o arquivo SOURCES para compilar sua aplicação de teste, assim como estou fazendo no exemplo deste post, você precisará adicionar a linha em destaque abaixo para que o símbolo WIN32_LEAN_AND_MEAN não seja definido pelo makefile padrão do WDK.

TARGETNAME=TestApp
TARGETTYPE=PROGRAM
USE_LIBCMT=1
UMTYPE=console
NOT_LEAN_AND_MEAN=1
 
SOURCES=TestApp.cpp

Utilizando um buffer de sistema

O primeiro método que veremos aqui é o Buffered I/O, definido pela utilização do valor METHOD_BUFFERED como parâmetro da macro CTL_CODE. Aqui não teremos grandes novidades para quem leu o post anterior. A grande diferença aqui é que na mesma chamada ao driver, dois bufferes são passados para a função DeviceIoControl, um de entrada e outro de saída. Aqui o I/O Manager vai alocar um único buffer de sistema com o tamanho igual ao maior deles. Complicou? Um exemplo ajuda. Numa chamada em que a aplicação ofereça o buffer de entrada com 50 bytes e um buffer de saída com 100 bytes, o buffer de sistema será alocado com 100 bytes. O I/O Manager vai copiar os 50 bytes do buffer de entrada da aplicação para o buffer de sistema. A IRP é enviada ao driver, e ao ser completada, o I/O Manager copia o conteúdo do buffer de sistema para o buffer de saída da aplicação. Quantidade de bytes copiada de volta à aplicação é determinada pelo campo pIrp->IoStatus.Information, assim como no post anterior.

Uma coisa importante a ser notada aqui é que já que o buffer de sistema é único tanto para a entrada como para saída dos dados, o driver precisa ler os dados de entrada antes de começar a escrever os dados de saída, que sobrescreveriam o buffer de entrada.

Conforme já comentei, nosso driver de exemplo vai copiar o buffer de entrada para o buffer de saída. Vamos dar uma olhada na implementação da nossa rotina que vai tratar o IOCTL que usará um buffer de sistema. Leiam os comentários.

/****
***     OnCopyBuffered
**
**      Rotina de tratamento da IOCTL responsável por
**      fazer a cópia do buffer de entrada para o buffer
**      de saída. (usando METHOD_BUFFERED)
*/
 
NTSTATUS
OnCopyBuffered(IN PDEVICE_OBJECT    pDeviceObj,
               IN PIRP              pIrp)
{
    NTSTATUS            nts;
    PIO_STACK_LOCATION  pStack;
 
    //-f--> Obtemos um ponteiro para a stack location
    //      corrente.
    pStack = IoGetCurrentIrpStackLocation(pIrp);
 
    //-f--> Output dos valores obtidos.
    //      Reparem que o buffer de entrada e o buffer
    //      de saída é o mesmo. Isso significa que você
    //      não pode escrever no buffer de saída até que
    //      tenha lido todos os bytes do buffer de entrada.
    DbgPrint("========== OnCopyBuffered ===========\n"
             "Input buffer address: 0x%p\n"
             "Input buffer size:    %d\n"
             "Output buffer addres: 0x%p\n"
             "Output buffer size:   %d\n\n",
             pIrp->AssociatedIrp.SystemBuffer,
             pStack->Parameters.DeviceIoControl.InputBufferLength,
             pIrp->AssociatedIrp.SystemBuffer,
             pStack->Parameters.DeviceIoControl.OutputBufferLength);
 
    //-f--> Vamos verificar se o buffer de entrada cabe no buffer
    //      de saída antes de fazer a cópia.
    if (pStack->Parameters.DeviceIoControl.OutputBufferLength <
        pStack->Parameters.DeviceIoControl.InputBufferLength)
    {
        //-f--> Ops!
        nts = STATUS_BUFFER_TOO_SMALL;
        pIrp->IoStatus.Information = 0;
    }
    else
    {
        //-f--> Copiar pra quê?
        //      Como o buffer de entrada e o buffer de saída
        //      oferecidos pela aplicação são copiados para um
        //      único buffer de sistema, não precisamos fazer
        //      nenhuma cópia. O I/O Manager já fará isso por
        //      nós. Vamos apenas informar à aplicação quantos
        //      bytes são válidos no buffer de saída.
        nts = STATUS_SUCCESS;
        pIrp->IoStatus.Information =
            pStack->Parameters.DeviceIoControl.InputBufferLength;
    }
 
    //-f--> Fecha a conta e passa a régua.
    pIrp->IoStatus.Status = nts;
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    return nts;
}

Travando a memória da Aplicação

Apesar de o IOCTL ser uma solicitação normalmente utilizada para controle, nada nos impede de utilizar este meio de comunicação para obter ou enviar dados para o driver. Se nestas leituras ou escritas, grandes volumes de dados forem trocados, o método Buffered passa a ser pouco eficiente. Utilizando o método Direct não haverá cópias intermediárias em buffer de sistema. Nada muito diferente do que já vimos no post anterior, mas aqui temos dois bufferes em uma só chamada. O buffer de entrada faltou na escola bem no dia da aula sobre MDLs, e por isso ainda chega ao driver utilizando um buffer de sistema. É isso mesmo! Igualzinho ao método buffered. Por esta razão, não usaremos o buffer de entrada para enviar grandes quantidades de dados ao driver.

Mas e se eu quiser enviar uma grande quantidade de dados para o driver através de um IOCTL? Aqui a conversa entorta um pouco. Repare que para utilizar o método Direct em IOCTLs, podemos usar tanto o parâmetro METHOD_IN_DIRECT como o METHOD_OUT_DIRECT. Com o método Direct, você pode utilizar o buffer de saída como entrada para o driver. Hein? Tá bom, vamos mais devagar. Ambas as opções criam uma MDL para descrever as páginas que compõem o buffer de saída oferecido pela aplicação. Quando a IRP chega ao driver, você utiliza a função MmGetSystemAddressForMdlSafe para obter um ponteiro de System Space que mapeia as mesmas páginas físicas oferecidas pela aplicação. Isso significa que o ponteiro que você recebe vai escrever diretamente nas páginas oferecidas pela aplicação. Já sei! Se o ponteiro aponta para as mesmas páginas da aplicação, então podemos ler os dados contidos nestas páginas? É exatamente isso. Podemos enviar dados ao driver preenchendo o buffer de saída antes de chamar a função DeviceIoControl. Assim, quando o driver receber a IRP, ele pode ler estes dados. Isso permite que o driver receba grandes quantidades de dados de entrada, mas utilizando o buffer de saída. O parâmetro METHOD_IN_DIRECT sinaliza ao I/O Manager que o buffer que será utilizado para montar a MDL será utilizado para leitura, assim o buffer é testado para leituras no processo de criação da MDL. Alternativamente, o parâmetro METHOD_OUT_DIRECT indica que o buffer receberá leituras e escritas do driver.

Lembre-se que METHOD_IN_DIRECT ou METHOD_OUT_DIRECT define apenas o tipo de teste que será feito sobre o buffer de saída, permitindo do driver ler o buffer de saída. O buffer de entrada sempre virá por intermédio de um buffer de sistema. Tá tá tá, vamos ao código por favor?

/****
***     OnCopyDirect
**
**      Rotina de tratamento da IOCTL responsável por
**      fazer a cópia do buffer de entrada para o buffer
**      de saída. (usando METHOD_DIRECT_XXX)
*/
 
NTSTATUS
OnCopyDirect(IN PDEVICE_OBJECT  pDeviceObj,
             IN PIRP            pIrp)
{
    NTSTATUS            nts;
    PIO_STACK_LOCATION  pStack;
    PVOID               pOutputBuffer;
 
    //-f--> Obtemos um ponteiro para a stack location
    //      corrente.
    pStack = IoGetCurrentIrpStackLocation(pIrp);
 
    //-f--> O ponteiro de saída vem de uma MDL criada
    //      pelo I/O Manager.
    pOutputBuffer = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress,
                                                 LowPagePriority);
 
    if (!pOutputBuffer)
    {
        //-f--> Ops! Estamos sem recursos para mapear as
        //      páginas descritas pelo MDL em System Space.
        pIrp->IoStatus.Status = STATUS_INSUFFICIENT_RESOURCES;
        pIrp->IoStatus.Information = 0;
        IoCompleteRequest(pIrp, IO_NO_INCREMENT);
        return STATUS_INSUFFICIENT_RESOURCES;
    }
 
    //-f--> Já o ponteiro de entrada sempre vem por um
    //      buffer de sistema como no método Buffered
    DbgPrint("=========== OnCopyDirect ============\n"
             "Input buffer address: 0x%p\n"
             "Input buffer size:    %d\n"
             "Output buffer addres: 0x%p\n"
             "Output buffer size:   %d\n\n",
             pIrp->AssociatedIrp.SystemBuffer,
             pStack->Parameters.DeviceIoControl.InputBufferLength,
             pOutputBuffer,
             pStack->Parameters.DeviceIoControl.OutputBufferLength);
 
    //-f--> Vamos verificar de o buffer de entrada cabe no buffer
    //      de saída antes de fazer a cópia.
    if (pStack->Parameters.DeviceIoControl.OutputBufferLength <
        pStack->Parameters.DeviceIoControl.InputBufferLength)
    {
        //-f--> Ops!
        nts = STATUS_BUFFER_TOO_SMALL;
        pIrp->IoStatus.Information = 0;
    }
    else
    {
        //-f--> Neste caso teremos que fazer a cópia, já que o buffer
        //      de entrada e o buffer de saída são fisicamente distintos
        RtlCopyMemory(pOutputBuffer,
                      pIrp->AssociatedIrp.SystemBuffer,
                      pStack->Parameters.DeviceIoControl.InputBufferLength);
 
        //-f--> Sinaliza sucesso ao I/O Manager e informa à aplicação a
        //      quantidade de bytes válidos no buffer de saída.
        nts = STATUS_SUCCESS;
        pIrp->IoStatus.Information =
            pStack->Parameters.DeviceIoControl.InputBufferLength;
    }
 
    //-f--> Fecha a conta e passa a régua.
    pIrp->IoStatus.Status = nts;
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    return nts;
}

Nem Buffered I/O nem Direct I/O

Isso pode parecer repetitivo para você, mas neste método, indicado pelo parâmetro METHOD_NEITHER, o I/O Manager não fará nada por você. Assim, você terá que testar o contexto do processo e também testar o acesso aos bufferes como vimos no post anterior. Mais uma vez, a grande diferença aqui é que teremos dois bufferes. O buffer de entrada virá por pStack->Parameters.DeviceIoControl.Type3InputBuffer e o buffer de saída é obtido por pIrp->UserBuffer. O buffer de entrada deve ser testado com ProbeForRead, já que o driver fará leituras neste buffer, e o buffer de saída deve ser testado com ProbeForWrite. Acho que o resto o código de exemplo é capaz de explicar. Para testar o contexto do processo, utilizamos a função que já foi explicada no post anterior.

/****
***     OnCopyNeither
**
**      Rotina de tratamento da IOCTL responsável por
**      fazer a cópia do buffer de entrada para o buffer
**      de saída. (usando METHOD_NEITHER)
*/
 
NTSTATUS
OnCopyNeither(IN PDEVICE_OBJECT pDeviceObj,
              IN PIRP           pIrp)
{
    NTSTATUS            nts;
    PIO_STACK_LOCATION  pStack;
 
    //-f--> Obtemos um ponteiro para a stack location
    //      corrente.
    pStack = IoGetCurrentIrpStackLocation(pIrp);
 
    //-f--> Output dos valores obtidos.
    DbgPrint("=========== OnCopyNeither ===========\n"
             "Input buffer address: 0x%p\n"
             "Input buffer size:    %d\n"
             "Output buffer addres: 0x%p\n"
             "Output buffer size:   %d\n\n",
             pStack->Parameters.DeviceIoControl.Type3InputBuffer,
             pStack->Parameters.DeviceIoControl.InputBufferLength,
             pIrp->UserBuffer,
             pStack->Parameters.DeviceIoControl.OutputBufferLength);
 
    //-f--> Como estamos utilizando o método Neither, temos que
    //      estar executando no mesmo contexto do processo que
    //      gerou a IRP., pois vamos acessar o User Space em Kernel-Mode.
    if (!EstouNoContextoDoProcessoQueGerouEssaIrp(pIrp))
    {
        //-f--> Ops!
        pIrp->IoStatus.Status = STATUS_INVALID_ADDRESS;
        pIrp->IoStatus.Information = 0;
        IoCompleteRequest(pIrp, IO_NO_INCREMENT);
        return STATUS_INVALID_ADDRESS;
    }
 
    //-f--> Aqui já sabemos que estamos no contexto certo, mas ainda
    //      precisamos testar os bufferes oferecidos pela aplicação.
    //      Não queremos que uma aplicação infeliz envie um ponteiro
    //      inválido e o sistema termine em tela azul por conta disso.
    __try
    {
        //-f--> O driver fará leituras no buffer de entrada
        ProbeForRead(pStack->Parameters.DeviceIoControl.Type3InputBuffer,
                     pStack->Parameters.DeviceIoControl.InputBufferLength,
                     1);
 
        //-f--> E fará escritas no buffer de saída
        ProbeForWrite(pIrp->UserBuffer,
                      pStack->Parameters.DeviceIoControl.OutputBufferLength,
                      1);
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        //-f--> Ahaaa!!
        nts = GetExceptionCode();
 
        //-f--> Completa a IRP e xinga a mãe do cara que escreveu
        //      a aplicação (a menos que tenha sido você mesmo).
        pIrp->IoStatus.Status = nts;
        pIrp->IoStatus.Information = 0;
        IoCompleteRequest(pIrp, IO_NO_INCREMENT);
        return nts;
    }
 
    //-f--> Vamos verificar de o buffer de entrada cabe no buffer
    //      de saída antes de fazer a cópia.
    if (pStack->Parameters.DeviceIoControl.OutputBufferLength <
        pStack->Parameters.DeviceIoControl.InputBufferLength)
    {
        //-f--> Ops!
        nts = STATUS_BUFFER_TOO_SMALL;
        pIrp->IoStatus.Information = 0;
    }
    else
    {
        //-f--> Feche os olhos, diga "Sangue de Jesus tem poder",
        //      acredita em São Walter Oney e copia o buffer de
        //      entrada para o buffer de saída.
        RtlCopyMemory(pIrp->UserBuffer,
                      pStack->Parameters.DeviceIoControl.Type3InputBuffer,
                      pStack->Parameters.DeviceIoControl.InputBufferLength);
 
        //-f--> Ufa! Todos vivos?
        //      Sinaliza sucesso ao I/O Manager e informa à aplicação a
        //      quantidade de bytes válidos no buffer de saída.
        nts = STATUS_SUCCESS;
        pIrp->IoStatus.Information =
            pStack->Parameters.DeviceIoControl.InputBufferLength;
    }
 
    //-f--> Fecha a conta e passa a régua.
    pIrp->IoStatus.Status = nts;
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    return nts;
}

Compilando o exemplo

Para compilar o exemplo disponível para download, você pode utilizar o atalho de ambiente instalado pelo WDK e chamar o Build do diretório raiz do exemplo. Repare que em um único passo compilamos o driver e a aplicação de teste. A figura abaixo ilustra isso. Ou você pode usar o DDKBUILD como eu já expliquei neste outro post para compilar a partir do Visual Studio.


Fernando, mais uma dúvida antes de você sumir na névoa. Na tabela de Dispatch Routines, que preenchemos na estrutura DRIVER_OBJECT, contém apenas uma entrada para IRP_MJ_DEVICE_CONTROL. Como você criou uma rotina para cada método? Essa eu vou deixar o código de exemplo abaixo responder, mas se ainda assim você tiver alguma dúvida, é só me mandar um e-mail, que está em meu perfil do Blogger, e aí a gente sai na porrada.

/****
***     OnDeviceControl
**
**      Aqui recebemos todos os DeviceIoControl
**      enviados para o driver e separamos em rotinas
**      específicas para o tratamento de cada IOCTL.
**      Todo o tratamento de todas as IOCTLs poderiam
**      estar em uma só função, mas não custa nada ser
**      organizado de vez em quando.
*/
 
NTSTATUS
OnDeviceControl(IN PDEVICE_OBJECT   pDeviceObj,
                IN PIRP             pIrp)
{
    PIO_STACK_LOCATION  pStack;
 
    //-f--> Obtemos um ponteiro para a stack location
    //      corrente.
    pStack = IoGetCurrentIrpStackLocation(pIrp);
 
    //-f--> Obtém o código do IOCTL para encaminhar
    //      para a rotina certa, ou não. 🙂
    switch(pStack->Parameters.DeviceIoControl.IoControlCode)
    {
    case IOCTL_COPY_BUFFERED:
        return OnCopyBuffered(pDeviceObj,
                              pIrp);
 
    case IOCTL_COPY_DIRECT:
        return OnCopyDirect(pDeviceObj,
                            pIrp);
 
    case IOCTL_COPY_NEITHER:
        return OnCopyNeither(pDeviceObj,
                             pIrp);
    }
 
    //-f--> Ops! Recebemos um IOCTL diferente dos que
    //      estávamos esperando.
    pIrp->IoStatus.Status = STATUS_NOT_IMPLEMENTED;
    pIrp->IoStatus.Information = 0;
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
 
    return STATUS_NOT_IMPLEMENTED;
}

Hoje vou me despedir ao estilo mr4nd3r50n, que é um amigo que trabalhou comigo na SCUA.

Intel mais, já vou Windows!
🙂

IoctlCopy.zip

Buffered, Direct ou Neither

1 de October de 2008

No post passado apresentei uma breve introdução sobre alguns pontos referentes à memória virtual que considero ser os mais relevantes aos desenvolvedores de drivers. Com essa pequena carga de conhecimento, ficará mais simples de explicar as diferenças entre os métodos de transferências de dados entre aplicações e drivers, assim como outras questões, tais como o atendimento de interrupções, mapeamento de memória e contexto de execução.

Se formos pensar de maneira bem simplificada (e bota simplificada nisso), drivers são basicamente os módulos que extraem dados de dispositivos e os disponibilizam para as aplicações e vice-versa. Deste ponto de vista, parece mesmo que escrever drivers seja fácil. Parece até uma maneira de fazer um memcpy de software para hardware. Hoje veremos um pouco sobre as maneiras as quais um driver pode optar para fazer essa transferência de dados.

Utilizando um Buffer de sistema

O primeiro método é o chamado Buffered I/O. Este método utiliza um buffer de sistema para fazer a transferência de dados entre a aplicação e o driver. Quando uma aplicação chama a função WriteFile passando os dados a serem enviados para o driver, o I/O Manager faz uma cópia dos dados da aplicação para um buffer de sistema. Mas o que é um buffer de sistema? Se trata de uma alocação em System Space que foi feita em Kernel-Mode, e por isso, é acessível em qualquer contexto de processo. Já falamos disso no post passado.

BOOL WINAPI WriteFile(
  __in         HANDLE hFile,
  __in         LPCVOID lpBuffer,
  __in         DWORD nNumberOfBytesToWrite,
  __out_opt    LPDWORD lpNumberOfBytesWritten,
  __inout_opt  LPOVERLAPPED lpOverlapped
);

O I/O Manager obtém o tamanho do buffer a ser alocado do parâmetro nNumberOfBytesToWrite, então realiza uma alocação de memória do tipo não paginada e faz a cópia dos dados que estão em User Space para System Space. Se você estiver derrapando nos termos System Space, User Space e tipos de alocação de memória, dê uma lida neste post para que as coisas fiquem menos obscuras para você.

No driver, o ponteiro para este buffer é obtido pela IRP e seu tamanho é obtido pela stack location da IRP. Legal, mas o que é uma IRP? Dê uma olhada neste mini exemplo abaixo para ter uma idéia, ou você pode ver o exemplo utilizado neste outro post que também utiliza o método Buffered para enviar e receber strings de um driver de exemplo.

//-f--> Rotina de tratamento da IRP_MJ_WRITE.
//      Exemplo de obtenção do buffer de sistema
//      alocado pelo I/O Manager no método Buffered
 
NTSTATUS
OnDispatchWrite(__in PDEVICE_OBJECT pDeviceObj,
                __in PIRP           pIrp)
{
    PIO_STACK_LOCATION  pStack;
    PVOID               pBuffer;
    ULONG               ulLength;
 
    //-f--> Obtém a stack corrente da IRP
    pStack = IoGetCurrentIrpStackLocation();
 
    //-f--> Aqui obtemos o ponteiro para o buffer
    pBuffer = pIrp->AssociatedIrp.SystemBuffer;
 
    //-f--> Aqui o tamanho do buffer
    ulLength = pStack->Parameters.Write.Length;
 
    ...
}

Nas operações de leitura, o método é bem parecido, mas apesar de neste caso o I/O Manager também fazer a locação em memória não paginada, ele não faz nenhuma cópia para o buffer de sistema. O buffer de sistema é recebido pelo driver também através de pIrp->AssociatedIrp.SystemBuffer, mas o seu tamanho é obtido em pStack->Parameters.Read.Length. O driver preencherá o buffer com os dados vindos do dispositivo, o I/O Manager fará a cópia do buffer de System Space para User Space quando a IRP for completada, preenchendo o buffer da aplicação.

Opa opa! Até onde eu sei, drivers normalmente completam IRPs em contexto arbitrário, o que significa que “só Deus sabe em qual contexto de processo”. Tudo bem se o driver escrever em um buffer de sistema, o qual é válido para qualquer processo, mas o buffer da aplicação está em User Space e só é acessível no contexto do próprio processo. Como o I/O Manager faria a cópia do buffer de sistema para o buffer da aplicação em contexto arbitrário? Nossa, essa realmente foi uma excelente pergunta. Acho que nem eu teria imaginado uma pergunta tão boa. As IRPs podem ser tratadas tanto sincronamente como assincronamente. O I/O Manager sabe como a IRP foi processada, e no caso síncrono, o I/O Manager já está no contexto do processo que fez a solicitação, e neste caso ele tem acesso a ambos os bufferes, já que o I/O Manager roda em Kernel-Mode. Quando a IRP é tratada assincronamente, o I/O Manager enfila uma APC (Asynchronous Procedure Call), que é uma chamada assincrona executada no contexto de uma dada thread. Essa thread é justamente a thread que iniciou a operação, e que portanto, estará no contexto do processo certo para acessar o User Space da aplicação.

BOOL WINAPI ReadFile(
  __in         HANDLE hFile,
  __out        LPVOID lpBuffer,
  __in         DWORD nNumberOfBytesToRead,
  __out_opt    LPDWORD lpNumberOfBytesRead,
  __inout_opt  LPOVERLAPPED lpOverlapped
);

Na chamada da função ReadFile, o parâmetro nNumberOfBytesToRead indica o tamanho do buffer que a aplicação está oferecendo ao driver. O driver recebe este valor como quantidade máxima de bytes que podem ser retornados à aplicação. Supondo que a aplicação tenha oferecido 1000 bytes, o I/O Manager faz uma alocação de 1000 bytes e repassa o buffer para o driver. Vamos supor que o driver tenha apenas 500 bytes a serem retornados à aplicação, neste caso, o I/O Manager terá de copiar para o buffer da aplicação apenas 500 dos 1000 bytes alocados. O I/O Manager recebe este valor através do campo pIrp->IoStatus.Information, que é preenchido pelo driver antes da IRP ser completada. Desta forma, o I/O Manager copia somente os bytes válidos do buffer de sistema para o buffer da aplicação. Este mesmo valor é retornado à aplicação através do parâmetro lpNumberOfBytesRead.

Utilizar memória não paginada para o manter o buffer de sistema nos assegura que as páginas não serão removidas da RAM por paginação. Isso permite que a página seja acessada mesmo a partir de threads que estejam rodando em alto nível de prioridade. Contudo, o método Buffered é indicado apenas para pequenas movimentações de dados. Imagine que uma aplicação queira fazer uma escrita de 10 MB de uma só vez. O I/O Manager teria que fazer uma alocação em System Space de 10 MB de memória não paginada, o que não seria nada adequado, pois memória não paginada é um recurso escasso. Se o I/O Manager conseguir fazer a alocação, ele ainda terá que fazer uma cópia de 10 MB do buffer da aplicação para o buffer de sistema. Isso até funcionaria, mas teriamos sérios problemas de performace. Neste caso, o mais indicado seria utilizar o método que é visto em seguida.

Travando a memória da Aplicação

No método chamado Direct I/O, como o nome já sugere, o driver faz acesso diretamente às páginas de memória da aplicação sem utilizar um buffer intermedirário. Desta forma, o I/O Manager não faz uma alocação em memória não paginada e também não tem que ficar no BPL-BPC (Buffer pra lá – Buffer pra cá). Ao invés disso, o I/O Manager testa as páginas de memória que compõem o buffer oferecido pela aplicação, cria uma MDL e trava as páginas de memória na RAM. Nossa! Calma aí meu amigo, vamos devagar!

  • Testa as páginas de memória – Nada impede um programador de fazer besteira. Um buffer inválido pode ser passado para o I/O Manager. Pode ser que o buffer não tenha sido alocado, ou que o buffer seja menor que o valor indicado na chamada às funções ReadFile ou WriteFile, pode ser também que as páginas de memória oferecidas à ReadFile estejam protegidas contra escrita, pode ser também que algumas das páginas utilizadas pelo buffer tenham sido paginadas para o disco. Se um driver tenta acessar uma página inválida ou protegida, uma exceção é gerada e uma tela azul sugirá das trevas. Mas como o I/O Manager vai testar a memória? Se o buffer é passado como parâmetro para a função ReadFile, então o driver fará escritas neste buffer. Para ganhar tempo, o I/O Manager fará uma escrita de um byte de cada página de RAM apenas para testar o acesso a elas. Essa escrita é feita sob um manipulador de exceção. Se o buffer for inválido ou protegido, o manipulador de exceção tratará isso e devolverá um erro para a aplicação. Se o buffer é passado para a função WriteFile, então o buffer será lido pelo driver, e neste caso, o teste seria a leitura de um byte de cada página.

  • Cria uma MDL – Uma MDL (Memory Descriptor List) é uma estrutura de dados que descreve as várias páginas de memória que compõem um buffer. Estas páginas são as mesmas páginas físicas utilizadas pela aplicação, ou seja, quando o driver escrever nestas páginas, este já estará escrevendo diretamente no buffer da aplicação. Assim o I/O Manager não precisará fazer nenhum BPL-BPC.

  • Trava as páginas na RAM – Esse passo faz com que as páginas de memória que compõem o buffer da aplicação se tornem não pagináveis. Assim, estas poderão ser acessadas pelo driver em threads que estejam sendo executadas em alto nível de prioridade.

Depois de todo esse ritual, o driver agora recebe o buffer através de pIrp->MdlAddress, mas o que teremos aqui é um ponteiro para uma MDL. Mas o que eu faço com uma MDL? Na maioria das vezes, você vai passar como argumento em um serviço oferecido por outro driver ou componente do sistema. Alguns exemplos são drivers de DMA (Direct Memory Access) que utilizam MDL na chamada para a função MapTransfer, ou mesmo quando uma MDL é repassada para rotinas de controladores USB (Universal Serial Bus), tais como UsbBuildInterruptOrBulkTransferRequest. MDLs são estruturas opacas, mas se você quer ter acesso ao buffer da aplicação, então devemos chamar a função MmGetSystemAddressForMdlSafe para conseguir o endereço para o buffer a ser escrito/lido. Reparem que o endereço retornado por esta função está em System Space, e assim, acessível em qualquer contexto de processo. Mas o buffer da aplicação não está em User Space? Sim, mas o que temos aqui é uma página de memória física sendo mapeada tanto para o espaço de endereçamento da aplicação quanto para o espaço de endereçamemto de sistema.

O tamanho do buffer descrito pela MDL também é obtido pela stack location da IRP, assim como no método Buffered.

Nem Buffered I/O nem Direct I/O

O terceiro método é simplesmente o não uso dos dois primeiros. No método chamado Neither, o I/O Manager não faz uma cópia em um buffer de sistema nem monta uma MDL para descrever o buffer da aplicação. Quando a IRP chega ao seu driver, você tem acesso ao endereço virtual do buffer oferecido pela aplicação por pIrp->UserBuffer. Este endereço aponta para User Space, e por isso, lembre-se que este endereço só é valido no contexto do processo que solicitou o I/O. O uso deste método requer mais cuidado, pois seu driver precisa ser o primeiro driver na pilha de dispositivos, e dessa forma, garantir que a IRP chegue ao seu driver no contexto do prcesso que fez a solicitação de I/O.

Você pode verificar se você está no contexto do processo que solicitou a operação fazendo o teste abaixo.

/****
***     EstouNoContextoDoProcessoQueGerouEssaIrp
**
**      Função com nome ridículo que verifica se o
**      estamos no contexto do processo que gerou
**      a IRP passada como parâmetro.
*/
 
BOOLEAN EstouNoContextoDoProcessoQueGerouEssaIrp(__in PIRP pIrp)
{
    PETHREAD    pEThread;
    PEPROCESS   pEProcess;
 
    //-f--> Obtém a thread que gerou a IRP
    pEThread = pIrp->Tail.Overlay.Thread;
 
    //-f--> Obtém o processo referente a thread
    pEProcess = IoThreadToProcess(pEThread);
 
    //-f--> Aqui comparamos o processo corrente com
    //      o processo que gerou a IRP
    return (PsGetCurrentProcess() == pEProcess);
}

O fato de estar no contexto do processo correto não garante que o buffer passado como parâmetro seja válido. Diferente do método Direct, neste ponto o I/O Manager não testou o buffer antes de passar a IRP para o driver. Teremos que fazer isso por nós mesmos. Lembre-se que, assim como em User-Mode, ao acessar um buffer inválido o driver receberá uma exceção que deve ser manipulada, caso contrário, tudo azul.

Para realizar o teste, teremos que acessar um byte de cada página do buffer que nos foi passado e verificar se o mundo acaba. As rotinas ProbeForRead e ProbeForWrite fazem isso por nós, mas estas devem ser chamadas dentro de um manipulador de exceção. Vale lembrar que em uma operação de leitura, a aplicação nos envia um buffer onde o driver escreverá dados para a aplicação. Já que o driver fará uma escrita neste buffer, teremos que realizar um teste de escrita (ProbeForWrite) nas operações de leitura (ReadFile). De maneira análoga, o driver deverá fazer um teste de leitura (ProbeForRead) nas operações de escrita (WriteFile). Dê uma olhada no exemplo de rotina de leitura que segue abaixo. Como você já deve estar acostumado, leia os comentários que complementam o texto.

/****
***     OnDispatchRead
**
**      Outra função com nominho besta de exemplo.
**      Valida o buffer enviado pela aplicação pelo
**      método Neither e escreve uma seqüência numérica
**      no buffer da aplicação.
*/
 
NTSTATUS OnDispatchRead(__in PDEVICE_OBJECT pDeviceObj,
                        __in PIRP           pIrp)
{
    PIO_STACK_LOCATION  pStack;
    PVOID               pBuffer;
    ULONG               ulLength;
 
    //-f--> Verifica se estamos no contexto do processo
    //      que gerou esta IRP
    if (!EstouNoContextoDoProcessoQueGerouEssaIrp(pIrp))
    {
        //-f--> Sinaliza ao I/O Manager que a casa caiu
        pIrp->IoStatus.Status = STATUS_INVALID_PARAMETER;
        pIrp->IoStatus.Information = 0;
 
        //-f--> Completa a IRP com falha
        IoCompleteRequest(pIrp, IO_NO_INCREMENT);
        return STATUS_INVALID_PARAMETER;
    }
 
    //-f--> Obtém a stack location corrente
    pStack = IoGetCurrentIrpStackLocation(pIrp);
 
    //-f--> Aqui obtemos o endereço do buffer oferecido
    //      pela aplicação bem como seu tamanho.
    //      Note que este buffer deve estar em User Space
    pBuffer = pIrp->UserBuffer;
    ulLength = pStack->Parameters.Read.Length;
 
    //-f--> As funções que testam o buffer lançam exceções
    //      no caso de o buffer ser inválido. Por isso temos
    //      que fazer o teste dentro de um manipulador de exceções
    __try
    {
        //-f--> Se você der uma olhada na referência, verá que
        //      esta rotina retorna VOID, por isso a única
        //      maneira de saber se o buffer é inválido é
        //      manipulando a exceção que será gerada.
        ProbeForWrite(pBuffer,
                      ulLength,
                      1);
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        NTSTATUS    nts;
 
        //-f--> Ops! Buffer inválido.
        //      Vamos obter o código da exceção e dar um
        //      fim nesse sofrimento
        nts = GetExceptionCode();
 
        pIrp->IoStatus.Status = nts;
        pIrp->IoStatus.Information = 0;
        IoCompleteRequest(pIrp, IO_NO_INCREMENT);
        return nts;
    }
 
    //-f--> Aqui sabemos que o buffer está seguro para receber
    //      escritas. Vamos apenas escrever uma seqüência numérica
    //      só pra...
    for (ULONG i = 0; i < ulLength, i++)
        ((PUCHAR)pBuffer)[i] = (UCHAR)i;
 
    //-f--> Aqui informamos que a operação foi realizada com
    //      sucesso.
    pIrp->IoStatus.Status = STATUS_SUCCESS;
 
    //-f--> Apesar de no método Neither o I/O Manager não
    //      utilizar este número para fazer BPL-BPC, este
    //      número ainda é retornado pela função ReadFile
    //      para informar à aplicação quantos bytes do buffer
    //      são válidos para a aplicação ler.
    pIrp->IoStatus.Information = ulLength;
 
    //-f--> Dá um Fatality na IRP
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}

Para todos os métodos, é importante notar que setar o campo pIrp->IoStatus.Information diz à aplicação a quantidade de bytes foram lidos ou escritos no buffer, independente de haver ou não a cópia de buffer de sistema como no caso do método Buffered.

Outra coisa que não vai mudar para os diferentes métodos é a obtenção do tamanho do buffer oferecido pela aplicação. Este valor sempre vem pela stack location como já foi visto nos métodos já discutidos.

Como selecionar o método

Faz todo o sentido que a escolha do método seja feita antes da primeira IRP chegar ao driver. Isso é feito logo depois que o device é criado. Depois que a chamada à função IoCreateDevice termina, recebemos o novo device através de um ponteiro de saída. O membro Flags da estrutura DEVICE_OBJECT é uma máscara de bits e os bits DO_BUFFERED_IO e DO_DIRECT_IO configuram o método de transferência.

Então é fácil assim. Para configurar o método Buffered, setamos o bit DO_BUFFERED_IO, para configurar o método Direct, setamos o bit DO_DIRECT_IO, e finalmente para setar o método Neither, não setamos nenhum destes bits. Já li em algum livro, que não encontro agora, que o comportamento não é previsto se você setar ambos os bits.

Segue mais um exemplinho besta de como setar o device que acaba de ser criado para transferências no método Buffered.

    //-f--> Cria o device que irá receber as IRPs
    nts = IoCreateDevice(pDriverObj,
                         0,
                         &usDeviceName,
                         FILE_DEVICE_UNKNOWN,
                         0,
                         FALSE,
                         &pDeviceObj);
 
    //-f--> Verifica se o device foi criado
    if (!NT_SUCCESS(nts))
        return nts;
 
    //-f--> Aqui já podemos configurar o método
    //      de transferência desejado, que neste
    //      exemplo é o Buffered I/O
    pDeviceObj->Flags |= DO_BUFFERED_IO;

Mas e se uma IRP for entregue ao driver antes de setarmos estes bits? Outra excelente pergunta. As coisas acontecem assim. Sempre quando um novo device é criado, o bit DO_DEVICE_INITIALIZING é setado. As IRPs só começam a ser entregues a este device quando o driver baixar este bit. Isso nos permite inicializar o device antes que qualquer IRP chegue.

Boa tentativa espertão, mas seu exemplo não baixa este bit. Como você explica isso? Você hoje está impossível! Ao termino da rotina DriverEntry, quando o controle volta ao I/O Manager, ele varre a lista de devices criados pelo driver e baixa este bit por nós. É importante lembrar que ainda precisamos baixar este bit quando criamos um novo device depois que a rotina DriverEntry terminou. Um exemplo muito comum são os devices de WDM, que são criados na rotina AddDevice, mas isso vai ficar para uma outra vez. Esse post já ficou muito longo.

Até mais! 🙂

Uma pitada de Memória Virtual

11 de September de 2008

Agora vamos deixar de conversinha mole e vamos logo ao que interessa. O exemplo que descrevi em outro post mostra como implementar um driver bem simples que armazena uma lista de strings que são enviadas ao driver através de operações de escrita (WriteFile), e as mesmas strings são retornadas em subseqüentes operações de leitura (ReadFile). Esse é um bom exemplo de como os dados das aplicações vão para os drivers e vice-versa. Existem três maneiras de acessar os dados oferecidos por aplicações a partir de um driver. Neste post vamos dar uma olhadinha em memória virtual para que possamos ter uma base para posts futuros, onde poderei explicar as diferenças entre aquelas tais três maneiras. Vamos nos ater a apenas algumas características básicas da memória virtual para que as coisas que explicarei em seguida façam mais sentido para você, mas se você quiser mais detalhes a respeito de Memória Virtual, você pode ler o Capítulo 7, “Memory Management”, do Windows Internals, ou dar uma olhadinha em uma apresentação feita pelo meu amigo Strauss.

O que é uma página de memória

Página de memória é uma unidade utilizada pelo hardware nas proteções de acesso à memória. Mais sobre proteções de páginas de memória aqui. Essa é uma unidade definida pelo hardware que ajuda a subdividir a memória em espaços menores e gerenciáveis. Apesar de existirem dois tamanhos de páginas (uma pequena de 4KB bytes, e outra grande de 4MB), toda referência ao tamanho de página vista na documentação são referentes apenas às páginas pequenas. O tamanho da página de memória varia com a plataforma de hardware. Em sistemas x86 e x64, as paginas são de 4KB enquanto que em sistemas IA64 a página é de 8KB. O tamanho da página pode ser obtido pela constante PAGE_SIZE.

Espaço de endereçamento

De uma maneira bem resumida, memória virtual é um mecanismo, que em um trabalho conjunto entre hardware e software, permite que processos que rodem no Windows tenham seu próprio espaço de endereçamento. Espaço quem? Espaço de endereçamento, ou Address Space como normalmente vimos na referência, permite que cada processo tenha uma visão privada da memória que esteja utilizando. Ou seja, um processo não pode ler ou escrever em páginas de memória de outro processo. Isso evita que um programa mal escrito possa erroneamente escrever em páginas de memória de outro processo ou mesmo em páginas do sistema operacional, comprometendo assim a estabilidade de todo o sistema. Portanto, podemos assumir que o espaço de endereçamento de um processo só é acessível pelo processo ao qual ele pertence.

Nota: É possível compartilhar memória entre processos, mas mesmo neste caso, a mesma página física de memória é referenciada por dois ou mais espaços de endereçamentos diferentes.

Memória Virtual e Memória Física

Logo após a descoberta do fogo, a memória era endereçada diretamente em modo real. Isso significa que um ponteiro utilizado por uma aplicação acessava o dado exatamente onde ele se encontrava fisicamente nos chips de memória RAM. Páginas de memória virtual associadas aos processos refletem páginas físicas de memória através de um endereçamento virtual. Os bits que compõem o endereço virtual mapeam a memória física que se deseja acessar. Ao deferenciar um ponteiro que contem um endereço virtual, o processador, de maneria transparente ao processo, consulta estruturas preenchidas pelo sistema operacional de forma a traduzir o endereço virtual em real, e dessa forma, encontra a página física onde o dado realmente se encontra.


A disposição das páginas de memória virtual não tem relação com sua disposição física. Isso significa que você quando você faz uma grande alocação de memória, que é composta por várias páginas de memória, você ganha um ponteiro que, do ponto de vista da aplicação, aponta para páginas de memória que estão linearmente distribuídas (uma seguida da outra), enquanto que as páginas de memória física podem estar todas espalhadas pelos chips de RAM.

A memória pode não estar na RAM

A memória utilizada pelas aplicações não se limita à quantidade de memória oferecida pelos chips de RAM instalados em sua placa mãe. Quando os processos, cada vez mais sedentos por memória, vão sendo executados, o espaço disponível em RAM vai sendo compartilhado entre os vários processos. Páginas de memória acessadas com menos frequência são removidas dos chips de RAM e vão para disco (para o Pagefile.sys para ser mais preciso), dando lugar à uma outra página de memória que é necessária naquele momento. Esse processo recebe o nome de paginação. Quando a aplicação finalmente acessar o dado que está naquela página agora em disco, o sistema aloca espaço em RAM física para trazer de volta a página do disco para a RAM. Isso pode resultar em outras páginas que estavam em RAM a serem paginadas para o disco.


Para as aplicações, o processo de paginação é completamente transparente. O desenvolvedor não está nem um pouco preocupado se a região de memória que o programa vai acessar está em disco ou está em RAM. É só acessar o dado e o processador junto com o gerenciador de memória resolve isso para você. Infelizmente, a história não é tão florida para os desenvolvedores de drivers. Aplicações sempre rodam em uma prioridade de thread baixa (PASSIVE_LEVEL) e ao acessar um dado que esteja em uma página em disco, a thread é interrompida pelo gerenciador de memória para que este tenha a oportunidade de acionar os drivers de File System e por conseqüência os drivers de disco, aguardar o I/O ser realizado (apesar de levar um cacalhésimo de segundo, ainda temos que esperar) e então a thread é liberada. Embora haja pessoas que não acreditem nessas coisas, a plataforma NT é completamente preemptiva. Isso significa que uma thread pode ser interrompida por outra de maior prioridade. Em Kernel Mode, uma thread pode estar em prioridades superiores à APC_LEVEL, que é a prioridade em que as paginações são realizadas. Se a thread que acessará o dado estiver em um nível de prioridade muito alta (maior ou igual a DISPATCH_LEVEL), o processador não vai conseguir fazer a paginação e uma tela azul irá aflorar aos seus olhos. Um exemplo típico de execução em alta prioridade é o tratamento de interrupções, mas não vamos nos desviar do assunto.

Uma página pode ser acessada não só por um software, mas também por hardware. Hein? Uma aplicação pode passar o ponteiro de uma região de memória para um driver, que por sua vez, irá utilizar um canal de DMA para preencher este buffer. A cópia de um buffer através do processo de DMA não utiliza ciclos de CPU para ser realizada. Preencher um buffer é algo tão simples que até um chimpanzé autista pode fazer. Assim um grupo de chimpanzés autistas engenheiros criaram um chip que faz isso, mas isso não vem ao caso agora. Este chip pode estar na placa mãe ou na própria placa controlada pelo driver. O chip é programado pelo driver e uma transferência é iniciada. Durante a cópia, nem a aplicação, nem o sistema operacional e nem mesmo o processador ficam cientes do que está acontecendo. O sistema pode então determinar que as páginas utilizadas na transferência, que não estão sendo acessadas por nenhum processo, devem ser paginadas para disco. Se isso ocorrer, o chip de DMA continuará tranferido bytes para aquele endereço físico de RAM e… Bom, já sabe né? Para evitar isso, existem meios de avisar o gerenciador de memória que uma ou mais páginas não devem ser paginadas.

Durante o desenvolvimento de um driver, você pode querer alocar um buffer que será utilizado por uma ISR (Interrupt Service Routine). Como uma ISR é executada em alta prioridade, podemos fazer uma alocação de memória solicitando um buffer que não seja paginável, e assim, garantir que o buffer obtido por esta alocação esteja sempre em RAM. Mas vá com calma, memória não paginável é um recurso escasso e deve ser utilizado com parcimônia. Caso contrário, algumas operações vão deixar de ser realizadas pelo sistema por falta de RAM, mesmo que ainda haja muita memória paginável disponível.


User Space e System Space

Adotando um sistema de 32 bits como exemplo, um ponteiro é capaz de endereçar 4 GB de memória. Destes, 2 GB são reservados para endereçar páginas privadas para cada processo, ou seja, vão compor o espaço de endereçamento privado de cada aplicação. Esta faixa, que vai de 0x00000000 ao 0x7FFFFFFF, recebe o nome de User Space e são os endereços que aplicações acessam em User Mode. Conforme vimos, esta faixa compõe o espaço de endereçamento privado de um processo, protegido contra acessos indevidos de outros processos. Um exemplo seria: Somente as threads do Processo A terão acesso ao User Space do Processo A. O mesmo endereço aponta para diferentes páginas físicas de RAM em diferentes processos, dependendo do contexto do processo que faz o acesso. Os outros 2 GB restantes do endereçamento de 32 bits são reservados ao endereçamento de páginas de sistema. A faixa de endereços de 0x80000000 ao 0xFFFFFFFF define o então chamado System Space, que diferente do User Space, endereçam páginas de memória que não são privados a um determinado processo. Isso significa que o mesmo dado (na mesma página física) pode ser acessado através do mesmo endereço virtual em diferentes processos. Um dado em System Space pode ser acessado apenas em Kernel Mode, enquanto que um dado em User Space pode ser acessado tanto em Kernel Mode quanto em User Mode.


Cagamba! Isso foi uma metralhadora de conceitos? Se você apresentar sintomas de visão turva ou vômito seguido de diarréia e tremedeira, larga a mão de ser frouxo e se prepare para os próximos posts. Caso você não tenha apresentado nenhum destes sintomas, não se preocupe, para algumas pessoas o processo é mais demorado. Em posts futuros vou explicar como os drivers acessam os endereços virtuais oferecidos pelas aplicações, como lidam com a paginação de memória e ainda testam buffers oferecidos pelas aplicações. Um driver mal escrito pode permitir que um parâmetro inválido em uma aplicação cause uma tela azul.

Por hoje chega, mas vale lembrar que o que foi apresentado aqui é apenas a ponta do iceberg. Essa pontinha é o que acredito ser o mais relevante para o desevolvimento de drivers, mas ainda existem toneladas de detalhes referentes à memória virtual.

Have fun! 🙂

Step into Kernel (Firewire)

2 de July de 2008

Já sei! Seu computador de teste não tem porta serial e você precisa fazer debug de Kernel nele. Creio que depois da porta serial, a maneira mais utilizada para depurar o Kernel do Windows seja utilizando uma interface firewire. Ainda existe a opção de se fazer o debug de Kernel utilizando USB 2.0, mas isso ainda é para poucos, já que além de apenas ser suportado pelo Windows Vista, ainda é necessário ter um cabo especial. Os detalhes sobre debug de Kernel pela porta USB podem ser encontrados neste post. Hoje a história é outra.

Mas eu não tenho porta firewire

Larga a mão de ser chorão. O que importa aqui não é o fato de você ter ou não uma porta firewire em seu micro de desenvolvimento, mas sim o fato da máquina do cliente ter uma porta firewire. Você sabe muito bem que pela Lei de Murphy, aquele problema que você nem sabia que existia só acontece naquela máquina que não tem portas seriais. Então melhor você estar preparado para encontrar esse tipo de coisa. Elas realmente acontecem. Sua reclamação poderia ainda ser diferente: “Mas eu não tenho porta serial”. Alguns notebooks que não possuem portas seriais oferecem portas firewire, mas idependente disso, existem placas tanto PCI quanto PCMCIA capazes que disponibilizar a interface IEEE 1394. Desta forma, seja sua máquina um desktop ou um notebook, existem meios delas ganharem portas firewire.

Configurando o lado TARGET

Se você ainda não sabe o que significa lado HOST/TARGET e está completamente perdido sobre o assunto, leia este post introdutório antes de continuar. Configurar o lado TARGET não é muito diferente do que já vimos em outros posts desta série. Podemos editar o aquivo boot.ini, como já vimos neste post para adicionar as seguintes configurações de debug.

[boot loader]
timeout=10
default=multi(0)disk(0)rdisk(0)partition(1)\WINDOWS
[operating systems]
multi(0)disk(0)<<...>>/fastdetect /debugport=1394 /channel=44
multi(0)disk(0)<<...>>/fastdetect

O número do canal a ser utilizado pode ser qualquer um, mas o valor deve ser o mesmo em ambos os lados TARGET e HOST. Caso você esteja querendo depurar um Windows Vista, o método de configurar as mesmas coisas mudaram um pouco como vimos neste outro post. A figura a seguir mostra os passos para setar o modo de configuração do sistema para interface IEEE 1394.

Lembre-se que aqui estamos apenas configurando a maneira com a qual o sistema seria depurado caso exista uma entrada de debug na lista de boot da máquina. Este post mostra os detalhes de como criar uma entrada adicional nesta lista e configurá-la para debug.


Só isso? Nem doeu!

Para se fazer Kernel Debug utilizando um cabo firewire, ambos os micros devem estar rodando Windows XP ou superior, não necessariamente a mesma versão em ambos os lados. Existe uma particularidade quanto utilizar sistemas anteriores ao Windows XP SP2 ou Windows 2003 Server sem service pack no lado TARGET da história, conforme informa esta página. Para estes sistemas, deve-se desabilitar a controladora do barramento 1394. Isso é necessário porque o Windows, que está sendo depurado inconscientemente, pode querer tentar conversar com a interface Firewire durante o debug, e isso pode fazer com que a conexão com o depurador caia. Para desabilitar essa interface nos sistemas acima citados, você deve simplesmente selecionar o ítem “Disable” no menu de contexto que aparece quando você clica com o botão direito do mouse sobre a controladora firewire.

Posso desabilitar a controladora Firewire independente da versão do Windows? Não, se você desabilitar a controladora em sistemas posteriores aos acima citados, você poderá não conseguir depurar o sistema quando ele mudar entre os estados de energia do sistema. Estados de energia? Você está falando da aura do computador? Supondo que você está querendo depurar seu driver durante as transições energia do sistema que são gerenciadas pelo Power Manager. O Power Manager determina qual barramento pode ser desligado para economizar energia. Assim, o sistema pode decidir desligar a interface firewire durante seu debug de um driver qualquer, e dessa forma, você não vai conseguir acompanhar as IRPs de gerenciamento de energia chegando ao seu driver.

Configurando o lado HOST

Normalmente, para fazer iniciar uma sessão de debug do lado HOST, basta abrir o WinDbg, selecionar o ítem “Kernel Debug…” do menu File, clicar na aba 1394, preencher o número do canal que se deseja utilizar, clicar OK como mostra a figura abaixo e correr para o abraço.


Tudo isso que acabei de descrever continua valendo, mas para que seja possível utilizar firewire do lado HOST, o WinDbg precisa instalar os drivers virtuais de acesso ao barramento IEEE 1394, como mostra nesta página. O WinDbg faz isso automagicamente quando você seleciona as opções acima, mas os drivers só poderão ser instalados se você estiver logado como administrador do sistema. Caso contrário você receberá a seguinte mensagem.


Pô Fernando, sou desenvolvedor de driver! Você acha mesmo que não sou administrador da minha máquina? Tudo bem, você pode até ser, mas mesmo sendo um administrador no Windows Vista você precisará executar o WinDbg clicando com o botão direito do mouse sobre o ícone do WinDbg e selecionar o ítem “Run as Administrator”. Aí sim, você repete o procedimento descrito acima para que os drivers virtuais sejam instalados. Esse procedimento é necessário somente na primeira vêz que você utiliza a porta firewire para debug, nas próximas vezes, os drivers já estarão instalados. Para quem estiver utilizando um sistema anterior ao Vista e já estiver utilizando uma conta admininstrativa, é como meu amigo Thiago diz: “Sai na urina”. Ou seja, o driver virtual será instalado e você terá a saída como mostra a figura abaixo, que demonstra as mesmas operações realizadas no Windows Vista rodando o WinDbg como Administrador.


Daí em diante é só debug mesmo.

Dump Racing

Aproveitando que estamos todos aqui reunidos, vamos fazer um teste e verificar se a velocidade do firewire ajuda mesmo com relacão à fazer Kernel Debug. Vamos imaginar a situação onde você esteja em visita a um cliente onde obviamente seu driver não está funcionando adequadamente. Lembra daquele bug que você nem sabia que existia? Pois bem, se trata de um deadlock. Deadlock são especialmente queridos na hora de fazer debug, porque você está lá quando o problema acontece, mas tela azul que é bom nada. Nessa situação, você pode gerar um arquivo de dump da máquina e deixar para analizar o problema em casa, afinal de contas, roupa suja se lava em casa, e assim poder liberar a máquina do cliente para uso, já que normalmente nessas situações, ficam umas três pessoas em cima de você perguntando “E aí? Descobriu o problema?” a cada 3 minutos. Configurei meu desktop aqui de casa para fazer debug do Windows Vista por uma porta serial. Esta máquina tem 2GB de memória RAM. Um arquivo de dump full é uma cópia de tudo que esta na memória do computador naquele instante, por isso, nada mais justo que este arquivo tenha aproximadamente 2GB de tamanho. Este arquivo pode ser gerado na máquina HOST durante uma sessão de debug utilizando o comando .dump. Segue abaixo a saída deste comando quando utilizado sobre uma conexão serial.

0: kd> .dump /f c:\Temp\SERIAL.DMP
Creating a full kernel dump over the COM port is a VERY VERY slow operation.
This command may take many HOURS to complete.  Ctrl-C if you want to terminate the command.
Creating c:\Temp\SERIAL.DMP - Full kernel dump
Percent written 0
Percent written 1
Percent written 2
        :
        :

Vocês repararam na mensagem ameaçadora que nos foi exibida? Particularmente penso que estes engenheiros de software são todos uns desesperados, provavelmente por causa quantidade de café que eles consomem por dia. Já posso até imaginar quantos nem esperam o dump começar para já pressionar CTRL+C e interromper o processo. São uns covardes mesmo. Bom, já que isso vai me custar algum tempo, vou aproveitar para dar uma mijada.

Muito, mas muito tempo mesmo depois…

        :
        :
Percent written 97
Percent written 98
Percent written 99
Dump successfully written
0: kd>

OK, tudo bem até aqui. Agora vamos repetir o processo em uma sessão de debug utilizando o cabo firewire. A mesma máquina com a mesma quantidade de memória e até o mesmo comando.

1: kd> .dump /f c:\Temp\FIREWIRE.DMP
Creating c:\Temp\FIREWIRE.DMP - Full kernel dump
Percent written 0
Percent written 5
Percent written 10
        :
        :
Percent written 90
Percent written 95
Dump successfully written
1: kd>

Tá, tudo bem, a contagem vai de cinco em cinco ao invés de um em um como foi com o cabo serial, grande coisa. Não é a toa que demore mais pela porta serial, eles gastam processamento com tudo. Agora podemos comparar as datas de criação e modificação nos atributos de cada arquivo para poder determinar quanto tempo levou para poder gerá-los.


Lembrando que o mês de junho termina no dia 30, podemos concluir que o dump pela porta serial levou aproximadamente 2 dias, 5 horas e 12 minutos. É uma pena essa janela não mostrar os segundos para termos mais precisão aqui. De qualquer forma, lembrando também que cada minuto tem 60 segundos, o mesmo dump gerado pela porta firewire levou aproximadamente 7 minutos. Nossa, foi quase! Se não fosse por essa pequena vantagem de 3187 minutos.

Até mais…

Utilizando o Registry (Parte 2)

23 de June de 2008

Pois é, como eu estava dizendo na primeira parte deste post. Isso é muito código. Todo esse negócio parece trabalhoso, não? Montar uma UNICODE_STRING para o caminho da chave que se deseja acessar, montar um OBJECT_ATTRIBUTES com ela, abrir handle, determinar o tamanho dos valores a serem lidos, alocar um buffer grande suficiente, ler o valor que se deseja, desalocar qualquer buffer temporário utilizado no processo, e por fim, fechar o handle para a chave do registro. Você pode conferir quanto código utilizamos em nosso exemplo do post anterior para se ler dois valores no registro. Hoje veremos um grupo de rotinas de manipulação de registro que facilita esse processo.

Rotinas RTL de Registro

São apenas cinco as rotinas que compõem esse grupinho. Na referência você encontra uma descrição de cada uma delas. Esse grupo de rotinas tem como característica comum o fato de não termos que utilizar handles para as chaves. Os handles são abertos e fechados para cada operação. Outra característica comum é que você não precisa compor o nome completo da chave na qual estão os valores que você quer ler ou escrever. Essas rotinas trabalham com um esquema de nome de chave relativo a algum lugar pré-definido. Confuso? Vamos dar uma olhada numas das rotinas para que tenhamos um exemplo mais prático. Tentando seguir a mesma idéia do exemplo dado no post anterior, precisamos inicialmente verificar se a chave, onde os valores estão armazenados, existe de fato. Tem uma funçãozinha justamente para isso.

NTSTATUS 
  RtlCheckRegistryKey(
    IN ULONG  RelativeTo,
    IN PWSTR  Path
    );

Notem que o primeiro parâmetro dessa rotina é uma constante que indica qual será a referência a ser aplicada ao segundo parâmetro. Observe a tabela abaixo que foi retirada da referência.


Um exemplo seria o seguinte: Se usarmos RTL_REGISTRY_ABSOLUTE como primeiro parâmetro, o segundo deve ser o caminho completo da chave de registro. Afinal, temos que passar o caminho absoluto da chave. Por outro lado, se utilizarmos RTL_REGISTRY_SERVICES, o segundo parâmetro deveria ser apenas o complemento do caminho referente a chave Services. Foi esta a opção que utilizei no exemplo deste post. Observe como ficou nossa rotina de criação dos parâmetros no registro.

/****
***     CreateParams
**
**      Esta rotina é chamada quando não for detectada a chave
**      que contém os parâmetros. Ela cria a chave e os parâmetros
**      com valores pré-definidos.
*/
NTSTATUS
CreateParams(VOID)
{
    NTSTATUS        nts = STATUS_SUCCESS;
    WCHAR           wzParam[] = L"DriverEntry.com.br";
    ULONG           ulParam = 0x12345678;
 
    __try
    {
        //-f--> Cria a chave onde os parâmetros serão armazenados.
        //      Simples assim.
        nts = RtlCreateRegistryKey(RTL_REGISTRY_SERVICES,
                                   L"KernelReg2\\Parameters");
        if (!NT_SUCCESS(nts))
            ExRaiseStatus(nts);
 
        //-f--> Aqui escrevemos o valor numérico
        nts = RtlWriteRegistryValue(RTL_REGISTRY_SERVICES,
                                    L"KernelReg2\\Parameters",
                                    L"DoubleWord",
                                    REG_DWORD,
                                    &ulParam,
                                    sizeof(ulParam));
        if (!NT_SUCCESS(nts))
            ExRaiseStatus(nts);
 
        //-f--> E aqui a String. Notem que não foi necessário
        //      criar um UNICODE_STRING para isso.
        nts = RtlWriteRegistryValue(RTL_REGISTRY_SERVICES,
                                    L"KernelReg2\\Parameters",
                                    L"String",
                                    REG_SZ,
                                    wzParam,
                                    sizeof(wzParam));
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        //-f--> Ops!
        nts = GetExceptionCode();
        ASSERT(FALSE);
    }
 
    return nts;
}

Mais uma vêz, o código demonstrado aqui está disponível para download ao final deste post.

O fácil que complica

Pelo que pudemos ver até aqui, esse grupo de rotinas já facilitou um pouco nossa vida. Mas é na rotina de leitura que podemos ver a economia de código acontecer de verdade. O custo disso fica por conta de entender como a rotina de leitura funciona.

NTSTATUS 
  RtlQueryRegistryValues(
    IN ULONG  RelativeTo,
    IN PCWSTR  Path,
    IN PRTL_QUERY_REGISTRY_TABLE  QueryTable,
    IN PVOID  Context,
    IN PVOID  Environment  OPTIONAL
    );

`

Olhando assim nem parece que ela morde, mas se você der uma olhada na referência, verá que ela pode ser bem flexivel. Flexibilidade as vezes é traduzido por “Difícil de acertar a maneira certa de utilizar”. Os dois primeiros parâmetros são os já conhecidos de antes. A coisa começa a mudar com o terceiro parâmetro, que é uma tabela que contém os detalhes dos valores a serem lidos do registro. Não vou entrar nos detalhes de cada parâmetro desta rotina neste post. Vou apenas fazer o código equivalente ao exemplo do post anterior.

/****
***     LoadParams
**
**      Esta rotina carrega as variaveis globais com os
**      valores recuperados do registro.
*/
NTSTATUS
LoadParams(VOID)
{
    NTSTATUS                    nts = STATUS_SUCCESS;
    RTL_QUERY_REGISTRY_TABLE    QueryTable[] =
    {
        { NULL, RTL_QUERY_REGISTRY_DIRECT | RTL_QUERY_REGISTRY_REQUIRED,
          L"DoubleWord", &g_ulParam, REG_NONE, NULL, 0 },
        { NULL, RTL_QUERY_REGISTRY_DIRECT | RTL_QUERY_REGISTRY_REQUIRED,
          L"String", &g_usParam, REG_NONE, NULL, 0 },
        { NULL, 0, NULL, NULL, REG_NONE, 0, NULL }
    };
 
    __try
    {
        //-f--> Caso esta rotina seja chamada mais de uma vez, vamos liberar
        //      o buffer deste parâmetro.
        if (g_usParam.Buffer)
            RtlFreeUnicodeString(&g_usParam);
 
        //-f--> Aqui todos os valores da tabela são lidos
        nts = RtlQueryRegistryValues(RTL_REGISTRY_SERVICES,
                                     L"KernelReg2\\Parameters",
                                     QueryTable,
                                     NULL,
                                     NULL);
        if (!NT_SUCCESS(nts))
            ExRaiseStatus(nts);
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        //-f--> Ops!
        nts = GetExceptionCode();
        ASSERT(FALSE);
    }
 
    return nts;
}

Não, não está faltando nada. É só isso mesmo. Vamos olhar mais de perto o que fizemos aqui. A tabela que preenchemos tem os dados das leituras a serem realizadas e é definida como mostra abaixo.

typedef struct _RTL_QUERY_REGISTRY_TABLE {
    PRTL_QUERY_REGISTRY_ROUTINE QueryRoutine;
    ULONG Flags;
    PWSTR Name;
    PVOID EntryContext;
    ULONG DefaultType;
    PVOID DefaultData;
    ULONG DefaultLength;
} RTL_QUERY_REGISTRY_TABLE, *PRTL_QUERY_REGISTRY_TABLE;

Olhando cada membro dela temos:


  • QueryRoutine: Aqui você tem um ponteiro de função que será chamado quando o este ítem da tabela for lido no registro. Não usamos este método em nosso exemplo. Existem algumas coisas interessantes sobre essa rotina de callback. Ela é chamada para cada ítem desta tabela, mas se o valor a ser lido for do tipo REG_MULTI_SZ, a rotina de callback é chamada uma vez para cada string contida neste valor. Consulte a referência para uma completa descrição destes detalhes.
  • Flags: O flag que utilizamos aqui foi o RTL_REGISTRY_DIRECT que sinaliza que não usaremos o método do callback. Outro flag é o RTL_QUERY_REGISTRY_REQUIRED que indica que o valor descrito nesta linha da tabela é obrigatório. Caso o valor não seja encontrado, RtlQueryRegistryValues retorna erro e os demais valores não serão lidos.
  • Name: Aqui vem o nome do valor a ser lido. Note que não é um UNICODE_STRING, mas um array de WCHAR terminado em zero.
  • EntryContext: Em nosso exemplo, este parâmetro é o buffer onde o valor lido será armazenado, mas nos casos onde recebemos a chama pela a rotina de callback, este valor é passado como um dos parâmetros.


Os parâmetros DefaultType, DefaultData e DefaultLengh seriam utilizados caso o valor não existisse e não fosse um valor obrigatório.

Cada valor a ser lido é representado por uma linha nesta tabela. A rotina identifica o fim da tabela quando encontrar os membros QueryRotuine e Name forem NULL.

Calma aí Fernando! Muito bem, nada nessa manga, nada nessa e os valores são lidos? Espertinho você hein? Pensou que ia enganar todo mundo com essa conversinha? Muito bem! Onde é que eu indico o tamanho do buffer oferecido para a leitura?

Muito boa a sua pergunta! Acho até que eu mesmo não teria feito uma pergunta tão boa. O tamanho o buffer é indicado pelo próprio buffer. No caso da string, passamos um ponteiro para um UNICODE_STRING, o tamanho do buffer da string já é descrito por essa estrutura, mas se o buffer desta estrutura for NULL, a rotina aloca o buffer do tamanho necessário. Temos que desalocar esse buffer quando não for mais utilizá-lo. Para leituras onde o buffer resultante é menor ou igual ao sizeof(ULONG) então o valor resultante é armazenado diretamente no lugar apontado por EntryContext. Para demais detalhes consulte a referência.

Esta função é bem flexível e não vou conseguir colocar tudo que essa rotina oferece em um único post, mas sintam-se a vontade para mandar suas dúvidas em relação a ela.

Have fun!

KernelReg2.zip

Utilizando o Registry (Parte 1)

16 de June de 2008

No último post, em resposta à uma dúvida de um leitor, comentei um pouco sobre como criar e usar novas IOCTLs. Afinal de contas, o lema desse blog é “Servir bem para servir sempre”. Dúvidas de leitores são ótimas fontes de sugestões para novos posts. Creio que como em qualquer outra especialidade, o desenvolvimento de drivers é um tópico que pode ser desmembrado em muitas e muitas partes. Não é a toa que o menor livro que eu conheço sobre desenvolvimento de drivers não tenha menos de quatrocentas páginas. Por isso é bom saber quais são as dificuldades dos leitores para saber sobre qual assunto postar aqui. Sintam-se à vontade para mandar novas sugestões ou dúvidas. Já há algum tempo, recebi um e-mail de Fábio Dias (Fortaleza, CE), que sugeriu um post sobre como acessar o Registry a partir de um driver. Muito bem, vamos lá.

HKEY_LOCAL_MACHINE é coisa se User Mode

Antes de sair dizendo quais APIs você deve usar para acessar o registro, vamos antes dar uma olhada em como o registro está organizado. Creio que o primeiro passo aqui seja falar sobre como abrir uma chave do registro. Assim como em User Mode, temos que ter uma string que informe o caminho da chave a qual queremos abrir, e assim obter um handle para ela. Opa! Eu disse handle? Sendo assim, vale comentar que as chaves do registro também são recursos gerenciados pelo Object Manager. As chaves do registro estão todas armazenadas sob o namespace “\Registry”. Desta forma, enquanto usamos HKEY_LOCAL_MACHINE em User Mode como nome de uma das divisões básicas do registro, em Kernel Mode usamos “\Registry\Machine”. E de maneira análoga à HKEY_USERS temos “\Registry\Users”. Mais detalhes aqui.

Qual é o CurrentControlSet?

A rotina DriverEntry de qualquer driver para Windows NT recebe dois parâmetros de entrada, sendo um deles um ponteiro para a estrutura DRIVER_OBJECT que representa a instância do nosso driver, e o outro parâmetro é um UNICODE_STRING contendo o caminho do registro que indica onde nosso driver está cadastrado. Mas como assim? Nosso driver pode não saber como ele foi cadastrado no registro? Aqui na referência é simples:



“The registry path string pointed to by RegistryPath is of the form \Registry\Machine\System\CurrentControlSet\Services\DriverName”

Tudo bem, vamos pegar carona em um driver qualquer e dar uma olhada nisso utilizando o WinDbg. Vamos abrir um parênteses aqui para informar aos mais novos neste blog que estou utilizando uma máquina virtual para fazer os testes com o driver de exemplo, como explica neste post. Isso permite que eu possa fazer Debug de Kernel sem necessariamente ter que utilizar duas máquinas. Para facilitar a substituição do driver a cada modificação que faço, estou utilizando o mapeamento de drivers do WinDbg, como explica neste outro post. Este recurso permite que o WinDbg sempre carregue uma nova versão driver na máquina TARGET sem que eu tenha que necessariamente substituir o driver nela. Fecha parênteses.

Antes mesmo do driver ser carregado, coloquei um breakpoint na rotina DriverEntry. Opa! Como você pode colocar um breakpoint em um driver que ainda não foi carregado? Ah tá… Você pode fazer isso utilizando o comando bu como mostra abaixo. Os breakpoints são setados quando o driver for carregado. Na linha seguinte, eu apenas listo os breakpoints, só pra… Quando o driver é carregado, a execução pára em meu breakpoint e uso a extensão !ustr, que mostra UNICODE_STRINGs, para ver o valor desse meu segundo parâmetro da DriverEntry.

kd> bu KernelReg!DriverEntry
kd> bl
 0 eu             0001 (0001) (KernelReg!DriverEntry)
 
kd> g
KD: Accessing 'Z:\Sources\DriverEntry\KernelReg\objchk_w2k_x86\i386\KernelReg.sys'
    (\??\C:\WINDOWS\system32\drivers\KernelReg.sys)
  File size 2K.
MmLoadSystemImage: Pulled \??\C:\WINDOWS\system32\drivers\KernelReg.sys from kd
Breakpoint 0 hit
KernelReg!DriverEntry:
f8d394a0 8bff            mov     edi,edi
kd> !ustr poi(pusRegistryPath)
String(114,114) at 82302000: \REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\KernelReg

Mas não era pra ser CurrentControlSet? Na verdade o CurrentControlSet é um link para um outro ControlSet. No caso observado acima, o CurrentControlSet está refletindo o ControlSet001, ou seja, as alterações feitas no ControlSet001 são vistas no CurrentControlSet e vice-versa. O CurrentControlSet pode refletir qualquer outro ControlSet. Algumas regras determinam quando eles mudam, como em casos de mudanças de configurações de sistema ou instalações de drivers. Para saber para onde o CurrentControlSet está apontando, dê uma olhada no valor Current que está na chave “\Registry\Machine\System\Select” como mostra a figura abaixo.


Mas isso é apenas para efeito de curiosidade. Quando você utiliza CurrentControlSet como parte do caminho da chave que você deseja acessar, o Object Manager faz a traducão pra você e todos vivem felizes para sempre.

Cadê \Registry\Machine\Software?

Já pode ter acontecido com alguns de vocês. Vocês escrevem um driver que deve ler uma certa chave dentro de “\Registry\Machine\Software” quando o driver é carregado. Enquanto você faz testes com o driver, que neste momento tem seu Start configurado para Manual(3), tudo funciona que é uma maravilha, mas na hora de mudar o Start do driver para Boot(0) ou System(1), não conseguimos abrir a chave recebendo STATUS_OBJECT_NAME_NOT_FOUND (0xC00000034).

Como assim nome não encontrado? Estava aqui agora mesmo! O que acontece é que a chave SOFTWARE é montada mais tarde. Assim, durante o boot do sistema ela ainda não existe. Para fazer acesso a essa chave, você terá que esperar um cadim, talvez utilizando a tática deste post. Aí tudo funciona que é uma beless.

Algo a dizer sobre HKEY_CURRENT_USER?

Então, veja bem. Quem é o Current User? Usuário que está fazendo a chamada, certo? Assim, conforme a tradição, você começa pensando que é fácil e estando logado como Paulo, você inicia seu driver manualmente. O driver, durante a chamada à DriverEntry, lê configurações referentes ao usuário logado, que de fato estão armazenadas dentro de HKEY_CURRENT_USER do Paulo. Bom, acho que está certo até aqui. Já ouviram aquela expressão “Quem muito acha acaba se perdendo”? Pois é, tá tudo errado. Para Paulo, as configurações estão lá mesmo, mas a rotina DriverEntry é chamada em contexto de sistema, que por sinal não é Paulo.

Esse é um problema enfrentado mesmo em User Mode, quando serviços, que são executados em conta de sistema, tentam abrir a chave HKEY_CURRENT_USER. Uma coisa que temos que ter em mente é que “O usuário logado” não é uma informação única do sistema. Tentem imaginar um Terminal Service, podem haver vários usuários logados ao mesmo tempo. Mesmo quando há somente um usuário logado no sistema, threads podem ser executadas em contexto de outros usuários ou mesmo em contexto de sistema. Drivers não tem contexto próprio. Algumas partes do driver são executadas em contexto de sistema, outras em contexto arbitrário. Dependendo do tipo de driver e sua posiçao dentro da pilha de dispositivos, ele pode receber as chamadas em contexto do usuário. Que é o caso dos drivers de File System por exemplo.

Ah tá! Aí sim HKEY_CURRENT_USER vai funcionar! Er… Como posso dizer isso? Não, não vai funcionar. Segundo a referência da Microsoft, não existe uma tradução simples para HKEY_CURRENT_USER, mas existem rotinas que oferecem simplificações. Depois de toda essa conversinha sobre contexto e tals, você ainda terá que acessar a chave HKEY_USERS, ou melhor dizendo, “\Registry\User”, no formato já conhecido dos programadores User Mode utilizado com HKEY_USERS. Um exemplo é: “\Registry\User\S-1-5-21-73586283-1897051121-839522115-500”. O equivalente para uma conta de sistema é a chave “\Registry\User\.DEFAULT”.

Tá tá tá! Exemplo agora

Mais uma vez, estou assumindo que você já sabe compilar e instalar um driver como explicado neste post. O código exemplo disponível para download ao final deste post também pode ser compilado pelo Visual Studio utilizando o DDKBUILD, como explica este post. Neste exemplo vou mostrar a leitura de dois valores do registro que estão em uma chave como mostra a figura abaixo. Para deixar a brincadeira mais divertida, quando o driver for executado pela primeira vez, este criará a chave e os valores nela contidos quando perceber que estes ainda não existem.


Os valores estarão contidos em uma sub-chave da chave que recebemos como parâmetro na rotina DriverEntry. Então, nosso trabalho básico na codificação da DriverEntry é justamente criar o caminho completo da chave do registro que conterá estes valores. Com esta UNICODE_STRING em mãos, a passaremos para as rotinas de leitura e escrita no registro. Segue abaixo a codificação da rotina DriverEntry. Ela não mostra nada de especial com relação ao registro. Como sempre, vale a pena dar uma lida nos comentários.

/****
***     DriverEntry
**
**      Ponto de entrada do nosso driver.
**      Faca nos dentes e sangue nos olhos.
*/
extern "C"
NTSTATUS
DriverEntry(__in PDRIVER_OBJECT  pDriverObject,
            __in PUNICODE_STRING pusRegistryPath)
{
    UNICODE_STRING  usParameters = RTL_CONSTANT_STRING(L"\\Parameters");
    UNICODE_STRING  usFullRegPath = {0, 0, NULL};
    PWCHAR          pwzBuffer = NULL;
    NTSTATUS        nts = STATUS_SUCCESS;
 
    //-f--> Aqui recebemos como parâmetro o caminho do registro até
    //      a chave do nosso driver. Nossos parâmetros estão em uma
    //      sub-chave chamada "Parameters". Vamos montar o caminho
    //      completo apendando o nome da sub-chave ao caminho do
    //      registro que recebemos como parâmetro.
    __try
    {
        __try
        {
            //-f--> Seta a rotina de callback de descarga do driver.
            pDriverObject->DriverUnload = OnDriverUnload;
 
            //-f--> Para fazer esse append, precisaremos de um buffer
            //      que seja grande o bastante para armazenar a string
            //      original mais o tamanho da sub-chave. Aqui alocamos
            //      este buffer.
            pwzBuffer = (PWCHAR)ExAllocatePoolWithTag(PagedPool,
                                                      pusRegistryPath->Length +
                                                      usParameters.Length,
                                                      _KRN_REG_TAG);
            if (pwzBuffer == NULL)
                ExRaiseStatus(STATUS_INSUFFICIENT_RESOURCES);
 
            //-f--> Agora que temos o buffer, vamos inicializar a estrutura
            //      UNICODE_STRING referente a string resultante deste append.
            RtlInitEmptyUnicodeString(&usFullRegPath,
                                      pwzBuffer,
                                      pusRegistryPath->Length +
                                      usParameters.Length);
 
            //-f--> Copia a string recebida como parâmetro
            RtlCopyUnicodeString(&usFullRegPath,
                                 pusRegistryPath);
 
            //-f--> Concatena a string "\Parameters"
            RtlAppendUnicodeStringToString(&usFullRegPath,
                                           &usParameters);
 
            //-f--> Utilizando o caminho completo, tenta carregar os parâmetros
            nts = LoadParams(&usFullRegPath);
            if (!NT_SUCCESS(nts))
            {
                //-f--> Em caso de falha, verifica se a causa foi a falta da
                //      sub-chave no registro. Isso deve acontecer quando o
                //      driver for executado pela primeira vez. Mas se a falha
                //      for outra, então cada um com seus pobrema.
                if (nts != STATUS_OBJECT_NAME_NOT_FOUND)
                    ExRaiseStatus(nts);
 
                //-f--> Vamos criar a sub-chave e gravar os valores
                //      pré-definidos.
                nts = CreateParams(&usFullRegPath);
                if (!NT_SUCCESS(nts))
                    ExRaiseStatus(nts);
 
                //-f--> Tenta ler os parâmetros novamente.
                //      Tá, eu sei que isso é burrice. Afinal, se eu
                //      acabei de criar os parâmetros, por quê eu iria
                //      lê-los novamente? Bom, pra ilustrar.
                nts = LoadParams(&usFullRegPath);
                if (!NT_SUCCESS(nts))
                    ExRaiseStatus(nts);
            }
        }
        __finally
        {
            //-f--> Limpando a bagunça.
            if (pwzBuffer)
                ExFreePool(pwzBuffer);
        }
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        //-f--> Ops!
        nts = GetExceptionCode();
        ASSERT(FALSE);
    }
 
    return nts;
}

Aqui é mais brincadeira de UNICODE_STRING mesmo. Vamos dar uma olhada na rotina que grava os valores no registro. Esta rotina também não oferece nada de muito diferente do que já estamos habituados a ver em User Mode. Mas é como se diz por aí: “Um exemplo vale mais que mil artigos”. Nossa, essa foi terrível! Eu preciso parar com isso. Vocês devem estar pensando agora: “Caraca! Cada coisa que a gente precisa ler pra poder ter um exemplo de driver”.

/****
***     CreateParams
**
**      Esta rotina é chamada quando não for detectada a chave
**      que contém os parâmetros. Ela cria a chave e os parâmetros
**      com valores pré-definidos.
*/
NTSTATUS
CreateParams(__in    PUNICODE_STRING pusRegistryPath)
{
    HANDLE              hKey = NULL;
    NTSTATUS            nts = STATUS_SUCCESS;
    OBJECT_ATTRIBUTES   ObjAttributes;
    UNICODE_STRING      usStringParam = RTL_CONSTANT_STRING(L"String");
    UNICODE_STRING      usDWordParam = RTL_CONSTANT_STRING(L"DoubleWord");
    ULONG               ulParam = 0x12345678;
    WCHAR               wzParam[] = L"DriverEntry.com.br";
 
    __try
    {
        __try
        {
            //-f--> Aqui não tem segredo. É tudo pá pum.
            InitializeObjectAttributes(&ObjAttributes,
                                       pusRegistryPath,
                                       OBJ_CASE_INSENSITIVE,
                                       NULL,
                                       NULL);
 
            //-f--> Cria a nova chave e já solicitamos
            //      acesso para setar valores dentro dela.
            nts = ZwCreateKey(&hKey,
                              KEY_SET_VALUE,
                              &ObjAttributes,
                              0,
                              NULL,
                              REG_OPTION_NON_VOLATILE,
                              NULL);
            if (!NT_SUCCESS(nts))
                ExRaiseStatus(nts);
 
            //-f--> Seta o valor numérico
            nts = ZwSetValueKey(hKey,
                                &usDWordParam,
                                0,
                                REG_DWORD,
                                &ulParam,
                                sizeof(ulParam));
            if (!NT_SUCCESS(nts))
                ExRaiseStatus(nts);
 
            //-f--> Seta o valor string
            nts = ZwSetValueKey(hKey,
                                &usStringParam,
                                0,
                                REG_SZ,
                                &wzParam,
                                sizeof(wzParam));
            if (!NT_SUCCESS(nts))
                ExRaiseStatus(nts);
        }
        __finally
        {
            //-f--> Fecha o handle da nova chave criada
            if (hKey)
                ZwClose(hKey);
        }
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        //-f--> Ops!
        nts = GetExceptionCode();
        ASSERT(FALSE);
    }
    return nts;
}

Por fim a parte mais divertida desta história. Creio que a maior parte da diversão está concentrada na rotina ZwQueryValueKey, que realiza a leitura de valores no registro.

NTSTATUS 
  ZwQueryValueKey(
    IN HANDLE  KeyHandle,
    IN PUNICODE_STRING  ValueName,
    IN KEY_VALUE_INFORMATION_CLASS  KeyValueInformationClass,
    OUT PVOID  KeyValueInformation,
    IN ULONG  Length,
    OUT PULONG  ResultLength
    );

O ponto que difere da API usada em User Mode para fazer a mesma tarefa é o terceiro parâmetro KEY_VALUE_INFORMATION_CLASS. Um enum que informa quais as informações gostaríamos de obter sobre determinado valor no registro. Um deles solicita apenas o nome enquanto outro solicita todas as informações e o último informações parciais referentes ao valor.

typedef enum _KEY_VALUE_INFORMATION_CLASS {
  KeyValueBasicInformation,
  KeyValueFullInformation,
  KeyValuePartialInformation
} KEY_VALUE_INFORMATION_CLASS;

Para cada valor do enum, uma estrutura diferente é retornada. Sempre em um único bloco, a estrutura pode trazer várias informações utilizando Offsets de onde encontrar o dado dentro daquela única alocação de memória.

No exemplo, utilizei o KeyValuePartialInformation, que imagino ser o valor mais utilizado. Mais uma vez fica minha dica para que leiam os comentários trecho abaixo.

/****
***     LoadParams
**
**      Esta rotina carrega as variáveis globais com os
**      valores recuperados do registro.
*/
NTSTATUS
LoadParams(__in PUNICODE_STRING pusRegistryPath)
{
    HANDLE              hKey = NULL;
    NTSTATUS            nts = STATUS_SUCCESS;
    OBJECT_ATTRIBUTES   ObjAttributes;
    UNICODE_STRING      usStringParam = RTL_CONSTANT_STRING(L"String");
    UNICODE_STRING      usDWordParam = RTL_CONSTANT_STRING(L"DoubleWord");
    ULONG               ulBytes = 0;
    PWCHAR              pwzBuffer = NULL;
    PKEY_VALUE_PARTIAL_INFORMATION  pValueInfo = NULL;
 
    __try
    {
        __try
        {
            //-f--> Monta o ObjectAttribute
            InitializeObjectAttributes(&ObjAttributes,
                                       pusRegistryPath,
                                       OBJ_CASE_INSENSITIVE,
                                       NULL,
                                       NULL);
 
            //-f--> Tenta abrir a chave do registro
            nts = ZwOpenKey(&hKey,
                            GENERIC_READ,
                            &ObjAttributes);
            if (!NT_SUCCESS(nts))
                ExRaiseStatus(nts);
 
            //-f--> Como o valor tem tamanho fixo, podemos determinar
            //      o tamanho do buffer necessário para ler o valor do
            //      registro.
            ulBytes = sizeof(KEY_VALUE_PARTIAL_INFORMATION) + 
                      sizeof(ULONG) - sizeof(UCHAR);
 
            //-F--> Aloca o buffer para a leitura.
            pValueInfo = (PKEY_VALUE_PARTIAL_INFORMATION)
                ExAllocatePoolWithTag(PagedPool,
                                      ulBytes,
                                      _KRN_REG_TAG);
            if (!pValueInfo)
                ExRaiseStatus(STATUS_INSUFFICIENT_RESOURCES);
 
            //-f--> Aqui fazemos a leitura.
            nts = ZwQueryValueKey(hKey,
                                  &usDWordParam,
                                  KeyValuePartialInformation,
                                  pValueInfo,
                                  ulBytes,
                                  &ulBytes);
            if (!NT_SUCCESS(nts))
                ExRaiseStatus(nts);
 
            //-f--> Nos certificamos de que lemos um DWORD
            ASSERT(pValueInfo->Type == REG_DWORD);
 
            //-f--> Aqui nós carregamos nossa variável global
            //      com o valor lido do registro.
            g_ulParam = *(PULONG)pValueInfo->Data;
 
            //-f--> Libera buffer da leitura e zera ponteiro.
            //      Você pode decidir alocar o buffer de leituras
            //      baseado no maior valor que você precisa ler,
            //      e assim, utilizar o mesmo buffer para ler todos
            //      os valores menores. Aqui, mais uma vez estou
            //      refazendo tudo para demonstrar.
            ExFreePool(pValueInfo);
            pValueInfo = NULL;
 
            //-f--> Como a string pode ter qualquer tamanho no registro,
            //      vamos oferecer zero bytes de buffer de leitura para
            //      que a API nos diga quanto ela precisa para todo o buffer.
            nts = ZwQueryValueKey(hKey,
                                  &usStringParam,
                                  KeyValuePartialInformation,
                                  NULL,
                                  0,
                                  &ulBytes);
 
            //-f--> Temos que receber um destes erros.
            ASSERT(nts == STATUS_BUFFER_OVERFLOW ||
                   nts == STATUS_BUFFER_TOO_SMALL);
 
            //-f--> Aqui alocamos o um buffer do tamanho
            //      solicitado pela API.
            pValueInfo = (PKEY_VALUE_PARTIAL_INFORMATION)
                ExAllocatePoolWithTag(PagedPool,
                                      ulBytes,
                                      _KRN_REG_TAG);
            if (!pValueInfo)
                ExRaiseStatus(STATUS_INSUFFICIENT_RESOURCES);
 
            //-f--> Agora faremos a leitura novamente, mas agora
            //      oferencendo um buffer de tamanho descente.
            nts = ZwQueryValueKey(hKey,
                                  &usStringParam,
                                  KeyValuePartialInformation,
                                  pValueInfo,
                                  ulBytes,
                                  &ulBytes);
            if (!NT_SUCCESS(nts))
                ExRaiseStatus(nts);
 
            //-f--> Temos que ter lido uma string.
            ASSERT(pValueInfo->Type == REG_SZ);
 
            //-f--> Agora vamos alocar o buffer que será utilizado para manter
            //     a string lida do registro. Assim podemos descartar o buffer
            //     utilizado pela leitura. O campo DataLength traz o tamanho da
            //     string unicode com o terminador zero. Repare que estra estrutura
            //     não traz uma UNICODE_STRING, e sim um array de WCHAR.
            pwzBuffer = (PWCHAR)ExAllocatePoolWithTag(PagedPool,
                                                      pValueInfo->DataLength,
                                                      _KRN_REG_TAG);
            if (!pwzBuffer)
                ExRaiseStatus(STATUS_INSUFFICIENT_RESOURCES);
 
            //-f--> Depois de algum tempo programando, você fica ligêro com algumas
            //      coisas. Aposto que alguém, e isso inclui a mim mesmo, um dia vai
            //      dar um Copy-Paste desta função para usar em outro lugar. Aqui,
            //      prevendo que a leitura do registro pode acontecer várias vezes,
            //      e assim, se houver a alocação de uma leitura anterior, vamos
            //      desalocá-la.
            if (g_usParam.Length)
                RtlFreeUnicodeString(&g_usParam);
 
            //-f--> Inicializa string global com o buffer que acabamos de alocar.
            //      Apesar de setarmos um buffer para esta UNICODE_STRING, este
            //      ainda está vazio (Length=0)
            RtlInitEmptyUnicodeString(&g_usParam,
                                      pwzBuffer,
                                      (USHORT)pValueInfo->DataLength);
 
            //-f--> Aqui apendamos o WCSTR (array de WCHAR terminado em zero), lido
            //      do registro em nosso buffer vazio. Isso é similar a uma cópia,
            //      mas com uma incrivel vantagem. A de não utilizar wcslen().
            //      Liga não, é paranóia de purista. Assim deixamos por conta da API
            //      determinar o Length e MaximumLegth da UNICODE_STRING. Lembre-se
            //      de que o terminador zero não é contado como parte da UNICODE_STRING.
            RtlAppendUnicodeToString(&g_usParam,
                                     (PCWSTR)pValueInfo->Data);
        }
        __finally
        {
            //-f--> Faxina geral
            if (pValueInfo)
                ExFreePool(pValueInfo);
 
            if (hKey)
                ZwClose(hKey);
        }
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        //-f--> Ops! I did it again.
        nts = GetExceptionCode();
        ASSERT(FALSE);
    }
 
    return nts;
}

Ufa! Quanto código! Nesta primeira parte do assunto busquei utilizar as APIs que mais se parecem com as APIs de User Mode. No próximo post trarei uma maneira diferente de fazer a mesma coisa que fizemos aqui.

Espero ter ajudado,
Até mais…

KernelReg.zip

Criando e usando IOCTLs

7 de June de 2008

Essa semana recebi a seguinte pergunta de um leitor:

“É possível meu aplicativo passar um IOCTL customizável (feito por mim) para o driver, e este reconhecer sem problemas?”

A resposta à curto prazo é: “Sim, e boa sorte!”, mas que graça tem ter um blog se não podemos falar um pouco mais a respeito e até dar um exemplo assim de lambuja? Este post assume que você já sabe alguns conceitos básicos, tais como compilar, instalar e testar drivers. Mas se você ainda não sabe fazer isso, não se preocupe, a vida ainda vale a pena. Basta ler este post.

O driver que calculava

Vamos criar um driver de exemplo que usa a mesma idéia que meu amigo Heldai já usava para ilustrar o uso de IOCTLs quando eu ainda estava aprendendo a fazer telas azuis. No exemplo de hoje vamos fazer um driver que some dois números contidos em uma estrutura que será recebida via um IOCTL.

Sem mais blablablas acho que podemos começar pela definição da estrutura. Como a tia do prezinho já nos ensinou, vamos criar apenas um arquivo de header que seja incluído tanto pelo projeto da aplicação como pelo projeto do driver. Este arquivo de header não deve incluir nenhum outro arquivo de header específico de User Mode e nem de Kernel Mode, ou seja, nada de Windows.h nem de ntddk.h. Tudo isso já está prontinho, testadinho, e como meu amigo Rafael costuma dizer, “compilandinho” num projeto disponível para download ao final do post.

typedef struct _KERNEL_MATH_REQUEST
{
    //-f--> Eu posso definir minha estrutura da maneira
    //      que eu achar o maior dos melhor de bão.
    //      Estes serão os dois números a serem somados.
    ULONG   x;
    ULONG   y;
 
} KERNEL_MATH_REQUEST, *PKERNEL_MATH_REQUEST;
 
 
typedef struct _KERNEL_MATH_RESPONSE
{
    //-f--> Aos mais mocinhos: Eu sei que dá para fazer
    //      tudo em uma só estrutura. Estou fazendo desta
    //      forma para melhor ilustrar.
    ULONG   r;
 
} KERNEL_MATH_RESPONSE, *PKERNEL_MATH_RESPONSE;

Criando o IOCTL

O IOCTL é mais do que simplesmente um número para identificar a operação desejada. Ele é composto por uma máscara de bits que são interpretados pelo Windows. Esta máscara é definida como mostra a figura abaixo. Você pode obter maiores detalhes sobre esta macro neste link.


Para definir o IOCTL nós utilizamos a macro CTL_CODE que tem seus parâmentros como mostra abaixo.

#define IOCTL_Device_Function CTL_CODE(DeviceType, Function, Method, Access)

Vamos definir nosso IOCTL como mostra abaixo.

//-f--> Aqui definimos o IOCTL para nosso driver.
#define IOCTL_SOMA_QUE_EU_TO_MANDANDO \
    CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
 
//-f--> Apesar deste driver ter "Soma" como parte do nome, resolvi
//      colocar um IOCTL de subtração para exemplificar.
#define IOCTL_SUBTRAI_QUE_EU_TO_MANDANDO \
    CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)

Agora vamos dar uma olhada em cada parâmetro que foi escolhido aqui e dar uma pincelada a respeito.

FILE_DEVICE_UNKNOWN – Dando uma olhada em nossa DriverEntry podemos verificar que este foi o mesmo tipo utilizado na chamada à rotina IoCreateDevice.

/****
***     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)
{
    UNICODE_STRING  usDeviceName = RTL_CONSTANT_STRING(L"\\Device\\KernelSum");
    UNICODE_STRING  usSymbolicLink = RTL_CONSTANT_STRING(L"\\DosDevices\\KernelSum");
    NTSTATUS        nts;
    PDEVICE_OBJECT  pDeviceObj;
 
    //-f--> Setando a rotina de unload e permitir que o driver
    //      possa ser descarregado dinamicamente
    pDriverObj->DriverUnload = OnDriverUnload;
 
    //-f--> As rotinas de Open, Cleanup e Close são idênticas.
    //      Então, pela lei do mínimo esforço, serão uma única.
    //      Além dessa, temos que tratar a DeviceControl, que é
    //      onde os IOCTLs são recebidos
    pDriverObj->MajorFunction[IRP_MJ_CREATE] =
    pDriverObj->MajorFunction[IRP_MJ_CLEANUP] =
    pDriverObj->MajorFunction[IRP_MJ_CLOSE] = OnCreateCleanupClose;
    pDriverObj->MajorFunction[IRP_MJ_DEVICE_CONTROL] = OnDeviceIoControl;
 
    //-f--> Criamos o device de controle do driver. Repare que utilizamos
    //      FILE_DEVICE_UNKNOWN como device type. Este mesmo device type
    //      é utilizado na macro CTL_CODE. Veja: IoCtl.h
    nts = IoCreateDevice(pDriverObj,
                         0,
                         &usDeviceName,
                         FILE_DEVICE_UNKNOWN,
                         0,
                         FALSE,
                         &pDeviceObj);
 
    //-f--> Tudo bem até aqui? Então beleza...
    if (!NT_SUCCESS(nts))
    {
        ASSERT(FALSE);
        return nts;
    }
 
    //-f--> Cria symbolic link para que a aplicação consiga
    //      obter um handle para o device de controle
    nts = IoCreateSymbolicLink(&usSymbolicLink,
                               &usDeviceName);
 
    //-f--> Tá tudo bem?
    if (!NT_SUCCESS(nts))
    {
        //-f--> Ops... Acho que eu pisei em alguma coisa mole...
        IoDeleteDevice(pDeviceObj);
 
        ASSERT(FALSE);
        return nts;
    }
 
    //-f--> Beleza, fechou, é nóis na fita!
    return STATUS_SUCCESS;
}

O segundo parâmetro, o Function, recebe o número 0x800, já que os números abaixo disso são reservados à Microsoft.

METHOD_BUFFERED – Indica ao IoManager que o driver receberá uma cópia do buffer de entrada passado à função DeviceIoControl. O IoManager aloca um buffer de sistema, ou seja, em Kernel Space, suficientemente grande para que caibam tanto os dados de entrada como os dados de saída. Quando a função DeviceIoControl é chamada, o IoManager aloca o buffer de sistema e copia o buffer de entrada para dentro dele. O driver recebe a IRP, obtém os parâmetros de entrada, processa-os e escreve os dados de saída no mesmo buffer de sistema. Quando a IRP é completada, o IoManager copia os dados de saída do buffer de sistema para o buffer de saída oferecido pela aplicação.

Além do método Buffered, ainda existem os métodos de Direct I/O e Neither. Para o nosso exemplo de hoje, o méthodo Buffered está ótimo. Fico devendo um post que fala sobre como usar cada um dos outros métodos.

FILE_ANY_ACCESS – Indica que o handle não precisa ser aberto com um tipo de acesso especial para poder executar este IOCTL.

Da aplicação para o driver

Para enviar um IOCTL para o driver, você precisa utilizar a função DeviceIoControl como mostra o exemplo abaixo. Este é um programa bem simples que mostra este uso.

/****
***     main
**
**      Espero que todos saibam que este é o ponto
**      de entrada de uma aplicação. Caso contrário,
**      você pode estar se preciptando em ler um blog
**      de driver para Windows.
*/
int __cdecl main(int argc, CHAR* argv[])
{
    HANDLE                  hDevice;
    DWORD                   dwError = ERROR_SUCCESS,
                            dwBytes;
    KERNEL_MATH_REQUEST     Request;
    KERNEL_MATH_RESPONSE    Response;
 
    printf("Opening \\\\.\\KernelSum device...\n");
 
    //-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("\\\\.\\KernelSum",
                         GENERIC_ALL,
                         0,
                         NULL,
                         OPEN_EXISTING,
                         0,
                         NULL);
 
    //-f--> Verifica se o handle foi aberto.
    if (hDevice == INVALID_HANDLE_VALUE)
    {
        printf("Error #%d opening device...\n",
               (dwError = GetLastError()));
        return dwError;
    }
 
    //-f--> Fiquei com preguiça e coloquei os valores hard-coded mesmo.
    Request.x = 3;
    Request.y = 2;
 
    printf("Calling DeviceIoControl...\n");
 
    //-f--> Envia o IOCTL
    if (!DeviceIoControl(hDevice,
                         IOCTL_SOMA_QUE_EU_TO_MANDANDO,
                         &Request,
                         sizeof(KERNEL_MATH_REQUEST),
                         &Response,
                         sizeof(KERNEL_MATH_RESPONSE),
                         &dwBytes,
                         NULL))
    {
        //-f--> Ops...
        printf("Error #%d calling DeviceIoControl...\n",
               (dwError = GetLastError()));
 
        CloseHandle(hDevice);
        return dwError;
    }
 
    //-f--> Mostrando resultatdos
    printf("%d + %d = %d\n",
           Request.x,
           Request.y,
           Response.r);
 
    printf("Closing device...\n");
    //-f--> Fim de conversa
    CloseHandle(hDevice);
    return 0;
}

Note que não existe nenhuma amarração forte entre o IOCTL e os bufferes de entrada ou de saída utilizados, mas é preciso ficar atento aos tamanhos dos bufferes quando o driver fizer uso deles. Ninguém vai querer corromper o heap de alocações de Kernel ou sair disparando excessões em Kernel Mode, vai?

Vejam como os dados são tratados pelo driver ao receber a IRP_MJ_DEVICE_CONTROL. Leiam os comentários, eles fazem parte do texto explicativo. Nossa, até que essa frase que representa a minha preguiça ficou legal. No fundo mesma frase quer dizer: “Ah meu! Fala sério que além de preparar esse exemplo, você ainda quer que eu duplique toda a informação dos comentários. E na bunada não vai dinha?”

/****
***     OnDeviceIoControl
**
**      Esta rotina é chamada quando uma aplicação
**      envia um IOCTL via DeviceIoControl para nosso device.
*/
NTSTATUS
OnDeviceIoControl(__in PDEVICE_OBJECT   pDeviceObj,
                  __in PIRP             pIrp)
{
    PIO_STACK_LOCATION      pStack;
    NTSTATUS                nts;
    PKERNEL_MATH_REQUEST    pRequest;
    PKERNEL_MATH_RESPONSE   pResponse;
 
    //-f--> Obtemos os parâmetros para nosso driver
    pStack = IoGetCurrentIrpStackLocation(pIrp);
 
    switch(pStack->Parameters.DeviceIoControl.IoControlCode)
    {
    case IOCTL_SOMA_QUE_EU_TO_MANDANDO:
    case IOCTL_SUBTRAI_QUE_EU_TO_MANDANDO:
 
        //-f--> Vamos fazer as verificações de tamanho dos
        //      buffers de entrada e saída.
        if (pStack->Parameters.DeviceIoControl.InputBufferLength !=
            sizeof(KERNEL_MATH_REQUEST) ||
            pStack->Parameters.DeviceIoControl.OutputBufferLength !=
            sizeof(KERNEL_MATH_RESPONSE))
        {
            nts = STATUS_INVALID_BUFFER_SIZE;
            pIrp->IoStatus.Information = 0;
            break;
        }
 
        //-f--> O seguro morreu de velho.
        ASSERT(pIrp->AssociatedIrp.SystemBuffer != NULL);
 
        //-f--> Utilizando METHOD_BUFFERED, o sistema aloca um buffer único
        //      para transportar os dados de entrada e de saída. O tamanho
        //      este buffer é mesmo do maior deles. Assim, tome cuidado
        //      para não escrever nada na saída antes de ler toda a entrada.
        pRequest = (PKERNEL_MATH_REQUEST)pIrp->AssociatedIrp.SystemBuffer;
        pResponse = (PKERNEL_MATH_RESPONSE)pIrp->AssociatedIrp.SystemBuffer;
 
        //-f--> Faz a operação e sinaliza sucesso
        if (pStack->Parameters.DeviceIoControl.IoControlCode == IOCTL_SOMA_QUE_EU_TO_MANDANDO)
            pResponse->r = pRequest->x + pRequest->y;
        else
            pResponse->r = pRequest->x - pRequest->y;
 
        nts = STATUS_SUCCESS;
 
        //-f--> Informa ao IoManager quantos bytes devem ser tranferidos de
        //      volta para a aplicação
        pIrp->IoStatus.Information = sizeof(KERNEL_MATH_RESPONSE);
        break;
 
    default:
        //-f--> Ops... Recebemos um IOCTL não previsto.
        nts = STATUS_INVALID_DEVICE_REQUEST;
        pIrp->IoStatus.Information = 0;
    }
 
    //-f--> Copia o status final da IRP e a completa.
    pIrp->IoStatus.Status = nts;
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
 
    //-f--> NOTA: Lembre-se de que não podemos tocar na IRP depois de completa-la,
    //      então não seja espertão modificando a linha abaixo para a obter o Status
    //      de dentro da IRP, e assim usa-lo como retorno de função.
    return nts;
}

Compilando assim ou assado

O projeto de exemplo que está disponível para download ao final deste post pode ser compilado de duas maneiras. Uma delas é utilizando a maneira padrão de compilar drivers oferecida pelo WDK. Resumidamente você vai em Start->All Programs->Windows Drivers Kits->WDK 6000->Build Environments->Windows XP->Windows XP x86 Checked Build Environment. Isso deve abrir uma jabela de prompt como mostra abaixo. Depois disso é só ir ao diretório onde você baixa essas tranqueiras da internet e entrar no diretório raiz do projeto. Lá você chama o Build.exe e um abraço.


A outra maneira de compilar todo o projeto permite que você compile de dentro do Visual Studio 2008, que foi o ambiente que utilizei para compor este projeto. Porém, para compilar o projeto pela IDE, você precisará usar o DDKBUILD como mostra este outro post que fala a respeito.

Enquanto estive fora, recebi uma outra dúvida de um leitor que queria saber como ler/escrever no registro usando um driver. No próximo post falarei sobre isso além de outros detalhes referentes ao registro do ponto de vista de um driver.

Até lá!

KernelSum.zip

Tirando o atraso :-P

5 de June de 2008

Mais uma vez fiquei sem postar por um longo tempo. Agora você deve estar pensando: “Lá vem ele com aquela ladainha de que não tem tempo, que tudo está difícil, que Deus não gosta dos programadores de Kernel e assim por diante”. Bom, vou pular essa parte de dar minhas desculpas e vou logo dizendo o que estive fazendo durante esse tempo.

Curso de Drivers para Windows

Bom deixe-me ver onde foi mesmo que eu parei. Meu último post foi durante o período em que eu estava dando um curso de desenvolvimento de drivers para uma turma fechada. O curso foi ministrado em cinco sábados de 8 horas cada. Não preciso dizer que isso me ocupou por um tempo. Tá tá tá, sem desculpas.

Quarto Encontro de Programadores de C/C++

No sábado subsequente ao final do curso, fui dar uma passeada neste evento. Deixaram a porta meio aberta e consegui entrar para dar uma espiada e falar um pouco sobre Arquitetura e Desenvolvimento de drivers para Windows com linguagem C. Confesso que o convite para participar deste evento foi uma surpresa para mim. Não sou um grande programador de C++, mas utilizo a linguagem C com classes como meu amigo Strauss gosta de dizer. Posso dizer que foi muito interessante poder participar desse evento de altíssimo nível técnico e ter a oportunidade de ver como a linguagem pode ser utilizada em diferentes cenários. Os slides da minha palestra estão disponíveis neste link.

De volta à Boston

Até parece que as coisas só acontecem aos sábados. De qualquer forma, no sábado subsequente ao encontro, embarquei em uma viajem de um mês para Boston. Da última vez que estive por lá foi para fazer um treinamento na OSR como descrevi neste post. Desta vez foi um programa de integração da IBM. Pude conhecer pessoalmente as pessoas com quem trabalho e que antes só conhecia por Web Conferência. Mais uma vez foi bem interessante e tentei aproveitar ao máximo. Fomos em dois brasileiros e um Indiano. Senti muita saudade do nosso cafézinho brasileiro. Nessa foto abaixo estão, da esquerda para a direita, David E. Jones (Gerente), Scott D. Hankin (Documentação), William C. Oliveira (Linux), William H. Taber (Linux), Mridula Muppana (Testes), Paul Ganshirt (Windows), Niraj Kumar (AIX), Kaveesh Mishra (Windows), Fernando Roberto (Windows) e Richard Kissel (AIX e Linux). Esta é apenas uma parte de todo o time de MVFS.


Não sabe o que é MVFS? É um File System que faz parte de um produto chamado ClearCase. Não conhece o ClearCase? Bom, além da Wikipedia, pude ter o prazer de esbarrar com uma observação sobre o ClearCase no livro de Rajeev Nagar. Este livro, como eu não me canso de dizer, ainda é a única referência respeitável sobre desenvolvimento de File Systems para Windows apesar de ter sido publicado pela primeira vez em 1997. Mas onde está o ClearCase nesta história? Bom, se você é doente como eu e tem esse livro, dê uma olhadinha no início do capítulo nove. Na primeira página deste capítulo você encontrará as seguintes passagens.


Isso é realmente muito legal! 🙂

Volta às aulas

Durante um mês inteiro após minha chegada de volta ao Brasil, dediquei algum tempo correndo atrás da matéria que perdi na faculdade. Pois é, ainda estou me graduando em Engenharia da Computação. Já estou trabalhando em meu TCC (Trabalho de Conclusão de Curso). Meu TCC terá obrigatóriamente que ter um driver de Windows, é o mínimo que eu poderia fazer. Finalmente vou poder mostrar o que sei fazer aos meus amiguinhos de sala. Durante o curso, alguns amigos me perguntam com o quê eu trabalho, já que freqüentemente estou lendo grandes livros da Microsoft. Depois de tentar explicar das maneiras mais simples possíveis, desenhando ou mesmo usando fantoches, eles ainda ficam com aquela cara de interrogação. Bom, na hora que eu mostrar a tela azul, eles vão acabar entendendo.


Aproveitei o passeio nos Estados Unidos para comprar esse kit de desenvolvimento da ALTERA. Esse kit vai fazer parte do meu TCC e futuramente poderei utilizá-lo em meus cursos de desenvolvimento de drivers. Hoje conto apenas com as placas de treinamento da OSR, conforme já falei a respeito neste post. Diferente das placas da OSR, este kit pode ter seu hardware definido por uma linguagem chamada VHDL, e assim, poderei fazer com que a placa tenha os mais variados comportamentos e poderei ilustrar a construção de drivers de interface para cada situação. Essa placa custa em torno de R$ 1.700,00 aqui no Brasil, enquanto que paguei apenas US$ 150,00 lá. Praticamente veio no doce. Sabe quando íamos à padaria comprar aquela casquinha de sorvete que vem recheada com maria-mole, e enfiada nela vem um brinquedo. Então, foi quase a mesma coisa. Uma outra coisa interessante nesse kit, além do preço, é que é possível fazer interface USB e interface PCI. Isso vai ser bem divertido.

Seminário de Portabilidade e Performance

Neste último final de semana, participei de mais esse evento organizado pelo grupo de C/C++ Brasil. É impressionante ver como estes eventos estão trazendo cada vez mais gente. Bati algumas fotos, mas vou deixar os comentários por conta do meu amigo Lesma, que já fez um post muito legal à respeito.

De volta ao mundo dos bloggers, vou tentar não abandoná-los por tanto tempo. Desculpinhas à parte, num tá fáci não.

Até a próxima.