1120 Alameda Orquidea, Atibaia, SP,  BRA

fernando@driverentry.com.br

Usando FileObject e FsContext

Muito bem, com alguns posts e um pouco de paciência para agüentar minhas piadinhas, podemos construir um simples driver que responda às chamadas de aplicações. A chamada à função CreateFile cria uma conexão entre a aplicação e o device e nos retorna um handle, que mais tarde será utilizado para encaminhar solicitaçoes de leitura e/ou escrita ao device. Se houverem mais chamadas ao CreateFile, outros handles serão retornados. Como com arquivos, cada handle possui seu próprio contexto. Suponha que você abra duas vezes o mesmo arquivo, obtendo assim dois handles com contextos diferentes. Uma leitura de 100 bytes a partir do primeiro handle faz com que a posição atual deste arquivo agora seja diferente da posição inicial. Uma nova leitura utilizando este mesmo handle resultaria nos próximos 100 bytes adiante dos bytes já lidos, mas se você utilizar o segundo handle, você obteria novamente os 100 primeiros bytes do arquivo. Neste post vou falar como manter este contexto entre os vários handles abertos para o seu device.

O problema na prática

Há pouco tempo atrás, em um dos meus posts, dei um exemplo básico de driver que implementa uma lista ligada de buffers que foram escritos no device. Os mesmos buffers são obtidos em leituras posteriores ao mesmo device. Este post vai se basear nesse driver de exemplo para fazer os testes e modificações sugeridas. A lista desse driver foi implementada como uma única variável global. Se existe uma única lista, os diversos handles retornados às aplicações manipularão a mesma lista.

Lembre-se que o programa de teste escreve as strings digitadas no console no device até que uma string vazia seja fornecida. Quando isso ocorre, a aplicação passa a realizar leituras e exibir o resultado na tela. Assim, a mesma seqüência de strings fornecidas deveria ser exibida. Para reproduzir o problema, siga os passos abaixo com o driver já instalado.

  1. Abra uma janela de Prompt e execute uma instância do programa de teste.
  2. Entre com a seqüência de strings de “111”, “222”, até “555”, mas ainda não entre com a string vazia.
  3. Abra uma nova janela de Prompt e execute uma outra instância do programa de teste.
  4. Nesta, entre com uma seqüência de strings de “666” até “999” e em seguida uma string vazia.
  5. Volte ao Prompt anterior e entre com a string vazia na primeira instância do programa de teste.

Devemos obter uma saída como mostra a figura abaixo. Note que a primeira instância do programa de teste não leu nenhuma string apesar de ter entrado com várias. As strings faltantes foram parar na segunda instância do programa.

Separando as listas

Criar um algoritmo que separe as listas por processo até funcionaria, mas tente imaginar que uma mesma aplicação utilize duas bibliotecas distintas, que por sua vez utilizam as listas do driver. Desta forma, a lista de uma biblioteca se misturaria com a lista de outra, já que ambas as bibliotecas estão no mesmo processo. Para separar o contexto das várias aberturas do device utilizamos o membro FileObject da stack location atual.

Quando uma aplicação chama a função CreateFile, esta requisição vai para o ObjectManager que verifica a existência do objeto desejado, em seguida, verifica se a aplicação tem direitos para obter um handle para este objeto, e se tudo estiver de acordo, a solicitação chega até seu driver em forma de uma IRP com o MajorFunction igual a IRP_MJ_CREATE. Para obter FileObject referente a esta conexão que está sendo criada, você precisa fazer como mostra o código abaixo.

    //-f--> Recupera o FileObject referente a esta lista
    pStack = IoGetCurrentIrpStackLocation(pIrp);
    pFileObject = pStack->FileObject;

Essa ou aquela lista?

Todas as operações que vierem da mesma instância do objeto, virão com o mesmo FileObject. Isso ajuda a rastrear o contexto de uma entre as várias solicitações de aberturas que chegarão ao seu driver.

Então é só criar uma lista de FileObjects e vincular cada um a uma lista? A idéia de vincular o FileObject a uma estrutura que carregue este contexto é tão óbvia que o sistema já reservou um espaço na estrutura FILE_OBJECT para que você coloque os dados referentes a este contexto.

typedef struct _FILE_OBJECT
{
    CSHORT  Type;
    CSHORT  Size;
    PDEVICE_OBJECT  DeviceObject;
    PVPB  Vpb;
    PVOID  FsContext;
    PVOID  FsContext2;
    PSECTION_OBJECT_POINTERS  SectionObjectPointer;
    PVOID  PrivateCacheMap;
    NTSTATUS  FinalStatus;
    struct _FILE_OBJECT  *RelatedFileObject;
    BOOLEAN  LockOperation;
    BOOLEAN  DeletePending;
    BOOLEAN  ReadAccess;
    BOOLEAN  WriteAccess;
    BOOLEAN  DeleteAccess;
    BOOLEAN  SharedRead;
    BOOLEAN  SharedWrite;
    BOOLEAN  SharedDelete;
    ULONG  Flags;
    UNICODE_STRING  FileName;
    LARGE_INTEGER  CurrentByteOffset;
    ULONG  Waiters;
    ULONG  Busy;
    PVOID  LastLock;
    KEVENT  Lock;
    KEVENT  Event;
    PIO_COMPLETION_CONTEXT  CompletionContext;
    KSPIN_LOCK  IrpListLock;
    LIST_ENTRY  IrpList;
    PVOID  FileObjectExtension;
 
} FILE_OBJECT, *PFILE_OBJECT;

Os campos FsContext e FsContext2 são utilizados para este fim, a menos que você implemente um filtro, você pode utilizá-los à vontade. Para fazer com que cada abertura ao device tenha sua própria lista, vamos armazenar o endereço da ponta de nossa lista em um destes campos como é mostrado abaixo na nova implementação do OnCreate. Todo o código fonte alterado está disponível para download ao final deste post.

/****
***     OnCreate
**
**      Esta rotina é chamada quando uma aplicação ou driver
**      tenta obter um handle para o device que criamos.
*/
 
NTSTATUS
OnCreate(IN PDEVICE_OBJECT  pDeviceObj,
         IN PIRP            pIrp)
{
    NTSTATUS            nts = STATUS_SUCCESS;
    PBUFFER_LIST_HEAD   pListHead;
    PIO_STACK_LOCATION  pStack;
 
    //-f--> Um Olá para o depurador...
    KdPrint(("Opening EchoDevice...\n"));
 
    //-f--> Aloca a ponta da lista para este IRP_MJ_CREATE
    pListHead = (PBUFFER_LIST_HEAD) ExAllocatePoolWithTag(
        PagedPool,
        sizeof(BUFFER_LIST_HEAD),
        ECHO_TAG);
 
    if (pListHead)
    {
        //-f--> Inicializando a lista de buffers e mutex
        InitializeListHead(&pListHead->BufferList);
        KeInitializeMutex(&pListHead->Mutex, 0);
 
        //-f--> Armazenamos nosso contexto no FileObject.
        pStack = IoGetCurrentIrpStackLocation(pIrp);
        pStack->FileObject->FsContext = pListHead;
    }
    else
        nts = STATUS_INSUFFICIENT_RESOURCES;
 
    //-f--> Completando a IRP com sucesso.
    pIrp->IoStatus.Status = nts;
    pIrp->IoStatus.Information = 0;
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    return nts;
}

FileObject é equivalente a um Handle?

Não. Inicialmente temos um handle para cada FileObject, mas a quantidade de handles para um FileObject aumenta à medida que duplicamos os handles. Quando a função CreateFile é chamada, seu driver recebe uma IRP_MJ_CREATE, mas seu driver não é avisado quando alguém chama a função DuplicateHandle. Assim, cada handle aponta para um objeto, mas um objeto pode ser apontado por vários handles.

Quem vai limpar esta bagunça?

Quando todas as referências a um determinado FileObject forem liberadas, seu device receberá um IRP_MJ_CLOSE. Vamos utilizar este evento para efetuar a limpeza de qualquer string que ainda estiver na lista.

/****
***     OnClose
**
**      Vou simplificar dizendo que esta rotina é chamada quando
**      a aplicação ou driver fecha o handle que foi previamente
**      aberto. Mas no fundo não é isso (fica pra um outro post).
*/
 
NTSTATUS
OnClose(IN PDEVICE_OBJECT  pDeviceObj,
        IN PIRP            pIrp)
{
    PBUFFER_LIST_HEAD   pListHead;
    PIO_STACK_LOCATION  pStack;
    PLIST_ENTRY         pEntry;
    PBUFFER_ENTRY       pBufferEntry;
 
    //-f--> Um olá para o depurador
    KdPrint(("Closing EchoDevice...\n"));
 
    //-f--> Recupera a lista referente a este FileObject
    pStack = IoGetCurrentIrpStackLocation(pIrp);
    pListHead = (PBUFFER_LIST_HEAD)pStack->FileObject->FsContext;
 
    //-f--> Aqui removemos todos os nós que ainda não foram lidos
    //      pela aplicação. Isso aconteceria se a aplicação chamasse
    //      WriteFile e não chamasse ReadFile.
    while(!IsListEmpty(&pListHead->BufferList))
    {
        //-f--> Pega o primeiro nó da lista
 
        pEntry = RemoveHeadList(&pListHead->BufferList);
 
        //-f--> Obtém o endereço a partir do nó
        pBufferEntry = CONTAINING_RECORD(pEntry, BUFFER_ENTRY, Entry);
 
        //-f--> Finalmente libera a memória utilizada por
        //      este nó
        ExFreePool(pBufferEntry);
    }
 
    //-f--> Libera a memória utilizada pela ponta da lista
 
    ExFreePool(pListHead);
 
    //-f--> Completando a IRP com sucesso.
    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = 0;
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}

Quando uma aplicação é terminada ou mesmo quando ela cai sem sua vontade, todos os handles desta aplicação são fechados pelo sistema. Desta forma, é garantido que seu driver sempre receberá o IRP_MJ_CLOSE reference ao objeto que foi liberado. Na verdade existe uma novelinha a respeito do IRP_MJ_CLEANUP e IRP_MJ_CLOSE que vai ficar para uma próxima vez.

Depois de implementada a alteração, cada handle aberto terá sua própria lista, a menos que o handle seja duplicado. Se repetirmos a mesma seqüência de passos enumerados para forçar o erro, teremos a seguinte saída.


Em um post futuro, mostrarei como dar nomes às listas de forma a permitir que processos distintos possam abrir a mesma lista a partir de um nome conhecido.

Até mais! 🙂

FileObjEcho.zip

7 Responses

  1. Mesmo sabendo que a duplicação do handle não é avisada para o driver, ainda assim é possível para ele ter acesso ao contador de handles do usuário? Não que seja necessário nesse caso, é apenas por curiosidade.

    []s

    1. Olá Lesma,

      Sim, você pode obter esta informação, mas utilizando uma API não documentada.

      Use ZwQueryObject com o ObjectInformationClass igual a ObjectBasicInformation.

      Tudo detalhado bunitinho no Windows NT/2000 Native API Reference.

      Essa informação é mantida pelo ObjectManager, e mesmo que você a obtenha com a API acima descrita, o valor não será confiável, pois enquanto seu buffer é preenchido e retornado ao seu driver, o handle pode ser fechado ou duplicado.

      Essa informação serve só pra matar a curiosidade mesmo.

      []s,

  2. Olá,
    Será que alguém poderia me dar uma dica de como usar dentr de um driver função para criar handle para arquivo e o manipular ?

    Abraços

    1. Olá Daniel,

      Creio que este post possa te ajudar. Nele eu deixo disponível para download um código fonte de um driver, que além de outras coisas, cria um arquivo e faz escritas nele.

      Dentro do fonte Inject.cpp, dê uma olhada na função ExportFile.

      Have fun!

  3. Olá, Fernando.
    A respeito das partes que falamos em Device como a abaixo:

    “A chamada à função CreateFile cria uma conexão entre a aplicação e o device e nos retorna um handle, que mais tarde será utilizado para encaminhar solicitaçoes de leitura e/ou escrita ao device.”

    Pelo que entendi até agora, o nosso Driver cria o Device que será usado pela aplicação mais tarde. Neste caso quais seriam as diferenças entre e o Device e o nosso Driver carregado? Acho que por serem parecidas as palavras eu não assimilei ainda…
    Abraços.

    1. Olá Fábio,

      Gosto de usar o exemplo do driver de porta serial para explicar a diferença entre Driver e Device. Supondo que uma determinada máquina possua duas portas seriais, um único driver será carregado pelo kernel para gerenciar ambas as portas, pois o hardware será manipulado da mesma maneira para ambas as portas seriais.

      Quando o driver é carregado, ele detecta a presença de duas portas seriais e, embora a maneira de lidar com o hardware seja praticamente a mesma, o driver precisa criar entidades separadas para cada porta. Afinal, uma aplicação A pode utilizar a porta COM1 sem sofrer qualquer intervenção da aplicação B que utiliza a porta COM2.

      Neste caso, um único driver cria dois devices. O contexto entre os devices será mantido pelos campos FsContext, conforme visto neste post. Cada aplicação receberá um handle para um certo device, mas ambos os devices foram criados pelo mesmo driver.

      Ficou claro?

      Um abraço.

Leave a Reply

Your email address will not be published.