1120 Alameda Orquidea, Atibaia, SP,  BRA

fernando@driverentry.com.br

Buffered, Direct ou Neither em IOCTLs

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

2 Responses

  1. Muito útil a explicação, especialmente para verificarmos que é necessário conhecer o comportamento do sistema para programar drivers, como o detalhe que o IO Manager copia o buffer de entrada para o de saída, e que o sistema não possui as proteções disponíveis em user mode para o kernel mode, como demonstra o uso de ProbeForRead/Write para encontrar memória inválida.

    []s

  2. Hahahahaha, muito boa a despedida!
    Gostei!!!

    Abraços Fernando!!!
    ASS: MR4ND3R50N 😉

    Intel mais!

Leave a Reply

Your email address will not be published.