Mais um Engenheiro à solta

27 de November de 2009 - Fernando Roberto

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!

Um Helimodelo no XV Simpósio Multidisciplinar

8 de September de 2009 - Fernando Roberto

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!

Notificando eventos à aplicação

18 de August de 2009 - Fernando Roberto

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

Strings no Kernel

7 de July de 2009 - Fernando Roberto

Tem coisa mais besta que manipular strings? Creio que a resposta correta seria “Depende“. Quando eu tinha terminado meu curso técnico de Informática Industrial em 1995, eu pensava que sabia muito de linguagem C. Afinal de contas eu já sabia manipular strings. Copiar, concatenar, inverter, fazer buscas por palavras… O que mais um programador deveria saber? Quando comecei a fazer meu estágio e a pegar programas do mundo real, descobri que eu não sabia nada. Mas uma coisa é certa, eu sabia manipular strings. Este post deve ajudar bastante os programadores C++ que manjam tudo de Templates, Smart Pointers, STL e coisas mágicas que abstraem a realidade facilitando a vida do programador. Com todos esses recursos de contadores de referências e sobrecarga de tudo que é operador, as coisas começam a ficar nebulosas e você começa a se perguntar: “Mas onde está o buffer mesmo?”. Hoje vamos apenas dar uma passadinha de leve nas estruturas UNICODE_STRING, ANSI_STRING e algumas funções de conversão entre elas. Pode parecer besteira, mas se você não souber brincar de strings, pra quê aprender o resto? Vai tudo acabar em tela azul mesmo.

E no prézinho…

Nós aprendemos com a tia da escolinha que strings são cadeias de caracteres. Assim poderíamos contar a história de nossas vidas apenas colocando um caractere na frente do outro.

CHAR    szExemplo[] = "Tava ruim lá na Bahia, profissão de bóia-fria\n"
                      "Trabalhando noite e dia, num era isso que eu queria\n"
                      "Eu vim-me embora pra \"Sum Paulo\",\n"
                      "Eu vim no lombo dum jumento com pouco conhecimento\n"
                      "Enfrentando chuva e vento e dando uns peido fedorento (vish)\n"
                      "Até minha bunda fez um calo\n"
                      "Chegando na capital, uns puta predião legal\n"
                      "As mina pagando um pau, mas meu jumento tava mal\n"
                      "Precisando reformar\n"
                      "Fiz a pintura, importei quatro ferradura\n"
                      "Troquei até dentadura e pra completar a belezura\n"
                      "Eu instalei um Road-Star!";

Jumento Celestino / Mamonas Assassinas

Duas coisas são obrigatórias que você saiba para que você possa continuar lendo este post. Uma delas é que estes caracteres precisam estar armazenados em algum lugar, seja em uma variável local, alocadas no heap ou mesmo no segmento de dados inicializados. A outra coisa é que strings normalmente são terminadas por um caractere NULL, mas a falta dele não descaracteriza uma string. Isso significa que podemos ter strings sem terminadores onde seu tamanho é indicado por uma outra variável. Deixo aqui um gancho para o Lesma explicar essas coisas aos meninos e meninas interessados.

Uma outra característica importante é que nem sempre um caractere é igual a um Byte. Existem strings compostas por caracteres largos. Caracteres largos, ao contrário do que se pensa, não são caracteres sortudos, mas sim, caracteres formados por valores de 16 bits. Com strings formadas por tais caracteres pode-se expressar palavras em qualquer idioma. Isso explica como o Windows consegue lidar com nomes de arquivos alienígenas quando você tenta instalar os drivers do HiPhone que você comprou no China.

Além disso, imagine que ocorra um erro durante o processo de criptografia do seu disco rídigo. Seria vital que uma mensagem de erro detalha lhe fosse exibida independente da nacionalidade do produto. Se a informação vai ajudar já é outro assunto.

Quatro tipos de strings

Destes temos duas strings que já estamos acostumados a ver em user-Mode, ambas terminadas com caractere NULL.

CHAR    szString[] = "Uma string de CHAR";
WCHAR   wzString[] = L"Uma string de WCHAR";

As outras duas strings são as que normalmente vemos em kernel-mode.

typedef struct _UNICODE_STRING {
  USHORT  Length;
  USHORT  MaximumLength;
  PWSTR  Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
 
typedef struct _STRING {
  USHORT  Length;
  USHORT  MaximumLength;
  PCHAR  Buffer;
} ANSI_STRING, *PANSI_STRING;

Estas strings são definidas por estruturas com três membros, os quais descrevo abaixo. O comportamento das rotinas e macros que às manipulam é muito similar. Por isso vou concentrar meus exemplos em UNICODE_STRING já que o sub-sistema Win32 converte tudo para unicode quando passa uma chamada para o kernel.

  • Buffer: E um ponteiro para a região de memória onde os caracteres são armazenados.

  • Length: Indica a quantidade de bytes válidos da string. Este tamanho é sempre expresso em bytes, mesmo que esta seja uma string de WCHAR.

  • MaximumLength: Indica o tamanho máximo que esta string pode ter.

Para entender de como estes membros são interpretados, dê uma olhada no exemplo abaixo:

void OsTresMembros(void)
{
    WCHAR           wsUmArray[200];
    UNICODE_STRING  usString;
 
    //--f-> Indico onde os caracteres desta string
    //      serão armazenados.
    usString.Buffer = wsUmArray;
 
    //-f--> Ainda não escrevemos nada no buffer,
    //      por isso, nada do que esteja no buffer
    //      é válido.
    usString.Length = 0;
 
    //-f--> Embora o buffer não esteja inicializado
    //      ele ainda está lá e pode conter uma string
    //      de no máximo seu tamanho em bytes.
    usString.MaximumLength = sizeof(wsUmArray);
}

Aqui vemos uma string unicode vazia, pois tem zero bytes válidos, mas sua capacidade de armazenar é de até 200 caracteres. Notem que os caracteres são armazenados em um array que está na pilha. Isso significa que nenhum leak de memória será causado com o termino desta rotina.

Inicializando Strings

Podemos ainda utilizar algumas macros que fazem a inicialização destas estruturas.

void InitString(void)
{
    UNICODE_STRING  usOutraConstante;
    UNICODE_STRING  usVazia;
    WCHAR           wzBuffer[30] = L"Isso não será considerado.";
 
    //-f--> Inicaliza uma string na sua criação.
    UNICODE_STRING  usConstante = RTL_CONSTANT_STRING(L"Uma string.");
 
    //-f--> Inicializa uma string constante.
    RtlInitUnicodeString(&usOutraConstante,
                         L"Uma outra string constante que não muda.");
 
    //-f--> Inicializa uma string vazia para uso posterior
    RtlInitEmptyUnicodeString(&usVazia,
                              wzBuffer,
                              sizeof(wzBuffer));
}

Reparem nos valores destas estruturas.

kd> ?? usConstante
struct _UNICODE_STRING
 "Uma string."
   +0x000 Length           : 0x16
   +0x002 MaximumLength    : 0x18
   +0x004 Buffer           : 0xf8cd8600  "Uma string."
 
kd> db 0xf8cd8600 L0x18
f8cd8600  55 00 6d 00 61 00 20 00-73 00 74 00 72 00 69 00  U.m.a. .s.t.r.i.
f8cd8610  6e 00 67 00 2e 00 00 00                          n.g.....

Embora a capacidade máxima desta string seja de 0x18 caracteres, somente 0x16 bytes são válidos. Isso porque a macro desconsidera o terminador NULL que o compilador C/C++ deixou de brinde.

A macro RTL_CONSTANT_STRING() nos permite inicializar a string na sua criação, mas um outro notável recurso dela é que ela ferra o IntelliSence do Visual Studio (2008 pelo menos). Então se você gosta mesmo do IntelliSence, prefira utilizar a macro RtlInitUnicodeString().

kd> ?? usVazia
struct _UNICODE_STRING
 "Isso não será considerado."
   +0x000 Length           : 0
   +0x002 MaximumLength    : 0x3c
   +0x004 Buffer           : 0xf8ae5c34  "Isso não será considerado."

Oops! Como pode uma string com zero bytes válidos ser exibida pelo WinDbg? Na verdade, o que acontece é que o WinDbg desmonta a estrutura UNICODE_STRING e mostra cada um dos mebros aqui. No caso, existe um array de WCHAR com valores bem comportados aqui. Não culpe o coitadinho. Você é que está mal acostumado com o Visual Studio.

kd> db 0xf8ae5c34 L0x3c
f8ae5c34  49 00 73 00 73 00 6f 00-20 00 6e 00 e3 00 6f 00  I.s.s.o. .n...o.
f8ae5c44  20 00 73 00 65 00 72 00-e1 00 20 00 63 00 6f 00   .s.e.r... .c.o.
f8ae5c54  6e 00 73 00 69 00 64 00-65 00 72 00 61 00 64 00  n.s.i.d.e.r.a.d.
f8ae5c64  6f 00 2e 00 00 00 00 00-00 00 00 00              o...........

Mas estes dados só estão aí por acaso. Eles não são considerados como parte válida de uma string unicode. Isso atrapalha um pouco na hora de fazer o debug pois mesmo a janela de variáveis locais também mostra esse conteúdo inválido.


A extensão !ustr mostra apenas os dados válidos de uma string unicode.

kd> !ustr usConstante
String(22,24) at f899fc6c: Uma string.
 
kd> !ustr usVazia
String(0,60) at f8ae5c1c:

O terminador é necessário?

Não mesmo! Você pode bem observar que nos exemplos anteriores, a macro deixou o terminador de fora dos bytes válidos. Considerar o terminador como byte válido é um erro e pode gerar confusão. Imagine comparar duas strings distintas que carregam o mesmo conteúdo, mas uma delas considera o terminador como informação válida. Tais strings serão diferentes, já que possuem comprimento diferente.

Creio que o importante mesmo é não contar com o terminador nas strings que você recebe de outros componentes. É fato que na maioria das vezes, o buffer possui um terminador, e que apesar de não ser considerado informação válida, o terminador ainda está lá. Nunca conte com isso a menos que haja alguma nota na documentação. Já vi pessoas que utilizarem o membro Buffer como parâmetro para uma chamada à rotina wcslen() por exemplo. Além do risco de obter a informação incorreta, ainda tem um plus de poder gerar uma tela azul.

“Mas Fernando, eu já testei isso em vários sistemas operacionais e sempre funcionou.”

Isso não justifica nada, você não pode se apoiar em testes, mas em documentações. Quando seu produto se espalha no mercado, ele enfrenta muitos ambientes diferentes, com os mais diversos filtros, anti-virus, monitores e por aí vai. Não se pode ter certeza da implementação de qualquer software. O melhor que podemos esperar deles é que se apoiem na documentação.

Manipulando Strings

Os membros da estrutura UNICODE_STRING são basicamente utilizados por rotinas de manipulação a fim de verificar se o buffer existente é suficiente para a operação desejada. Portanto, antes de utilizar uma string, certifique-se de esta foi inicializada corretamente. No caso de uma cópia de strings, a string de destino precisará ser inicilizada mesmo que vazia.

void CopyString(void)
{
    UNICODE_STRING  usSource;
    UNICODE_STRING  usTarget;
    WCHAR           wzTarget[10];
 
    //-f--> Aqui inicializamos nossa string de origem.
    RtlInitUnicodeString(&usSource,
                         L"12345678901234567890");
 
    //-f--> Aqui inicializamos nossa string de destino.
    RtlInitEmptyUnicodeString(&usTarget,
                              wzTarget,
                              sizeof(wzTarget));
 
    //-f--> Realiza a cópia
    RtlCopyUnicodeString(&usTarget,
                         &usSource);
}

Aqui vemos o exemplo de uma cópia de string onde a string de origem é maior que a de destino. Nessa situação não teremos uma violação de acesso, mas o buffer de destino será preenchido completamente. Notem que a rotina não nos deixou o confortável terminador.

kd> !ustr usTarget
String(20,20) at f89a3c6c: 1234567890
 
kd> ?? usTarget
struct _UNICODE_STRING
 "1234567890"
   +0x000 Length           : 0x14
   +0x002 MaximumLength    : 0x14
   +0x004 Buffer           : 0xf89a3c54  "1234567890"
 
kd> db 0xf89a3c54 L0x20
f89a3c54  31 00 32 00 33 00 34 00-35 00 36 00 37 00 38 00  1.2.3.4.5.6.7.8.
f89a3c64  39 00 30 00 98 5d 5f 00-14 00 14 00 54 3c 9a f8  9.0..]_.....T<..

Diferentes da rotina RtlCopyUnicodeString(), algumas outras rotinas nos retornam STATUS_BUFFER_TOO_SMALL quando o buffer é insuficiente.

VOID 
  RtlCopyUnicodeString(
    IN OUT PUNICODE_STRING  DestinationString,
    IN PCUNICODE_STRING  SourceString
    );
 
NTSTATUS 
  RtlAppendUnicodeStringToString(
    IN OUT PUNICODE_STRING  Destination,
    IN PUNICODE_STRING  Source
    );

Existe uma série de rotinas de manipulação de strings no WDK, mas sempre fica faltando alguma rotina se comparado com a ampla biblioteca de rotinas da biblioteca padrão C/C++. Rotinas como strrchr() por exemplo. Quando necessário teremos que construir uma versão que manipule estruturas UNICODE_STRING da mesma maneira. Aqui está uma lista básica de rotinas de string que o WDK suporta. Outras funções são listadas aqui, mas falaremos delas mais tarde.

Mas onde está o buffer mesmo?

A estrutura UNICODE_STRING não armazena buffer, mas sim um ponteiro para ele. Dessa forma, a maneira de descartar uma string varia dependendo da maneira que você a obteve. Nos exemplos que vimos até agora, os bufferes utilizados estão em dados inicializados ou de arrays locais da função de exemplo. Existem rotinas que inicializam e alocam strings como forma de retornar a informação desejada. Nestes casos, é necessário liberar o buffer que você recebeu. É o caso das rotinas que fazem a conversão de ANSI_STRING para UNICODE_STRING e vice versa. São elas RtlUnicodeStringToAnsiString() e RtlAnsiStringToUnicodeString().

NTSTATUS 
  RtlAnsiStringToUnicodeString(
    IN OUT PUNICODE_STRING  DestinationString,
    IN PANSI_STRING  SourceString,
    IN BOOLEAN  AllocateDestinationString
    );
 
NTSTATUS 
  RtlUnicodeStringToAnsiString(
    IN OUT PANSI_STRING  DestinationString,
    IN PUNICODE_STRING  SourceString,
    IN BOOLEAN  AllocateDestinationString
    );

O exemplo abaixo faz uma conversão de ANSI para UNICODE com alocação do resultado, e em seguida, libera o buffer recebido.

void ConvertString(void)
{
    ANSI_STRING     asString;
    UNICODE_STRING  usString;
 
    //-f--> Aqui inicializamos nossa string de origem.
    RtlInitAnsiString(&asString,
                      "Um exemplo simples.");
 
    //-f--> Neste caso não precisaremos inicializar a
    //     string de destino. A rotina fará isso por nós.
    RtlAnsiStringToUnicodeString(&usString,
                                 &asString,
                                 TRUE);
 
    //-f--> Imprime a string resultante
    DbgPrint("String convertida: %wZ\n",
             &usString);
 
    //-f--> Liberamos o buffer alocado na conversão.
    RtlFreeUnicodeString(&usString);
}

Você mesmo pode escrever uma rotina que gere UNICODE_STRINGs alocando o buffer dinamicamente. O buffer pode ser alocado dinamicamente utilizando ExAllocatePoolWithTag() ou uma de suas irmãs. No entanto, na hora de liberar o buffer desta string, utilize a função adequada, que neste exemplo seria ExFreePoolWithTag(). Não saia utilizando RtlFreeUnicodeString() a torto e a direito. Aprecie com moderação. Apenas utilize essa rotina para liberar strings que foram obtidas por funções como RtlAnsiStringToUnicodeString(), cuja documentação indica o uso de RtlFreeUnicodeString().

"..., the caller must deallocate the buffer by calling RtlFreeUnicodeString."

Safe Strings em Kernel

Uma parte das rotinas de manipulação de strings da biblioteca padrão do C/C++, tais como strcpy() e sprintf(), também estão disponíveis em kernel, mas a crescente preocupação com a seguraça na manipulação de buffers fez com que as funções seguras fossem disponibilizadas tanto para user-mode como para kernel-mode. Para ter detalhes sobre o uso destas funções consulte este link.

Mais um driver de exemplo

Este outro post traz o exemplo de um driver que guarda uma lista de strings em memória. Já neste outro post, esse mesmo exemplo foi evoluido para que diferentes listas fossem mantidas sob diferentes contextos. Agora vou evoluir esse exemplo novamente. A aplicação continuará enviando strings com terminadores NULL durante a escrita, o driver criará ANSI_STRINGs a partir delas e às converterão em UNICODE_STRINGs antes de colocá-las na lista.

A maior parte das modificações estão nas rotinas de leitura e escrita, então vou apenas exibir o código dessas rotinas aqui. De qualquer forma, todo o projeto incluindo o driver e uma aplicação de teste estão disponíveis para download ao final deste post. Vamos começar com a rotina de escrita que envia as strings ao driver. Como sempre, toda informação revelante estão nos comentários.

/****
***     OnWrite
**
**      A aplicação está enviando uma string.
*/
NTSTATUS
OnWrite(IN PDEVICE_OBJECT  pDeviceObj,
        IN PIRP            pIrp)
{
    PIO_STACK_LOCATION  pStack;
    ANSI_STRING         asString;
    PSTRING_LIST        pStringList;
    PSTRING_REG         pStringReg;
    ULONG               ulBytes;
    KIRQL               kIrql;
    NTSTATUS            nts;
 
    //-f--> Obtemos a Stack Location corrente.
    pStack = IoGetCurrentIrpStackLocation(pIrp);
 
    //-f--> Obtém a ponta da lista.
    pStringList = (PSTRING_LIST)pStack->FileObject->FsContext;
 
    //-f--> O que temos no buffer de sistema aqui é um array
    //      de bytes terminados com NULL. Vamos inicializar
    //      uma ANSI_STRING com este buffer.
    RtlInitAnsiString(&asString,
                      (PCSZ)pIrp->AssociatedIrp.SystemBuffer);
 
    //-f--> Aqui alocamos o nó que vai ser colocado na lista
    pStringReg = (PSTRING_REG) ExAllocatePoolWithTag(NonPagedPool,
                                                     sizeof(STRING_REG),
                                                     STR_LST_TAG);
 
    //-f--> Para fazer a conversão para UNICODE_STRING, vamos
    //      solicitar que a rotina faça a alocação do bufer
    //      resultante. Por essa razão, não precisaremos inicializar
    //      a string de saída.
    nts = RtlAnsiStringToUnicodeString(&pStringReg->usString,
                                       &asString,
                                       TRUE);
    if (!NT_SUCCESS(nts))
    {
        //-f--> Ops! Provavelmente não tivemos memória para isso.
        //      Vamos sinalizar a falha e informar ao IoManager
        //      que zero bytes foram copiados.
        ExFreePoolWithTag(pStringReg, STR_LST_TAG);
        pIrp->IoStatus.Information = 0;
    }
    else
    {
        //-f--> Vamos segurar o spinlock para evitar concorrência
        //      no acesso à lista.
        KeAcquireSpinLock(&pStringList->SpinLock,
                          &kIrql);
 
        //-f--> Insere o nó na lista.
        InsertTailList(&pStringList->ListHead,
                       &pStringReg->Entry);
 
        //-f--> Libera o spinlock.
        KeReleaseSpinLock(&pStringList->SpinLock,
                          kIrql);
 
        //-f--> Aqui informamos ao IoManager que todos os bytes
        //      enviados pela aplicação foram recebidos com
        //      sucesso pelo driver.
        pIrp->IoStatus.Information = pStack->Parameters.Write.Length;
    }
 
    //-f--> O campo de Information já foi preenchido, vamos apenas
    //      copiar o status da operação e completar a IRP.
    pIrp->IoStatus.Status = nts;
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    return nts;
}

Agora vejamos a recuperação das strings na leitura.

/****
***     OnRead
**
**      A aplicação está querendo receber as strings eviadas
**      por ela.
*/
NTSTATUS
OnRead(IN PDEVICE_OBJECT  pDeviceObj,
       IN PIRP            pIrp)
{
    ANSI_STRING         asString;
    PIO_STACK_LOCATION  pStack;
    PSTRING_LIST        pStringList;
    PSTRING_REG         pStringReg;
    PLIST_ENTRY         pEntry;
    KIRQL               kIrql;
    NTSTATUS            nts;
 
    //-f--> Vamos deixar este campo com zero até que tenhamos
    //      certeza de que a cópia foi feita paro o buffer da
    //      aplicação
    pIrp->IoStatus.Information = 0;
 
    //-f--> Obtemos a Stack Location corrente.
    pStack = IoGetCurrentIrpStackLocation(pIrp);
 
    //-f--> Obtém a ponta da lista.
    pStringList = (PSTRING_LIST)pStack->FileObject->FsContext;
 
    //-f--> Vamos retornar para aplicação apenas um array de CHAR
    //      com terminador NULL. As strings estão armexadas como
    //      UNICODE_STRING. Vamos convertê-las para ANSI_STRING.
    //      Aqui vamos inicializar a ANSI_STRING que receberá
    //      o resultado da conversão de UNICODE_STRING.
 
    //-f--> Vamos oferecer Lengh-1 para reservar um byte para
    //      o terminador null após conversão.
    RtlInitEmptyAnsiString(&asString,
                           (PCHAR)pIrp->AssociatedIrp.SystemBuffer,
                           (USHORT)pStack->Parameters.Read.Length - 1);
 
    //-f--> Aqui vamos adquirir o spinlock para evitar concorrência
    //      no acesso à lista.
    KeAcquireSpinLock(&pStringList->SpinLock,
                      &kIrql);
 
    //-f--> Verifica se a lista está vazia.
    if (IsListEmpty(&pStringList->ListHead))
    {
        //-f--> Sinaliza erro na leitura e já libera o spinlock.
        nts = STATUS_NO_MORE_ENTRIES;
        KeReleaseSpinLock(&pStringList->SpinLock,
                          kIrql);
    }
    else
    {
        //-f--> Remove o registro da lista e já libera o spinlock.
        pEntry = RemoveHeadList(&pStringList->ListHead);
        KeReleaseSpinLock(&pStringList->SpinLock,
                          kIrql);
 
        pStringReg = CONTAINING_RECORD(pEntry,
                                       STRING_REG,
                                       Entry);
 
        //-f--> Aqui convertemos a string. Reparem que não solicitamos
        //      a alocação do buffer. Estamos utilizando o buffer de
        //      sistema para receber o resultado da conversão. Neste
        //      caso a string de destino deve estar inicializada.
        nts = RtlUnicodeStringToAnsiString(&asString,
                                           &pStringReg->usString,
                                           FALSE);
        if (NT_SUCCESS(nts))
        {
            //-f--> Aqui usamos aquele byte que reservamos e assim
            //      o terminador null também é copiado pelo IoManager
            //      do SystemBuffer para o buffer da aplicação
            asString.Buffer[asString.Length] = 0;
 
            //-f--> Precisamos informar ao IoManager a quantidade de
            //      bytes que serão copiados para o buffer da aplicação.
            //      Esse tamanho é o tamanho da string convertida mais
            //      um byte ocupado pelo terminador NULL que colocamos.
            pIrp->IoStatus.Information = asString.Length+1;
        }
 
        //-f--> Neste exemplo, não estamos lidando com o caso de erro
        //      na conversão, isso pode acontecer se a aplicação mandar
        //      um buffer pequeno para a string. Caso algum erro ocorra
        //      durante a conversão, vamos simplesmente descartar a string
 
        //-f--> Libera o buffer da string e em seguida o registro que ela
        //      ocupava na lista.
        RtlFreeUnicodeString(&pStringReg->usString);
        ExFreePoolWithTag(pStringReg, STR_LST_TAG);
    }
 
    //-f--> O campo de Information já foi preenchido, vamos apenas
    //      copiar o status da operação e completar a IRP.
    pIrp->IoStatus.Status = nts;
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    return nts;
}

Com este driver rodando, já posso pensar em alguns exemplos de filtros. Já venho pensando em exemplos de filtros há algum tempo, além de receber sujestões de post sobre esse assunto. O fato é que não tinhamos base para tal. O importante é termos um driver simples o suficiente para o fácil entendimento das coisas. Não adianta eu fazer um post com um filtro de disco rígido, um filtro de rede ou qualquer outro driver com plug-and-play e gerenciamento de energia. Estes precisariam de muito conhecimento acumulado e não caberiam num post.

Enfim, mais uma vêz espero ter ajudado. E se alguma dúvida surgir é só me mandar um e-mail.

Have fun!

StringList.zip

Levando a tela azul pra casa

18 de June de 2009 - Fernando Roberto

Nada melhor que uma bela choraderia para começar este post. Meu rítmo está baixo por conta da universidade estar sugando todas as minhas energias vitais. Se você tem acompanhado meu blog nos últimos posts, já sabe do que estou falando. No meu tempo livre estive correndo com meu projeto, meu estágio e meu emprego. Meu blog também participa dessa lista de tarefas, mas o coitadinho tem menos prioridade aqui. Alguns de vocês devem saber que sou helimodelista por hobby, mas como eu disse ao meu amigo Heldai outro dia: “Hobby é o nome que se dá àquilo que fazemos para ocupar o tempo que temos livre, mas ainda estou para descobrir o nome que daríamos àquilo que gostaríamos de fazer se tivéssemos tempo livre…”. Enfim, como isso não tem nada a ver com o post de hoje, vamos mudar de assunto.

Entre uma coisa e outra, estive tentando pensar em algo simples para um post pequeno. Foi então que a dúvida do leitor Ismael Rocha (Brasília – DF) gerou este post.

“Existe uma maneira de salvar as BSOD’s para posteriormente verificar eventuais problemas?”

Salvar uma tela azul? Salvar o quê? A máquina já morreu meu amigo! Já era! Acabou! O que você ainda pode tentar salvar é seu emprego.

Brincadeiras à parte, existe sim.

O sistema operacional está pré-configurado para reiniciar automagicamente quando uma falha crítica acontece. Falha crítica é a maneira polida de se dizer que a casa caiu, a vaca foi pro brejo, o jacaré te abraçou, o tambor girou, o ferro berrou, o tempo fechou, ficou pequeno pra você… enfim, uma tela azul aconteceu. Não que eu não goste de telas azuis, mas do efeito colateral que ela nos traz. Pela norma mundial dos consumidores de drivers de terceiros, se você é o autor de um driver que estiver instalado em uma máquina no momento da falha, esteja ele rodando ou não, então a culpa da falha é sua até que se prove o contrário. É triste, mas é a realidade. A partir do momento que uma tela azul acontece, você é o culpado padrão e terá que ficar aguentando piadinhas pelo resto da eternidade. Gostaria de aproveitar o contexto para mandar um abraço pro meu amigo Heldai.

Exibindo a tela azul

Na tentativa de salvar sua dignidade, você tenta provar que a culpa não é sua. Dizer que o reset da máquina é uma feature do seu driver e que felizmente funcionou muito bem não vai colar, não na segunda vez. Mas o que você pode fazer se a tela azul é apenas um flash de informações enquanto a máquina não reinicia? Felizmente você pode mudar isso. Clicando com o botão direito do mouse sobre o “Meu computador”, selecionando “Propriedades”. Daí em diante é só dar uma olhada na figura abaixo para descobrir que você pode evitar que a máquina reinicie automaginamente.


Você terá de desmarcar a opção “Reiniciar automaticamente”, e assim ter todo o tempo que for necessário para mostrar a todos que o problema não é seu. Na maioria das vezes o sistema consegue detectar o driver que provavelmente é o causador de toda essa dor de cabeça e exibir o nome do arquivo na tela azul como podemos ver na figura abaixo.

Desmontando uma tela azul


“Nossa! Então o Windows tem um algorítmo de inteligência artificial, que provalvelmente usa nanotecnologia de alguma forma para descobrir o driver culpado?”

Na verdade é um pouco mais simples que isso, o Windows simplesmemte pega a imagem do driver que lançou uma exceção que não foi manipulada ou que voluntariamente derrubou o sistema por detectar alguma incoerência. Por isso, nem sempre o nome do driver exibido é de fato o nome do driver culpado. Se pensarmos no simples exemplo onde o driver MetralhadoraGiratoria.sys escreve onde não deveria corrompendo algum Pool de alocações, esse erro mais tarde pode ser detectado pelo driver Laranja.sys que, na hora de fazer uma alocação de memória, chama uma rotina de sistema que por sua vez chama a rotina KeBugCheckEx() ao detectar tal incoerência. Consegue adivinhar o nome do driver que aparecerá no BO?

Outras informações ainda podem ser obtidas da tela azul. Se é o nome do seu driver que aparece na tela, então você ainda pode obter o endereço da instrução onde a desgraça ocorreu. Em nosso exemplo o endereço é o 0xF8DD8A415 partir daí podemos chegar na função que estava sendo executada no momento da falha se tivermos o arquivo de mapa gerado pelo linker. Também é possível obter a data da imagem do arquivo e tirar aquela dúvida de que realmente era a versão certa que estava sendo executada. A data do arquivo é obtida no campo DateStamp e é expressa em um valor hexadecimal de 32 bits representando a quantidade de segundos deste meia noite de primeiro de Janeiro de 1970. Difícil mesmo é achar alguém com paciência suficiente para calcular isso diante de uma tela azul. Existem meios bem menos trabalhosos de descobrir que a culpa foi sua mesmo.

Na minha opinião, a informação mais relevante que a tela azul oferece é o Stop Code. Como o nome já sugere, Stop Code é um código que vai indicar o motivo da falha do sistema. você pode consultar a lista de Stop Codes neste link ou ainda dar uma olhada no arquivo C:\WinDDK\6001.18002\inc\api\BugCodes.h que vem no WDK.


Stop Codes vêm com até quatro parâmetros que trazem informações adicionais ao código de parada. A interpretação destes valores dependerá do código de falha, que em nosso exemplo é 0x7E. Consultando no link que informei a pouco, teremos a seguinte interpretação para os valores que nos foi apresentado.


Mas existe um jeito de salvar a BSOD ou não?

Tá tá tá… É que começo a escrever e acabo me empolgando. Mas enfim, quando uma falha crítica ocorre, o sistema cria um arquivo conhecido como Crash Dump. Existem três opções de crash dumps que podem ser geradas.

  • Dump Completo: Nesta opção, todo o conteúdo da memória física no momento da falha será copiado em um arquivo. Obviamente o tamanho deste arquivo será a quantidade de memória presente na máquina com um acréssimo de 1MB de header. Essa opção não aparece nas máquinas que possuam mais de 2GB de memória física, mas ainda é possivel configurar o dump completo sem utilizar essa interface gráfica escrevendo diretamente no registro. Esse método é também conhecido como “configurar na unha”. O dump completo é muito útil quando a informação presente em páginas de memória em User Space for relevante para o problema, tal como situações de Dead Locks. Se você não sabe o que significa User Space, este post pode ajudar.

  • Dump de Kernel: Aqui somente as páginas em System Space serão copiadas para disco. O tamanho deste arquivo vai variar dependendo de quantidade de memória física a máquina tem instalada, mas não existe uma proporção exata. Muito do balanceamento de páginas utilizado pelo gerenciador de memória virtual vai determinar o tamanho deste arquivo, mas ele fica pela ordem de 200MB num sistema com 4GB de memória total (já dá pra levar no pen drive). Essa opção é normalmente a mais viável, já que só carrega a informação mais relevante para um crash de sistema.

  • Dump Mínimo: Aqui um arquivo de 64KB será gerado para sistemas 32 bits ( 128KB para sistemas 64 bits). Neste arquivo temos apenas o Stop Code e seus parâmetros, a lista de drivers carregados no momento da falha, informações sobre o processo e thread corrente e o Call Stack da thread que causou a falha.

Na mesma janela onde você configura o reinicio automático do sistema, existem dois outros campos que vão configurar o tipo de dump desejado e o caminho onde este será gerado. Agora você já pode levar sua tela azul no coração e depurar onde você quiser. Em casa, no trabalho, no trêm, no metrô… Você pode ainda pedir que clientes enviem seus crash dumps para que você possa diagnosticar o problema ser ter que se deslocar através de rios e montanhas sob o frio e a chuva.

Tenho o Crash Dump, e agora?

Agora que você é um feliz proprietário de um maravilhoso arquivo de Crash Dump, o que mais você poderia querer da vida? Talvez ser capaz descobrir a causa do problema já seria um bom começo. Para isso vamos utilizar o depurador nativo do sistema operacional. Se você ainda não conhece o WinDbg, então dê uma olhada neste post para que você sabia do que estamos falando aqui.

Admitindo que você tenha Windbg instalado em sua máquina de desenvolvimento, e que este esteja com o servidor de símbolos configurado, tudo que temos a fazer agora é abrir o WinDbg, selecionar o ítem “Open Crash Dump…” no menu “File” e apontar o caminho do arquivo de dump que você copiou da pobre máquina que ousou rodar seu driver. O texto abaixo é o resultado exibido na janela de comandos quando o Crash Dump é aberto.

Microsoft (R) Windows Debugger Version 6.11.0001.404 AMD64
Copyright (c) Microsoft Corporation. All rights reserved.
 
 
Loading Dump File [Z:\Sources\MEMORY.DMP]
Kernel Summary Dump File: Only kernel address space is available
 
Symbol search path is: srv*
Executable search path is: 
Windows XP Kernel Version 2600 (Service Pack 3) UP Free x86 compatible
Product: WinNt, suite: TerminalServer SingleUserTS
Built by: 2600.xpsp.080413-2111
Machine Name:
Kernel base = 0x804d7000 PsLoadedModuleList = 0x80553fc0
Debug session time: Thu Jun 18 14:46:24.969 2009 (GMT-3)
System Uptime: 0 days 0:03:20.375
Loading Kernel Symbols
...............................................................
.........................................................
Loading User Symbols
 
Loading unloaded module list
...........
*******************************************************************************
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *
*******************************************************************************
 
Use !analyze -v to get detailed debugging information.
 
BugCheck 7E, {c0000005, f8d9f415, f8af1bb4, f8af18b0}
 
Probably caused by : Useless.sys ( Useless!DriverEntry+5 )
 
Followup: MachineOwner
---------

Agora se simplesmente executarmos o comando sugerido, já teremos uma boa descrição do que aconteceu com máquina que sofreu a falha crítica.

kd> !analyze -v
*******************************************************************************
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *
*******************************************************************************
 
SYSTEM_THREAD_EXCEPTION_NOT_HANDLED (7e)
This is a very common bugcheck.  Usually the exception address pinpoints
the driver/function that caused the problem.  Always note this address
as well as the link date of the driver/image that contains this address.
Arguments:
Arg1: c0000005, The exception code that was not handled
Arg2: f8d9f415, The address that the exception occurred at
Arg3: f8af1bb4, Exception Record Address
Arg4: f8af18b0, Context Record Address
 
Debugging Details:
------------------
 
 
EXCEPTION_CODE: (NTSTATUS) 0xc0000005 - The instruction at 0x%08lx referenced
memory at 0x%08lx. The memory could not be %s.
 
FAULTING_IP: 
Useless!DriverEntry+5 [z:\sources\driverentry\useless\useless.c @ 7]
f8d9f415 c7050000000000000000 mov dword ptr ds:[0],0
 
EXCEPTION_RECORD:  f8af1bb4 -- (.exr 0xfffffffff8af1bb4)
ExceptionAddress: f8d9f415 (Useless!DriverEntry+0x00000005)
   ExceptionCode: c0000005 (Access violation)
  ExceptionFlags: 00000000
NumberParameters: 2
   Parameter[0]: 00000001
   Parameter[1]: 00000000
Attempt to write to address 00000000
 
CONTEXT:  f8af18b0 -- (.cxr 0xfffffffff8af18b0)
eax=07263867 ebx=00000000 ecx=bb40e64e edx=1be10003 esi=e19feea8 edi=81eb41d0
eip=f8d9f415 esp=f8af1c7c ebp=f8af1c7c iopl=0         nv up ei ng nz na po nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00010282
Useless!DriverEntry+0x5:
f8d9f415 c7050000000000000000 mov dword ptr ds:[0],0  ds:0023:00000000=????????
Resetting default scope
 
PROCESS_NAME:  System
 
ERROR_CODE: (NTSTATUS) 0xc0000005 - The instruction at 0x%08lx referenced memory
at 0x%08lx. The memory could not be %s.
 
EXCEPTION_PARAMETER1:  00000001
 
EXCEPTION_PARAMETER2:  00000000
 
WRITE_ADDRESS:  00000000 
 
FOLLOWUP_IP: 
Useless!DriverEntry+5 [z:\sources\driverentry\useless\useless.c @ 7]
f8d9f415 c7050000000000000000 mov dword ptr ds:[0],0
 
BUGCHECK_STR:  0x7E
 
DEFAULT_BUCKET_ID:  NULL_DEREFERENCE
 
LAST_CONTROL_TRANSFER:  from 8057677f to f8d9f415
 
STACK_TEXT:  
f8af1c7c 8057677f 81eb41d0 81d46000 00000000 Useless!DriverEntry+0x5
 [z:\sources\driverentry\useless\useless.c @ 7]
f8af1d4c 8057688f 80000360 00000001 00000000 nt!IopLoadDriver+0x66d
f8af1d74 80534c02 80000360 00000000 823c68b8 nt!IopLoadUnloadDriver+0x45
f8af1dac 805c6160 b29accf4 00000000 00000000 nt!ExpWorkerThread+0x100
f8af1ddc 80541dd2 80534b02 00000001 00000000 nt!PspSystemThreadStartup+0x34
00000000 00000000 00000000 00000000 00000000 nt!KiThreadStartup+0x16
 
 
FAULTING_SOURCE_CODE:  
     3: NTSTATUS DriverEntry(IN PDRIVER_OBJECT  pDriverObject,
     4:                      IN PUNICODE_STRING pusRegistryPath)
     5: {
     6:     //-f--> Diga olá à BSOD e vá se acostumando com ela...
>    7:     *(PVOID*)0x00000000 = 0;
     8:
     9:     //-f--> Não vamos viver para ver isso.
    10:     return STATUS_SUCCESS;
    11: }
 
 
SYMBOL_STACK_INDEX:  0
 
SYMBOL_NAME:  Useless!DriverEntry+5
 
FOLLOWUP_NAME:  MachineOwner
 
MODULE_NAME: Useless
 
IMAGE_NAME:  Useless.sys
 
DEBUG_FLR_IMAGE_TIMESTAMP:  4a3844ef
 
STACK_COMMAND:  .cxr 0xfffffffff8af18b0 ; kb
 
FAILURE_BUCKET_ID:  0x7E_Useless!DriverEntry+5
 
BUCKET_ID:  0x7E_Useless!DriverEntry+5
 
Followup: MachineOwner
---------

Se a máquina que está abrindo o arquivo de dump for a máquina de desenvolvimento do seu driver, o Windbg será capaz de automagicamente achar os fontes do seu driver e apontar a causa da falha com grandes detalhes. Então certifique-se que seu gerente não esteja por perto nesse momento. Isso já é muito mais informação do que você poderia obter simplesmente olhando para a tela azul do computador. Neste exemplo utilizei o driver de exemplo do post Getting Started para reproduzir a tela azul. Mas não se preocupe com isso, mesmo sendo um programador novato em drivers, uma das primeiras coisas que você aprenderá é como gerar telas azuis.

Mais uma vez espero ter ajudado.
Have fun!

Enumerando dispositivos

1 de May de 2009 - Fernando Roberto

Dentre minhas tarefas atuais, estava a de ler as amostras de um giroscópio utilizando uma porta serial e gerar um arquivo com elas. Este arquivo seria lido por um driver semelhante ao demonstrado no post anterior a este. No datasheet do giroscópio há uma descrição do protocolo, sem falar do exemplo feito em Visual Basic que existe no site do fabricante, que demonstra a aceleração angular em cada eixo bem como as acelerações lineares deles como mostra abaixo.


Toda a interface de configuração e obtenção dos dados do giroscópio é realizada através da serial. Mesmo sem uma aplicação dedicada pode-se utilizar um programa qualquer, como o HyperTerminal, para ter acesso aos menus oferecidos pelo dispositivo. Utilizando uma porta serial, qualquer kit de microcontrolador que não tenha display e nem mesmo um teclado pode oferecer uma interface bem amigável.

Escrever protocolos seriais não é algo que me assuste. Duro é ter que lembrar como programar uma aplicação User-Mode que tenha janelas, botões e outros controles, mas nada que um pouco de MSDN não resolva. Programadores de drivers normalmente testam tudo que podem utilizando aplicações console, então desenvolvi um pequeno software de terminal para lidar diretamente com o dispositivo. O fonte dele segue abaixo e fica de brinde para quem precisar brincar com portas seriais qualquer dia.

/****
***     main
**
**      E lá vamos nós...
*/
int _tmain(int argc, _TCHAR* argv[])
{
    DWORD           dwBytes,
                    dwError = ERROR_SUCCESS;
    HANDLE          hCom;
    ULONG           i;
    UCHAR           ucByteIn[512], ucByteOut;
    DCB             dcb;
    COMMTIMEOUTS    CommTimeouts;
 
    //-f--> Aqui abrimos aporta serial desejada
    hCom = CreateFile(L"COM3",
                      GENERIC_READ | GENERIC_WRITE,
                      0,
                      NULL,
                      OPEN_EXISTING,
                      0,
                      NULL);
 
    //-f--> Verifica se obtivemos sucesso
    if (hCom == INVALID_HANDLE_VALUE) 
    {
        //-f--> Oops!
        dwError = GetLastError();
        printf("CreateFile failed with error %d.\n",
                dwError);
        return dwError;
    }
 
    //-f--> Existem 7 kilos de configurações da porta serial.
    //      Ao invés de configirar cada uma delas, vamos apenas
    //      obter as configurações padrão do sistema e modificar
    //      apenas as que nos interessa.
    if (!GetCommState(hCom, &dcb))
    {
        //-f--> Oops!
        dwError = GetLastError();
        printf ("GetCommState failed with error %d.\n",
                dwError);
        CloseHandle(hCom);
        return dwError;
    }
 
    //-f--> Aqui modificamos as configurações para setar as
    //      configurações exigidas pelo giroscópio.
    //      57600, 8 N 1 (sem controle de fluxo)
    dcb.DCBlength = sizeof(DCB);
    dcb.BaudRate = CBR_57600;
    dcb.ByteSize = 8;
    dcb.Parity = NOPARITY;
    dcb.StopBits = ONESTOPBIT;
    dcb.fDtrControl = DTR_CONTROL_DISABLE;
    dcb.fRtsControl = RTS_CONTROL_DISABLE;
 
    //-f--> Aqui aplicamos as configurações que modificamos
    if (!SetCommState(hCom, &dcb))
    {
        //-f--> Oops!
        dwError = GetLastError();
        printf ("SetCommState failed with error %d.\n",
                dwError);
        CloseHandle(hCom);
        return dwError;
    }
 
    //-f--> Vou configurar os timeouts de forma que uma
    //      leitura seja completada depois de 100ms mesmo
    //      nenhum caracter seja recebido pela aplicação.
    //      Isso evita da aplicação ficar travada em ReadFile
    //      até que um byte seja recebido pela porta serial
    CommTimeouts.ReadIntervalTimeout = 0;
    CommTimeouts.ReadTotalTimeoutMultiplier = 0;
    CommTimeouts.ReadTotalTimeoutConstant = 100;
    CommTimeouts.WriteTotalTimeoutConstant = 0;
    CommTimeouts.WriteTotalTimeoutMultiplier = 0;
 
    //-f--> Aplica as configurações de timeouts
    if (!SetCommTimeouts(hCom, &CommTimeouts))
    {
        //-f--> Oops!
        dwError = GetLastError();
        printf ("SetCommTimeouts failed with error %d.\n",
                dwError);
        CloseHandle(hCom);
        return dwError;
    }
 
    //-f--> Aqui começa o loop infinito que vai executar
    //      para todo o sempre até o fim dos dias.
    //      Na verdade, se você teclar [ESC] ele termina.
    while(1)
    {
        //-f--> Verifica se há um byte a ser recebido pela
        //      aplicação no buffer de teclado.
        if (_kbhit())
        {
            //-f--> Obtém a tecla
            ucByteOut = _getch();
 
            //-f--> Verifica se a tecla recebida é [ESC]
            if (ucByteOut == 27)
                break;
 
            //-f--> Envia o byte recebido para a porta serial
            if (!WriteFile(hCom,
                           &ucByteOut,
                           1,
                           &dwBytes,
                           NULL))
            {
                //--> Oops!
                dwError = GetLastError();
                break;
            }
        }
 
        //-f--> Verifica se algum byte foi recebido pela
        //      porta serial. Observe que neste loop não
        //      existe o típico Sleep() para não levar a CPU
        //      a 100%. Esta espera é realizada dentro da
        //      chamada à ReadFile(). A função espera por um
        //      byte por até 100ms, conforme configurado nos
        //      timeouts.
        if (!ReadFile(hCom,
                      ucByteIn,
                      sizeof(ucByteIn),
                      &dwBytes,
                      NULL))
        {
            //-f--> Oops!
            dwError = GetLastError();
            break;
        }
 
        //-f--> Imprime na tela a sequência de caracteres recebida
        for (i=0; i
        {
            if (ucByteIn[i] == 0x0d)
                puts("");
            else
                printf("%c", ucByteIn[i]);
        }
    }
 
    //-f--> Bom se chegamos neste ponto é porque chegamos
    //      ao fim dos dias ou a tecle ESC foi pressionada.
    //      Vamos fechar a porta serial e sair correndo.
    CloseHandle(hCom);
    return dwError;
}

Abrir uma porta serial é a parte mais simples desta história, o problema é decidir qual porta abrir. Como qualquer programa decente, deveria haver um combo list com as portas seriais disponíveis no computador, onde o usuário escolheria uma e pronto. A partir daí é só obter a porta selecionada pelo usuário e montar uma chamada para a rotina CreateFile() como ilustrado no cógido acima.

Tudo bem, arrastei o controle para a janela que eu estava programando e agora é só preenchê-lo. Então pensei: "Deve haver alguma função do tipo EnumerateCommPorts() na API", mas não foi o que a página da referência me mostrava. - Como assim não tem? O Google deve saber algo a respeito. - Acabei descobrindo que esta é uma dúvida bem comum por aí. Uns resolvem este problema fazendo um loop que tenta abrir as portas seriais em sequência (COM1, COM2,... ), outros utilizam a função QueryDosDevice() e filtram os Symbolic Links que iniciam com "COM(n)", mas o método que vou mostrar aqui é capaz de enumerar qualquer tipo de interface utilizando a SetupAPI.

Setup quem?

A SetuAPI é uma parte do Plug-And-Play que fornece serviços às aplicações User-Mode. O Plug-And-Play tem como um dos seus objetivos, unificar a configuração, uso e a enumeração de dispositivos e serviços semelhantes. Desta forma todos os fabricantes de placas que oferecem serviços de porta serial podem ter seus dispositivos configurados de uma única maneira. Dispositivos que oferecem serviços de porta serial devem implementar uma interface pré-definida, ou seja, devem se mostrar dispostos a receber IOCTLs e responder a eles de maneira prevista na documentação.

O driver que desejar criar devices que implementem a interface de porta serial deverá declarar isso através da chamada à rotina IoRegisterDeviceInterface(). Aqui um device é associado à uma classe de interface de dispositivo, a qual é identificada por um GUID. Existem diversas classes de interfaces de dispositivos pré-definidas no sistema, como listado aqui, e a classe de portas seriais é uma delas. A partir daí, seu device será enumerado por rotinas do Plug-and-Play como provedor de uma determinada interface.

Então tá. O que temos que fazer é utilizar estas funções de enumeração de interfaces para descobrir quais os dispositivos que implementam a interface de porta serial. A SetupAPI vai nos ajudar com essa tarefa. Mas antes de darmos uma olhada no fonte, vamos resolver uma coisinha. Tenho visto diferentes maneiras de usar GUID_DEVINTERFACE_COMPORT, uns incluem o header de Kernel-Mode Ntddser.h, outros definem o GUID na unha, mas qual seria o jeito correto?

Definindo o GUID de interface

Tudo começa com a chamada à rotina SetupDiGetClassDevs() que vai reunir informações sobre o grupo de dispositivos que correspondem aos critérios de busca adotados nos parâmetros. Vamos querer dispositivos que implementem a interface identificada por GUID_DEVINTERFACE_COMPORT, mas se simplesmente fizermos a chamada como mostra abaixo...

hDevInfoSet = SetupDiGetClassDevs(&GUID_DEVINTERFACE_COMPORT,
                                  NULL,
                                  NULL,
                                  DIGCF_PRESENT | DIGCF_INTERFACEDEVICE);

...obteremos o erro exibido em seguida.

1>z:\sources\samples\enumserialport.obj : error LNK2001: unresolved external
 symbol _GUID_DEVINTERFACE_COMPORT

Isso ocorre porque GUID_DEVINTERFACE_COMPORT está declarado em WinIoCtl.h. Lembre-se que este header é indiretamente incluído por Windows.h quando o símbolo WIN32_LEAN_AND_MEAN não é definido, como já comentei neste outro post. Mas mesmo incluindo este header ainda temos o mesmo problema. Vamos olhar isso um pouco mais de perto.

No header WinIoCtl.h temos:

DEFINE_GUID(GUID_DEVINTERFACE_COMPORT, 0x86e0d1e0L, 0x8089, 0x11d0,
            0x9c, 0xe4, 0x08, 0x00, 0x3e, 0x30, 0x1f, 0x73);

Mas o que é DEFINE_GUID afinal?

Em GuidDef.h temos:

#ifdef INITGUID
#define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \
        EXTERN_C const GUID DECLSPEC_SELECTANY name \
                = { l, w1, w2, { b1, b2,  b3,  b4,  b5,  b6,  b7,  b8 } }
#else
#define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \
    EXTERN_C const GUID FAR name
#endif // INITGUID

Então agora nos vem o sonoro "Aaaah táaa!". GUID_DEVINTERFACE_COMPORT é uma constante que é definida quando o símbolo INITGUID é definido, caso contrário, esta constante é apenas declarada. O símbolo INITGUID é definido no header InitGuid.h. Assim, quando você quiser utilizar os GUIDs declarados com DEFINE_GUID, você terá que em um dos seus módulos incluir o header Initguid.h antes de Windows.h. Como nosso exemplo só tem um módulo, então fica fácil. Como de costume, todo código fonte está contido num exemplo disponível para download.

//-f--> Vamos ter que colocar estes includes na ordem certa
//      para que o GUID que identifica a interface
//      GUID_DEVINTERFACE_COMPORT seja definido.
#include 
#include 
#include 

Enumerando interfaces

/****
***     EnumSerialInterfaces
**
**      Rotina que enumera dispositivos que
**      implementam a interface de porta serial
*/
 
DWORD EnumSerialInterfaces(void)
{
    CHAR                        szFriendlyName[100];
    HDEVINFO                    hDevInfoSet = NULL;
    SP_DEVICE_INTERFACE_DATA    DevInterfaceData;
    SP_DEVINFO_DATA             DevInfoData;
    DWORD                       dwReturn,
                                dwInterfaceIndex = 0;
    try
    {
        //-f--> Reunindo informações sobre dispositivos que implementam
        //      a interface desejada que estejam presentes no
        //      momento em que esta rotina é chamada.
        hDevInfoSet = SetupDiGetClassDevs(&GUID_DEVINTERFACE_COMPORT,
                                          NULL,
                                          NULL,
                                          DIGCF_PRESENT | DIGCF_INTERFACEDEVICE);
 
        if (hDevInfoSet == INVALID_HANDLE_VALUE)
            throw GetLastError();
 
        DevInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
 
        //-f--> Agora enumera cada uma das interfaces
        while (SetupDiEnumDeviceInterfaces(hDevInfoSet,
                                           0,
                                           &GUID_DEVINTERFACE_COMPORT,
                                           dwInterfaceIndex++,
                                           &DevInterfaceData))
        {
            DevInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
 
            //-f--> Para cada uma das interfaces, obtemos o
            //      device que a implementa.
            if (SetupDiGetDeviceInterfaceDetail(hDevInfoSet,
                                                &DevInterfaceData,
                                                NULL,
                                                0,
                                                NULL,
                                                &DevInfoData))
                throw GetLastError();
 
            //-f--> Agora apenas obtemos o nome camarada do dispositivo
            if (!SetupDiGetDeviceRegistryProperty(hDevInfoSet,
                                                  &DevInfoData,
                                                  SPDRP_FRIENDLYNAME,
                                                  NULL,
                                                  (PBYTE)szFriendlyName,
                                                  sizeof(szFriendlyName),
                                                  NULL))
                throw GetLastError();
 
            //-f--> Printf neles...
            printf("%d) %s\n",
                   dwInterfaceIndex,
                   szFriendlyName);
        }
    }
    catch(DWORD dwError)
    {
        //-f--> Oops!
 
        printf("Error %d on trying enumerate device interfaces.\n",
               dwError);
 
        dwReturn = dwError;
    }
 
    //-f--> Libera as informações obtidas
    if (hDevInfoSet)
        SetupDiDestroyDeviceInfoList(hDevInfoSet);
 
    return dwReturn;
}

Com essa implementação, teremos a seguinte saída.


Legal, mas não era bem isso...

Muito bem. As portas foram enumeradas, mas como eu passaria uma string dessas para a função CreateFile()? Terei que ficar interpretando essa string para pegar a parte "COM1" que está entre parênteses?

Na verdade existem meios de você obter o Symbolic Link dos dispositivos também utilizando funções da SetupAPI, mas eu imagino que o que você queria é o mesmo que eu quero. Preencher um Combo Box com o nome simples das portas seriais, como qualquer programa normal.

O port name, que é a string "COMx" que estamos procurando, está escrito como um valor de registro na chave do dispotivo. Todo driver de porta serial tem que ter esse valor conforme mostra esta página. Para obter a chave de registro referente ao dispositivo, utilizaremos uma outra rotina da SetupAPI. O fonte abaixo vai enumerar as portas seriais da maneira que estamos querendo.

/****
***     EnumSerialPorts
**
**      Rotina que enumera dispositivos que
**      implementam a interface de porta serial e
**      imprime um nome que não é camarada mas que
**      ainda servem para alguma coisa.
*/
 
DWORD EnumSerialPorts(void)
{
    CHAR                        szPortName[10];
    HDEVINFO                    hDevInfoSet = NULL;
    SP_DEVICE_INTERFACE_DATA    DevInterfaceData;
    SP_DEVINFO_DATA             DevInfoData;
    DWORD                       dwReturn,
                                dwSize,
                                dwInterfaceIndex = 0;
    HKEY                        hKey;
 
    try
    {
        //-f--> Reunindo informações sobre dispositivos que implementam
        //      a interface desejada que estejam presentes no
        //      momento em que esta rotina é chamada.
 
        hDevInfoSet = SetupDiGetClassDevs(&GUID_DEVINTERFACE_COMPORT,
                                          NULL,
                                          NULL,
                                          DIGCF_PRESENT | DIGCF_INTERFACEDEVICE);
 
        if (hDevInfoSet == INVALID_HANDLE_VALUE)
            throw GetLastError();
 
        DevInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
 
        //-f--> Agora enumera cada uma das interfaces
        while (SetupDiEnumDeviceInterfaces(hDevInfoSet,
                                           0,
                                           &GUID_DEVINTERFACE_COMPORT,
                                           dwInterfaceIndex++,
                                           &DevInterfaceData))
        {
            DevInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
 
            //-f--> Para cada uma das interfaces, obtemos o
            //      device que a implementa.
            if (SetupDiGetDeviceInterfaceDetail(hDevInfoSet,
                                                &DevInterfaceData,
                                                NULL,
                                                0,
                                                NULL,
                                                &DevInfoData))
                throw GetLastError();
 
            //-f--> Aqui obtemos a chave de registro do
            //      device que implementa a interface.
            hKey = SetupDiOpenDevRegKey(hDevInfoSet,
                                        &DevInfoData,
                                        DICS_FLAG_GLOBAL,
                                        0,
                                        DIREG_DEV,
                                        KEY_QUERY_VALUE);
 
            if (hKey == INVALID_HANDLE_VALUE)
                throw GetLastError();
 
            //-f--> Aqui obtemos o valor PortName do Registry
            dwSize = sizeof(szPortName);
            dwReturn = RegQueryValueEx(hKey,
                                       "PortName",
                                       NULL,
                                       NULL,
                                       (LPBYTE)szPortName,
                                       &dwSize);
            RegCloseKey(hKey);
 
            if (dwReturn != ERROR_SUCCESS)
                throw dwReturn;
 
            //-f--> Printf neles...
            printf("%d) %s\n",
                   dwInterfaceIndex,
                   szPortName);
        }
    }
    catch(DWORD dwError)
    {
        //-f--> Oops!
        printf("Error %d on trying enumerate device interfaces.\n",
               dwError);
 
        dwReturn = dwError;
    }
 
    //-f--> Libera as informações obtidas
    if (hDevInfoSet)
        SetupDiDestroyDeviceInfoList(hDevInfoSet);
 
    return dwReturn;
}

Agora sim...


Colocar isso num combo box já é assunto para um outro blog. Até que para um desenvolvedor de drivers, a janela abaixo não está tão ruim assim.


Até mais! 😉

EnumSerialPort.zip

Lendo Arquivos

1 de April de 2009 - Fernando Roberto

Como vocês viram em meu último post, meu trabalho de graduação utilizará uma ferramenta chamada LabView para receber e tratar os dados de uma placa USB. Estes dados serão coletados de um dispositivo chamado giroscópio usando TTL 232, que é um RS 232 com tensões de 0 e 5 volts. Mas como uma prova de conceito, teríamos que fazer o driver simular a recepção de dados para enviar ao LabView. Utilizamos um circuito que transforma TTL 232 em RS 232 apenas para permitir que os dados pudessem ser lidos de uma porta serial convencional. Então fiz um programinha idiota que grava em um arquivo tudo que recebe pela porta serial. Como o firmware ainda não estava nem começado, resolvi fazer um driver que lesse esse arquivo e repassasse os dados para a camada de aplicação. Perguntas sobre como manipular arquivos são especialmente frequentes. Muitos leitores gostariam de saber como criar, ler, escrever e até mesmo apagar arquivos em Kernel Mode. Talvez eu possa desapontá-los um pouco ao dizer que não é assim tão diferente de User Mode, mas já que estamos aqui sem fazer nada, por que não demonstrar?

Acredito que a maior diferença esteja no passo onde obtemos o handle para o arquivo. Vamos começar dando uma olhada na rotina ZwCreateFile().

NTSTATUS  
  ZwCreateFile(
    OUT PHANDLE  FileHandle,
    IN ACCESS_MASK  DesiredAccess,
    IN POBJECT_ATTRIBUTES  ObjectAttributes,
    OUT PIO_STATUS_BLOCK  IoStatusBlock,
    IN PLARGE_INTEGER  AllocationSize  OPTIONAL,
    IN ULONG  FileAttributes,
    IN ULONG  ShareAccess,
    IN ULONG  CreateDisposition,
    IN ULONG  CreateOptions,
    IN PVOID  EaBuffer  OPTIONAL,
    IN ULONG  EaLength
    );

A parte interessante desse passo é que a rotina não tem o clássico parâmetro FileName que vimos na API equivalente CreateFile() para User Mode. O nome do arquivo é descrito na estrutura OBJECT_ATTRIBUTES descrita abaixo.

typedef struct _OBJECT_ATTRIBUTES {
    ULONG  Length;
    HANDLE  RootDirectory;
    PUNICODE_STRING  ObjectName;
    ULONG  Attributes;
    PVOID  SecurityDescriptor;
    PVOID  SecurityQualityOfService;
 
} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES;
 
typedef CONST OBJECT_ATTRIBUTES *PCOBJECT_ATTRIBUTES;

Para preencher esta estrutura utilizamos a macro InitializeObjectAttributes().

VOID 
  InitializeObjectAttributes(
    OUT POBJECT_ATTRIBUTES  InitializedAttributes,
    IN PUNICODE_STRING  ObjectName,
    IN ULONG  Attributes,
    IN HANDLE  RootDirectory,
    IN PSECURITY_DESCRIPTOR  SecurityDescriptor
    );

O caminho do arquivo é descrito no parâmetro ObjectName, que é um ponteiro para uma UNICODE_STRING. Em Kernel, o caminho completo para um arquivo seria descrito como “\Device\HarddiskVolume0\Diretorio\Arquivo.ext” por exemplo. Isso acontece porque a parte “C:”, que normalmente utilizamos no caminho de arquivo em User Mode, é um Symbolic Link. Symbolic quem? Um Symbolic Link seria como um atalho para o nome em Kernel Mode. Aplicações em User Mode não podem abrir qualquer objeto do Kernel assim de cara. Então cada driver cria Symbolic Links para os objetos que desejam torná-los disponíveis para User Mode. Quando uma aplicação quer abrir o arquivo “C:\Temp\Test.txt”, o sub-sistema Win32 prefixa este caminho com “\??\”, que é o diretório inicial desta busca, resultando em “\??\C:\Temp\Test.txt”. Quando este nome chega ao Object Manager, o prefixo indica que a busca deve iniciar no diretório “\DosDevices”, o mesmo que utilizamos na chamada à API IoCreateSymbolicLink(). Enfim, pulando alguns detalhes para terminar esse post ainda nessa vida, o prefixo vai nos levar ao diretório “\GLOBAL??\”. Depois que o prefixo “\??” foi processado, a próxima parte a ser processada é “C:”. Utilizando a ferramenta WinObj da Systernals ilustrada na figura abaixo, vemos que aqui em minha máquina “C:” será substituído por “\Device\HarddiskVolume3”, que neste caso é o caminho para o device que receberá o restante da string a ser processada. Depois de realizada esta substituição, a string agora é “\Device\HarddiskVolume3\Temp\Test.txt”. O Object Manager agora recomeça a processar a string e encontra o device nela descrito.


Depois disso, sabendo que se trata de um device de volume de dados, o sistema consulta uma estrutura chamada Volume Parameter Block (VPB). Ela cria um link que vai nos informar se o volume indicado foi montado por algum driver de File System. No meu caso, o NTFS seria este driver. O device que ele criou faria o restante do tratamento da string para encontrar o arquivo desejado. Você não vai ter que percorrer todo esse caminho para abrir o arquivo. Basta colocar o prefixo “\??\” no caminho do arquivo que desejar abrir e todos os seus problemas se acabaram-se. Se você quiser mais detalhes sobre as traduções de nomes que ocorrem durante a abertura de um arquivo, este artigo da OSR Online é ótimo. Este é o link para a referência que fala sobre isso.

Depois de aberto, ler o arquivo fica fácil com a rotina ZwReadFile().

NTSTATUS 
  ZwReadFile(
    IN HANDLE  FileHandle,
    IN HANDLE  Event  OPTIONAL,
    IN PIO_APC_ROUTINE  ApcRoutine  OPTIONAL,
    IN PVOID  ApcContext  OPTIONAL,
    OUT PIO_STATUS_BLOCK  IoStatusBlock,
    OUT PVOID  Buffer,
    IN ULONG  Length,
    IN PLARGE_INTEGER  ByteOffset  OPTIONAL,
    IN PULONG  Key  OPTIONAL
    )

Os passos necessários para se abrir e ler um arquivo podem ser resumidos neste pequeno exemplo a seguir.

/****
***     ReadTestFile
**
**      Rotina que demostra de maneira simples como abrir e
**      ler um arquivo.
*/
 
NTSTATUS ReadTestFile(PVOID     pBuffer,
                      ULONG     cbBuffer,
                      PULONG    pulBytesRead)
{
    UNICODE_STRING      usFileName;
    OBJECT_ATTRIBUTES   ObjAttributes;
    IO_STATUS_BLOCK     IoStatusBlock;
    HANDLE              hFile = NULL;
    NTSTATUS            nts = STATUS_SUCCESS;
 
    //-f--> Montamos o UNICODE_STRING contendo o caminho
    //      do arquivo que desejamos abrir
    RtlInitUnicodeString(&usFileName,
                         L"\\??\\C:\\Temp\\Test.txt");
 
    //-f--> Aqui a macro nos ajuda com a estrutura
    //      OBJECT_ATTRIBUTES
    InitializeObjectAttributes(&ObjAttributes,
                               &usFileName,
                               OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE,
                               NULL,
                               NULL);
 
    //-f--> Aqui abrimos o arquivo.
    nts = ZwCreateFile(&hFile,
                       GENERIC_READ | SYNCHRONIZE,
                       &ObjAttributes,
                       &IoStatusBlock,
                       NULL,
                       FILE_ATTRIBUTE_NORMAL,
                       FILE_SHARE_READ,
                       FILE_OPEN,
                       FILE_SYNCHRONOUS_IO_NONALERT,
                       NULL,
                       0);
 
    //-f--> Retorna o erro em caso de falha.
    if (!NT_SUCCESS(nts))
        return nts;
 
    //-f--> Uma simples leitura no arquivo.
    nts = ZwReadFile(hFile,
                     NULL,
                     NULL,
                     NULL,
                     &IoStatusBlock,
                     Buffer,
                     cbBuffer,
                     NULL,
                     NULL);
 
    //-f--> Em caso de falha, fecha o arquivo, retorna o
    //      erro e finge que não é com você.
    if (!NT_SUCCESS(nts))
    {
        ZwClose(hFile);
        return nts;
    }
 
    //-f--> Aqui obtemos a quantidade de bytes lidos
    *pulBytesRead = IoStatusBlock.Information;
 
    //-f--> Fecha o handle do arquivo
    ZwClose(hFile);
    return nts;
}

A prova de conceito

Agora que todos nós sabemos como ler um arquivo, fica mais fácil de explicar como fiz um driver que simularia as leituras de um giroscópio apenas lendo o conteúdo de um arquivo. Durante a existência deste blog, já vimos como criar um projetinho do zero, como compilar drivers utilizando o Visual Studio, já vimos também o que é uma IRP, como oferecer serviços de leitura e escrita, como utilizar o FsContext para manter o contexto entre diferentes operações, e como debug é parte do desenvolvimento, também vimos como depurar drivers mesmo em uma máquina virtual. Vamos utilizar toda essa tranqueirada para montar um driver que abra um arquivo, armazene seu handle em uma área de contexto, e que conforme realizamos leituras ao device criado e exportado por ele, este retorne os dados de um arquivo em disco. Isso vai servir direitinho para simular leituras contínuas que o LabView fará ao meu driver USB.

Abrindo o Device e Arquivo

O driver receberá uma chamada de IRP_MJ_CREATE quando um handle para o device for aberto. Vou aproveitar esse evento para já abrir o arquivo e guardar seu handle resultante no FsContext do FILE_OBJECT que receberei. Se você está boiando, dê uma olhada nos posts indicados anteriormente.

E se o arquivo não existir?

Bom, caso tais eventos infelizes ocorram, vou retornar o erro pela própria IRP recebida. Assim, caso o arquivo não exista ou você não tenha permissão para abri-lo, o código de erro poderá ser verificado através da rotina GetLastError() caso obtivermos INVALID_HANDLE_VALUE como retorno da abertura do handle do device.

Dêem uma olhada como ficou a abertura do handle do device, que na mesma operação, abre o handle para o arquivo. Atenção, não misture as coisas. A aplicação vai obter o handle para o device, e através dele, fará leituras ao device. O device por sua vez utilizará o handle do arquivo para fazer leituras e retornar os dados à aplicação.

/****
***     OnCreate
**
**      A aplicação está chamado CreateFile com o path
**      do nosso device.
*/
 
NTSTATUS OnCreate(PDEVICE_OBJECT    pDeviceObj,
                  PIRP              pIrp)
{
    UNICODE_STRING      usFileName;
    OBJECT_ATTRIBUTES   ObjAttributes;
    IO_STATUS_BLOCK     IoStatusBlock;
    PIO_STACK_LOCATION  pStack;
    NTSTATUS            nts = STATUS_SUCCESS;
    HANDLE              hFile = NULL;
 
    //-f--> Montamos o UNICODE_STRING contendo o caminho
    //      do arquivo que desejamos abrir
    RtlInitUnicodeString(&usFileName,
                         L"\\??\\C:\\Temp\\Test.txt");
 
    //-f--> Aqui a macro nos ajuda com a estrutura
    //      OBJECT_ATTRIBUTES
    InitializeObjectAttributes(&ObjAttributes,
                               &usFileName,
                               OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE,
                               NULL,
                               NULL);
 
    //-f--> Aqui abrimos o arquivo. Como vamos repassar qualquer
    //      erro para a aplicação, então podemos utilizar a estrutura
    //      IO_STATUS_BLOCK da nossa IRP. Caso contrário poderiamos
    //      utilizar uma criada como variavel local.
    nts = ZwCreateFile(&hFile,
                       GENERIC_READ | SYNCHRONIZE,
                       &ObjAttributes,
                       &pIrp->IoStatus,
                       NULL,
                       FILE_ATTRIBUTE_NORMAL,
                       FILE_SHARE_READ,
                       FILE_OPEN,
                       FILE_SYNCHRONOUS_IO_NONALERT,
                       NULL,
                       0);
 
    //-f--> Vamos guardar o handle do arquivo em nossa área de
    //      contexto. Isso permite que várias aplicações de teste
    //      possam ser executas ao mesmo tempo. Para isso teremos
    //      obter a Stack Location atual.
    pStack = IoGetCurrentIrpStackLocation(pIrp);
    pStack->FileObject->FsContext = (PVOID)hFile;
 
    //-f--> Agora é só ler o arquivo, mas vamos fazer isso na IRP
    //      de leitura, só pra...
    IoCompleteRequest(pIrp,
                      IO_NO_INCREMENT);
    return nts;
}

Repare que a API ZwCreateFile() pede um ponteiro para IO_STATUS_BLOCK. Utilizei a mesma estrutura que está contida na IRP que recebemos. Assim eu não tenho que repassar o status de uma operação para a outra. A aplicação continua obtendo o handle para o device da mesma maneira como sempre foi feito, mas lembre-se que se houver uma falha nessa obtenção, o erro pode ter sido gerado por um problema ao abrir o handle do arquivo. Confira como a aplicação vai utilizar o driver.

/****
***     main
**
**      Ponto de entrada da aplicação
**
*/
 
int __cdecl main(int argc,
                 char* argv[])
{
    char    szBuffer[4096];
    HANDLE  hDevice = NULL;
    DWORD   dwError = ERROR_SUCCESS,
            dwBytes,
            i;
 
    //-f--> Obtendo um handle para o device
    printf("Opening the device \"\\\\.\\FileReader\"...\n");
 
    hDevice = CreateFile("\\\\.\\FileReader",
                         GENERIC_ALL,
                         0,
                         NULL,
                         OPEN_EXISTING,
                         0,
                         NULL);
 
    //-f--> Verifica se o handle foi aberto.
    if (hDevice == INVALID_HANDLE_VALUE)
    {
        //-f--> Ops!
        dwError = GetLastError();
        printf("Error #%d opening device...\n",
               dwError);
        return dwError;
    }
 
    //-f--> Realiza as leituras no device
    while (ReadFile(hDevice,
                    szBuffer,
                    sizeof(szBuffer),
                    &dwBytes,
                    NULL))
    {
        //-f--> Exibe dados na tela
        //      Tá tá, eu sei que não é a maneira mais eficiente do mundo.
        for (i = 0; i < dwBytes; i++)
            printf("%c", szBuffer[i]);
    }
 
    //-f--> Qualquer falha da chamada à função ZwReadFile é repassada para
    //      a estrutura IO_STATUS_BLOCK da IRP. Por isso é que vemos este
    //      erro aqui.
    if ((dwError = GetLastError()) != ERROR_NO_MORE_ITEMS)
        printf("\n\n Error #%d reading device...\n");
 
    //-f--> Põe a casa em ordem.
    printf("Closing device...\n");
    CloseHandle(hDevice);
 
    //-f--> Fim de festa! Chega!
    return dwError;
}

Lendo o Device e o Arquivo

Faremos a leitura do arquivo de forma similar à abertura. Vamos obter o handle do arquivo a partir do FsContext. Este ponteiro foi originalmente disponibilizado para que o driver pudesse armazenar nele o endereço de uma estrutura definida pelo desenvolvedor. Este ponteiro sempre será o mesmo para todas as operações que utilizam o mesmo FILE_OBJECT até que a operação de IRP_MJ_CLOSE seja chamada. Como um handle é algo muito pequeno, podemos gravar seu valor ao invés de um ponteiro para uma estrutura alocada em memória que contenha o valor do handle.

Aqui também vamos utilizar a repassagem da estrutura IO_STATUS_BLOCK para transferir o status da operação de leitura do arquivo para a aplicação.

/****
***     OnRead
**
**      Rotina que realiza leitura do arquivo já
**      aberto.
*/
 
NTSTATUS OnRead(PDEVICE_OBJECT    pDeviceObj,
                PIRP              pIrp)
{
    NTSTATUS            nts = STATUS_SUCCESS;
    PIO_STACK_LOCATION  pStack;
    HANDLE              hFile;
 
    //-f--> Obtemos a stack location atual
    pStack = IoGetCurrentIrpStackLocation(pIrp);
 
    //-f--> Aqui recuperamos o handle do arquivo.
    ASSERT(pStack->FileObject->FsContext != NULL);
    hFile = (HANDLE)pStack->FileObject->FsContext;
 
    //-f--> Uma simples leitura no arquivo.
    nts = ZwReadFile(hFile,
                     NULL,
                     NULL,
                     NULL,
                     &pIrp->IoStatus,
                     pIrp->AssociatedIrp.SystemBuffer,
                     pStack->Parameters.Read.Length,
                     NULL,
                     NULL);
 
    //-f--> STATUS_END_OF_FILE não é repassado para a camada aplicação
    //      como uma falha de leitura, então vamos usar um erro mais
    //      fácil de detectar.
    if (pIrp->IoStatus.Status == STATUS_END_OF_FILE)
        nts = pIrp->IoStatus.Status = STATUS_NO_MORE_ENTRIES;
 
    //-f--> Completa a IRP.
    IoCompleteRequest(pIrp,
                      IO_NO_INCREMENT);
    return nts;
}

Se nosso driver retornar STATUS_END_OF_FILE para a aplicação, a API ReadFile() não sinalizará uma falha, mas apenas informará que zero bytes foram lidos. Para facilitar a detecção do fim de arquivo lá na aplicação, vou retornar um código de erro diferente, assim a rotina ReadFile() retornará FALSE e o loop de leitura será interrompido.

Fechando o handle do Device e do Arquivo

Agora fica muito fácil. Vamos fechar o handle do arquivo quando o handle do device for fechado. Sem muitas novidades por aqui.

/****
***     OnCleanup
**
**      O handle para nosso device foi fechado. Vamos
**      aproveitar e fechar o handle do arquivo também.
*/
 
NTSTATUS OnCleanup(PDEVICE_OBJECT    pDeviceObj,
                   PIRP              pIrp)
{
    PIO_STACK_LOCATION  pStack;
    HANDLE              hFile;
 
    //-f--> Obtemos a stack location atual
    pStack = IoGetCurrentIrpStackLocation(pIrp);
 
    //-f--> Aqui recuperamos o handle do arquivo.
    ASSERT(pStack->FileObject->FsContext != NULL);
    hFile = (HANDLE)pStack->FileObject->FsContext;
 
    //-f--> Fecha o handle
    ZwClose(hFile);
 
    //-f--> Completa a IRP normalmente
    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = 0;
    IoCompleteRequest(pIrp,
                      IO_NO_INCREMENT);
 
    return STATUS_SUCCESS;
}

Se vocês já são leitores deste blog há algum tempo, verão que o restante do driver contém código elementar e que já foi comentado em outros posts. De qualquer forma, tanto o fonte do driver quando o fonte da aplicação de teste estão disponíveis para download. Caso vocês tenham alguma dúvida é só mandar um e-mail. Normalmente eu digo que vocês só precisarão torcer para eu saber a resposta, mas ultimamente vocês terão que torcer para eu também ter tempo de responder.

Como sempre, espero ter ajudado.
Have fun!

FileReader.zip

110% de CPU

20 de March de 2009 - Fernando Roberto

Eu não queria postar algo que começasse com choradeiras dizendo que estou sem tempo, que tive que buscar minha tia no aeroporto e coisa e tal, mas estou vendo que não vai ter jeito. Até comecei a escrever uns posts para tirar dúvidas de leitores, mas aí eles começam a tomar tempo quando tenho que montar um exemplo, fazer uma figura, etc. Por fim, já tenho dois posts começados, mas que foram abandonados durante sua concepção por falta de tempo. Tenho recebido alguns e-mails do tipo “Você ainda está vivo por aí?” de alguns leitores. As coisas estão realmente complicadas para mim esse ano, mas não vou deixar de postar. Só estou precisando arrumar um tempinho pra sentar. Já tentei escrever posts enquanto almoçava ou tomava banho, mas não deu muito certo. De fato eu consegui escrever um post enquanto dormia, mas quando acordei, por algum motivo, o post não estava publicado. Nota: “Reclamar com o pessoal do blogger”. Mas enfm, na falta de tempo suficiente para escrever algo que os ajudem a desenvolver algo, neste post vou apenas relatar o que tenho feito. De repente vocês precisem de algo parecido e queiram me pedir alguma ajuda ou referência.

O Último Ano…

Parece mentira, mas este é meu último ano da minha graduação em Engenharia da Computação, e por ser o último ano, três agravantes aparecem para disputar meu tempo (como se eu tivesse algum), sendo eles: Dependências, Trabalho de graduação (TG) e o Estágio.

As Dependências…

Para quem não lembra ou não sabe o que são dependências, são matérias as quais você não conseguiu ser aprovado no ano em que você estudou, mas consegue passar de ano mesmo assim, só que você terá que estudar estas matérias em separado, seja de sábado ou num horário em que você tenha livre (dá até vontade de rir…). Enfim, cheguei ao sexto ano com três matérias para fazer em meu horário livre. Assim, parte do meu sábado foi e será dedicado a essa atividade. Para fazer uma dependência, é necessário que a universidade agrupe alunos com a mesma matéria para fechar uma turma. Duas das três dependências já formaram turmas, mas ainda preciso rezar para que a terceira turma se forme. Dessa forma eu poderei me matricular. Caso contrário, terei que vir para universidade mais um ano para estudar a matéria restante. Isola…(Toc, toc, toc) vai dar tudo certo.

O Estágio…

Em paralelo à essa aventura de estudar três matérias adicionais e ainda ter que passar em tudo, terei que fazer o estágio. Estágio é uma das matérias que preciso fazer para me formar. Terei que cumprir 192 horas de estágio para passar nesta matéria. Felizmente, meu curso pode ter o estágio tanto com ênfase eu software como em hardware, já que a Engenharia da Computação desenvolve as duas especialidades no estudante.

Mas Fernando, se me lembro bem, você trabalha na IBM e seu estágio pode sair sem que você tenha que mover um único músculo. Basta alguém assinar seu estágio e pronto.

Pois é, acho que tenho sérias tendências para querer me enrolar. É como meu amigo Heldai me disse uma vez: “Ê seu Fêrnando, o senhor é como eu. Não pode ver uma corda que já quer enrolar no pescoço”. O fato é que eu gostaria imensamente de desenvolver meu lado mais fraco, e sabendo que já brinco de fazer telas azuis há um tempo razoável, meu objetivo seria arrumar um estágio em hardware. Mas como isso seria possível se já trabalho numa empresa de software durante a semana?

Bom, há um ano atrás, eu dei um curso de desenvolvimento de drivers para Windows em uma empresa que projeta hardware. Uma empresa dessa não se acha assim em qualquer esquina. A Commodity é uma empresa que desenvolve um hardware que faz interface USB, cujo firmware é escrito em linguagem C e roda num chip da Freescale, e como se não fosse o bastante, o hardware ainda faz compressão de audio utilizando um chip da Altera com FPGA. Quem entende de eletrônica sabe que este seria um excelente lugar para fazer estágio e aprender muito. O problema é que eu não poderia fazer o estágio de domingo, que é meu único dia disponível, já que tenho aulas aos sábados e trabalho durante toda a semana. Já que não posso fazer o estágio de domingo, então posso trabalhar de domingo. Mais uma grande vantagem de se fazer Home Office. A solução foi me oferecer para fazer o estágio que seria cumprido apenas de segunda-feira. Estes dias seriam compensados trabalhando de domingo. Pedi autorização para meus gerentes do Brasil e dos Estados Unidos e pronto. Sabendo que um estagiário que aparece uma vez por semana mais atrapalha que ajuda, facilitei minha entrada na empresa solicitando um estágio não remunerado. Como eles já me conheciam, ficou fácil.

Apesar de eu não receber nem um centavo e ainda pagar minha gasolina e refeição, tenho certeza de que fiz a escolha certa. Meu estágio começou há duas semanas e tenho tido contato com coisas bem interessantes. Para dar uma acelerada no acúmulo de horas, trabalharei no estágio durantes todos os dias da semana enquanto eu estiver de férias da IBM. Juntando tudo, meu estágio terminará no início de agosto.

O TG…

Bom, eu já não tinha os sábados e agora não tenho mais os domingos. Só preciso dar um jeito que acabar com qualquer fragmento de tempo livre que eu puder encontrar. Para isso, a universidade nos abençoou com o trabalho de graduação. Nós teremos que montar um projeto que utilize as especialidades que vimos durante o curso de engenharia da computação. Como meta pessoal, não importa o que o projeto faça, terá que haver um driver. Afinal de contas é a única coisa que sei fazer direito. Não posso perder essa oportunidade. Nosso projeto tem como objetivo estabilizar um helimodelo em vôo.


Ah tá, e onde o driver entra na história mesmo?

Para resumir muito (mas muito mesmo), faremos leituras de um giroscópio embarcado num helimodelo à distância utilizando o protocolo ZigBee. Tais leituras serão realizadas por um kit de desenvolvimento com microcontrolador que fará interface USB com um computador. O driver encaminhará as leituras de movimentos para uma ferramenta chamada LabView. Em resposta à percepção dos movimentos, o sistema reagirá enviando comandos via USB para a placa. A mesma placa fará interface com o rádio controle do helimodelo a fim de corrigir o curso.

Em linhas gerais é isso que faremos. Eu poderia escrever muito à respeito de como estamos fazendo isso. Começamos a trabalhar no projeto no ano passado por que já sabíamos que não seria nada fácil. Aproveitei uma das minhas idas para os Estados Unidos para comprar os sensores e kits necessários. O importante é que o projeto está indo bem até aqui, mas ainda vamos apanhar muito. Estou escrevendo o firmware do microcontrolador e obviamente o driver que fará interface com ele, mas nosso principal desafio será fechar a malha de controle e fazer o helimodelo ficar parado no ar.

Parado? Você não tem vergonha de dizer que está fazendo tudo isso para fazer um helimodelo ficar parado?

Nosso sonho dourado é definir dois pontos distintos e fazer o trajeto completo com decolagem, delsocamento e pouso, mas quem é helimodelista como eu sabe que fazer um helimodelo ficar parado é um excelente primeiro passo. Nosso orientador já nos disse que nosso projeto é o mais desafiador das engenharias e que se nós fizéssemos apenas a parte da leitura à distância já seria um bom trabalho de graduação.

Eu escreveria muito mais sobre o projeto, mas isso vai ficar para depois. Este post já está ficando longo para mais um Off-Topic. Meus agradecimentos à prefeitura municipal de São Paulo por ter criado o rodízio municipal de veículos, e por consequência, ter me trazido para a faculdade com duas horas de antecedência. Isso me deu a oportunidade de começar este post. Agora são 01:45 da manhã e ainda tenho que revisar e publicar esse post.

Vou tentar escrever mais. Talvêz minhas aventuras com o TG ou com o estágio me tragam assuntos interessantes e curtos o suficiente para publicar aqui.

Até mais mais. 🙂

Gerenciando paginação do driver

18 de November de 2008 - Fernando Roberto

Depois de tanto falar sobre memória virtual e paginação, recebi uma pergunta que coincidentemente tem tudo a ver com o assunto dos últimos posts. “Para que servem os pragma alloc_text que vemos nos exemplos do WDK?” (Thiago Cardoso, Recife-PE). Essa pergunta já deve ter passado pela cabeça de muitos que já deram uma olhada nos exemplos do WDK. Visto que todos os exemplos do WDK que conheço utilizam este pragma, até que demorou para alguém perguntar sobre isso. Mas enfim, vamos ao que interessa.

Como já vimos, páginas de memória podem estar tanto nos chips de RAM como em disco. Também já vimos que threads que estão em alta prioridade de execução não podem acessar dados que são pagináveis. Mas como saberiamos quais dados são pagináveis ou não?

Controlando Paginação de Dados

Quando alocamos memória dinamicamente, podemos escolher se a área de memória a ser alocada será paginável ou não. A função ExAllocatePool, e suas irmãs (ExAllocatePoolWithTag, ExAllocatePoolWithQuota, ExAllocatePoolWithQuotaTag e ExAllocatePoolWithTagPriority), recebem um parâmetro do tipo POOL_TYPE que define se a memória a ser alocada será paginável ou não.

typedef enum _POOL_TYPE {
  NonPagedPool,
  PagedPool,
  NonPagedPoolMustSucceed,
  DontUseThisType,
  NonPagedPoolCacheAligned,
  PagedPoolCacheAligned,
  NonPagedPoolCacheAlignedMustS
} POOL_TYPE;
 
 
PVOID 
  ExAllocatePool(
    IN POOL_TYPE  PoolType,
    IN SIZE_T  NumberOfBytes
    );

Você terá que gerenciar quais alocações serão acessadas por diferentes prioridades de execução. Por exemplo: Uma determinada lista ligada é consultada apenas por funções que rodam somente em IRQL baixa, logo, todos os seus elementos podem ser alocados em memória paginável (PagedPool). Por outro lado, uma lista ligada que é consultada por funções que rodam em IRQLs altas, deve ter seus elementos alocados em memória não paginável (NonPagedPool).

Legal Fernando, mas nem tudo é alocado dinamicamente. Como ficam as variáveis estáticas?

Por padrão, todas as variáveis globais são não pagináveis. Isso é ruim se seu driver possui muitas variáveis globais, o que exigiria mais memória não paginável para que seu driver pudesse ser carregado, e como também já foi visto, memória não paginável deve ser poupada. Felizmente, podemos definir que um conjunto de variáveis globais possa ser paginável, desde que estas sejam apenas acessadas por threads em baixa IRQL.

Dentro de um módulo, seja uma aplicação, uma DLL ou mesmo um driver, o controle de paginação da memória é aplicado nas sessões que os compõem. Para que um grupo de variáveis seja definido em uma sessão paginável, podemos utilizar o pragma data_seg. Porém, não é todo compilador que nos permite fazer isso. Para saber se o compilador que estamos utilizando suporta o uso deste pragma, contamos com os headers do WDK, que definem o símbolo ALLOC_DATA_PRAGMA quando o compilador oferece suporte a este recurso. Isso, obviamente, tem se tornado menos significante, já que o aconselhável é utilizar o compilador do próprio WDK, mas não custa nada prever que seu código possa ser compilado por algum outro compilador. Veja o exemplo abaixo que define tanto variáveis não pagináveis como pagináveis.

//-f--> Estas são variáveis definidas em uma sessão não paginável
PDEVICE_OBJECT  g_pControlDeviceObj;
PDRIVER_OBJECT  g_pDriverObj;
 
//-f--> Aqui eu verifico se o compilador que estou utilizando
//      suporta o uso do #pragma data_seg.
//      Se sim, eu abro a sessão PAGEDATA que é uma sessão de
//      dados paginável.
#ifdef ALLOC_DATA_PRAGMA
    #pragma data_seg("PAGEDATA")
#endif
 
//-f--> Todas as variáveis declaradas aqui serão pagináveis.
//      Assim, apenas threads que rodam em baixo nível de
//      prioridades poderão acessar tais variáveis
 
//-f--> Definimos um buffer gigante. Ainda bem que é paginável
UCHAR   g_Buffer[100000];
 
//-f--> Aqui marcamos o fim da sessão paginável. As variáveis
//      definidas após este #pragma serão não pagináveis.
#ifdef ALLOC_DATA_PRAGMA
    #pragma data_seg()
#endif

Controlando Paginação de Código

Memória é memória, seja para armazenar dados ou código. Podemos também controlar onde as funções que você escreve serão definidas. Assim, podemos colocar todas as funções que executam em baixa prioridade em sessões de código pagináveis. Aqui usaremos o pragma que originou a dúvida do Thiago, o pragma alloc_text.

Este pragma tem duas limitações. A primeira delas é que o pragma deve ser aplicado depois da declaração da função, mas antes da definição da mesma. A outra é que este pragma não é aplicável em funções C++, ou seja, métodos de classes ou funções com sobrecargas não terão esse luxo de ser pagináveis. Se você é como eu, que prefere utilizar a tipagem forte do C++ em drivers, mesmo que você só escreva simples funções, você deverá utilizar o modificador extern “C” nas declarações das funções.

Esse pragma não é obrigatoriamente suportado por todos os compiladores, e assim como o data_seg, os headers do WDK definem o símbolo ALLOC_PRAGMA para sinalizar que o compilador utilizado suporta esse recurso. Segue mais um exemplo.

//-f--> Normalmente esta declaração é feita em um arquivo de
//      header. Observe que se estivermos compilando em C++,
//      teremos que usar o extern "C" para poder definir a
//      sessão onde as funções aqui declaradas serão definidas.
#ifdef __cplusplus
extern "C"
{
#endif
    //-f--> Declara uma
    ULONG SumOne(IN ULONG ulParam);
 
    //-f--> Declara outra
    ULONG SumTwo(IN ULONG ulParam);
 
#ifdef __cplusplus
}
#endif
 
 
//-f--> A parte a seguir fica no mesmo módulo onde a função é
//      definida, e deve ficar antes da definição da função.
//      Repare que aqui testamos se este pragma é suportado, e
//      se sim, cada função deve receber seu pragma alloc_text
#ifdef ALLOC_PRAGMA
    #pragma alloc_text(PAGE, SumOne);
    #pragma alloc_text(PAGE, SumTwo);
#endif
 
 
//-f--> Depois disso, podemos definir as funções normalmente
 
ULONG SumOne(IN ULONG ulParam);
{
    //--f-> Função SumOne que foi escrita por alguém.
 
    //-f--> Esse comentário só tem graça em Inglês.
    //
    //      Function SumOne that has been written by someone
 
    PAGED_CODE();
 
    return ulParam + 1;
}
 
ULONG SumTwo(IN ULONG ulParam);
{
    //-f--> Função desgraçada. (que não tem graça nenhuma)
    PAGED_CODE();
 
    return SumOne(SomeOne(ulParam));
}

Fernando, para quê servem estas macros PAGED_CODE que você usou no exemplo?

Bom, lá vem histórinha. Uma coisa desconfortável é que problemas com paginação de memória vão ocorrer apenas quando a página que você está acessando estiver em disco. Isso significa que você pode escrever um driver com problema, testá-lo, e se por “sorte”, as páginas estiverem todas em RAM durante o teste, você não verá nenhum problema. Uma das coisas que ajuda bastante é essa macro PAGED_CODE. Em Checked Build, esta macro é traduzida para uma função que irá verificar se a prioridade corrente é baixa suficiente para executar código paginável. Caso não seja, uma exceção de breakpoint será lançada. Espero que você esteja com o depurador atachado para ver isso acontecer. Caso contrário, não se preocupe, uma linda tela azul irá aparecer e você vai acabar conectando o depurador mais cedo ou mais tarde. Quando compilado em Free Build, essa macro é traduzida para nada, evitando perda de performance. Concluindo, isso evita de você chamar uma função paginável em IRQL alta e tudo funcionar por “sorte”.

...
 
#elif DBG
 
#define PAGED_CODE() {                                                       \
    if (KeGetCurrentIrql() > APC_LEVEL) {                                    \
        KdPrint(("EX: Pageable code called at IRQL %d\n", KeGetCurrentIrql())); \
        NT_ASSERT(FALSE);                                                    \
    }                                                                        \
}
 
...
 
#else
 
#define PAGED_CODE()        NOP_FUNCTION;
 
...
 
#endif

Uma excelente maneira de pegar problemas de paginação de cógido é utilizar o Driver Verifier com a opção de Force IRQL Checking habilitada. Esta opção, além de verificar se você está chamando as funções da API nas prioridades corretas, também força a paginação de tudo que seja paginável em seu driver toda vez que a IRQL subir para DISPATCH_LEVEL ou superior. Isso acaba com aquela “sorte” de utilizar um recurso paginável em IRQL alta quando o recurso já estiver em RAM.

Sessão Descartável

Além da sessão de código paginável PAGE que vimos, uma outra sessão também é vista com grande frequência nos exemplos do WDK. A sessão INIT é descartada quando a chamada à função DriverEntry do seu driver retornar ao sistema, e se for o caso do seu driver, depois que qualquer função de reinicialização for terminada. Se você não sabe o que é uma função de reinicialização, então dê uma passada por este post. Assim, se você tem funções que são somente utilizadas durante a inicialização do seu driver (que neste caso significa: funções chamadas pela DriverEntry, ou funções chamadas por funções que foram chamadas pela DriverEntry, ou ainda funções chamadas por funções que foram chamadas por funções… Ah! Eu acho que você entendeu), vocé pode utilizar o mesmo procedimento para defini-las na sessão INIT da mesma maneira como foi feito anteriormente. Mas não custa deixar um exemplo.

//-f--> As funções aqui declaradas serão removidas da RAM quando
//      a chamada à função DriverEntry retornar. Não tente chamá-las
//      depois disso, porque estas já foram para o céu das funções
//      de inicialização.
 
#ifdef ALLOC_PRAGMA
    #pragma alloc_text(INIT, DriverEntry);
    #pragma alloc_text(INIT, FunctionCalledByDriverEntry);
    #pragma alloc_text(INIT, AnotherFunctionCalledByDriverEntry);
 
    #pragma alloc_text(PAGE, SumOne);
    #pragma alloc_text(PAGE, SumTwo);
#endif

Isso é especialmente útil em Legacy Drivers, que realizam muitos passos na inicialização. Legacy drivers, além de procurar o hardware que vão controlar, ainda precisam fazer a associação dos recursos disponíveis (portas, interrupções, canais de DMA e etc), e isso acaba consumido uma quantidade expressiva de código. Por outro lado, drivers de WDM recebem tudo mastigadinho do Plug-and-Play Manager. O hardware foi detectado e a associação de recursos já foi negociada. Coisa linda de Deus!

Paginável ou não paginável, eis a questão

Supondo que você tenha muitas funções que rodem em IRQL alta, e que por isso, devem estar em memória não paginável, isso faria com que uma grande quantidade de memória não paginável seja utilizada para manter tais funções, mesmo que ninguém esteja utilizando o driver. Também podemos definir nossas próprias sessões e assim torná-las pagináveis ou não pagináveis quando nos for conveniente. Isso permitiria que todas aquelas funções não pagináveis sejam pagináveis enquanto ninguém obtiver uma referência para nosso driver. Desta forma, quando recebermos um IRP_MJ_CREATE ou quando programarmos o hardware para disparar interrupções, podemos dizer ao sistema que agora precisaremos tornar a sessão onde tais funções foram definidas como não paginável.

Primeiro de tudo, teremos que criar nossas sessões do coração, e faremos isso utilizando o pragma alloc_text para definir sessões que devem ter seu nome no formato com PAGExxxx, onde xxxx seja um nome único no seu driver. Veja o exemplinho de desencargo de consciência.

//-f--> As funções aqui declaradas serão definidas em uma sessão
//      que pode ser não paginável quando nos for conveniente.
 
#ifdef ALLOC_PRAGMA
    #pragma alloc_text(PAGEABCD, FunctionOne);
    #pragma alloc_text(PAGEABCD, FunctionTwo);
#endif
 
 
//-f--> As funções aqui declaradas serão definidas em uma outra 
//      sessão que pode ser não paginável. Assim podemos definir
//      diferentes grupos de funções em diferentes sessões.
 
#ifdef ALLOC_PRAGMA
    #pragma alloc_text(PAGE1234, FunctionThree);
    #pragma alloc_text(PAGE1234, FunctionFour);
#endif

Agora que já definimos quais funções estarão definidas nessa sessão, podemos torná-la não paginável somente quando tais funções forem utilizadas. Para tal devemos utilizar a função MmLockPagableCodeSection.

PVOID 
  MmLockPagableCodeSection(
    IN PVOID  AddressWithinSection
    );

Para identificar qual sessão será marcada como não paginável, teremos que passar um endereço que esteja dentro da sessão. Um nome de função definida na sessão já resolve o problema, mas lembre-se que todas as funções dentro da mesma sessão serão marcadas como não paginável.

A função MmLockPagableCodeSection nos retorna um endereço opaco que pode ser utilizado como parâmetro para a chamada à função MmUnlockPagableImageSection, que marca a sessão novamente como paginável. Essa função é normalmente chamada antes do driver ser descarregado. Seguindo a linha do nosso exemplo, poderiamos chamar esta função ao receber um IRP_MJ_CLOSE. O mesmo endereço retornado por MmLockPagableCodeSection pode ser utilizado como parâmetro às chamadas à função MmLockPagableSectionByHandle para novamente tornar uma sessão não paginável, o que é muito mais rápido que a chamada à MmLockPagableCodeSection. Assim, devemos chamar MmLockPagableCodeSection pelo menos uma vez para obter o ponteiro opaco, e depois disso, podemos chamar MmLockPagableSectionByHandle e MmUnlockPagableImageSection.

VOID 
  MmLockPagableSectionByHandle(
    IN PVOID  ImageSectionHandle
    );
 
VOID 
  MmUnlockPagableImageSection(
    IN PVOID  ImageSectionHandle
    );

O mesmo pode ser feito em sessões customizadas que definem dados, mas devemos utilizar a função MmLockPagableDataSection para obter o ponteiro opaco que identifica a sessão.

Minha função pode ser paginável?

Fernando, se minha função é chamada em PASSIVE_LEVEL, então ela pode ser paginável?

Não é bem assim. Sua função, mesmo sendo chamada em PASSIVE_LEVEL, pode conter intervalos de código que precisam ser não pagináveis. Se você chamar funções que elevam a IRQL, tais como KeAcquireSpinLock, sua função não pode ser definida em sessão paginável.

Mas Fernando, acompanhe o meu raciocínio. Se a função foi chamada e está em execução no momento, não é obvio que a página à qual ela está contida está em RAM?

Você pode até não acreditar, mas uma função é composta por uma cadeia de bytes que podem estar nas fronteiras de páginas. Isso significa que o inicio da sua função pode estar no final de uma página, que de fato foi paginada para RAM quando a chamada foi feita, mas não sabemos onde uma nova página pode começar. Esta nova página pode estar em disco, e se for acessada em IRQL alta, a paginação causará uma tela azul. Veja o código abaixo para ter uma idéia do que estou falando e não esqueça de ler os comentários.

/****
***     FunctionCalledAtPassiveLevel
**
**      Rotina que é chamada em PASSIVE_LEVEL,
**      mas tem elevação de IRQL durante a chamada.
*/
 
PLIST_ENTRY
FunctionCalledAtPassiveLevel(VOID)
{
    KIRQL       Irql;
    PLIST_ENTRY pEntry = NULL;
 
    PAGED_CODE();
 
    //-f--> Aqui nossa IRQL vai para as alturas, se o codigo que
    //      vier depois desta chamada estiver na próxima página
    //      de memória que estiver em disco, então (CABUM !!!)
    KeAquireSpinLock(&g_SpinLock, &Irql);
 
 
    //-f-->    -------======= Fronteira de página =======-------
 
 
    //-f--> O código aqui é executado em DISPATCH_LEVEL, o que
    //      impede esta função de ser definida em uma sessão
    //      paginável.
 
    if (!IsListEmpty(&g_NonPagedList))
    {
        pEntry = RemoveHeadList(&g_NonPagedList);
    }
 
    //-f--> Tudo bem que você acesse apenas dados não pagináveis,
    //      mas o código que você usa para tal acesso também precisa
    //      estar em memória não paginável. Afinal, esse código está
    //      sendo executado em IRQL alta e recuperar uma página de
    //      código em disco resultaria em coisas horríveis.
 
    //-f--> Voltamos a PASSIVE_LEVEL
    KeReleaseSpinLock(Irql);
 
    return pEntry;
}

Se você possui funções que são grandes, mas que contém intervalos pontuais com elevação de IRQL, então separe tais intervalos em funções isoladas que podem ser definidas em sessões não pagináveis, permitindo assim que você defina sua grande e complexa funcão em uma sessão paginável.

Ufa! Eu ainda poderia escrever mais alguns comentários sobre este assunto, mas se eu já estou cansado de escrever, imagino como vocês estão. O assunto é tratado com todos detalhes na referência. Mas se tiverem qualquer dúvida a respeito, é só me enviar um e-mail e torcer pra eu saber responder.

Até mais!

Buffered, Direct ou Neither em IOCTLs

16 de October de 2008 - Fernando Roberto

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