Blog

  • Levando a tela azul pra casa

    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

    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<dwBytes; 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 <InitGuid.h>
    #include <Windows.h>
    #include <SetupApi.h>

    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

    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

    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. 🙂

  • Buffered, Direct ou Neither em IOCTLs

    Depois de uma pitada de memória virtual para entendermos os conceitos mais relevantes e darmos uma boa passeada nos métodos de transferências de dados entre aplicação e driver, hoje vamos fechar essa trilogia falando sobre os métodos de transferências de dados em IOCTLs. Se você não sabe criar ou utilizar IOCTLs, este outro post pode ajudar.

    Flags não ajudam aqui

    No post referente aos métodos de transferências de dados foi visto que definimos o método de transferência através de uma máscara de bits que está localizada no campo Flags em um DEVICE_OBJECT. O método escolhido aqui define como o I/O Manager vai manipular os dados nas operações de leitura (IRP_MJ_READ) e escrita (IRP_MJ_WRITE) do driver. O método escolhido é aplicado para ambas as operações. Não podemos ter escritas utilizando um método enquanto as leituras são realizadas utilizando outro. No caso das IOCTLs, a história é diferente. O método de transferência é escolhido quando se define o control code utilizando a macro CTL_CODE.

    #define IOCTL_Device_Function CTL_CODE(DeviceType, Function, Method, Access)

    Para se ter uma explicação mais detalhada sobre o uso desta macro, visite este post, ou dê uma olhada na referência. Aqui comentarei apenas sobre os métodos de transfêrencias de dados que é selecionado pelo parâmetro Method desta macro. A utilização desta macro para definir IOCTLs é normalmente feita em um arquivo de header que será compartilhado entre a aplicação e o driver. A definição desta macro é obtida a partir do header Windows.h para User-Mode e Ntddk.h para Kernel-Mode. Abaixo segue a definição dos IOCTLs que implementaremos neste post.

    //-f--> Aqui definimos os IOCTLs de cópia utilizando os
    //      deferentes métodos de transferência de dados entre
    //      aplicação e driver.
     
    //-f--> Utilizando cópia de sistema
    #define IOCTL_COPY_BUFFERED CTL_CODE(FILE_DEVICE_UNKNOWN,   \
                                         0x800,                 \
                                         METHOD_BUFFERED,       \
                                         FILE_ANY_ACCESS)
     
    //-f--> Travando as páginas da aplicação
    #define IOCTL_COPY_DIRECT   CTL_CODE(FILE_DEVICE_UNKNOWN,   \
                                         0x801,                 \
                                         METHOD_OUT_DIRECT,     \
                                         FILE_ANY_ACCESS)
     
    //-f--> Seja o que Deus quiser
    #define IOCTL_COPY_NEITHER  CTL_CODE(FILE_DEVICE_UNKNOWN,   \
                                         0x802,                 \
                                         METHOD_NEITHER,        \
                                         FILE_ANY_ACCESS)

    Como se pode observar, podemos ter diferentes métodos de transferência de dados para diferentes IOCTLs. Neste post vou criar um driver que ofereça três IOCTLs que simplesmente copiam os dados recebidos no buffer de entrada para buffer de saída. O que teremos que fazer inicialmente é utilizar a macro CTL_CODE para criar as IOCTLs dos serviços que nosso driver de exemplo irá oferecer. O código completo do driver de exemplo está disponível para download ao final deste post.

    Pô Fernando, eu incluí o Windows.h em minha aplicação de teste, mas ainda está faltando a definição da macro CTL_CODE e recebo a mensagem de erro abaixo. Estou usando um Windows.h incompleto?

    Z:\sources\testapp.cpp(45) : error C3861: 'CTL_CODE': identifier not found

    O negócio é o seguinte: O Wizard das versões mais recentes do Visual Studio cria o arquivo StdAfx.h contendo, entre outras, as seguintes linhas:

    #define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
    // Windows Header Files:
    #include <windows.h>

    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

  • Buffered, Direct ou Neither

    No post passado apresentei uma breve introdução sobre alguns pontos referentes à memória virtual que considero ser os mais relevantes aos desenvolvedores de drivers. Com essa pequena carga de conhecimento, ficará mais simples de explicar as diferenças entre os métodos de transferências de dados entre aplicações e drivers, assim como outras questões, tais como o atendimento de interrupções, mapeamento de memória e contexto de execução.

    Se formos pensar de maneira bem simplificada (e bota simplificada nisso), drivers são basicamente os módulos que extraem dados de dispositivos e os disponibilizam para as aplicações e vice-versa. Deste ponto de vista, parece mesmo que escrever drivers seja fácil. Parece até uma maneira de fazer um memcpy de software para hardware. Hoje veremos um pouco sobre as maneiras as quais um driver pode optar para fazer essa transferência de dados.

    Utilizando um Buffer de sistema

    O primeiro método é o chamado Buffered I/O. Este método utiliza um buffer de sistema para fazer a transferência de dados entre a aplicação e o driver. Quando uma aplicação chama a função WriteFile passando os dados a serem enviados para o driver, o I/O Manager faz uma cópia dos dados da aplicação para um buffer de sistema. Mas o que é um buffer de sistema? Se trata de uma alocação em System Space que foi feita em Kernel-Mode, e por isso, é acessível em qualquer contexto de processo. Já falamos disso no post passado.

    BOOL WINAPI WriteFile(
      __in         HANDLE hFile,
      __in         LPCVOID lpBuffer,
      __in         DWORD nNumberOfBytesToWrite,
      __out_opt    LPDWORD lpNumberOfBytesWritten,
      __inout_opt  LPOVERLAPPED lpOverlapped
    );

    O I/O Manager obtém o tamanho do buffer a ser alocado do parâmetro nNumberOfBytesToWrite, então realiza uma alocação de memória do tipo não paginada e faz a cópia dos dados que estão em User Space para System Space. Se você estiver derrapando nos termos System Space, User Space e tipos de alocação de memória, dê uma lida neste post para que as coisas fiquem menos obscuras para você.

    No driver, o ponteiro para este buffer é obtido pela IRP e seu tamanho é obtido pela stack location da IRP. Legal, mas o que é uma IRP? Dê uma olhada neste mini exemplo abaixo para ter uma idéia, ou você pode ver o exemplo utilizado neste outro post que também utiliza o método Buffered para enviar e receber strings de um driver de exemplo.

    //-f--> Rotina de tratamento da IRP_MJ_WRITE.
    //      Exemplo de obtenção do buffer de sistema
    //      alocado pelo I/O Manager no método Buffered
     
    NTSTATUS
    OnDispatchWrite(__in PDEVICE_OBJECT pDeviceObj,
                    __in PIRP           pIrp)
    {
        PIO_STACK_LOCATION  pStack;
        PVOID               pBuffer;
        ULONG               ulLength;
     
        //-f--> Obtém a stack corrente da IRP
        pStack = IoGetCurrentIrpStackLocation();
     
        //-f--> Aqui obtemos o ponteiro para o buffer
        pBuffer = pIrp->AssociatedIrp.SystemBuffer;
     
        //-f--> Aqui o tamanho do buffer
        ulLength = pStack->Parameters.Write.Length;
     
        ...
    }

    Nas operações de leitura, o método é bem parecido, mas apesar de neste caso o I/O Manager também fazer a locação em memória não paginada, ele não faz nenhuma cópia para o buffer de sistema. O buffer de sistema é recebido pelo driver também através de pIrp->AssociatedIrp.SystemBuffer, mas o seu tamanho é obtido em pStack->Parameters.Read.Length. O driver preencherá o buffer com os dados vindos do dispositivo, o I/O Manager fará a cópia do buffer de System Space para User Space quando a IRP for completada, preenchendo o buffer da aplicação.

    Opa opa! Até onde eu sei, drivers normalmente completam IRPs em contexto arbitrário, o que significa que “só Deus sabe em qual contexto de processo”. Tudo bem se o driver escrever em um buffer de sistema, o qual é válido para qualquer processo, mas o buffer da aplicação está em User Space e só é acessível no contexto do próprio processo. Como o I/O Manager faria a cópia do buffer de sistema para o buffer da aplicação em contexto arbitrário? Nossa, essa realmente foi uma excelente pergunta. Acho que nem eu teria imaginado uma pergunta tão boa. As IRPs podem ser tratadas tanto sincronamente como assincronamente. O I/O Manager sabe como a IRP foi processada, e no caso síncrono, o I/O Manager já está no contexto do processo que fez a solicitação, e neste caso ele tem acesso a ambos os bufferes, já que o I/O Manager roda em Kernel-Mode. Quando a IRP é tratada assincronamente, o I/O Manager enfila uma APC (Asynchronous Procedure Call), que é uma chamada assincrona executada no contexto de uma dada thread. Essa thread é justamente a thread que iniciou a operação, e que portanto, estará no contexto do processo certo para acessar o User Space da aplicação.

    BOOL WINAPI ReadFile(
      __in         HANDLE hFile,
      __out        LPVOID lpBuffer,
      __in         DWORD nNumberOfBytesToRead,
      __out_opt    LPDWORD lpNumberOfBytesRead,
      __inout_opt  LPOVERLAPPED lpOverlapped
    );

    Na chamada da função ReadFile, o parâmetro nNumberOfBytesToRead indica o tamanho do buffer que a aplicação está oferecendo ao driver. O driver recebe este valor como quantidade máxima de bytes que podem ser retornados à aplicação. Supondo que a aplicação tenha oferecido 1000 bytes, o I/O Manager faz uma alocação de 1000 bytes e repassa o buffer para o driver. Vamos supor que o driver tenha apenas 500 bytes a serem retornados à aplicação, neste caso, o I/O Manager terá de copiar para o buffer da aplicação apenas 500 dos 1000 bytes alocados. O I/O Manager recebe este valor através do campo pIrp->IoStatus.Information, que é preenchido pelo driver antes da IRP ser completada. Desta forma, o I/O Manager copia somente os bytes válidos do buffer de sistema para o buffer da aplicação. Este mesmo valor é retornado à aplicação através do parâmetro lpNumberOfBytesRead.

    Utilizar memória não paginada para o manter o buffer de sistema nos assegura que as páginas não serão removidas da RAM por paginação. Isso permite que a página seja acessada mesmo a partir de threads que estejam rodando em alto nível de prioridade. Contudo, o método Buffered é indicado apenas para pequenas movimentações de dados. Imagine que uma aplicação queira fazer uma escrita de 10 MB de uma só vez. O I/O Manager teria que fazer uma alocação em System Space de 10 MB de memória não paginada, o que não seria nada adequado, pois memória não paginada é um recurso escasso. Se o I/O Manager conseguir fazer a alocação, ele ainda terá que fazer uma cópia de 10 MB do buffer da aplicação para o buffer de sistema. Isso até funcionaria, mas teriamos sérios problemas de performace. Neste caso, o mais indicado seria utilizar o método que é visto em seguida.

    Travando a memória da Aplicação

    No método chamado Direct I/O, como o nome já sugere, o driver faz acesso diretamente às páginas de memória da aplicação sem utilizar um buffer intermedirário. Desta forma, o I/O Manager não faz uma alocação em memória não paginada e também não tem que ficar no BPL-BPC (Buffer pra lá – Buffer pra cá). Ao invés disso, o I/O Manager testa as páginas de memória que compõem o buffer oferecido pela aplicação, cria uma MDL e trava as páginas de memória na RAM. Nossa! Calma aí meu amigo, vamos devagar!

    • Testa as páginas de memória – Nada impede um programador de fazer besteira. Um buffer inválido pode ser passado para o I/O Manager. Pode ser que o buffer não tenha sido alocado, ou que o buffer seja menor que o valor indicado na chamada às funções ReadFile ou WriteFile, pode ser também que as páginas de memória oferecidas à ReadFile estejam protegidas contra escrita, pode ser também que algumas das páginas utilizadas pelo buffer tenham sido paginadas para o disco. Se um driver tenta acessar uma página inválida ou protegida, uma exceção é gerada e uma tela azul sugirá das trevas. Mas como o I/O Manager vai testar a memória? Se o buffer é passado como parâmetro para a função ReadFile, então o driver fará escritas neste buffer. Para ganhar tempo, o I/O Manager fará uma escrita de um byte de cada página de RAM apenas para testar o acesso a elas. Essa escrita é feita sob um manipulador de exceção. Se o buffer for inválido ou protegido, o manipulador de exceção tratará isso e devolverá um erro para a aplicação. Se o buffer é passado para a função WriteFile, então o buffer será lido pelo driver, e neste caso, o teste seria a leitura de um byte de cada página.

    • Cria uma MDL – Uma MDL (Memory Descriptor List) é uma estrutura de dados que descreve as várias páginas de memória que compõem um buffer. Estas páginas são as mesmas páginas físicas utilizadas pela aplicação, ou seja, quando o driver escrever nestas páginas, este já estará escrevendo diretamente no buffer da aplicação. Assim o I/O Manager não precisará fazer nenhum BPL-BPC.

    • Trava as páginas na RAM – Esse passo faz com que as páginas de memória que compõem o buffer da aplicação se tornem não pagináveis. Assim, estas poderão ser acessadas pelo driver em threads que estejam sendo executadas em alto nível de prioridade.

    Depois de todo esse ritual, o driver agora recebe o buffer através de pIrp->MdlAddress, mas o que teremos aqui é um ponteiro para uma MDL. Mas o que eu faço com uma MDL? Na maioria das vezes, você vai passar como argumento em um serviço oferecido por outro driver ou componente do sistema. Alguns exemplos são drivers de DMA (Direct Memory Access) que utilizam MDL na chamada para a função MapTransfer, ou mesmo quando uma MDL é repassada para rotinas de controladores USB (Universal Serial Bus), tais como UsbBuildInterruptOrBulkTransferRequest. MDLs são estruturas opacas, mas se você quer ter acesso ao buffer da aplicação, então devemos chamar a função MmGetSystemAddressForMdlSafe para conseguir o endereço para o buffer a ser escrito/lido. Reparem que o endereço retornado por esta função está em System Space, e assim, acessível em qualquer contexto de processo. Mas o buffer da aplicação não está em User Space? Sim, mas o que temos aqui é uma página de memória física sendo mapeada tanto para o espaço de endereçamento da aplicação quanto para o espaço de endereçamemto de sistema.

    O tamanho do buffer descrito pela MDL também é obtido pela stack location da IRP, assim como no método Buffered.

    Nem Buffered I/O nem Direct I/O

    O terceiro método é simplesmente o não uso dos dois primeiros. No método chamado Neither, o I/O Manager não faz uma cópia em um buffer de sistema nem monta uma MDL para descrever o buffer da aplicação. Quando a IRP chega ao seu driver, você tem acesso ao endereço virtual do buffer oferecido pela aplicação por pIrp->UserBuffer. Este endereço aponta para User Space, e por isso, lembre-se que este endereço só é valido no contexto do processo que solicitou o I/O. O uso deste método requer mais cuidado, pois seu driver precisa ser o primeiro driver na pilha de dispositivos, e dessa forma, garantir que a IRP chegue ao seu driver no contexto do prcesso que fez a solicitação de I/O.

    Você pode verificar se você está no contexto do processo que solicitou a operação fazendo o teste abaixo.

    /****
    ***     EstouNoContextoDoProcessoQueGerouEssaIrp
    **
    **      Função com nome ridículo que verifica se o
    **      estamos no contexto do processo que gerou
    **      a IRP passada como parâmetro.
    */
     
    BOOLEAN EstouNoContextoDoProcessoQueGerouEssaIrp(__in PIRP pIrp)
    {
        PETHREAD    pEThread;
        PEPROCESS   pEProcess;
     
        //-f--> Obtém a thread que gerou a IRP
        pEThread = pIrp->Tail.Overlay.Thread;
     
        //-f--> Obtém o processo referente a thread
        pEProcess = IoThreadToProcess(pEThread);
     
        //-f--> Aqui comparamos o processo corrente com
        //      o processo que gerou a IRP
        return (PsGetCurrentProcess() == pEProcess);
    }

    O fato de estar no contexto do processo correto não garante que o buffer passado como parâmetro seja válido. Diferente do método Direct, neste ponto o I/O Manager não testou o buffer antes de passar a IRP para o driver. Teremos que fazer isso por nós mesmos. Lembre-se que, assim como em User-Mode, ao acessar um buffer inválido o driver receberá uma exceção que deve ser manipulada, caso contrário, tudo azul.

    Para realizar o teste, teremos que acessar um byte de cada página do buffer que nos foi passado e verificar se o mundo acaba. As rotinas ProbeForRead e ProbeForWrite fazem isso por nós, mas estas devem ser chamadas dentro de um manipulador de exceção. Vale lembrar que em uma operação de leitura, a aplicação nos envia um buffer onde o driver escreverá dados para a aplicação. Já que o driver fará uma escrita neste buffer, teremos que realizar um teste de escrita (ProbeForWrite) nas operações de leitura (ReadFile). De maneira análoga, o driver deverá fazer um teste de leitura (ProbeForRead) nas operações de escrita (WriteFile). Dê uma olhada no exemplo de rotina de leitura que segue abaixo. Como você já deve estar acostumado, leia os comentários que complementam o texto.

    /****
    ***     OnDispatchRead
    **
    **      Outra função com nominho besta de exemplo.
    **      Valida o buffer enviado pela aplicação pelo
    **      método Neither e escreve uma seqüência numérica
    **      no buffer da aplicação.
    */
     
    NTSTATUS OnDispatchRead(__in PDEVICE_OBJECT pDeviceObj,
                            __in PIRP           pIrp)
    {
        PIO_STACK_LOCATION  pStack;
        PVOID               pBuffer;
        ULONG               ulLength;
     
        //-f--> Verifica se estamos no contexto do processo
        //      que gerou esta IRP
        if (!EstouNoContextoDoProcessoQueGerouEssaIrp(pIrp))
        {
            //-f--> Sinaliza ao I/O Manager que a casa caiu
            pIrp->IoStatus.Status = STATUS_INVALID_PARAMETER;
            pIrp->IoStatus.Information = 0;
     
            //-f--> Completa a IRP com falha
            IoCompleteRequest(pIrp, IO_NO_INCREMENT);
            return STATUS_INVALID_PARAMETER;
        }
     
        //-f--> Obtém a stack location corrente
        pStack = IoGetCurrentIrpStackLocation(pIrp);
     
        //-f--> Aqui obtemos o endereço do buffer oferecido
        //      pela aplicação bem como seu tamanho.
        //      Note que este buffer deve estar em User Space
        pBuffer = pIrp->UserBuffer;
        ulLength = pStack->Parameters.Read.Length;
     
        //-f--> As funções que testam o buffer lançam exceções
        //      no caso de o buffer ser inválido. Por isso temos
        //      que fazer o teste dentro de um manipulador de exceções
        __try
        {
            //-f--> Se você der uma olhada na referência, verá que
            //      esta rotina retorna VOID, por isso a única
            //      maneira de saber se o buffer é inválido é
            //      manipulando a exceção que será gerada.
            ProbeForWrite(pBuffer,
                          ulLength,
                          1);
        }
        __except(EXCEPTION_EXECUTE_HANDLER)
        {
            NTSTATUS    nts;
     
            //-f--> Ops! Buffer inválido.
            //      Vamos obter o código da exceção e dar um
            //      fim nesse sofrimento
            nts = GetExceptionCode();
     
            pIrp->IoStatus.Status = nts;
            pIrp->IoStatus.Information = 0;
            IoCompleteRequest(pIrp, IO_NO_INCREMENT);
            return nts;
        }
     
        //-f--> Aqui sabemos que o buffer está seguro para receber
        //      escritas. Vamos apenas escrever uma seqüência numérica
        //      só pra...
        for (ULONG i = 0; i < ulLength, i++)
            ((PUCHAR)pBuffer)[i] = (UCHAR)i;
     
        //-f--> Aqui informamos que a operação foi realizada com
        //      sucesso.
        pIrp->IoStatus.Status = STATUS_SUCCESS;
     
        //-f--> Apesar de no método Neither o I/O Manager não
        //      utilizar este número para fazer BPL-BPC, este
        //      número ainda é retornado pela função ReadFile
        //      para informar à aplicação quantos bytes do buffer
        //      são válidos para a aplicação ler.
        pIrp->IoStatus.Information = ulLength;
     
        //-f--> Dá um Fatality na IRP
        IoCompleteRequest(pIrp, IO_NO_INCREMENT);
        return STATUS_SUCCESS;
    }

    Para todos os métodos, é importante notar que setar o campo pIrp->IoStatus.Information diz à aplicação a quantidade de bytes foram lidos ou escritos no buffer, independente de haver ou não a cópia de buffer de sistema como no caso do método Buffered.

    Outra coisa que não vai mudar para os diferentes métodos é a obtenção do tamanho do buffer oferecido pela aplicação. Este valor sempre vem pela stack location como já foi visto nos métodos já discutidos.

    Como selecionar o método

    Faz todo o sentido que a escolha do método seja feita antes da primeira IRP chegar ao driver. Isso é feito logo depois que o device é criado. Depois que a chamada à função IoCreateDevice termina, recebemos o novo device através de um ponteiro de saída. O membro Flags da estrutura DEVICE_OBJECT é uma máscara de bits e os bits DO_BUFFERED_IO e DO_DIRECT_IO configuram o método de transferência.

    Então é fácil assim. Para configurar o método Buffered, setamos o bit DO_BUFFERED_IO, para configurar o método Direct, setamos o bit DO_DIRECT_IO, e finalmente para setar o método Neither, não setamos nenhum destes bits. Já li em algum livro, que não encontro agora, que o comportamento não é previsto se você setar ambos os bits.

    Segue mais um exemplinho besta de como setar o device que acaba de ser criado para transferências no método Buffered.

        //-f--> Cria o device que irá receber as IRPs
        nts = IoCreateDevice(pDriverObj,
                             0,
                             &usDeviceName,
                             FILE_DEVICE_UNKNOWN,
                             0,
                             FALSE,
                             &pDeviceObj);
     
        //-f--> Verifica se o device foi criado
        if (!NT_SUCCESS(nts))
            return nts;
     
        //-f--> Aqui já podemos configurar o método
        //      de transferência desejado, que neste
        //      exemplo é o Buffered I/O
        pDeviceObj->Flags |= DO_BUFFERED_IO;

    Mas e se uma IRP for entregue ao driver antes de setarmos estes bits? Outra excelente pergunta. As coisas acontecem assim. Sempre quando um novo device é criado, o bit DO_DEVICE_INITIALIZING é setado. As IRPs só começam a ser entregues a este device quando o driver baixar este bit. Isso nos permite inicializar o device antes que qualquer IRP chegue.

    Boa tentativa espertão, mas seu exemplo não baixa este bit. Como você explica isso? Você hoje está impossível! Ao termino da rotina DriverEntry, quando o controle volta ao I/O Manager, ele varre a lista de devices criados pelo driver e baixa este bit por nós. É importante lembrar que ainda precisamos baixar este bit quando criamos um novo device depois que a rotina DriverEntry terminou. Um exemplo muito comum são os devices de WDM, que são criados na rotina AddDevice, mas isso vai ficar para uma outra vez. Esse post já ficou muito longo.

    Até mais! 🙂

  • Uma pitada de Memória Virtual

    Agora vamos deixar de conversinha mole e vamos logo ao que interessa. O exemplo que descrevi em outro post mostra como implementar um driver bem simples que armazena uma lista de strings que são enviadas ao driver através de operações de escrita (WriteFile), e as mesmas strings são retornadas em subseqüentes operações de leitura (ReadFile). Esse é um bom exemplo de como os dados das aplicações vão para os drivers e vice-versa. Existem três maneiras de acessar os dados oferecidos por aplicações a partir de um driver. Neste post vamos dar uma olhadinha em memória virtual para que possamos ter uma base para posts futuros, onde poderei explicar as diferenças entre aquelas tais três maneiras. Vamos nos ater a apenas algumas características básicas da memória virtual para que as coisas que explicarei em seguida façam mais sentido para você, mas se você quiser mais detalhes a respeito de Memória Virtual, você pode ler o Capítulo 7, “Memory Management”, do Windows Internals, ou dar uma olhadinha em uma apresentação feita pelo meu amigo Strauss.

    O que é uma página de memória

    Página de memória é uma unidade utilizada pelo hardware nas proteções de acesso à memória. Mais sobre proteções de páginas de memória aqui. Essa é uma unidade definida pelo hardware que ajuda a subdividir a memória em espaços menores e gerenciáveis. Apesar de existirem dois tamanhos de páginas (uma pequena de 4KB bytes, e outra grande de 4MB), toda referência ao tamanho de página vista na documentação são referentes apenas às páginas pequenas. O tamanho da página de memória varia com a plataforma de hardware. Em sistemas x86 e x64, as paginas são de 4KB enquanto que em sistemas IA64 a página é de 8KB. O tamanho da página pode ser obtido pela constante PAGE_SIZE.

    Espaço de endereçamento

    De uma maneira bem resumida, memória virtual é um mecanismo, que em um trabalho conjunto entre hardware e software, permite que processos que rodem no Windows tenham seu próprio espaço de endereçamento. Espaço quem? Espaço de endereçamento, ou Address Space como normalmente vimos na referência, permite que cada processo tenha uma visão privada da memória que esteja utilizando. Ou seja, um processo não pode ler ou escrever em páginas de memória de outro processo. Isso evita que um programa mal escrito possa erroneamente escrever em páginas de memória de outro processo ou mesmo em páginas do sistema operacional, comprometendo assim a estabilidade de todo o sistema. Portanto, podemos assumir que o espaço de endereçamento de um processo só é acessível pelo processo ao qual ele pertence.

    Nota: É possível compartilhar memória entre processos, mas mesmo neste caso, a mesma página física de memória é referenciada por dois ou mais espaços de endereçamentos diferentes.

    Memória Virtual e Memória Física

    Logo após a descoberta do fogo, a memória era endereçada diretamente em modo real. Isso significa que um ponteiro utilizado por uma aplicação acessava o dado exatamente onde ele se encontrava fisicamente nos chips de memória RAM. Páginas de memória virtual associadas aos processos refletem páginas físicas de memória através de um endereçamento virtual. Os bits que compõem o endereço virtual mapeam a memória física que se deseja acessar. Ao deferenciar um ponteiro que contem um endereço virtual, o processador, de maneria transparente ao processo, consulta estruturas preenchidas pelo sistema operacional de forma a traduzir o endereço virtual em real, e dessa forma, encontra a página física onde o dado realmente se encontra.

     

    A disposição das páginas de memória virtual não tem relação com sua disposição física. Isso significa que você quando você faz uma grande alocação de memória, que é composta por várias páginas de memória, você ganha um ponteiro que, do ponto de vista da aplicação, aponta para páginas de memória que estão linearmente distribuídas (uma seguida da outra), enquanto que as páginas de memória física podem estar todas espalhadas pelos chips de RAM.

    A memória pode não estar na RAM

    A memória utilizada pelas aplicações não se limita à quantidade de memória oferecida pelos chips de RAM instalados em sua placa mãe. Quando os processos, cada vez mais sedentos por memória, vão sendo executados, o espaço disponível em RAM vai sendo compartilhado entre os vários processos. Páginas de memória acessadas com menos frequência são removidas dos chips de RAM e vão para disco (para o Pagefile.sys para ser mais preciso), dando lugar à uma outra página de memória que é necessária naquele momento. Esse processo recebe o nome de paginação. Quando a aplicação finalmente acessar o dado que está naquela página agora em disco, o sistema aloca espaço em RAM física para trazer de volta a página do disco para a RAM. Isso pode resultar em outras páginas que estavam em RAM a serem paginadas para o disco.

     

    Para as aplicações, o processo de paginação é completamente transparente. O desenvolvedor não está nem um pouco preocupado se a região de memória que o programa vai acessar está em disco ou está em RAM. É só acessar o dado e o processador junto com o gerenciador de memória resolve isso para você. Infelizmente, a história não é tão florida para os desenvolvedores de drivers. Aplicações sempre rodam em uma prioridade de thread baixa (PASSIVE_LEVEL) e ao acessar um dado que esteja em uma página em disco, a thread é interrompida pelo gerenciador de memória para que este tenha a oportunidade de acionar os drivers de File System e por conseqüência os drivers de disco, aguardar o I/O ser realizado (apesar de levar um cacalhésimo de segundo, ainda temos que esperar) e então a thread é liberada. Embora haja pessoas que não acreditem nessas coisas, a plataforma NT é completamente preemptiva. Isso significa que uma thread pode ser interrompida por outra de maior prioridade. Em Kernel Mode, uma thread pode estar em prioridades superiores à APC_LEVEL, que é a prioridade em que as paginações são realizadas. Se a thread que acessará o dado estiver em um nível de prioridade muito alta (maior ou igual a DISPATCH_LEVEL), o processador não vai conseguir fazer a paginação e uma tela azul irá aflorar aos seus olhos. Um exemplo típico de execução em alta prioridade é o tratamento de interrupções, mas não vamos nos desviar do assunto.

    Uma página pode ser acessada não só por um software, mas também por hardware. Hein? Uma aplicação pode passar o ponteiro de uma região de memória para um driver, que por sua vez, irá utilizar um canal de DMA para preencher este buffer. A cópia de um buffer através do processo de DMA não utiliza ciclos de CPU para ser realizada. Preencher um buffer é algo tão simples que até um chimpanzé poderia fazer. Assim um grupo de chimpanzés engenheiros criaram um chip que faz isso, mas isso não vem ao caso agora. Este chip pode estar na placa mãe ou na própria placa controlada pelo driver. O chip é programado pelo driver e uma transferência é iniciada. Durante a cópia, nem a aplicação, nem o sistema operacional e nem mesmo o processador ficam cientes do que está acontecendo. O sistema pode então determinar que as páginas utilizadas na transferência, que não estão sendo acessadas por nenhum processo, devem ser paginadas para disco. Se isso ocorrer, o chip de DMA continuará tranferido bytes para aquele endereço físico de RAM e… Bom, já sabe né? Para evitar isso, existem meios de avisar o gerenciador de memória que uma ou mais páginas não devem ser paginadas.

    Durante o desenvolvimento de um driver, você pode querer alocar um buffer que será utilizado por uma ISR (Interrupt Service Routine). Como uma ISR é executada em alta prioridade, podemos fazer uma alocação de memória solicitando um buffer que não seja paginável, e assim, garantir que o buffer obtido por esta alocação esteja sempre em RAM. Mas vá com calma, memória não paginável é um recurso escasso e deve ser utilizado com parcimônia. Caso contrário, algumas operações vão deixar de ser realizadas pelo sistema por falta de RAM, mesmo que ainda haja muita memória paginável disponível.

     

    User Space e System Space

    Adotando um sistema de 32 bits como exemplo, um ponteiro é capaz de endereçar 4 GB de memória. Destes, 2 GB são reservados para endereçar páginas privadas para cada processo, ou seja, vão compor o espaço de endereçamento privado de cada aplicação. Esta faixa, que vai de 0x00000000 ao 0x7FFFFFFF, recebe o nome de User Space e são os endereços que aplicações acessam em User Mode. Conforme vimos, esta faixa compõe o espaço de endereçamento privado de um processo, protegido contra acessos indevidos de outros processos. Um exemplo seria: Somente as threads do Processo A terão acesso ao User Space do Processo A. O mesmo endereço aponta para diferentes páginas físicas de RAM em diferentes processos, dependendo do contexto do processo que faz o acesso. Os outros 2 GB restantes do endereçamento de 32 bits são reservados ao endereçamento de páginas de sistema. A faixa de endereços de 0x80000000 ao 0xFFFFFFFF define o então chamado System Space, que diferente do User Space, endereçam páginas de memória que não são privados a um determinado processo. Isso significa que o mesmo dado (na mesma página física) pode ser acessado através do mesmo endereço virtual em diferentes processos. Um dado em System Space pode ser acessado apenas em Kernel Mode, enquanto que um dado em User Space pode ser acessado tanto em Kernel Mode quanto em User Mode.

     

    Cagamba! Isso foi uma metralhadora de conceitos? Se você apresentar sintomas de visão turva ou vômito seguido de diarréia e tremedeira, larga a mão de ser frouxo e se prepare para os próximos posts. Caso você não tenha apresentado nenhum destes sintomas, não se preocupe, para algumas pessoas o processo é mais demorado. Em posts futuros vou explicar como os drivers acessam os endereços virtuais oferecidos pelas aplicações, como lidam com a paginação de memória e ainda testam buffers oferecidos pelas aplicações. Um driver mal escrito pode permitir que um parâmetro inválido em uma aplicação cause uma tela azul.

    Por hoje chega, mas vale lembrar que o que foi apresentado aqui é apenas a ponta do iceberg. Essa pontinha é o que acredito ser o mais relevante para o desevolvimento de drivers, mas ainda existem toneladas de detalhes referentes à memória virtual.

    Have fun! 🙂

  • Recife Office

    Pode uma pessoa trabalhar do outro lado do país e ainda assim dar um curso de Drivers para Windows? A resposta para essa pergunta é: “Depende se essa pessoa é Home suficiente para isso”. O fato de ter recebido a oportunidade de trabalhar em casa me abriu a grande flexibilidade de poder trabalhar fora de casa. Trabalhar pela Internet não significa necessariamente trabalhar em casa.

    Fui convidado a ministrar o curso de Drivers em um centro de estudos que fica em Recife – PE. Eu nunca poderia aceitar um convite desses em condições normais de temperatura e pressão, já que trabalho regularmente em uma empresa em São Paulo, e como se não bastasse, ainda estou me graduando em Engenharia da Computação no período noturno. Mas agora que virei Home, as coisas são diferentes. Aproveitando o período de férias da universidade, fui fazer meu Home Office em Recife e dar o curso durante o período noturno.

    Uma empresa multi-nacional do ramo de eletrônicos contratou os serviços deste centro de estudo para o desenvolvimento e manutenção dos drivers que se comunicariam via USB com telefones celulares. O driver original foi escrito por ninguém menos que Walter Oney. Fiquei com a missão de ensinar o caminho das telas azuis para este grupo de 10 alunos, minha maior turma até agora. Quantas pessoas que trabalham com drivers você conhece? O número de alunos não foi problema em momento algum, pensei que seria mais difícil gerenciar isso. Todos interessados, esforçados e com vontade de aprender. Difícil mesmo foi ter que ficar duas semanas inteiras hospedado em um Hotel à beira-mar na praia de Boa Viagem e não poder dar um único mergulho (A primeira foto deste post foi tirada da janela do hotel). Um dos motivos é a constante presença de tubarões nas praias Pernambucanas, com algumas exceções. Outro fato foi a carga horária que foi adotada. Um curso básico de drivers é normalmente ministrado em 40 horas, o que é perfeito para duas semanas, sendo 10 aulas de 4 horas. Mas neste caso havia a necessidade de uma especialização em dispositivos USB. Isso nos tomou dois sábados, meio domingo e muita energia. Um curso de três semanas foi dado em duas. Não sei se foi o mais produtivo, mas minha agenda não me permitiu o luxo de descansar aos finais de semana.

    Uma pergunta freqüente em meu blog é “Quando será formada a próxima turma?”. No começo desse ano eu imaginei poder montar uma turma aberta durante o período de férias, mas a correria que veio em seguida, acentuada com pela minha viagem, não permitiu que eu me organizasse com relação a infra-estrutura e divulgação. O curso destas férias não existiria, mas no caso de Recife, já havia uma turma fechada e a disponibilidade dos recursos oferecidos por eles. A pergunta ainda persiste e a resposta agora será que estou me organizando para montar uma turma aberta para o início de Janeiro. Obviamente vou publicar qualquer novidade referente a isso aqui no blog, mas gostaria de já ter uma noção dos recursos que terei que reservar para isso. Assim, se você tem interesse de participar dessa turma, me envie um e-mail sem compromisso ou deixe um comentário neste post. Meu endereço de e-mail que ninguém encontra, fica na página apontada por este link, que é a página do meu perfil do blogger.

    Até mais…

  • Agora Que Virei Home

    De repente meu gerente me liga e pergunta – “Você é Home, certo?”. Fiquei meio constrangido com aquela situação, mas o que eu podia fazer? Eu tinha que responder àquela pergunta. “Olha, ainda não sou completamente, mas estou interessado em virar”, respondi. “Então preencha os formulários que eu aprovo”. Foi basicamente assim que virei Home Office.

    Como funcionário regular, eu ainda tinha o direito de fazer Home Office 2 dias por semana. No início vem um sentimento de culpa. Não dá pra aceitar, assim de cara, que não terei que levantar às cinco e meia da manhã e enfrentar quase duas horas de trânsito para sair de Santo André, pegar a Avenida dos Estados e finalmente chegar ao prédio onde trabalho. Confesso que não sair de casa tão cedo e me sentar em frente ao notebook da empresa usando um moletom causa um certo desconforto no início. Dá a impressão de que não estamos trabalhando, de que estamos fazendo alguma coisa errada.

    Depois do sentimento de culpa vem o relaxo. Até hoje eu continuo levantando cedo para poder levar minha esposa até a estação de trem, que fica à exatos 7 minutos de minha casa. Nas primeiras semanas, eu voltava para a cama e dava mais uma dormida até às nove da manhã. Já consegui chegar no trabalho com 30 minutos atraso mesmo estando a vinte metros dele. Tomar café da manhã em frente ao notebook virou rotina. Mas isso passou. Acho que conforme o tempo foi passando, o corpo e a mente vão ficando mais descançados, até uma hora que não dá mais vontade de voltar para a cama. Além do mais, eu ainda tenho uma lista razoável de livros para ler. O tempo que antes eu gastava no trânsito, agora eu gasto lendo e isso está dando muito certo.

    Meus vizinhos agora devem pensar que fiquei desempregado e que sou sustentado pela minha esposa. Tem uma pessoa que além limpar minha casa, prepara meu almoço. Assim, eu consigo almoçar em 15 minutos e não vou voltar correndo para o notebook depois disso. Eu preciso fazer minha fotosíntese. Para isso vou tomar um café expresso em uma padaria que fica láááááááá em cima e aproveito para tomar sol caminhando até a padaria. Ter horário para tomar sol até parece programa de presidiário. Às vezes eu levo meus cães para passear durante meu horário de almoço. Eu não culpo meus vizinhos. O que você pensaria se visse uma pessoa de uns trinta anos de idade, com a barba de dias na cara, passeando com cachorros ao meio dia? “Esse aí leva cães para passear para poder conseguir o dinheiro da pinga”.

    Bom, eu estou mesmo babando e andando para os vizinhos. Complicado é o que seus parentes pensam de você. Para tomar café, eu passo em frente à casa de uma tia. Sabe aquela tia que tudo o que ela conseguiu entender sobre o que você faz é que você trabalha com computadores, ela só não tem certeza se é limpando ou vendendo. Numa dessas idas e vindas do café, acabei me encontrando com ela. “E aí Fernando, está de folga?” – ela pergunta. “Não tia, agora eu trabalho em casa” – eu respondo todo satisfeito, mas olhando sua expressão, dá pra imaginar as engrenagens na cabeça dela – “Vixe tadinho, deve estar fazendo aqueles bicos do tipo Trabalhe em casa, pergunte-me como.

    Mais difícil que se convencer de que você pode trabalhar em casa, é convencer os outros de que você está trabalhando em casa. Na época em que eu fazia Home Office apenas 2 vezes por semana, minha esposa sempre me perguntava – “Você vai trabalhar amanhã?”, como se eu não trabalhasse em casa. Meu irmão uma vez me disse, ” …mas você não vai ficar em casa amanhã? Então, eu passo lá e a gente vai ver aquela peça do carro”.

    Trabalhar em casa exige disciplina, embora eu possa fazer meu próprio horário, eu prefiro ter horário fixo para almoçar, começar e parar de trabalhar. Não estou falando apenas de trabalhar menos. Trabalhar demais é um risco maior. Você já está alí com um pepino pra resolver, falta só mais aquele último teste, que é o último há uns 5 testes atrás, você não tem que pegar trânsito, já está no conforto do seu lar e quando você vê já são nove da noite.

    Não muda nada para a empresa?

    Eu trabalho em um time global. Uns aqui, outros nos Estados Unidos e India. O gerente do time de desenvolvimento está em outro hemisfério. Você acha mesmo que isso vai fazer alguma diferença pra ele? Mesmo quando eu estava trabalhando no prédio, tudo que eu precisava era de um ponto de rede. Apesar de eu ter um notebook, este é utilizado para abrir uma sessão remota em uma das máquinas que servem os laboratórios externos (Brasil e India). As máquinas de teste são virtuais em sua maioria, as reais também são acessadas remotamente. Todas as reuniões são feitas via Web Conference. Seu ramal é desviado para uma ferramenta VOIP no notebook. Você disca um ramal pra falar com alguém do outro lado do planeta. Tudo através de um ponto de rede.

    Quando você vira Home Office, a empresa te dá um limite de reembolso para comprar móveis, aparelho e conta telefônica, Internet banda larga, impressora e material de escritório. Obviamente tudo tem que ser devolvido quando você deixar de ser Home Office. Felizmente minha casa já tinha todos os requisitos necessários, e assim, não vou ficar usando uma mesa ou cadeira que não são minhas mesmo. O bom disso é que eu posso trocá-los quando eu quiser.

    O que principalmente vai mudar para a empresa, é que agora eles terão mais uma baia livre. Uma empresa deste tamanho não cabe nos prédios onde estamos (isso mesmo, no plural). A empresa incentiva o Home Office. Uma das cláusulas da transição para Home Office é a de que você deve permanecer no mínimo um ano nesta condição. Fiquei preocupado quando li essa parte, então fui perguntar às pessoas que já eram Home Office o que eu não estava conseguindo ler nas entrelinhas. O que acontece é que na empresa que trabalho, a grande maioria dos funcionários são vendedores. Vendedor é o tipo de profissional extrovertido, que fala bastante e preza muito o contato com as pessoas. Para esse tipo de pessoa, ficar enclausurado dentro de casa é a morte. Tanto que muitas pessoas que são Home Office deixam suas casas para disputar baias vagas aos tapas lá no prédio. Minha esposa mesmo, se ficasse em casa, morreria louca mordendo o braço do sofá em menos de um mês.

    Pois é, existem pessoas e pessoas. Como você já deve ter percebido, sou desenvolvedor de software. Sou do tipo de pessoa que prefere ter seu tempo para fazer as coisas, gosto de ter meu espaço. Não estou dizendo que tenho alergia a pessoas e que ando com um saco de papel enfiado na cabeça. Só estou dizendo que não me importo de ficar trabalhando sozinho em casa, onvindo meu som preferido enquanto faço tudo pela internet. Aliás, isso me dá um gancho pra falar de um livro que li e que imaginei nunca ter a oportunidade de comentar a respeito neste blog técnico. Este livro fala justamente das diferenças entre pessoas extrovertidas e introvertidas. Explica que apesar de 75% das pessoas serem extrovertidas, não há nada errado em ser introvertido. O livro explica que pessoas introvertidas tem lá suas vantagens. Elas se concentram com maior facilidade. Conhecem menos assuntos, mas com mais profundidade. Tem menos amigos, mas são amizades mais intensas. Achei o livro interessante, com suas estatísticas e fundamentos para explicar o por quê do comportamento das pessoas.

    Ainda não falei tudo que poderia falar sobre minha transição para Home Office, mas este post já está ficando longo demais para um Off Topic.
    Até mais…

  • Step into Kernel (Firewire)

    Já sei! Seu computador de teste não tem porta serial e você precisa fazer debug de Kernel nele. Creio que depois da porta serial, a maneira mais utilizada para depurar o Kernel do Windows seja utilizando uma interface firewire. Ainda existe a opção de se fazer o debug de Kernel utilizando USB 2.0, mas isso ainda é para poucos, já que além de apenas ser suportado pelo Windows Vista, ainda é necessário ter um cabo especial. Os detalhes sobre debug de Kernel pela porta USB podem ser encontrados neste post. Hoje a história é outra.

    Mas eu não tenho porta firewire

    Larga a mão de ser chorão. O que importa aqui não é o fato de você ter ou não uma porta firewire em seu micro de desenvolvimento, mas sim o fato da máquina do cliente ter uma porta firewire. Você sabe muito bem que pela Lei de Murphy, aquele problema que você nem sabia que existia só acontece naquela máquina que não tem portas seriais. Então melhor você estar preparado para encontrar esse tipo de coisa. Elas realmente acontecem. Sua reclamação poderia ainda ser diferente: “Mas eu não tenho porta serial”. Alguns notebooks que não possuem portas seriais oferecem portas firewire, mas idependente disso, existem placas tanto PCI quanto PCMCIA capazes que disponibilizar a interface IEEE 1394. Desta forma, seja sua máquina um desktop ou um notebook, existem meios delas ganharem portas firewire.

    Configurando o lado TARGET

    Se você ainda não sabe o que significa lado HOST/TARGET e está completamente perdido sobre o assunto, leia este post introdutório antes de continuar. Configurar o lado TARGET não é muito diferente do que já vimos em outros posts desta série. Podemos editar o aquivo boot.ini, como já vimos neste post para adicionar as seguintes configurações de debug.

    [boot loader]
    timeout=10
    default=multi(0)disk(0)rdisk(0)partition(1)\WINDOWS
    [operating systems]
    multi(0)disk(0)<<...>>/fastdetect /debugport=1394 /channel=44
    multi(0)disk(0)<<...>>/fastdetect

    O número do canal a ser utilizado pode ser qualquer um, mas o valor deve ser o mesmo em ambos os lados TARGET e HOST. Caso você esteja querendo depurar um Windows Vista, o método de configurar as mesmas coisas mudaram um pouco como vimos neste outro post. A figura a seguir mostra os passos para setar o modo de configuração do sistema para interface IEEE 1394.

    Lembre-se que aqui estamos apenas configurando a maneira com a qual o sistema seria depurado caso exista uma entrada de debug na lista de boot da máquina. Este post mostra os detalhes de como criar uma entrada adicional nesta lista e configurá-la para debug.


    Só isso? Nem doeu!

    Para se fazer Kernel Debug utilizando um cabo firewire, ambos os micros devem estar rodando Windows XP ou superior, não necessariamente a mesma versão em ambos os lados. Existe uma particularidade quanto utilizar sistemas anteriores ao Windows XP SP2 ou Windows 2003 Server sem service pack no lado TARGET da história, conforme informa esta página. Para estes sistemas, deve-se desabilitar a controladora do barramento 1394. Isso é necessário porque o Windows, que está sendo depurado inconscientemente, pode querer tentar conversar com a interface Firewire durante o debug, e isso pode fazer com que a conexão com o depurador caia. Para desabilitar essa interface nos sistemas acima citados, você deve simplesmente selecionar o ítem “Disable” no menu de contexto que aparece quando você clica com o botão direito do mouse sobre a controladora firewire.

    Posso desabilitar a controladora Firewire independente da versão do Windows? Não, se você desabilitar a controladora em sistemas posteriores aos acima citados, você poderá não conseguir depurar o sistema quando ele mudar entre os estados de energia do sistema. Estados de energia? Você está falando da aura do computador? Supondo que você está querendo depurar seu driver durante as transições energia do sistema que são gerenciadas pelo Power Manager. O Power Manager determina qual barramento pode ser desligado para economizar energia. Assim, o sistema pode decidir desligar a interface firewire durante seu debug de um driver qualquer, e dessa forma, você não vai conseguir acompanhar as IRPs de gerenciamento de energia chegando ao seu driver.

    Configurando o lado HOST

    Normalmente, para fazer iniciar uma sessão de debug do lado HOST, basta abrir o WinDbg, selecionar o ítem “Kernel Debug…” do menu File, clicar na aba 1394, preencher o número do canal que se deseja utilizar, clicar OK como mostra a figura abaixo e correr para o abraço.


    Tudo isso que acabei de descrever continua valendo, mas para que seja possível utilizar firewire do lado HOST, o WinDbg precisa instalar os drivers virtuais de acesso ao barramento IEEE 1394, como mostra nesta página. O WinDbg faz isso automagicamente quando você seleciona as opções acima, mas os drivers só poderão ser instalados se você estiver logado como administrador do sistema. Caso contrário você receberá a seguinte mensagem.


    Pô Fernando, sou desenvolvedor de driver! Você acha mesmo que não sou administrador da minha máquina? Tudo bem, você pode até ser, mas mesmo sendo um administrador no Windows Vista você precisará executar o WinDbg clicando com o botão direito do mouse sobre o ícone do WinDbg e selecionar o ítem “Run as Administrator”. Aí sim, você repete o procedimento descrito acima para que os drivers virtuais sejam instalados. Esse procedimento é necessário somente na primeira vêz que você utiliza a porta firewire para debug, nas próximas vezes, os drivers já estarão instalados. Para quem estiver utilizando um sistema anterior ao Vista e já estiver utilizando uma conta admininstrativa, é como meu amigo Thiago diz: “Sai na urina”. Ou seja, o driver virtual será instalado e você terá a saída como mostra a figura abaixo, que demonstra as mesmas operações realizadas no Windows Vista rodando o WinDbg como Administrador.


    Daí em diante é só debug mesmo.

    Dump Racing

    Aproveitando que estamos todos aqui reunidos, vamos fazer um teste e verificar se a velocidade do firewire ajuda mesmo com relacão à fazer Kernel Debug. Vamos imaginar a situação onde você esteja em visita a um cliente onde obviamente seu driver não está funcionando adequadamente. Lembra daquele bug que você nem sabia que existia? Pois bem, se trata de um deadlock. Deadlock são especialmente queridos na hora de fazer debug, porque você está lá quando o problema acontece, mas tela azul que é bom nada. Nessa situação, você pode gerar um arquivo de dump da máquina e deixar para analizar o problema em casa, afinal de contas, roupa suja se lava em casa, e assim poder liberar a máquina do cliente para uso, já que normalmente nessas situações, ficam umas três pessoas em cima de você perguntando “E aí? Descobriu o problema?” a cada 3 minutos. Configurei meu desktop aqui de casa para fazer debug do Windows Vista por uma porta serial. Esta máquina tem 2GB de memória RAM. Um arquivo de dump full é uma cópia de tudo que esta na memória do computador naquele instante, por isso, nada mais justo que este arquivo tenha aproximadamente 2GB de tamanho. Este arquivo pode ser gerado na máquina HOST durante uma sessão de debug utilizando o comando .dump. Segue abaixo a saída deste comando quando utilizado sobre uma conexão serial.

    0: kd> .dump /f c:\Temp\SERIAL.DMP
    Creating a full kernel dump over the COM port is a VERY VERY slow operation.
    This command may take many HOURS to complete.  Ctrl-C if you want to terminate the command.
    Creating c:\Temp\SERIAL.DMP - Full kernel dump
    Percent written 0
    Percent written 1
    Percent written 2
            :
            :

    Vocês repararam na mensagem ameaçadora que nos foi exibida? Particularmente penso que estes engenheiros de software são todos uns desesperados, provavelmente por causa quantidade de café que eles consomem por dia. Já posso até imaginar quantos nem esperam o dump começar para já pressionar CTRL+C e interromper o processo. São uns covardes mesmo. Bom, já que isso vai me custar algum tempo, vou aproveitar para dar uma mijada.

    Muito, mas muito tempo mesmo depois…

            :
            :
    Percent written 97
    Percent written 98
    Percent written 99
    Dump successfully written
    0: kd>

    OK, tudo bem até aqui. Agora vamos repetir o processo em uma sessão de debug utilizando o cabo firewire. A mesma máquina com a mesma quantidade de memória e até o mesmo comando.

    1: kd> .dump /f c:\Temp\FIREWIRE.DMP
    Creating c:\Temp\FIREWIRE.DMP - Full kernel dump
    Percent written 0
    Percent written 5
    Percent written 10
            :
            :
    Percent written 90
    Percent written 95
    Dump successfully written
    1: kd>

    Tá, tudo bem, a contagem vai de cinco em cinco ao invés de um em um como foi com o cabo serial, grande coisa. Não é a toa que demore mais pela porta serial, eles gastam processamento com tudo. Agora podemos comparar as datas de criação e modificação nos atributos de cada arquivo para poder determinar quanto tempo levou para poder gerá-los.


    Lembrando que o mês de junho termina no dia 30, podemos concluir que o dump pela porta serial levou aproximadamente 2 dias, 5 horas e 12 minutos. É uma pena essa janela não mostrar os segundos para termos mais precisão aqui. De qualquer forma, lembrando também que cada minuto tem 60 segundos, o mesmo dump gerado pela porta firewire levou aproximadamente 7 minutos. Nossa, foi quase! Se não fosse por essa pequena vantagem de 3187 minutos.

    Até mais…