Archive for April, 2009

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