Archive for August, 2009

Notificando eventos à aplicação

18 de August de 2009

Há algumas semanas, cá estava eu todo enrolado com meu projeto da faculdade. Com toda essa atividade, o que tenho comentado com meus amigos é que meu Twitter mais parece um cronograma. Mas em fim, em meio a tanta correria, recebi a seguinte dúvida do leitor Júlio César (Rio de Janeiro – RJ):

“Como implementar a comunicação entre um driver e uma aplicação de modo que o driver inicie a comunicação? Ou seja, não quero que a aplicação envie uma mensagem ao driver, mas sim que o driver envie uma mensagem à aplicação.”

Minha resposta curta, porém grossa, é que não existem meios de um driver simplesmente acordar numa manhã ensolarada, coçar a barriga enquanto se espreguiça e dizer a si mesmo: “Hoje vou fazer uma surpresa ao meu amigo notepad.exe. Vou mandar lhe um cartão postal da Kernel-lândia.”

Um modelo Cliente-Servidor

O Windows funciona num modelo Cliente-Servidor, onde o lado Servidor seria o Kernel, que atende às requisições de seus clientes, que nesse caso são as aplicações. Nenhuma atividade é iniciada pelo Kernel por vontade própria. Sempre são as aplicações, que utilizando a API nativa do sistema, solicitam notificações do sistema de uma série de eventos.

“Mas Fernando, como ficam as notificações do plug-and-play às aplicações em user-mode?”

Na verdade elas são solicitadas pelas aplicações utilizando a rotina RegisterDeviceNotification(). Esse assunto é bem legal para se comentar num post futuro. Deixa eu anotar aqui na minha lista de posts a escrever.

“Mas Fernando, quando o sistema inicia, as coisas não começam automagicamente?”

O Boot é um procedimento especial no qual o Kernel inicia apenas o Gerenciador de Sessão na User-lândia, também conhecido pelos mais chegados como Smss. O Smss é um processo nativo (que utiliza apenas API nativa) e é considerado um componente de confiança. Ele não utiliza API Windows porque o Subsistema Windows (Csrss) ainda não existe. Daí em diante ocorre uma série de inicializações originadas pelo Smss e seus processos filhos, mas vou deixar os detalhes sobre isso com o Lesma. Isso me fez lembrar que Csrss se extende por “Client Server Run-Time Subsystem”.

“Mas Fernando, e quanto aos serviços?”

Serviços são iniciados por um processo chamado Services.exe, que por sua vez também foi iniciado por outro componente durante o processo de Boot.

“Mas Fernando, e quanto aos drivers de boot?”

A carga de drivers não é considerada uma notificação para o user-mode.

“Mas Fernando, setembro chove?”

Bom, chega né? Vamos falar do que interessa agora.

Operações de I/O pendentes

Já vimos em um outro post que uma aplicação pode solicitar serviços ao driver. Para dar a impressão de que o driver enviou uma notificação à aplicação, podemos utilizar uma operação que ficaria pendente até que o evento desejado ocorra. Tal como uma operação de leitura numa porta serial, que ficaria presa na chamada ReadFile() até que um ou mais caracteres fossem recebidos.

Isso funciona razoavelmente bem, mas teríamos algumas complicações caso o evento nunca ocorra e sua aplicação precise sair porque deixou o feijão no fogo ou coisa assim. Dessa forma, teríamos que adotar uma solução multi-threaded, onde uma segunda thread avisaria à thread pendente de que é tarde demais, que não adianta mais esperar pelo evento, já era, miou, esquece, cai na real.

Para as pessoas que sofrem de “thread-fobia”, uma solução utilizando Overlapped I/O cairia como uma luva, mas não vou falar sobre isso hoje. Na verdade isso já está na minha lista, mas não vai ser hoje.

Compartilhando um evento

A maneira que mais gosto de trabalhar é compartilhando um evento. Todos sabem o que é um evento? Pode parecer besteira, mas tem muita gente não sabe direito o que é um handle e quer programar o Kernel. Isso me preocupa um pouco. Que tipo de drivers essas pessoas podem gerar? Me permitam abrir um parenteses aqui para fazer uma pergunta: O que vocês acham de além de eu oferecer posts de drivers, eu oferecer posts sobre System Programming? Coisas como Processos, Threads, Objetos, Handles, Memória Virtual, Heaps, Dispatch Objects, Sincronismo e por aí vai. Me mandem e-mails com sugestões, que serão muito bem vindas.

Voltando ao que interessa, se uma aplicação cria um evento e manda seu handle para o driver, este poderá sinalizar a existência de uma informação relevante à aplicação. Assim a aplicação pode esperar por este evento, e quando este for sinalizado, a aplicação faz o I/O para buscar tal informação usando os meios de comunicação que já vimos em outros posts.

Image Notifier

Para exemplificar a recepção de eventos gerados por um driver, vamos ver hoje um driver que nos avisará sempre que uma imagem for mapeada em um processo.

Primeiro vamos definir uma interface para essa comunicação. A aplicação precisará enviar o handle de um evento para o driver, isso também vai avisar o driver que a aplicação deseja receber notificações sobre o mapeamento de imagens. Para isso vamos definir nossas IOCTLs como já vimos neste outro post.

//-f--> Este será o IOCTL para notificar o driver de que uma
//      aplicação está interessada nos eventos de carga de
//      imagens. Este IOCTL deverá levar o handle do evento
//      a ser sinalizado quando houver dados para a aplicação.
#define IOCTL_IMG_START_NOTIFYING   CTL_CODE(FILE_DEVICE_UNKNOWN,   \
                                             0x800,                 \
                                             METHOD_BUFFERED,       \
                                             FILE_ANY_ACCESS)
 
 
//-f--> Sei que é besteira criar uma estrutura com um só membro,
//      mas isso além de ser mais didático, facilita para aquela
//      galera que vai fazer "Copy and Paste" do meu código para
//      outros projetos. Depois eles vão querer mandar mais dados
//      ao driver e vão se enrolar com isso. Aí já viu de quem é
//      a culpa: "Peguei esse código no blog daquela besta!".
typedef struct _IMG_START_NOTIFYING
{
    HANDLE  hEvent;
 
} IMG_START_NOTIFYING, *PIMG_START_NOTIFYING;
 
 
//-f--> Este será o IOCTL que a aplicação lançará ao driver para
//      obter os detalhes sobre a carga de imagens num processo.
#define IOCTL_IMG_GET_IMAGE_DETAIL  CTL_CODE(FILE_DEVICE_UNKNOWN,   \
                                             0x801,                 \
                                             METHOD_BUFFERED,       \
                                             FILE_ANY_ACCESS)
 
 
//-f--> Aqui vou definir um path máximo de 260 caracteres, mas
//      podem haver casos de paths mais longos. Não vou tratar
//      todos os casos e nem otimizar o transporte deste buffer
//      pegando apenas os bytes válidos.
#define IMG_MAX_IMAGE_NAME  260
 
 
//-f--> Aqui segue path da imagem que o driver obterá
//      antes de notificar a aplicação.
typedef struct _IMG_IMAGE_DETAIL
{
    CHAR    ImageName[IMG_MAX_IMAGE_NAME];
 
} IMG_IMAGE_DETAIL, *PIMG_IMAGE_DETAIL;
 
 
//-f--> Aqui a aplicação diz que não está mais interessada nas
//      notificações sobre imagens. Isso fará com que o driver
//      libere a referência que fez ao handle.
#define IOCTL_IMG_STOP_NOTIFYING    CTL_CODE(FILE_DEVICE_UNKNOWN,   \
                                             0x802,                 \
                                             METHOD_BUFFERED,       \
                                             FILE_ANY_ACCESS)

Não vou colocar todo o código aqui no post, mas está tudo disponível no exemplo para download ao final deste post. Lembrem-se que nove em cada dez dentistas recomendam a leitura dos comentários para o melhor entendimento do exemplo. A aplicação basicamente criará um evento e enviará seu handle para o driver através de um IOCTL.

    //-f--> Cria o evento que será compartilhado.
    hNotificationEvt = CreateEvent(NULL,
                                   TRUE,
                                   FALSE,
                                   NULL);
    _ASSERT(hNotificationEvt);
 
    printf("Requesting device to start notifying.\n");
 
    //-f--> Copiamos o handle do evento para a estrutura
    //      que será enviada ao driver. Como sabemos, handles
    //      são válidos apenas no contexto deste processo,
    //      então estamos admitindo que nosso driver estará
    //      no topo da device stack.
    StartNotifying.hEvent = hNotificationEvt;
    if (!DeviceIoControl(hDevice,
                         IOCTL_IMG_START_NOTIFYING,
                         &StartNotifying,
                         sizeof(StartNotifying),
                         NULL,
                         0,
                         &dwBytes,
                         NULL))
    {
        //-f--> Respira fundo e abre o WinDbg...
        dwError = GetLastError();
        printf("Error #%d on starting device notification.\n",
               dwError);
        __leave;
    }

Quando o driver receber este IOCTL, este irá adquirir uma referência ao objeto apontado pelo handle. Notem que para isso o driver utiliza a rotina ObReferenceObjectByHandle() do Object Manager, que além de incrementar o contador de referência do objeto, também certifica que o handle é do tipo de objeto que você espera receber. Isso evitaria que, por algum motivo, o handle de um outro objeto tenha sido passado no lugar do handle do evento. O resultado dessa chamada será um ponteiro para um evento recebido pelo driver. Como sabemos, objetos têm seu header num formato padrão, mas o corpo do objeto varia dependendo do seu tipo. Imagine que alguém enviasse um handle para uma thread no lugar de um handle para um evento, poderiamos usar as rotinas de evento para manipular uma thread e a chance de tudo ficar azul é alta. Por isso o uso do parâmetro ObjectType, apesar de opcional, é muito recomendado.

    //-f--> Obtém uma referência ao objeto
    nts =  ObReferenceObjectByHandle(pStartNotifying->hEvent,
                                     EVENT_ALL_ACCESS,
                                     *ExEventObjectType,
                                     UserMode,
                                     (PVOID*)&g_pEvent,
                                     NULL);

“Fernando, isso é mesmo necessário? Minha aplicação é a única que vai usar esse driver, e ela sempre vai enviar um handle para evento.”

Esse tipo de precaução evita que um programa engraçadinho envie qualquer coisa para seu driver produzindo uma tela azul propositalmente.

“Fernando, na minha opinião você gosta mesmo é de complicar as coisas. Eu não poderia simplesmente fazer uma cópia do handle e usar as rotinas do tipo ZwSetEvent() que recebem o handle do evento como parâmetro?”

Veja bem, o handle é válido apenas dentro do processo que o obteve. No nosso caso, tal handle é válido apenas no contexto da nossa aplicação de teste. As notificações de imagens rodam em contexto arbitrário, ou seja, sabe Deus em qual contexto de processo. Por isso teremos que obter uma referência que seja válida em qualquer contexto. O ponteiro obtido pela rotina ObReferenceObjectByHandle() é válido em qualquer contexto, pois aponta para o próprio objeto que reside em System Space. Se você não sabe o que significa System Space, então dê uma passeada por este post.

Bom, depois disso a aplicação vai ficar aguardando o evento ser sinalizado pelo driver. No código abaixo, dois eventos são monitorados, um deles é sinalizado pelo driver enquanto o outro é sinalizado pela própria aplicação no momento de encerrar sua atividade.

    //-f--> Aqui criamos um array de handles para a espera
    //      por múltiplos objetos.
    hObjects[0] = hFinishEvt;
    hObjects[1] = hNotificationEvt;
 
    do
    {
        //-f--> Espera ou por um sinal do device indicando a
        //      presença de dados no driver, ou um sinal da
        //      thread primária dizendo aquela baboseira de
        //      novela e tals.
        dwWait = WaitForMultipleObjects(2,
                                        hObjects,
                                        FALSE,
                                        INFINITE);
        switch(dwWait)
        {
        case WAIT_FAILED:
            //-f--> Pô Murphy, dá um tempo!
            dwError = GetLastError();
            printf("Error #%d on waiting for device notification.\n",
                   dwError);
            __leave;
 
        case WAIT_OBJECT_0 + 1:
            //-f--> Opa! O driver tem algo para nós, vamos buscar.
            if (GetImageDetail(hDevice) != ERROR_SUCCESS)
                __leave;
            break;
        }
 
        //-f--> Ficaremos nisso enquanto o evento de finalização
        //      não for sinalizado pela thread primária.
    } while(dwWait != WAIT_OBJECT_0);

Quando o evento é sinalizado, a aplicação enviará um IOCTL para obter os dados do driver. Nossa aplicação de teste também imprime esse dado na tela por pura diversão. Vamos dar uma olhada no código do driver para saber como isso acontece.

Durante a inicialização, o driver chama a rotina PsSetLoadImageNotifyRoutine() para registrar uma rotina de callback que é chamada sempre que uma imagem for mapeada para algum processo.

    //-f--> Registra rotina de callback para receber
    //      as notificações de imagens mapeadas para
    //      processos.
    nts = PsSetLoadImageNotifyRoutine(OnLoadImage);
    ASSERT(NT_SUCCESS(nts));

Nossa rotina de callback converte o path da imagem mapeada de Unicode para ANSI. Mais detalhes sobre conversão de strings neste post. Em seguida a rotina coloca esse path numa lista e seta o evento enviado pela aplicação. Se você ainda não sabe brincar de listas ligadas no kernel do Widows, então leia este post.

VOID
OnLoadImage(IN PUNICODE_STRING  pusFullImageName,
            IN HANDLE           hProcessId,
            IN PIMAGE_INFO      pImageInfo)
{
    PIMG_EVENT_NODE pNode;
    ANSI_STRING     asImageName;
    NTSTATUS        nts;
 
    //-f--> Vamos adquirir o controle das variáveis
    //      compartilhadas por diferentes threads.
    nts = KeWaitForMutexObject(&g_EventMtx,
                               UserRequest,
                               KernelMode,
                               FALSE,
                               NULL);
    ASSERT(NT_SUCCESS(nts));
 
    __try
    {
        //-f--> Verifica se a aplicação está interessada neste
        //      evento.
        if (!g_pEvent)
            __leave;
 
        //-f--> Aloca um nó para a lista de paths de imagens
        pNode = (PIMG_EVENT_NODE)ExAllocatePoolWithTag(PagedPool,
                                                       sizeof(IMG_EVENT_NODE),
                                                       IMG_TAG);
        if (!pNode)
        {
            //-f--> Ops!
            ASSERT(FALSE);
            __leave;
        }
 
        //-f--> Inicializa uma ANSI_STRING para usar na conversão
        //      do path da imagem. Vamos fornecer sempre um byte
        //      a menos para nos reservar espaço para adicionar um
        //      terminador nulo.
        RtlInitEmptyAnsiString(&asImageName,
                               pNode->ImageDetail.ImageName,
                               sizeof(pNode->ImageDetail.ImageName) - 1);
 
        //-f--> Faz a conversão sem alocação do resultado.
        nts = RtlUnicodeStringToAnsiString(&asImageName,
                                           pusFullImageName,
                                           FALSE);
        if (!NT_SUCCESS(nts))
        {
            //-f--> Ops!
            ASSERT(FALSE);
            ExFreePool(pNode);
            __leave;
        }
 
        //-f--> Coloca o terminador nulo para que a aplicação de
        //      teste possa contar com ele na hora de fazer o print.
        asImageName.Buffer[asImageName.Length] = 0;
 
        //-f--> Insere o nó na lista.
        InsertTailList(&g_ListHead,
                       &pNode->Entry);
 
        //-f--> Setamos o evento informando a aplicação que existem
        //      dados na lista a serem lidos.
        KeSetEvent(g_pEvent,
                   IO_NO_INCREMENT,
                   FALSE);
    }
    __finally
    {
        //-f--> Por fim, libera o mutex e corre pro abraço.
        KeReleaseMutex(&g_EventMtx,
                       FALSE);
    }
}

Quando o evento é sinalizado, a aplicação acorda de seu sono profundo e descobre que o driver tem dados para ela. Então ela envia um IOCTL para obter tais dados. Este IOCTL vai executar a rotina abaixo removendo o primeiro elemento da lista e verificar se ainda existem mais dados a serem coletados pela aplicação. Caso a lista esvazie nesta chamada, o driver reseta o evento para que a aplicação volte a dormir esperando pelos registros de novas imagens mapeadas.

NTSTATUS
OnGetImageDetail(PIMG_IMAGE_DETAIL  pImageDetail)
{
    NTSTATUS        nts;
    PLIST_ENTRY     pEntry;
    PIMG_EVENT_NODE pNode;
 
    //-f--> Adquire o mutex
    nts = KeWaitForMutexObject(&g_EventMtx,
                               UserRequest,
                               KernelMode,
                               FALSE,
                               NULL);
    ASSERT(NT_SUCCESS(nts));
 
    //-f--> Verifica se a lista está vazia. Sempre
    //      use esta rotina antes de tentar remover
    //      um elemento da lista.
    if (!IsListEmpty(&g_ListHead))
    {
        //-f--> Obtém o endereço do Entry
        pEntry = RemoveHeadList(&g_ListHead);
 
        //-f--> Obtém o endereço do nó
        pNode = CONTAINING_RECORD(pEntry,
                                  IMG_EVENT_NODE,
                                  Entry);
 
        //-f--> Copia para o buffer da aplicação.
        RtlCopyMemory(pImageDetail,
                      &pNode->ImageDetail,
                      sizeof(IMG_IMAGE_DETAIL));
 
        //-f--> Libera o nó e balezia
        ExFreePool(pNode);
        nts = STATUS_SUCCESS;
    }
    else
        nts = STATUS_NO_MORE_ENTRIES;
 
    //-f--> Pode ser que nesta chamada a lista tenha
    //      ficado vazia. Então verificamos novamente
    //      e resetamos o evento para que a aplicação
    //      não volte aqui.
    if (IsListEmpty(&g_ListHead))
        KeResetEvent(g_pEvent);
 
    //-f--> Libera o mutex e pronto.
    KeReleaseMutex(&g_EventMtx,
                   FALSE);
    return nts;
}

O resultado de tanto bla-bla-bla

Depois que o driver for compilado, instalado e iniciado, poderemos executar nossa aplicação de teste e esperar que algo seja executado. Quando um processo é criado, tanto seu módulo como as DLLs que ele depende são mapeadas no sistema. Isso vai disparar nossa rotina de callback no driver e fazer a coisa toda funcionar. Se você não sabe como compilar, instalar e iniciar um driver, este post pode te ajudar.


A imagem acima é o resultado da execução do notepad.exe enquanto nossa aplicação de teste esperava por eventos, mas qualquer outro processo poderia disparar tais eventos. Este post além de nos fornecer este exemplo de chamada invertida, também nos mostra como brincar com Mutex Objects, que foi a dúvida de outro leitor, Ismael Rocha (Brasília – DF).

Agora deixa eu voltar para o meu projeto da faculdade.
Até mais!

ImgNotifier.zip