28.4.10

Último post pelo Blogger

Olá cambada! Puts, eu já estava com saudade de postar alguma coisa. Pena este ser mais um daqueles chatos posts Off-Topic. O que posso fazer? É o que temos para o momento.

Hoje pela manhã recebi um mais um e-mail de aviso do Blogger dizendo que o suporte à publicação via FTP estão com os dias contatos. Como este recurso é utilizado por menos de 5% dos seus usuários, resolveram dar fim à ele. Mas essa não foi a principal razão pela qual eu decidi deixar de postar pelo Blogger para usar o WordPress. Eu já vinha ensaiando essa transição há pelo menos dois anos. A faculdade e outras desculpinhas esfarrapadas não me deixavam pôr essa idéia em prática. Alguns leitores já vinham reclamando da falta de um índice geral e da separação dos posts em categorias, coisas que o WordPress faz com uma mão nas costas.

Como um web designer, eu sou um excelente desenvolvedor de drivers, e eu já tinha ouvido falar que o WordPress usava PHP e MySQL, assuntos os quais eu não tenho a menor intimidade. Inicialmente contei com a ajuda do meu irmão que já possuia um blog pelo WordPress, mas assim como eu, ele também tinha faculdade e outras prioridades. Por um tempo eu contratei os serviços de um web designer que colocava em prática as minhas ídéias com relação ao novo layout, mas por fim ele se envolveu com algo maior e fiquei na mão novamente.

A idéia de migrar o blog persistia, mas a preguiça, a falta de tempo e de domínio de HTML e CSS me fizeram ir empurrando esse problema com a barriga. Cheguei até a ler o guia o oficial da famosa série "Preciso aprender isso de qualquer jeito", o livro "WordPress for Dummies". Pra quem está acostumado a ler livros de assuntos um tanto mais cabeludos, as mais de 380 páginas do livro foram como um passeio no parque. O mais surpreendente pra mim foi descobrir que o WordPress é uma ferramenta que não requer prática nem tampouco habilidade, e isso incluia as habilidades de PHP e MySQL que eu não tenho. Uma ferramenta poderosa, flexível e simples. A parte desagradável dessa história é que tudo que o livro falou sobre HTML e CSS é que ele não falava a respeito.

Até aqui excelente. Os 67 posts poderiam ser migrados mas o layout ainda foi o meu carma. Outras tentativas de encontrar alguém disposto a fazer o design pra mim me mostrou que estou no emprego errado. Esse negócio de desenvolver drivers não está com nada. Pra ganhar dinheiro mesmo o negócio é fazer Web Design.

Há uns três meses, quando recebi o primeiro aviso do Blogger sobre o fim do suporte ao FTP foi o momento "Agora ou nunca". Minha faculdade não me servia mais de desculpa e aos poucos fui colocando a mão na massa. Depois de muito trabalho manual de migrar cada post e cada comentário, veio o layout. Graças ao site W3Schools, às aulas particulares de Photohop do meu irmão e à ajuda de alguns amigos, essa semana consegui publicar o novo blog. Ainda faltam muitas coisas. A página "About Me" ainda não tem nada about me. A página de treinamentos, que vai falar sobre os cursos que ofereço ainda fala exatamente isso: "Essa página vai falar sobre os cursos que ofereço". Ainda estou instalando alguns plug-ins que vão ajudar em uma coisa aqui e outra ali. Mas o fato é que há dois dias do prazo final do suporte ao FTP, o novo blog já está no ar.

Esse post foi o último a ser contruído no Blogger com o objetivo principal de avisar àqueles que seguem o RSS original de que o endereço agora mudou. O novo RSS será provido em um novo endereço.

Eu não poderia terminar esse blog sem agraceder às pessoas que tiveram a paciência de me aturar com testes e dúvidas sobre HTML, CSS e sei-lá mais o que. Um muito obrigado aos meus amigos Lesma, Thiago Oliveira, Thiago Brito, meu irmão Kabloc, Willam, Francisco, à minha esposa Magda que migrou todos os comentários e a todos os outros que não me lembro agora (eu perturbei muita gente).

Se você não conhecia o blog antigo, aqui estão as páginas do velho aposentado.
Valeu!

26.2.10

Sexto Encontro de Programadores C/C++

Começo de ano é sempre a mesma correria. Depois que a gente começa a se acostumar com a idéia de que o feriadão de Natal e Ano Novo acabaram, logo vem o carnaval e desanda tudo. Aproveitei este início de ano para tirar merecidas férias, já que finalmente meu curso de Engenharia da Computação terminou. Como um belo start para minha reintegração à sociedade, nada melhor que uma viajem ao nordeste brasileiro. Semanas antes desta viajem fiquei sabendo do novo encontro da comunidade de programadores C/C++. Por causa da viagem eu perderia o evento.

Um mês se passou e semana passada vi que o evento foi adiado para o próximo dia seis de março. Muito boa notícia já que além de poder participar do evento também vou falar um pouquinho. Já participei de outro encontro de programadores e fiquei muito feliz de poder falar para tantas pessoas sobre esse assunto tão misterioso para muitos, que é o desenvolvimento de drivers para Windows.

Sob meu ponto de vista os resultados foram muito bons. A palestra deu uma introdução ao assunto de desenvolvimento de drivers e obviamente os 60 minutos de palestra não foram suficientes para explicar o assunto com detalhes, mas foi interessante mostrar a ponta do iceberg e poder responder a algumas perguntas dos presentes. Eu escrevi um post que comenta sobre o encontro, mas recomendo o post do meu amigo Lesma que ficou muito bom.

Lidando com Memória Virtual em Drivers

Bem, neste novo encontro não vou dar novamente uma introdução ao assunto, mas vou comentar sobre algumas carasterísticas e curiosidades sobre Memória Virtual no desenvolvimento de drivers. Entenda que não vou fazer um resumo do capítulo 9 do Windows Internals, "Memory Management" que fala sobre Page Table Entries e Working Sets, mas sim demonstrar que, diferentes de uma aplicação User-Mode, drivers precisam estar cientes dos conceitos fundamentais de memória virtual e paginação, controlando paginação de objetos e atendendo à requisítos de paginação de memória e espaço de endereçamento.

Os tópicos a serem discutidos na palestra serão os seguintes:

  • Overview de Memória Virtual e Paginação.
  • Operações de I/O e manipulação de Buffers.
  • Prioridade de thread e acesso à memória.
  • Pools de alocação, Tags e Quotas.
  • Evitando Fragmentação.
  • Drivers no caminho da paginação.
  • Controlando paginação de funções.
  • Obtendo endereços reais de memória.
  • Dispositivos de acesso direto à memória (DMA).
  • Recursos do Driver Verifier.
  • Hands on: Operações de memórias por drivers no WinDbg.
  • Dúvidas.

Esta é a página oficial do evento e as inscrições estão abertas. Até lá!

Download dos slides

20.1.10

Escrevendo Filtros

Vamos brincar de algo mais interessante agora. Obviamente ainda vamos dar passos pequenos para não nos perder com tantos detalhes. Hoje falarei sobre filtros de drivers. O IoManager do Windows nos permite adicionar funcionalidade a determinados drivers sem que tenhamos que substituí-lo. Um exemplo clássico seria um driver de criptografia de arquivos. Você não precisa escrever um novo driver de file system para que se tenha tal funcionalidade. Você pode simplesmente escrever um filtro que ficaria entre o driver de file system e o restante do sistema.

Na figura abaixo podemos observar o fluxo de IRPs que vão do IoManager para um certo driver, um filtro é instalado sobre o driver existente e passa a receber as IRPs no lugar do driver original. Com isso o filtro tem a oportunidade de alterar os parâmetros das IRPs recebidas, ou logar a atividade do driver original, ou duplicar solicitações do sistema para esse driver, ou mesmo negar serviço do driver original.

Numa operação de escrita, um filtro poderia criptografar os dados antes de enviá-los ao driver original, e de maneira similar, numa operação de leitura o filtro poderia decriptografar dados antes de entregá-los ao sistema.

Ainda não vai ser desta vez que construiremos um filtro de criptografia de file system. Filtros de criptografia de arquivos em tempo real estão entre os drivers mais complexos a serem escritos. Vamos escolher um driver mais simples, pra não dizer um bem besta, para aplicar os conceitos básicos demonstrados aqui.

Por falar em driver besta, vamos utilizar o driver deste post que já foi visto aqui. Este driver simplesmente armazena uma lista de strings enviadas por uma aplicação em operações de escrita. Tais strings são retornadas à aplicação em operações de leituras.

Escrevendo a DriverEntry

Como vimos neste outro post, uma das das coisas que a função DriverEntry() faz num driver comum é setar as Dispatch Routines que o driver vai atender para certo dispositivo. Para isso deve-se preencher o array de Major Functions que fica na estrutura DRIVER_OBJECT.

    //-f--> Seta as dispatch routines do driver.
    pDriverObj->MajorFunction[IRP_MJ_CREATE] = OnCreate;
    pDriverObj->MajorFunction[IRP_MJ_CLEANUP] = OnCleanup;
    pDriverObj->MajorFunction[IRP_MJ_CLOSE] = OnClose;
    pDriverObj->MajorFunction[IRP_MJ_READ] = OnRead;
    pDriverObj->MajorFunction[IRP_MJ_WRITE] = OnWrite;

No caso do nosso filtro de exemplo, queremos apenas monitorar a atividade do driver ao qual estamos atachados, dessa forma teremos sempre que encaminhar quaisquer IRPs recebidas ao driver de baixo. Uma forma simples e comum de fazer isso é setar todas dispath routines para uma única função. Essa função se encarrega de simplesmente logar a solicitação recebida e passá-la a diante.

Se dermos uma olhada na definicão de IRP_MJ_CREATE e seus amigos, veremos o seguinte trecho do arquivo wdm.h.

//
// Define the major function codes for IRPs.
//
 
 
#define IRP_MJ_CREATE                   0x00
#define IRP_MJ_CREATE_NAMED_PIPE        0x01
#define IRP_MJ_CLOSE                    0x02
#define IRP_MJ_READ                     0x03
#define IRP_MJ_WRITE                    0x04
#define IRP_MJ_QUERY_INFORMATION        0x05
#define IRP_MJ_SET_INFORMATION          0x06
#define IRP_MJ_QUERY_EA                 0x07
#define IRP_MJ_SET_EA                   0x08
#define IRP_MJ_FLUSH_BUFFERS            0x09
#define IRP_MJ_QUERY_VOLUME_INFORMATION 0x0a
#define IRP_MJ_SET_VOLUME_INFORMATION   0x0b
#define IRP_MJ_DIRECTORY_CONTROL        0x0c
#define IRP_MJ_FILE_SYSTEM_CONTROL      0x0d
#define IRP_MJ_DEVICE_CONTROL           0x0e
#define IRP_MJ_INTERNAL_DEVICE_CONTROL  0x0f
#define IRP_MJ_SHUTDOWN                 0x10
#define IRP_MJ_LOCK_CONTROL             0x11
#define IRP_MJ_CLEANUP                  0x12
#define IRP_MJ_CREATE_MAILSLOT          0x13
#define IRP_MJ_QUERY_SECURITY           0x14
#define IRP_MJ_SET_SECURITY             0x15
#define IRP_MJ_POWER                    0x16
#define IRP_MJ_SYSTEM_CONTROL           0x17
#define IRP_MJ_DEVICE_CHANGE            0x18
#define IRP_MJ_QUERY_QUOTA              0x19
#define IRP_MJ_SET_QUOTA                0x1a
#define IRP_MJ_PNP                      0x1b
#define IRP_MJ_PNP_POWER                IRP_MJ_PNP      // Obsolete....
#define IRP_MJ_MAXIMUM_FUNCTION         0x1b

Note que existe uma definição especial, a IRP_MJ_MAXIMUM_FUNCTION, que indica o índice máximo da tabela de dispatch rotines. Utilizaremos um loop simples para fazer com que todas rotinas em nossa tabela aponte para uma única rotina que daremos o nome de OnForwardDispatch.

    //-f--> Seta todas as dispatch routines do driver para
    //      uma que encaminhe a IRP para o driver original.
    for (i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++)
        pDriverObj->MajorFunction[i] = OnForwardDispatch;

Veremos a implementação dessa rotina mais tarde. O próximo passo que daremos aqui é localizar o device ao qual vamos nos atachar. Para fazer isso, usaremos a rotina IoGetDeviceObjectPointer(). Ela basicamente recebe o nome do device e nos retorna uma referência para ele.

NTSTATUS 
  IoGetDeviceObjectPointer(
    IN PUNICODE_STRING  ObjectName,
    IN ACCESS_MASK  DesiredAccess,
    OUT PFILE_OBJECT  *FileObject,
    OUT PDEVICE_OBJECT  *DeviceObject
    );

Observe que são dois os parâmetros de saída dessa rotina. Além do ponteiro para o device object ainda recebemos um ponteiro para um file object. Já ví algumas pessoas fazerem confusão com estes dois parâmetros, então vou dar alguma ênfase nisso.

O ponteiro para o file object representa uma conexão criada entre seu driver e o device que você abriu. Como vimos neste post, um file object é criado para respresentar conexões entre aplicações user mode e seu driver. Aplicações usam esse file object através do handle obtido na chamada à rotina CreateFile(). Aqui temos algo similar, mas apenas o Kernel foi envolvido. Isso significa que se você quizesse, você poderia lançar IRPs para o device solicitando operações como uma aplicação faria, mas não veremos isso hoje, ainda temos um filtro para terminar.

A grande confusão referente a estes dois parâmetros é com relação às referências entre os objetos. Na documentação vemos que o chamador desta rotina deverá liberar a referência que ganhou quando o device não for mais utilizado. Fazemos isso simplesmente usando a rotina ObDereferenceObject().

VOID 
  ObDereferenceObject(
    IN PVOID  Object
    );

"Já sei! Como estamos obtendo uma referência para um device object, então devo passar o ponteiro do device object. Certo?"

Er... Na verdade não. O file object é uma referência indireta ao device object. Conforme a figura abaixo, se imaginarmos que os contadores de referência apenas dizem respeito às nossas referências, quando chamarmos ObDereferenceObject() para o file object, seu contador de referência cairia para zero e uma nova chamada para ObDeferenceObject() seria feita indiretamente para o device object, fazendo com que seu contador de referência também caisse para zero destruindo o objeto.

Depois de obter o ponteiro para o device destino, teremos que criar nosso próprio device, o qual receberá as IRPs no lugar do device original. Para isso ainda usaremos a rotina IoCreateDevice() como faziamos com drivers, mas com algumas diferenças.

A primeira diferença é que seu device normalmente não tem nome. É possível criar filtros com nomes, mas isso pode gerar uma falha de segurança. Isso acontece pois quando um nome é consultado no Object Manager, suas regras de segurança são avaliadas. Quando criamos um filtro com nome, criamos a possibilidade de o mesmo objeto ser obtido por um nome diferente e que pode ter regras menos restritivas de segurança. Mas esse é outro assunto.

Quando criamos um device, precisamos informar o tamanho do device extension.

"O que vem a ser um device extension?"

Device extension é simplesmente um espaço de memória que está associada ao device object. Tal espaço normalmente mantém informações que dizem respeito ao device. O endereço do device ao qual estamos atachados normalmente fica no device extension. Desta forma, podemos definir que nosso device extension deve conter a seguinte estrutura.

//-f--> Nosso device externsion conterá apenas o endereço
//      do device ao qual estamos atachados.
typedef struct _DEVICE_EXTENSION
{
    PDEVICE_OBJECT  pNextDeviceObj;
 
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;

Depois de criar nosso device object, configuramos o método de I/O copiando os bits DO_BUFFERED_IO e DO_DIRECT_IO. Se você não se lembra destes bits, dê uma olhada neste post. O filtro deve propagar a escolha do driver original e o driver tem o compromisso de não mudar método durante seu tempo de vida.

Agora estamos prontos para nos atachar ao device escolhido, e faremos isso utilizando a rotina IoAttachDeviceToDeviceStackSafe() para fazer nosso device entrar na pilha de dispositivos.

NTSTATUS
  IoAttachDeviceToDeviceStackSafe(
    IN PDEVICE_OBJECT  SourceDevice,
    IN PDEVICE_OBJECT  TargetDevice,
    IN OUT PDEVICE_OBJECT  *AttachedToDeviceObject 
    );

Com esta chamada obteremos o ponteiro do device ao qual estaremos atachados, esse device será o próximo device que receberá a IRP depois que você a passar a diante.

"Mas Fernando, nós já não temos o ponteiro do device de destino?"

Muito bem, quando você obtém o ponteiro para um device, você teoricamente não sabe se existem filtros já atachados sobre ele. O ponteiro que você recebe nesta rotina pode não ser o ponteiro para o device de destino. Na figura abaixo podemos entender como essa relação acontece.

"Fernando, existe uma versão unsafe desta rotina?"

Na verdade existe, a IoAttachDeviceToDeviceStack().

PDEVICE_OBJECT 
  IoAttachDeviceToDeviceStack(
    IN PDEVICE_OBJECT  SourceDevice,
    IN PDEVICE_OBJECT  TargetDevice
    );

Ela é considerada unsafe por causa de uma pequena janela de tempo que pode causar um race condition. Repare que a diferença entre estas rotinas é a forma de obter o endereço do próximo device. Na versão original, este endereço é obtido pelo retorno na função. Se colocarmos essa função para rodar em câmera lenta veremos os seguintes passos.

  1. Seu device é atachado à pilha de dispositivos.
  2. O endereço do próximo device é retornado pela função.
  3. Seu driver recebe este endereço no retorno da função e atualiza o device extension.
  4. IRPs começam a chegar e seu driver às encaminham para o próximo device da lista.

Tudo parece lindo e até dá a impressão de que tudo vai funcionar muito bem em qualquer situação, mas desenvolvedor de driver é um bixo treinado para buscar race conditions. Dê mais uma olhada na sequência, mas agora em câmera super lenta. Com essa câmera super lenta agora podemos observar os passos que podem ocorrer entre os passos 2 e 3.

  1. Seu device é atachado à lista de dispositivos.
  2. O endereço do próximo device é retornado pela função.
    1. Sua thread é interrompida e uma IRP lançada por uma aplicação que roda em paralelo começa sua viagem por essa pilha de dispositivos.
    2. Seu device, que já está atachado, recebe a IRP e tenta encaminhá-la ao device de baixo.
    3. Oops! Nosso device extension ainda não foi atualizado com tal endereço.
    4. Seu driver se lembra de quando era uma criança e de tudo o que vivera até ali.
    5. Ele decide entrar de vez naquela dança e enviar a IRP para um device cujo ponteiro ainda é NULL causando um BSOD.
  3. "Jeremias, eu sou homem. Coisa que você não é e não atiro pelas costas não..."

Enfim, entenda que mesmo que você utilize o retorno da rotina IoAttachDeviceToDeviceStack() diretamente para atualizar seu device extension, ainda assim existe uma janela de tempo em que seu device estará atachado, mas que o valor ainda não foi atualizado no device extension. Isso porque o valor de retorno de uma rotina vem por um registrador. Pegar o valor desse registrador e atualizar uma variável ainda dá chances para o azar.

    //-f--> ----==== NÃO COPIE ISSO ====----
    //      Aqui ainda temos uma janela de tempo entre o device ser
    //      atachado e o valor de pNextDeviceObj ser atualizado.
    pDeviceExt->pNextDeviceObj = IoAttachDeviceToDeviceStack(pMyDeviceObj,
                                                             pTargetDeviceObj);

A rotina IoAttachDeviceToDeviceStackSafe() faz o sistema interromper o fluxo de IRPs nesta pilha até que a variável apontada pelo ponteiro de saída seja atualizado. Por essa razão, o endereço passado nesta rotina deve ser o endereço final da variável onde este valor será armazenado, que em nosso caso é pDeviceExt->pNextDeviceObj sem passar por variáveis intermediárias.

Esses detalhes são importantes e farão você entender que usar a versão safe desta rotina não é garantia de que tudo dará certo. Imagine que usando a versão safe você receba o endereço do próximo device em uma variável local e depois atualize seu device extension. Esse é um daqueles típicos casos que é necessário substituir aquele componente que fica entre o teclado e a cadeira.

Acha preciosismo? Tente ler o capítulo 5 do livro "Programming the Microsoft Windows Driver Model" onde Walter Oney fala sobre como lidar com cancelamento de IRPs.

Depois de atacharmos nosso device já podemos liberar a referência obtida por IoGetDeviceObjectPointer() utilizando a rotina ObDereferenceObject(), já que a rotina IoAttachDeviceToDeviceStackSafe() já garantiu a referência até que essa ligação seja desfeita.

Muito bem. Pra quem não conseguiu entender quase nada do que eu disse, segue o código fonte da implementação da nossa DriverEntry() de exemplo. Sabe como é cabeça de programador, as vezes um if vale mais que mil páginas de explicação.

/****
***     DriverEntry
**
**      Ponto de entrada do driver.
**      Bem vindo ao inferno.
*/
extern "C"
NTSTATUS
DriverEntry(IN PDRIVER_OBJECT  pDriverObj,
            IN PUNICODE_STRING pusRegistryPath)
{
    NTSTATUS            nts;
    PDEVICE_OBJECT      pMyDeviceObj;
    int                 i;
    UNICODE_STRING      usDeviceName;
    PDEVICE_OBJECT      pTargetDeviceObj;
    PFILE_OBJECT        pFileObj;
    PDEVICE_EXTENSION   pDeviceExt;
 
    //-f--> Seta a rotina de descarga do driver.
    pDriverObj->DriverUnload = OnDriverUnload;
 
    //-f--> Seta todas as dispatch routines do driver para
    //      uma que encaminhe a IRP para o driver original.
    for (i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++)
        pDriverObj->MajorFunction[i] = OnForwardDispatch;
 
    //-f--> Inicializamos a string com o nome do device ao
    //      qual queremos nos atachar.
    RtlInitUnicodeString(&usDeviceName,
                         L"\\Device\\StringList");
 
    //-f--> Obtemos o ponteito do device de destino
    nts = IoGetDeviceObjectPointer(&usDeviceName,
                                   FILE_READ_DATA,
                                   &pFileObj,
                                   &pTargetDeviceObj);
    if (!NT_SUCCESS(nts))
        return nts;
 
    //-f--> Criamos nosso device object
    nts = IoCreateDevice(pDriverObj,
                         sizeof(DEVICE_EXTENSION),
                         NULL,
                         pTargetDeviceObj->DeviceType,
                         pTargetDeviceObj->Characteristics,
                         FALSE,
                         &pMyDeviceObj);
    if (!NT_SUCCESS(nts))
    {
        //-f--> Oops!
        ObDereferenceObject(pFileObj);
        return nts;
    }
 
    //-f--> Obtem nosso DEVICE_EXTENSION
    pDeviceExt = (PDEVICE_EXTENSION)pMyDeviceObj->DeviceExtension;
 
    //-f--> Utiliza o mesmo método de IO do driver original
    pMyDeviceObj->Flags |= pTargetDeviceObj->Flags & (DO_BUFFERED_IO | DO_DIRECT_IO);
 
    //-f--> Aqui nosso driver entra na pilha de dispositivos
    nts = IoAttachDeviceToDeviceStackSafe(pMyDeviceObj,
                                          pTargetDeviceObj,
                                          &pDeviceExt->pNextDeviceObj);
    if (!NT_SUCCESS(nts))
    {
        //-f--> Oops!
        IoDeleteDevice(pMyDeviceObj);
        //-f--> Aqui não está faltando um return.
    }
 
    //-f--> Independente de estarmos atachados ou não, devemos liberar
    //      a referência obtida do device de destino.
    ObDereferenceObject(pFileObj);
    return nts;
}

Escrevendo a OnDriverUnload

Aqui é onde a festa acaba, antes de o nosso driver ser descarregado pelo sistema, teremos que remover nosso device da pilha de dispositivos e destruí-lo. (Risadas maléficas)

O código aqui é simples e não requer explicações se você for capaz de ler os comentários contidos nele.

/****
***     OnDriverUnload
**
**      Rotina de descarga do driver.
*/
VOID
OnDriverUnload(IN PDRIVER_OBJECT  pDriverObj)
{
    PDEVICE_OBJECT      pMyDeviceObj;
    PDEVICE_EXTENSION   pDeviceExt;
 
    //-f--> Nosso device está na lista de devices criados por este driver
    //      então vamos obtê-lo simples assim:
    pMyDeviceObj = pDriverObj->DeviceObject;
 
    //-f--> Aqui obtêmos o device extension.
    pDeviceExt = (PDEVICE_EXTENSION)pMyDeviceObj->DeviceExtension;
 
    //-f--> Aqui removemos nosso device da pilha passando o endereço do
    //      próximo device para a rotina abaixo.
    IoDetachDevice(pDeviceExt->pNextDeviceObj);
 
    //-f--> Agora podemos destruir nosso device (risadas maléficas)
    IoDeleteDevice(pMyDeviceObj);
}

"Fernando, nosso driver é forçado a se descarregar quando o driver original é descarregado?"

Essa é a filosofia do WDM, drivers são carregados automáticamente quandos seus dispositivos são detectados e descarregados quando seus dispositivos são desativados ou removidos. Os filtros seguem as mesmas regras e são carregados/descarregados basendo-se nestes eventos.

Mas não é isso o que acontece aqui. Os drivers de exemplo que uso neste blog são do modelo Legacy, e não WDM. No modelo Legacy, drivers são iniciados seguindo sua ordem de carga no registry, e não tem nada a ver com a detecção do seu dispositivo. Os filtros precisam iniciar depois dos drivers originais, e isso também é controlado pela sua ordem de carga. Este post fala sobre a ordem de carga dos legacy drivers.

"Tá! Falou, falou e não respondeu minha pergunta. O que acontece se eu solicitar a descarga do driver original enquanto houver um filtro atachado sobre ele?"

A descarga dos drivers que formam uma pilha deve ocorrer de forma inversa à sua carga. Neste caso o filtro deve ser descarregado antes do driver original, desempilhando os devices de cima para baixo. Caso o driver original receba uma solicitação de descarga enquanto ainda hoverem referências a seus devices, seja por um filtro ou por uma aplicação, a descarga é adiada até que suas referências sejam desfeitas. Até lá, a tentativa de obter novas referências para um device que recebeu a solicitação de descarga serão negadas.

Escrevendo Dispatch Routines

As dispatch routines de um filtro também são diferentes das dispatch routines de um driver. Apensar de elas poderem completar uma IRP chamando IoCompleteRequest(), elas normalmente repassam a socilitação adiante utilizando a rotina IoCallDriver(). Vou falar mais sobre o comportamento das dispatch routines de um filtro em posts futuros. Neste filtro de exemplo vamos apenas logar a atividade e repassar a solicitação ao próximo driver.

Quando falamos em repassar uma solicitação estamos na verdade falando de repassar IRPs. A leitura deste outro post é essencial para o que vamos fazer na implementação de nossas dispatch routines.

Para acabar esse post ainda nessa vida, segue código da implementação de nossa dispatch routine. Depois de ler o post sobre IRPs que acabei de recomendar, ler os comentários deste código devem ser suficientes para entender tudo o que foi feito aqui, ou não.

/****
***     OnForwardDispatch
**
**      Nossa dispatch routine simplesmente loga a IRP
**      recebida e repassa a solicitação adiante.
**
*/
NTSTATUS
OnForwardDispatch(IN PDEVICE_OBJECT    pDeviceObj,
                  IN PIRP              pIrp)
{
    PDEVICE_EXTENSION   pDeviceExt;
    PIO_STACK_LOCATION  pStack;
 
    //-f--> Obtém ponteiro para o device extesion
    pDeviceExt = (PDEVICE_EXTENSION)pDeviceObj->DeviceExtension;
 
    //-f--> Obtém stack location corrente
    pStack = IoGetCurrentIrpStackLocation(pIrp);
 
    //-f--> Manda o nome na major routine da IRP
    ASSERT(pStack->MajorFunction <= IRP_MJ_MAXIMUM_FUNCTION);
    DbgPrint("[StringFilter] : %s\n", g_szMajorNames[pStack->MajorFunction]);
 
    //-f--> Como não estamos modificando nada na stack location,
    //      vamos deixá-la para que o próximo device a use.
    IoSkipCurrentIrpStackLocation(pIrp);
 
    //-f--> Encaminha a IRP para o próximo device.
    return IoCallDriver(pDeviceExt->pNextDeviceObj,
                        pIrp);
}

Caso não tenham entendido nada, não esqueçam de me mandar um e-mail expondo suas dúvidas (sem ofenças pessoais). Isso me ajudará a entender suas dificuldades e a melhorar minhas explicações.

Testando o Filtro

Essa é a parte fácil do post. Para testar o filtro teremos primeiro que compilar, instalar e iniciar o driver deste post. Se você ainda não sabe como fazer isso, este outro post pode te dar um ponto de partida. Depois disso, compile, instale e inicie o filtro.

Depois de instalados, podemos utilizar a aplicação de teste para exercitar o driver. Poderemos acompanhar a atividade do filtro observando suas mensagens de debug que podem ser vistas no depurador de Kernel ou simplemente usando esta aplicação, que dispensa o uso de um depurador para ver as mensagens de debug de um driver.

"Fernando, fiz um teste aqui e ví que ao iniciar o filtro ele loga um evento de IRP_MJ_CLOSE mesmo antes de iniciar a aplicação de teste. O que eu fiz de errado?"

Não há nada de errado. Isso acontece por causa da sequência de passos seguidos na rotina DriverEntry() do filtro. Inicialmente o driver chama a rotina IoGetDeviceObjectPointer(), isso faz com que o IoManager envie uma solicitação de IRP_MJ_CREATE para o driver original. Depois disso nos atachamos à pilha de dispositivos e por fim chamamos a rotina ObDereferenceObject() que vai finalizar a única referência do file object que recebemos, enviando uma solicitação de IRP_MJ_CLOSE para o driver de baixo. Como já estamos atachados a ele, então somos capazes de ver nossa própria atividade sobre o driver original. Isso pode ser observado pela pilha de chamadas que teremos se houver um breakpoint em nossa dispatch routine quando liberarmos a referência do file object ao final da DriverEntry().

kd> k
ChildEBP RetAddr  
f8af9bcc 804ee129 StringFilter!OnForwardDispatch
f8af9bdc 80578f6a nt!IopfCallDriver+0x31
f8af9c14 805b0b18 nt!IopDeleteFile+0x132
f8af9c30 80522bd1 nt!ObpRemoveObjectRoutine+0xe0
f8af9c54 f8c80663 nt!ObfDereferenceObject+0x5f
f8af9c7c 805767ff StringFilter!DriverEntry+0xf3
f8af9d4c 8057690f nt!IopLoadDriver+0x66d
f8af9d74 80534c12 nt!IopLoadUnloadDriver+0x45
f8af9dac 805c61ee nt!ExpWorkerThread+0x100
f8af9ddc 80541de2 nt!PspSystemThreadStartup+0x34
00000000 00000000 nt!KiThreadStartup+0x16

Como de costume, o fonte do filtro que foi implementado neste post está disponível para download. Nosso filtro não faz quase nada, mas já servirá de base para posts futuros que darão mais funcionalidade a ele explicando como tais funcionalidades são implementadas.

Até mais!

Download StringFilter.zip

17.12.09

Drivers de Boot no Windows

Tenho acompanhado o trabalho do meu amigo Lesma, que em seu blog tem descrito como o processo de boot transforma um apanhado de bytes no disco rígido em um sistema operacional vivo. Pegando carona nesse tema, vou aproveitar para comentar sobre a ordem de carga dos drivers durante este processo. Com isso posso tentar responder uma pergunta frequente dos leitores: "Como fazer para que meu driver seja o primeiro a ser carregado?". Talvez este post possa clarear um pouco as coisas neste sentido, ou não.


Eu primeiro! Eu primeiro!

Um ponto importante a ser considerado no modelo Legacy quando escrevemos um driver é o referente ao momento no qual seu driver é carregado. Isso é configurado no valor "Start" na chave do driver no registro. Quatro valores configuram o momento da carga do seu driver, sendo eles:

  • Boot (0) - Drivers são carregados durante o boot, antes mesmo sistema operacional estar completamente pronto para execução.

  • System (1) - Drivers são carregados depois dos drivers de boot, quando o Kernel já está completamente funcional.

  • Automatic (2) - Neste grupo os drivers são carregados quando os subsistemas forem carregados. Basicamente junto com os serviços de User Mode.

  • Manual (3) - Nenhuma carga automática é realizada aqui, o driver é carregado somente quando alguém, ou algum componente, solicita sua carga.

  • Disabled (4)- Mesmo que o driver seja solicitado, sua carga é negada.

"Bom, então para meu driver ser o primeiro a ser carregado ele precisa ser iniciado como boot e pronto?"

Na verdade seu driver vai disputar um lugar na fila de drivers que querem ser iniciados no boot. Vários drivers estão configurados para ser iniciados nesse momento e o seu será apenas mais um. Mesmo entre os drivers de boot, uma ordem de carga precisa ser seguida para que certos drivers possam contar com os serviços de outros drivers. Por esse motivo, drivers se separam em grupos. Um grupo de cada vez vai sendo iniciado até que todos os drivers de boot passem por esse processo.

Drivers identificam seu grupo pelo valor "Group" encontrado em sua chave de registro. Esta chave deve conter o nome do grupo ao qual o driver pertence. Os nomes de todos os grupos podem ser encontrados na chave HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ServiceGroupOrder. Nela existe um valor do tipo REG_MULTI_SZ chamado "List" que contém a lista de todos os grupos existentes dispostos em sua ordem de carga.


"Tudo bem, meu driver está configurado para ser iniciado em Boot e está configurado para iniciar com o primeiro grupo de drivers. Pronto agora?"

Quase. Quando falamos em iniciar grupos de drivers, já fica sub-entendido que mais de um driver será carregado. A ordem que tais drivers são carregados dentro de cada grupo também pode ser determinada.

A chave KHEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\GroupOrderList mantém uma série de valores, cada um com o nome de um grupo. O valor é do tipo REG_BINARY e sua interpretação é um array de conjuntos de quatro bytes. O primeiro conjunto indica quantas tags estão contidas naquele buffer binário. Os demais conjuntos são as representações numéricas de cada tag. Dessa forma, a interpretação do buffer exibido na figura abaixo nos dá a informação de que temos seis tags, sendo elas: 1, 2, 3, 4, 5 e 6.


"Mas o que é uma tag?"

Uma tag é a identificação numérica de um driver dentro de um determinado grupo. Um driver se identifica pelo valor "Tag" que podemos encontrar na chave do driver no registro.

Apesar de o exemplo nos mostrar uma ordem crescente de tags, o valor da tag não determina a ordem de carga dos drivers. A ordem é determinada por sua posição dentro do buffer binário.

Ficou com nojinho de mexer em buffers binários? Você pode utilizar o OSR Driver Loader que configura tudo isso pra você quando utilizado para instalar um driver.


Colocar sua tag como primeiro na lista de tags classifica sua ordem de carga dentro de um determinado grupo, mas ainda não é fator determinante para ter seu driver carregado antes de todos os drivers do universo. Um novo grupo sempre pode ser criado e ter sua ordem de carga configurada para antes do seu grupo.

Todas essas regras sobre grupos, tags e afins não fazem sentido nos drivers gerenciados pelo Plug-And-Play (Pnp) Manager, já que a carga de tais drivers é solicitada quando o dispositivo ao qual seu driver está relacionado é detectado pelo driver de barramento.

"Aff! Fernando, pega leve e tenta explicar isso de novo."

Tudo bem, vamos lá. Quando você instala um driver Pnp, você o associa a um determinado dispositivo. Apenas como exemplo, digamos que esse dispositivo seja um conversor USB/Serial. Seu driver será carregado automagicamente quando seu dispositivo for detectado e será descarregado quando o dispositivo for removido.

Para que ele seja detectado, outros dispositivos precisam ser detectados antes, tais como controladora PCI, controladora USB e hub USB. Essa lista de dependência cria a pilha de dispositivos USB.

A controladora PCI, ao ser detectada, tem seu driver carregado e este enumera seus dispositivos filhos, já que PCI é um barramento. Para cada disposivito detectado, esse driver utiliza o barramento para detectar a identidade de cada dispositivo e cria um Phisical Device Object (PDO) para cada um deles. O Pnp Manager carrega o driver de cada dispositivo atachado a esse barramento. Esse driver criará o Functional Device Object (FDO) do dispositivo, dando funcionalidade a ele.

Um desses dispositivos é a controladora do barramento USB. Seguindo o ritual, o driver de barramento USB enumera seus dispositivos filhos, criando novos PDOs. Assim, os hubs USB são detectados e seu driver será carregado. Este driver criará um novo FDO para cada hub. O driver de hub USB vai enumerar seus dispositivos filhos e é nesse momento que seu dispositivo é detectado. O driver que você escreveu será carregado e o Pnp Manager irá chamar sua rotina AddDevice, que receberá o PDO que o driver de hub criou referente ao seu dispositivo.

Ufa! Tudo bem, tenham calma. O assunto Plug-And-Play não é o foco deste post e já está na minha lista de posts futuros.

Toda essa atividade que age recursivamente serve para montar a árvore de dispositivos do sistema. Sabendo que esta árvore é formada pelos nossos drivers e seus devices, fica explícito aqui que no fundo "As árveres somos nozes". A figura abaixo dá uma idéia de como a árvore de dispositivos é organizada.


Ainda falando sobre ordem de carga de drivers, não faria sentido ter seu driver carregado antes de todos os outros drivers, já que os componentes básicos para a comunicação com seu dispositivo ainda não foram carregados, e por isso, não possuem funcionalidade nenhuma. Além do mais, ter seu driver carregado muito cedo lhe trará problemas em lidar com outros componentes do sistema que ainda não estarão prontos para atender seu pedido. Mais detalhes neste post.


Depurando no Boot

Outro assunto curioso e que pode gerar alguma confusão é o referente ao debug de drivers que são carregados no boot. Apesar de a conexão de Debug usar o meio serial, firewire ou mesmo USB, os drivers referentes a estes meios não precisam estar carregados para que você possa realizar o debug do sistema. Em outras palavras, o driver de porta serial não é utilizado para fazer debug do sistema quando se usa o meio serial. Isso seria um problema se pensarmos que alguns drivers são carregados e inciados antes do driver de porta serial. Como estes drivers seriam depurados?

O fato é que o algoritmo que lida com os meios de depuração do sistema são definidos no próprio Kernel (mais precisamente no módulo ntoskrnl.exe e seus irmãos). Este módulo lida diretamente com o hardware responsável pelo meio utilizado. Essa é também a explicação para outra pergunta frequente: "Meu computador não tem porta serial. Posso usar um conversor USB/Serial do lado Target para fazer debug do sistema?". Como acabamos de ver, um conversor USB/Serial depende de toda uma pilha de dispositivos para que a porta serial esteja disponível. Tal funcionalidade não está implementada no algoritmo de debug do sistema, e como comentei neste outro post, sistemas mais novos implementam novas funcionalidades de debug no Kernel.

Mesmo que seu driver seja de boot, ele ainda pode ser depurado. Este outro post mostra ainda como fazer mapeamento de um driver de boot pelo WinDbg. Não sabe do que estou falando? É sobre ter seu driver substituído por uma nova versão automaticamente no lado target quando este for carregado. Vale a pena dar uma olhada.

Have fun!

27.11.09

Mais um Engenheiro à solta

Ufa! Quarta-feira passada foi minha última prova da universidade. Parece mentira mas finalmente o curso de Engenharia da Computação chegou ao fim. Ainda temos que entregar uma papelada do projeto de conclusão de curso, mas são apenas papéis. Acabaram as provas, os trabalhos, os relatórios e agora um baita peso começa a sair das minhas costas à medida que as notas vão sendo liberadas no site da universidade. Quem me segue no Twitter teve uma idéia da correria que tem sido. Esse ano foi especialmente apertado por conta das dependêcias que tive que cursar, do estágio que tive que fazer, e por fim, do projeto de formatura que tivemos que apresentar.

O projeto foi o maior responsável pela minha ausência no blog, foram muitos testes, sustos, sensores queimados, apresentações e feiras.

Pera aí! Você disse sensores queimados?

Isso mesmo. Murphy pode ser considerado um integrante do nosso grupo já que esteve sempre presente no desenvolvimento do projeto. Nas vésperas da apresentação para a banca avaliadora, um mal contato nos reguladores de tensão, originado pela trepidação do motor do helimodelo, fez com que quase tudo se queimasse. Foi um desespero geral, já que os sensores eram importados. Sem falar da grana que teríamos que desembolsar, ainda teríamos que aguardar a entrega que ocorre em média em duas semanas. Nesse momento Murphy nos deu uma trégua. Particularmente acho que ele ficou com pena de nós. Digo isso porque no dia seguinte encontramos uma revenda de sensores similares aos que estávamos usando no projeto aqui em Santo André (cidade onde moro). Quando entramos em contato com a revenda, descobrimos que eles tinham os sensores em estoque. Inacreditável não?

Depois da apresentação que fizemos para a banca avaliadora, ainda fomos convidados a participar da apresentação de gala da universidade, onde os melhores projetos do ano foram exibidos.


Também ficamos em segundo lugar no concurso "Melhor Aplicação Acadêmica Baseada em PC" promovido pela National Instruments, e com isso fomos convidados a expor nosso trabalho no stand da National no ISA Show 2009. O mais legal é que o primeiro lugar deste concurso foi conquistado por um grupo também da minha sala na Universidade São Judas Tadeu. Isso evidenciou que além de ficarmos na frente de várias outras universidades, mostrou que nossa sala né brinquedo não.

A National Instruments produziu este vídeo explicativo onde eu falo sobre a idéia básica do projeto. O vídeo foi feito na chácara de um dos componentes do grupo, onde ficamos internados nas últimas semanas antes das apresentações finais.

Este é apenas um post de "Ei, estou voltando do coma!". Agora vou parar de falar sobre o projeto e voltar a falar sobre o que realmente interessa: "Telas Azuis da Morte". O meu grande dilema agora é se devo também comentar algo sobre firmware e hardware. Desenvolvi muitas atividades com software embarcado não só durante a produção do projeto.

Durante o projeto, sobretudo desenvolvendo protocolo USB em um 8051 utilizando o Keil, além de ler sensores e trabalhar com barramentos em um PIC utilizando o compilador C18 no MPLAB.

Além disso, tive a oportunidade de contribuir no desenvolvimento de um projeto do IPEI. O hardware deste projeto mede os resultados de uma máquina de tração e os envia a um computador via USB. Contribui no desenvolvimento do firmware PIC implementando o protocolo HID mais uma vez utilizando o MPLAB.

Por último e não menos importante, também contribui no desenvolvimento de firmware FreeScale durante meu estágio na Commodity. Este firmware também lida com protocolo USB, e conta com o auxílio de um chip que dispoê desta interface, já que o DSP que compunha a solução não o fazia. Desta vez tive que utilizar o Code Warrior no desenvolvimento, e revivi o tempo em que trabalhei na Provectus desenvolvendo um Bootloader para a atualização do próprio firmware via rede 485, só que dessa vez foi através da USB.

Tanta interação com a USB de ambos os lados do cabo é principalmente fruto dos conhecimentos adquiridos com o livro "USB Complete" de Jan Alexlson. Não me lembro se já comentei sobre esse livro aqui antes, mas vale a pena falar dele principalmente sobre do ponto de vista eletrônico/firmware. O livro é simplesmente ótimo, mas diz não cobrir o desenvolvimento de drivers para USB pois tal assunto daria um livro à parte. Com isso ele indica um velho conhecido nosso, o "Programming the Windows Driver Model" de Walter Oney.

De qualquer forma, aguardem por reformas neste blog. Durante muito tempo tenho trabalhado no limite para conseguir postar e ainda dar conta de todo o resto. Agora meu tempo extra me permitirá fazer posts com mais frequência e finalmente montar turmas abertas do Curso de Drivers para Windows.

Como alguns de vocês já sabem, mantenho uma lista de interessados no curso e que serão notificados por e-mail quando as turmas forem abertas. Independente disso, gostaria de receber e-mails sobre preferências de horários e especialidades, tais como WDM/KMFD/UMDF/File Systems ou ainda sobre ênfase em USB/PCI.

É isso aí. Para um post que não tinha nada a dizer, até que esse disse muito.
Até breve!

8.9.09

Um Helimodelo no XV Simpósio Multidisciplinar

Esse é mais um daqueles posts Off-Topic que não tem nada a ver com nada. Ou quase nada. Acho que a maioria de vocês já esta cansada de saber que este é meu último ano da universidade e que este ano estou todo enrolado com nosso Trabalho de Graduação (TG). Quem acompanha meu Twitter tem uma idéia de como isso tem tomado meu tempo. Você pode estar se perguntando: "O que faz este projeto?". Em uma palavra: "Nada". Estamos trabalhando a mais de um ano desenvolvendo hardware, firmware, driver, software e controle para que o projeto não faça nada. Na verdade, o objetivo é que não aconteça nada com um helimodelo em voo. Tá, tudo bem, desde o começo agora.


Era uma vez um helimodelo

Alguns de vocês já sabem que além de programador retardado, também tento gastar o tempo que não tenho como helimodelista. Um helimodelo é um helicóptero em escala reduzida, equipado com motor à combustão ou mesmo elétrico. Com as funcionalidades de um helicóptero convencional, é capaz de realizar voos com as mesmas características e liberdade de movimentos. Na verdade, quanto à capacidade de voo, um helimodelo pode fazer mais que um helicóptero real. Não é incomum ver um helimodelo voando de cabeça para baixo.


Um helimodelo é controlado por um sistema de rádio controle que determina os movimentos de servomotores instalados na aeronave. Cada servo tem seu papel específico dentro do helimodelo. Um controla o acelerador, outro a inclinação das pás do rotor de cauda e assim por diante.


É fácil controlar um helimodelo?

Não mesmo. O rádio controle possui dois sticks, e assim, são 4 os movimentos que você deve comandar ao mesmo tempo, isso além das chaves adicionais. O grande problema é que para ganhar os reflexos necessários para fazer a correção leva um certo tempo de treino. Acredite, você não vai querer um helimodelo voando desgovernado perto de você. Além do risco de se ferir, é quase certo que o helimodelo acertará algo e se dividirá em vários pedaços. Posso dizer que este não é um equipamento barato e cada queda pode significar centenas de reais para colocar tudo para funcionar novamente. Para começar com esse hobby normalmente utilizamos um simulador.


Um simulador é muito parecido com um vídeo-game. Com ele você recebe um joystick especial que é uma réplica de um rádio controle, mas que faz interface USB com seu computador. Você utiliza tal joystick para controlar um helimodelo na tela do seu computador. Assim, a cada queda que o modelo sofrer, basta apertar um botão de reset no próprio controle para que você possa tentar novamente. Quando eu estava começando no hobby, todos me indicaram um simulador até que comprei um. Fiquei pensando: "Que dificil que nada... Pra cima de mim? Só se for pra esses coroas. Tenho mais de vinte anos de vídeo-game nas costas". Quando comecei a brincar com o simulador ví que não era tão fácil assim. Dezenas e dezenas de quedas. Os simuladores são realmente um excelente início. Eles conseguem reproduzir com grandes detalhes o comportamento de um modelo.


Depois do simulador, você normalmente faz aulas de voo. E para isso você contrata um instrutor. Mas o que pode fazer um instrutor além de lhe desejar boa sorte? O que acontece é que rádios controles podem ser ligados por um cabo de treinamento. Esse cabo permite que o instrutor possa controlar o helimodelo, e com o mudar de uma chave, o controle passa para a mão do aluno. Se o aluno perder o controle sobre o modelo, o instrutor pode tomar o controle de volta e evitar que seu helimodelo novinho se transforme num monte de lixo.


Legal, mas e o projeto?

Nosso projeto tem como objetivo controlar um helimodelo em voo de forma a estabilizá-lo. Por isso digo que nosso projeto vai fazer nada. Houve professor que ainda disse: "Caramba! Vocês vão fazer tudo isso para fazer um helicóptero ficar parado?". Se você é um helimodelista, sabe que manter o helimodelo parado é o primeiro desafio de um piloto. Mesmo em ambientes sem vento, estabilizar um helimodelo requer uma boa quantidade de experiência. Nosso objetivo final seria descrever um plano de voo simples onde ele decole, estabilize no ar, faça alguns movimentos e finalmente pouse. Não queremos dar um passo maior que a perna. Vamos ver o que conseguimos até o dia da apresentação.

Para detectar os movimentos do helimodelo, nós instalamos alguns sensores no helimodelo, os dados dos sensores são reunidos por um microcontrolador que está instalado numa placa também a bordo do helimodelo. Depois de reunidos, os dados são enviados à uma outra placa em solo através de um módulo ZigBee. Na foto abaixo pode-se ver nossa plaquinha. Essa placa em solo é um kit da Atmel que tem suporte a diversas interfaces, sendo a USB uma delas. Assim, escrevemos o firmware e o driver USB para fazer com que tais leituras agora fossem recebidas pelo nosso software de controle.


Para fazer o controle, utilizaremos o novo Toolkit de lógica Fuzzy que está disponível no novo LabVIEW 2009 da National Instruments. Para quem não conhece, o LabVIEW é uma das principais ferramentas de controle utilizadas pela engenharia moderna. É possível fazer programas de controle apenas desenhando e arrastando componentes sobre a tela, pode parecer contraditório um desenvolvedor de baixo nível dizer isso, mas toda essa abstração nos dá tempo para dedicar às coisas que realmente precisam de tempo. A ferramenta também dispõe de interfaces de I/O permitindo trabalhar com sensores e atuadores diretamente sobre o meio eletrônico.

Uma outra informação interessante é que o Toolkit de PID e Lógica Fuzzy foi completamente re-escrito e reformulado com a ajuda de um brasileiro. Isso mesmo, Bruno Cesar (na foto ao lado) trabalha na National Instruments Brasil e foi um dos responsáveis por esse desenvolvimento. Acha isso conhecidência? Então o que você me diria ao saber que ele também se formou na Universidade São Judas Tadeu? Bruno esteve semana passada no campus da Mooca dando uma palestra sobre o novo módulo de lógica Fuzzy. Isso nos deu mais certeza de que a lógica Fuzzy é a ideal para nosso problema de controle, já que ela é perfeita para lidar com problemas complexos onde não se tem o modelo matemático que descreva o comportamento de um helimodelo. A lógica Fuzzy se baseia na experiência de um operador para atuar sobre os controles. É bem interessante.

O LabVIEW também nos permite que façamos chamadas à DLLs. Essa foi uma maneira simples que conseguimos para fazer com os dados que estavam no driver chagar ao software de controle. Assim, criamos uma DLL que abstraísse muitas das complicações de se interagir com um dispositivo USB. O LabVIEW apenas chama funções do tipo LeAmostra() que já retorna o dado pronto para o uso. Todo aquele código com CreateFile() e DeviceIoControl() ficou por conta da DLL, além de outras funções auxiliares.

Agora vocês podem estar se perguntando: "Mas como o LabVIEW vai atuar sobre o helimodelo?". Lembra daquele cabo de treinamento? Nossa placa USB vai receber as ações de controle do LabVIEW através da mesma DLL. Para fazer com que tais comandos sejam aplicados sobre o helimodelo, fizemos com que nossa placa USB se comporte como um rádio controle, que utilizará o cabo de treinamento para aplicar o controle sobre o helimodelo. Obviamente ainda teremos um piloto segurando o rádio no papel de instrutor, principalmente para evitar acidentes, onde um mal funcionamento de nosso projeto poderia transformar meu helimodelo num monte de lixo.


O Simpósio Multidisciplinar

Desde que comecei a escrever sobre o projeto neste blog, algumas pessoas ficaram curiosas e disseram: "Me avise quando houver alguma apresentação". Bom, esta é a sua chance de dar uma olhada em alguns projetos. Nosso projeto foi aceito para ter um espaço neste evento que vai acontecer de 18 a 25 de setembro na Universidade São Judas Tadeu. Serão várias apresentações curtas de 15 cada. Os projetos ainda não estão terminados, e dessa forma, você ainda não verá nenhum helimodelo voando sozinho por lá, mas estaremos com nosso equipamento dispostos a responder algumas perguntas. O simpósio é aberto à visitação pública. Não é necessário ser aluno para participar, basta se inscrever gratuitamente no site da universidade e pronto. Se quiser discutir um pouco sobre sensores, microcontroladores, firmwares, drivers, controle ou ainda helimodelismo, mesmo que durante um café, é só aparecer.

Bom, é isso aí. Já escrevi demais. Agora preciso voltar ao meu projeto.
Até mais!

18.8.09

Notificando eventos à aplicação

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!

Download ImgNotifier.zip

web stats script