Escrevendo Filtros
Vamos brincar de algo mais interessante agora. Obviamente ainda vamos dar passos pequenos para não nos perder com tantos detalhes. Hoje falarei sobre filtros de drivers. O IoManager do Windows nos permite adicionar funcionalidade a determinados drivers sem que tenhamos que substituí-lo. Um exemplo clássico seria um driver de criptografia de arquivos. Você não precisa escrever um novo driver de file system para que se tenha tal funcionalidade. Você pode simplesmente escrever um filtro que ficaria entre o driver de file system e o restante do sistema. Na figura abaixo podemos observar o fluxo de IRPs que vão do IoManager para um certo driver, um filtro é instalado sobre o driver existente e passa a receber as IRPs no lugar do driver original. Com isso o filtro tem a oportunidade de alterar os parâmetros das IRPs recebidas, ou logar a atividade do driver original, ou duplicar solicitações do sistema para esse driver, ou mesmo negar serviço do driver original.
Numa operação de escrita, um filtro poderia criptografar os dados antes de enviá-los ao driver original, e de maneira similar, numa operação de leitura o filtro poderia decriptografar dados antes de entregá-los ao sistema. Ainda não vai ser desta vez que construiremos um filtro de criptografia de file system. Filtros de criptografia de arquivos em tempo real estão entre os drivers mais complexos a serem escritos. Vamos escolher um driver mais simples, pra não dizer um bem besta, para aplicar os conceitos básicos demonstrados aqui. Por falar em driver besta, vamos utilizar o driver deste post que já foi visto aqui. Este driver simplesmente armazena uma lista de strings enviadas por uma aplicação em operações de escrita. Tais strings são retornadas à aplicação em operações de leituras.
Escrevendo a DriverEntry Como vimos neste outro post, uma das das coisas que a função DriverEntry() faz num driver comum é setar as Dispatch Routines que o driver vai atender para certo dispositivo. Para isso deve-se preencher o array de Major Functions que fica na estrutura DRIVER_OBJECT. //-f--> Seta as dispatch routines do driver. pDriverObj->MajorFunction[IRP_MJ_CREATE] = OnCreate; pDriverObj->MajorFunction[IRP_MJ_CLEANUP] = OnCleanup; pDriverObj->MajorFunction[IRP_MJ_CLOSE] = OnClose; pDriverObj->MajorFunction[IRP_MJ_READ] = OnRead; pDriverObj->MajorFunction[IRP_MJ_WRITE] = OnWrite;
No caso do nosso filtro de exemplo, queremos apenas monitorar a atividade do driver ao qual estamos atachados, dessa forma teremos sempre que encaminhar quaisquer IRPs recebidas ao driver de baixo. Uma forma simples e comum de fazer isso é setar todas dispath routines para uma única função. Essa função se encarrega de simplesmente logar a solicitação recebida e passá-la a diante. Se dermos uma olhada na definicão de IRP_MJ_CREATE e seus amigos, veremos o seguinte trecho do arquivo wdm.h. // // Define the major function codes for IRPs. // #define IRP_MJ_CREATE 0x00 #define IRP_MJ_CREATE_NAMED_PIPE 0x01 #define IRP_MJ_CLOSE 0x02 #define IRP_MJ_READ 0x03 #define IRP_MJ_WRITE 0x04 #define IRP_MJ_QUERY_INFORMATION 0x05 #define IRP_MJ_SET_INFORMATION 0x06 #define IRP_MJ_QUERY_EA 0x07 #define IRP_MJ_SET_EA 0x08 #define IRP_MJ_FLUSH_BUFFERS 0x09 #define IRP_MJ_QUERY_VOLUME_INFORMATION 0x0a #define IRP_MJ_SET_VOLUME_INFORMATION 0x0b #define IRP_MJ_DIRECTORY_CONTROL 0x0c #define IRP_MJ_FILE_SYSTEM_CONTROL 0x0d #define IRP_MJ_DEVICE_CONTROL 0x0e #define IRP_MJ_INTERNAL_DEVICE_CONTROL 0x0f #define IRP_MJ_SHUTDOWN 0x10 #define IRP_MJ_LOCK_CONTROL 0x11 #define IRP_MJ_CLEANUP 0x12 #define IRP_MJ_CREATE_MAILSLOT 0x13 #define IRP_MJ_QUERY_SECURITY 0x14 #define IRP_MJ_SET_SECURITY 0x15 #define IRP_MJ_POWER 0x16 #define IRP_MJ_SYSTEM_CONTROL 0x17 #define IRP_MJ_DEVICE_CHANGE 0x18 #define IRP_MJ_QUERY_QUOTA 0x19 #define IRP_MJ_SET_QUOTA 0x1a #define IRP_MJ_PNP 0x1b #define IRP_MJ_PNP_POWER IRP_MJ_PNP // Obsolete.... #define IRP_MJ_MAXIMUM_FUNCTION 0x1b
Note que existe uma definição especial, a IRP_MJ_MAXIMUM_FUNCTION, que indica o índice máximo da tabela de dispatch rotines. Utilizaremos um loop simples para fazer com que todas rotinas em nossa tabela aponte para uma única rotina que daremos o nome de OnForwardDispatch. //-f--> Seta todas as dispatch routines do driver para // uma que encaminhe a IRP para o driver original. for (i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++) pDriverObj->MajorFunction[i] = OnForwardDispatch;
Veremos a implementação dessa rotina mais tarde. O próximo passo que daremos aqui é localizar o device ao qual vamos nos atachar. Para fazer isso, usaremos a rotina IoGetDeviceObjectPointer(). Ela basicamente recebe o nome do device e nos retorna uma referência para ele. NTSTATUS IoGetDeviceObjectPointer( IN PUNICODE_STRING ObjectName, IN ACCESS_MASK DesiredAccess, OUT PFILE_OBJECT *FileObject, OUT PDEVICE_OBJECT *DeviceObject );
Observe que são dois os parâmetros de saída dessa rotina. Além do ponteiro para o device object ainda recebemos um ponteiro para um file object. Já ví algumas pessoas fazerem confusão com estes dois parâmetros, então vou dar alguma ênfase nisso. O ponteiro para o file object representa uma conexão criada entre seu driver e o device que você abriu. Como vimos neste post, um file object é criado para respresentar conexões entre aplicações user mode e seu driver. Aplicações usam esse file object através do handle obtido na chamada à rotina CreateFile(). Aqui temos algo similar, mas apenas o Kernel foi envolvido. Isso significa que se você quizesse, você poderia lançar IRPs para o device solicitando operações como uma aplicação faria, mas não veremos isso hoje, ainda temos um filtro para terminar. A grande confusão referente a estes dois parâmetros é com relação às referências entre os objetos. Na documentação vemos que o chamador desta rotina deverá liberar a referência que ganhou quando o device não for mais utilizado. Fazemos isso simplesmente usando a rotina ObDereferenceObject(). VOID ObDereferenceObject( IN PVOID Object );
"Já sei! Como estamos obtendo uma referência para um device object, então devo passar o ponteiro do device object. Certo?"
Er... Na verdade não. O file object é uma referência indireta ao device object. Conforme a figura abaixo, se imaginarmos que os contadores de referência apenas dizem respeito às nossas referências, quando chamarmos ObDereferenceObject() para o file object, seu contador de referência cairia para zero e uma nova chamada para ObDeferenceObject() seria feita indiretamente para o device object, fazendo com que seu contador de referência também caisse para zero destruindo o objeto. Depois de obter o ponteiro para o device destino, teremos que criar nosso próprio device, o qual receberá as IRPs no lugar do device original. Para isso ainda usaremos a rotina IoCreateDevice() como faziamos com drivers, mas com algumas diferenças. A primeira diferença é que seu device normalmente não tem nome. É possível criar filtros com nomes, mas isso pode gerar uma falha de segurança. Isso acontece pois quando um nome é consultado no Object Manager, suas regras de segurança são avaliadas. Quando criamos um filtro com nome, criamos a possibilidade de o mesmo objeto ser obtido por um nome diferente e que pode ter regras menos restritivas de segurança. Mas esse é outro assunto. Quando criamos um device, precisamos informar o tamanho do device extension. "O que vem a ser um device extension?"
Device extension é simplesmente um espaço de memória que está associada ao device object. Tal espaço normalmente mantém informações que dizem respeito ao device. O endereço do device ao qual estamos atachados normalmente fica no device extension. Desta forma, podemos definir que nosso device extension deve conter a seguinte estrutura. //-f--> Nosso device externsion conterá apenas o endereço // do device ao qual estamos atachados. typedef struct _DEVICE_EXTENSION { PDEVICE_OBJECT pNextDeviceObj; } DEVICE_EXTENSION, *PDEVICE_EXTENSION;
Depois de criar nosso device object, configuramos o método de I/O copiando os bits DO_BUFFERED_IO e DO_DIRECT_IO. Se você não se lembra destes bits, dê uma olhada neste post. O filtro deve propagar a escolha do driver original e o driver tem o compromisso de não mudar método durante seu tempo de vida. Agora estamos prontos para nos atachar ao device escolhido, e faremos isso utilizando a rotina IoAttachDeviceToDeviceStackSafe() para fazer nosso device entrar na pilha de dispositivos. NTSTATUS IoAttachDeviceToDeviceStackSafe( IN PDEVICE_OBJECT SourceDevice, IN PDEVICE_OBJECT TargetDevice, IN OUT PDEVICE_OBJECT *AttachedToDeviceObject );
Com esta chamada obteremos o ponteiro do device ao qual estaremos atachados, esse device será o próximo device que receberá a IRP depois que você a passar a diante. "Mas Fernando, nós já não temos o ponteiro do device de destino?"
Muito bem, quando você obtém o ponteiro para um device, você teoricamente não sabe se existem filtros já atachados sobre ele. O ponteiro que você recebe nesta rotina pode não ser o ponteiro para o device de destino. Na figura abaixo podemos entender como essa relação acontece.
"Fernando, existe uma versão unsafe desta rotina?"
Na verdade existe, a IoAttachDeviceToDeviceStack(). PDEVICE_OBJECT IoAttachDeviceToDeviceStack( IN PDEVICE_OBJECT SourceDevice, IN PDEVICE_OBJECT TargetDevice );
Ela é considerada unsafe por causa de uma pequena janela de tempo que pode causar um race condition. Repare que a diferença entre estas rotinas é a forma de obter o endereço do próximo device. Na versão original, este endereço é obtido pelo retorno na função. Se colocarmos essa função para rodar em câmera lenta veremos os seguintes passos. - Seu device é atachado à pilha de dispositivos.
- O endereço do próximo device é retornado pela função.
- Seu driver recebe este endereço no retorno da função e atualiza o device extension.
- IRPs começam a chegar e seu driver às encaminham para o próximo device da lista.
Tudo parece lindo e até dá a impressão de que tudo vai funcionar muito bem em qualquer situação, mas desenvolvedor de driver é um bixo treinado para buscar race conditions. Dê mais uma olhada na sequência, mas agora em câmera super lenta. Com essa câmera super lenta agora podemos observar os passos que podem ocorrer entre os passos 2 e 3. - Seu device é atachado à lista de dispositivos.
- O endereço do próximo device é retornado pela função.
- Sua thread é interrompida e uma IRP lançada por uma aplicação que roda em paralelo começa sua viagem por essa pilha de dispositivos.
- Seu device, que já está atachado, recebe a IRP e tenta encaminhá-la ao device de baixo.
- Oops! Nosso device extension ainda não foi atualizado com tal endereço.
- Seu driver se lembra de quando era uma criança e de tudo o que vivera até ali.
- Ele decide entrar de vez naquela dança e enviar a IRP para um device cujo ponteiro ainda é NULL causando um BSOD.
- "Jeremias, eu sou homem. Coisa que você não é e não atiro pelas costas não..."
Enfim, entenda que mesmo que você utilize o retorno da rotina IoAttachDeviceToDeviceStack() diretamente para atualizar seu device extension, ainda assim existe uma janela de tempo em que seu device estará atachado, mas que o valor ainda não foi atualizado no device extension. Isso porque o valor de retorno de uma rotina vem por um registrador. Pegar o valor desse registrador e atualizar uma variável ainda dá chances para o azar. //-f--> ----==== NÃO COPIE ISSO ====---- // Aqui ainda temos uma janela de tempo entre o device ser // atachado e o valor de pNextDeviceObj ser atualizado. pDeviceExt->pNextDeviceObj = IoAttachDeviceToDeviceStack(pMyDeviceObj, pTargetDeviceObj);
A rotina IoAttachDeviceToDeviceStackSafe() faz o sistema interromper o fluxo de IRPs nesta pilha até que a variável apontada pelo ponteiro de saída seja atualizado. Por essa razão, o endereço passado nesta rotina deve ser o endereço final da variável onde este valor será armazenado, que em nosso caso é pDeviceExt->pNextDeviceObj sem passar por variáveis intermediárias.  Esses detalhes são importantes e farão você entender que usar a versão safe desta rotina não é garantia de que tudo dará certo. Imagine que usando a versão safe você receba o endereço do próximo device em uma variável local e depois atualize seu device extension. Esse é um daqueles típicos casos que é necessário substituir aquele componente que fica entre o teclado e a cadeira. Acha preciosismo? Tente ler o capítulo 5 do livro "Programming the Microsoft Windows Driver Model" onde Walter Oney fala sobre como lidar com cancelamento de IRPs. Depois de atacharmos nosso device já podemos liberar a referência obtida por IoGetDeviceObjectPointer() utilizando a rotina ObDereferenceObject(), já que a rotina IoAttachDeviceToDeviceStackSafe() já garantiu a referência até que essa ligação seja desfeita. Muito bem. Pra quem não conseguiu entender quase nada do que eu disse, segue o código fonte da implementação da nossa DriverEntry() de exemplo. Sabe como é cabeça de programador, as vezes um if vale mais que mil páginas de explicação. /**** *** DriverEntry ** ** Ponto de entrada do driver. ** Bem vindo ao inferno. */ extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObj, IN PUNICODE_STRING pusRegistryPath) { NTSTATUS nts; PDEVICE_OBJECT pMyDeviceObj; int i; UNICODE_STRING usDeviceName; PDEVICE_OBJECT pTargetDeviceObj; PFILE_OBJECT pFileObj; PDEVICE_EXTENSION pDeviceExt; //-f--> Seta a rotina de descarga do driver. pDriverObj->DriverUnload = OnDriverUnload; //-f--> Seta todas as dispatch routines do driver para // uma que encaminhe a IRP para o driver original. for (i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++) pDriverObj->MajorFunction[i] = OnForwardDispatch; //-f--> Inicializamos a string com o nome do device ao // qual queremos nos atachar. RtlInitUnicodeString(&usDeviceName, L"\\Device\\StringList"); //-f--> Obtemos o ponteito do device de destino nts = IoGetDeviceObjectPointer(&usDeviceName, FILE_READ_DATA, &pFileObj, &pTargetDeviceObj); if (!NT_SUCCESS(nts)) return nts; //-f--> Criamos nosso device object nts = IoCreateDevice(pDriverObj, sizeof(DEVICE_EXTENSION), NULL, pTargetDeviceObj->DeviceType, pTargetDeviceObj->Characteristics, FALSE, &pMyDeviceObj); if (!NT_SUCCESS(nts)) { //-f--> Oops! ObDereferenceObject(pFileObj); return nts; } //-f--> Obtem nosso DEVICE_EXTENSION pDeviceExt = (PDEVICE_EXTENSION)pMyDeviceObj->DeviceExtension; //-f--> Utiliza o mesmo método de IO do driver original pMyDeviceObj->Flags |= pTargetDeviceObj->Flags & (DO_BUFFERED_IO | DO_DIRECT_IO); //-f--> Aqui nosso driver entra na pilha de dispositivos nts = IoAttachDeviceToDeviceStackSafe(pMyDeviceObj, pTargetDeviceObj, &pDeviceExt->pNextDeviceObj); if (!NT_SUCCESS(nts)) { //-f--> Oops! IoDeleteDevice(pMyDeviceObj); //-f--> Aqui não está faltando um return. } //-f--> Independente de estarmos atachados ou não, devemos liberar // a referência obtida do device de destino. ObDereferenceObject(pFileObj); return nts; }
Escrevendo a OnDriverUnload Aqui é onde a festa acaba, antes de o nosso driver ser descarregado pelo sistema, teremos que remover nosso device da pilha de dispositivos e destruí-lo. (Risadas maléficas) O código aqui é simples e não requer explicações se você for capaz de ler os comentários contidos nele. /**** *** OnDriverUnload ** ** Rotina de descarga do driver. */ VOID OnDriverUnload(IN PDRIVER_OBJECT pDriverObj) { PDEVICE_OBJECT pMyDeviceObj; PDEVICE_EXTENSION pDeviceExt; //-f--> Nosso device está na lista de devices criados por este driver // então vamos obtê-lo simples assim: pMyDeviceObj = pDriverObj->DeviceObject; //-f--> Aqui obtêmos o device extension. pDeviceExt = (PDEVICE_EXTENSION)pMyDeviceObj->DeviceExtension; //-f--> Aqui removemos nosso device da pilha passando o endereço do // próximo device para a rotina abaixo. IoDetachDevice(pDeviceExt->pNextDeviceObj); //-f--> Agora podemos destruir nosso device (risadas maléficas) IoDeleteDevice(pMyDeviceObj); }
"Fernando, nosso driver é forçado a se descarregar quando o driver original é descarregado?"
Essa é a filosofia do WDM, drivers são carregados automáticamente quandos seus dispositivos são detectados e descarregados quando seus dispositivos são desativados ou removidos. Os filtros seguem as mesmas regras e são carregados/descarregados basendo-se nestes eventos. Mas não é isso o que acontece aqui. Os drivers de exemplo que uso neste blog são do modelo Legacy, e não WDM. No modelo Legacy, drivers são iniciados seguindo sua ordem de carga no registry, e não tem nada a ver com a detecção do seu dispositivo. Os filtros precisam iniciar depois dos drivers originais, e isso também é controlado pela sua ordem de carga. Este post fala sobre a ordem de carga dos legacy drivers. "Tá! Falou, falou e não respondeu minha pergunta. O que acontece se eu solicitar a descarga do driver original enquanto houver um filtro atachado sobre ele?"
A descarga dos drivers que formam uma pilha deve ocorrer de forma inversa à sua carga. Neste caso o filtro deve ser descarregado antes do driver original, desempilhando os devices de cima para baixo. Caso o driver original receba uma solicitação de descarga enquanto ainda hoverem referências a seus devices, seja por um filtro ou por uma aplicação, a descarga é adiada até que suas referências sejam desfeitas. Até lá, a tentativa de obter novas referências para um device que recebeu a solicitação de descarga serão negadas.
Escrevendo Dispatch Routines As dispatch routines de um filtro também são diferentes das dispatch routines de um driver. Apensar de elas poderem completar uma IRP chamando IoCompleteRequest(), elas normalmente repassam a socilitação adiante utilizando a rotina IoCallDriver(). Vou falar mais sobre o comportamento das dispatch routines de um filtro em posts futuros. Neste filtro de exemplo vamos apenas logar a atividade e repassar a solicitação ao próximo driver. Quando falamos em repassar uma solicitação estamos na verdade falando de repassar IRPs. A leitura deste outro post é essencial para o que vamos fazer na implementação de nossas dispatch routines. Para acabar esse post ainda nessa vida, segue código da implementação de nossa dispatch routine. Depois de ler o post sobre IRPs que acabei de recomendar, ler os comentários deste código devem ser suficientes para entender tudo o que foi feito aqui, ou não. /**** *** OnForwardDispatch ** ** Nossa dispatch routine simplesmente loga a IRP ** recebida e repassa a solicitação adiante. ** */ NTSTATUS OnForwardDispatch(IN PDEVICE_OBJECT pDeviceObj, IN PIRP pIrp) { PDEVICE_EXTENSION pDeviceExt; PIO_STACK_LOCATION pStack; //-f--> Obtém ponteiro para o device extesion pDeviceExt = (PDEVICE_EXTENSION)pDeviceObj->DeviceExtension; //-f--> Obtém stack location corrente pStack = IoGetCurrentIrpStackLocation(pIrp); //-f--> Manda o nome na major routine da IRP ASSERT(pStack->MajorFunction <= IRP_MJ_MAXIMUM_FUNCTION); DbgPrint("[StringFilter] : %s\n", g_szMajorNames[pStack->MajorFunction]); //-f--> Como não estamos modificando nada na stack location, // vamos deixá-la para que o próximo device a use. IoSkipCurrentIrpStackLocation(pIrp); //-f--> Encaminha a IRP para o próximo device. return IoCallDriver(pDeviceExt->pNextDeviceObj, pIrp); }
Caso não tenham entendido nada, não esqueçam de me mandar um e-mail expondo suas dúvidas (sem ofenças pessoais). Isso me ajudará a entender suas dificuldades e a melhorar minhas explicações.
Testando o Filtro Essa é a parte fácil do post. Para testar o filtro teremos primeiro que compilar, instalar e iniciar o driver deste post. Se você ainda não sabe como fazer isso, este outro post pode te dar um ponto de partida. Depois disso, compile, instale e inicie o filtro. Depois de instalados, podemos utilizar a aplicação de teste para exercitar o driver. Poderemos acompanhar a atividade do filtro observando suas mensagens de debug que podem ser vistas no depurador de Kernel ou simplemente usando esta aplicação, que dispensa o uso de um depurador para ver as mensagens de debug de um driver. "Fernando, fiz um teste aqui e ví que ao iniciar o filtro ele loga um evento de IRP_MJ_CLOSE mesmo antes de iniciar a aplicação de teste. O que eu fiz de errado?"
Não há nada de errado. Isso acontece por causa da sequência de passos seguidos na rotina DriverEntry() do filtro. Inicialmente o driver chama a rotina IoGetDeviceObjectPointer(), isso faz com que o IoManager envie uma solicitação de IRP_MJ_CREATE para o driver original. Depois disso nos atachamos à pilha de dispositivos e por fim chamamos a rotina ObDereferenceObject() que vai finalizar a única referência do file object que recebemos, enviando uma solicitação de IRP_MJ_CLOSE para o driver de baixo. Como já estamos atachados a ele, então somos capazes de ver nossa própria atividade sobre o driver original. Isso pode ser observado pela pilha de chamadas que teremos se houver um breakpoint em nossa dispatch routine quando liberarmos a referência do file object ao final da DriverEntry(). kd> k ChildEBP RetAddr f8af9bcc 804ee129 StringFilter!OnForwardDispatch f8af9bdc 80578f6a nt!IopfCallDriver+0x31 f8af9c14 805b0b18 nt!IopDeleteFile+0x132 f8af9c30 80522bd1 nt!ObpRemoveObjectRoutine+0xe0 f8af9c54 f8c80663 nt!ObfDereferenceObject+0x5f f8af9c7c 805767ff StringFilter!DriverEntry+0xf3 f8af9d4c 8057690f nt!IopLoadDriver+0x66d f8af9d74 80534c12 nt!IopLoadUnloadDriver+0x45 f8af9dac 805c61ee nt!ExpWorkerThread+0x100 f8af9ddc 80541de2 nt!PspSystemThreadStartup+0x34 00000000 00000000 nt!KiThreadStartup+0x16
Como de costume, o fonte do filtro que foi implementado neste post está disponível para download. Nosso filtro não faz quase nada, mas já servirá de base para posts futuros que darão mais funcionalidade a ele explicando como tais funcionalidades são implementadas. Até mais! Download StringFilter.zip
Drivers de Boot no Windows
 Tenho acompanhado o trabalho do meu amigo Lesma, que em seu blog tem descrito como o processo de boot transforma um apanhado de bytes no disco rígido em um sistema operacional vivo. Pegando carona nesse tema, vou aproveitar para comentar sobre a ordem de carga dos drivers durante este processo. Com isso posso tentar responder uma pergunta frequente dos leitores: "Como fazer para que meu driver seja o primeiro a ser carregado?". Talvez este post possa clarear um pouco as coisas neste sentido, ou não. Eu primeiro! Eu primeiro!
Um ponto importante a ser considerado no modelo Legacy quando escrevemos um driver é o referente ao momento no qual seu driver é carregado. Isso é configurado no valor "Start" na chave do driver no registro. Quatro valores configuram o momento da carga do seu driver, sendo eles: Boot (0) - Drivers são carregados durante o boot, antes mesmo sistema operacional estar completamente pronto para execução. System (1) - Drivers são carregados depois dos drivers de boot, quando o Kernel já está completamente funcional. Automatic (2) - Neste grupo os drivers são carregados quando os subsistemas forem carregados. Basicamente junto com os serviços de User Mode. Manual (3) - Nenhuma carga automática é realizada aqui, o driver é carregado somente quando alguém, ou algum componente, solicita sua carga. Disabled (4)- Mesmo que o driver seja solicitado, sua carga é negada.
"Bom, então para meu driver ser o primeiro a ser carregado ele precisa ser iniciado como boot e pronto?" Na verdade seu driver vai disputar um lugar na fila de drivers que querem ser iniciados no boot. Vários drivers estão configurados para ser iniciados nesse momento e o seu será apenas mais um. Mesmo entre os drivers de boot, uma ordem de carga precisa ser seguida para que certos drivers possam contar com os serviços de outros drivers. Por esse motivo, drivers se separam em grupos. Um grupo de cada vez vai sendo iniciado até que todos os drivers de boot passem por esse processo. Drivers identificam seu grupo pelo valor "Group" encontrado em sua chave de registro. Esta chave deve conter o nome do grupo ao qual o driver pertence. Os nomes de todos os grupos podem ser encontrados na chave HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ServiceGroupOrder. Nela existe um valor do tipo REG_MULTI_SZ chamado "List" que contém a lista de todos os grupos existentes dispostos em sua ordem de carga. "Tudo bem, meu driver está configurado para ser iniciado em Boot e está configurado para iniciar com o primeiro grupo de drivers. Pronto agora?" Quase. Quando falamos em iniciar grupos de drivers, já fica sub-entendido que mais de um driver será carregado. A ordem que tais drivers são carregados dentro de cada grupo também pode ser determinada. A chave KHEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\GroupOrderList mantém uma série de valores, cada um com o nome de um grupo. O valor é do tipo REG_BINARY e sua interpretação é um array de conjuntos de quatro bytes. O primeiro conjunto indica quantas tags estão contidas naquele buffer binário. Os demais conjuntos são as representações numéricas de cada tag. Dessa forma, a interpretação do buffer exibido na figura abaixo nos dá a informação de que temos seis tags, sendo elas: 1, 2, 3, 4, 5 e 6. "Mas o que é uma tag?" Uma tag é a identificação numérica de um driver dentro de um determinado grupo. Um driver se identifica pelo valor "Tag" que podemos encontrar na chave do driver no registro. Apesar de o exemplo nos mostrar uma ordem crescente de tags, o valor da tag não determina a ordem de carga dos drivers. A ordem é determinada por sua posição dentro do buffer binário. Ficou com nojinho de mexer em buffers binários? Você pode utilizar o OSR Driver Loader que configura tudo isso pra você quando utilizado para instalar um driver. Colocar sua tag como primeiro na lista de tags classifica sua ordem de carga dentro de um determinado grupo, mas ainda não é fator determinante para ter seu driver carregado antes de todos os drivers do universo. Um novo grupo sempre pode ser criado e ter sua ordem de carga configurada para antes do seu grupo. Todas essas regras sobre grupos, tags e afins não fazem sentido nos drivers gerenciados pelo Plug-And-Play (Pnp) Manager, já que a carga de tais drivers é solicitada quando o dispositivo ao qual seu driver está relacionado é detectado pelo driver de barramento. "Aff! Fernando, pega leve e tenta explicar isso de novo." Tudo bem, vamos lá. Quando você instala um driver Pnp, você o associa a um determinado dispositivo. Apenas como exemplo, digamos que esse dispositivo seja um conversor USB/Serial. Seu driver será carregado automagicamente quando seu dispositivo for detectado e será descarregado quando o dispositivo for removido. Para que ele seja detectado, outros dispositivos precisam ser detectados antes, tais como controladora PCI, controladora USB e hub USB. Essa lista de dependência cria a pilha de dispositivos USB. A controladora PCI, ao ser detectada, tem seu driver carregado e este enumera seus dispositivos filhos, já que PCI é um barramento. Para cada disposivito detectado, esse driver utiliza o barramento para detectar a identidade de cada dispositivo e cria um Phisical Device Object (PDO) para cada um deles. O Pnp Manager carrega o driver de cada dispositivo atachado a esse barramento. Esse driver criará o Functional Device Object (FDO) do dispositivo, dando funcionalidade a ele. Um desses dispositivos é a controladora do barramento USB. Seguindo o ritual, o driver de barramento USB enumera seus dispositivos filhos, criando novos PDOs. Assim, os hubs USB são detectados e seu driver será carregado. Este driver criará um novo FDO para cada hub. O driver de hub USB vai enumerar seus dispositivos filhos e é nesse momento que seu dispositivo é detectado. O driver que você escreveu será carregado e o Pnp Manager irá chamar sua rotina AddDevice, que receberá o PDO que o driver de hub criou referente ao seu dispositivo. Ufa! Tudo bem, tenham calma. O assunto Plug-And-Play não é o foco deste post e já está na minha lista de posts futuros. Toda essa atividade que age recursivamente serve para montar a árvore de dispositivos do sistema. Sabendo que esta árvore é formada pelos nossos drivers e seus devices, fica explícito aqui que no fundo "As árveres somos nozes". A figura abaixo dá uma idéia de como a árvore de dispositivos é organizada. Ainda falando sobre ordem de carga de drivers, não faria sentido ter seu driver carregado antes de todos os outros drivers, já que os componentes básicos para a comunicação com seu dispositivo ainda não foram carregados, e por isso, não possuem funcionalidade nenhuma. Além do mais, ter seu driver carregado muito cedo lhe trará problemas em lidar com outros componentes do sistema que ainda não estarão prontos para atender seu pedido. Mais detalhes neste post. Depurando no Boot
Outro assunto curioso e que pode gerar alguma confusão é o referente ao debug de drivers que são carregados no boot. Apesar de a conexão de Debug usar o meio serial, firewire ou mesmo USB, os drivers referentes a estes meios não precisam estar carregados para que você possa realizar o debug do sistema. Em outras palavras, o driver de porta serial não é utilizado para fazer debug do sistema quando se usa o meio serial. Isso seria um problema se pensarmos que alguns drivers são carregados e inciados antes do driver de porta serial. Como estes drivers seriam depurados? O fato é que o algoritmo que lida com os meios de depuração do sistema são definidos no próprio Kernel (mais precisamente no módulo ntoskrnl.exe e seus irmãos). Este módulo lida diretamente com o hardware responsável pelo meio utilizado. Essa é também a explicação para outra pergunta frequente: "Meu computador não tem porta serial. Posso usar um conversor USB/Serial do lado Target para fazer debug do sistema?". Como acabamos de ver, um conversor USB/Serial depende de toda uma pilha de dispositivos para que a porta serial esteja disponível. Tal funcionalidade não está implementada no algoritmo de debug do sistema, e como comentei neste outro post, sistemas mais novos implementam novas funcionalidades de debug no Kernel. Mesmo que seu driver seja de boot, ele ainda pode ser depurado. Este outro post mostra ainda como fazer mapeamento de um driver de boot pelo WinDbg. Não sabe do que estou falando? É sobre ter seu driver substituído por uma nova versão automaticamente no lado target quando este for carregado. Vale a pena dar uma olhada. Have fun!
Mais um Engenheiro à solta
 Ufa! Quarta-feira passada foi minha última prova da universidade. Parece mentira mas finalmente o curso de Engenharia da Computação chegou ao fim. Ainda temos que entregar uma papelada do projeto de conclusão de curso, mas são apenas papéis. Acabaram as provas, os trabalhos, os relatórios e agora um baita peso começa a sair das minhas costas à medida que as notas vão sendo liberadas no site da universidade. Quem me segue no Twitter teve uma idéia da correria que tem sido. Esse ano foi especialmente apertado por conta das dependêcias que tive que cursar, do estágio que tive que fazer, e por fim, do projeto de formatura que tivemos que apresentar. O projeto foi o maior responsável pela minha ausência no blog, foram muitos testes, sustos, sensores queimados, apresentações e feiras. Pera aí! Você disse sensores queimados? Isso mesmo. Murphy pode ser considerado um integrante do nosso grupo já que esteve sempre presente no desenvolvimento do projeto. Nas vésperas da apresentação para a banca avaliadora, um mal contato nos reguladores de tensão, originado pela trepidação do motor do helimodelo, fez com que quase tudo se queimasse. Foi um desespero geral, já que os sensores eram importados. Sem falar da grana que teríamos que desembolsar, ainda teríamos que aguardar a entrega que ocorre em média em duas semanas. Nesse momento Murphy nos deu uma trégua. Particularmente acho que ele ficou com pena de nós. Digo isso porque no dia seguinte encontramos uma revenda de sensores similares aos que estávamos usando no projeto aqui em Santo André (cidade onde moro). Quando entramos em contato com a revenda, descobrimos que eles tinham os sensores em estoque. Inacreditável não? Depois da apresentação que fizemos para a banca avaliadora, ainda fomos convidados a participar da apresentação de gala da universidade, onde os melhores projetos do ano foram exibidos. Também ficamos em segundo lugar no concurso "Melhor Aplicação Acadêmica Baseada em PC" promovido pela National Instruments, e com isso fomos convidados a expor nosso trabalho no stand da National no ISA Show 2009. O mais legal é que o primeiro lugar deste concurso foi conquistado por um grupo também da minha sala na Universidade São Judas Tadeu. Isso evidenciou que além de ficarmos na frente de várias outras universidades, mostrou que nossa sala né brinquedo não. A National Instruments produziu este vídeo explicativo onde eu falo sobre a idéia básica do projeto. O vídeo foi feito na chácara de um dos componentes do grupo, onde ficamos internados nas últimas semanas antes das apresentações finais. Este é apenas um post de "Ei, estou voltando do coma!". Agora vou parar de falar sobre o projeto e voltar a falar sobre o que realmente interessa: "Telas Azuis da Morte". O meu grande dilema agora é se devo também comentar algo sobre firmware e hardware. Desenvolvi muitas atividades com software embarcado não só durante a produção do projeto. Durante o projeto, sobretudo desenvolvendo protocolo USB em um 8051 utilizando o Keil, além de ler sensores e trabalhar com barramentos em um PIC utilizando o compilador C18 no MPLAB. Além disso, tive a oportunidade de contribuir no desenvolvimento de um projeto do IPEI. O hardware deste projeto mede os resultados de uma máquina de tração e os envia a um computador via USB. Contribui no desenvolvimento do firmware PIC implementando o protocolo HID mais uma vez utilizando o MPLAB. Por último e não menos importante, também contribui no desenvolvimento de firmware FreeScale durante meu estágio na Commodity. Este firmware também lida com protocolo USB, e conta com o auxílio de um chip que dispoê desta interface, já que o DSP que compunha a solução não o fazia. Desta vez tive que utilizar o Code Warrior no desenvolvimento, e revivi o tempo em que trabalhei na Provectus desenvolvendo um Bootloader para a atualização do próprio firmware via rede 485, só que dessa vez foi através da USB.  Tanta interação com a USB de ambos os lados do cabo é principalmente fruto dos conhecimentos adquiridos com o livro "USB Complete" de Jan Alexlson. Não me lembro se já comentei sobre esse livro aqui antes, mas vale a pena falar dele principalmente sobre do ponto de vista eletrônico/firmware. O livro é simplesmente ótimo, mas diz não cobrir o desenvolvimento de drivers para USB pois tal assunto daria um livro à parte. Com isso ele indica um velho conhecido nosso, o "Programming the Windows Driver Model" de Walter Oney. De qualquer forma, aguardem por reformas neste blog. Durante muito tempo tenho trabalhado no limite para conseguir postar e ainda dar conta de todo o resto. Agora meu tempo extra me permitirá fazer posts com mais frequência e finalmente montar turmas abertas do Curso de Drivers para Windows. Como alguns de vocês já sabem, mantenho uma lista de interessados no curso e que serão notificados por e-mail quando as turmas forem abertas. Independente disso, gostaria de receber e-mails sobre preferências de horários e especialidades, tais como WDM/KMFD/UMDF/File Systems ou ainda sobre ênfase em USB/PCI. É isso aí. Para um post que não tinha nada a dizer, até que esse disse muito. Até breve!
Um Helimodelo no XV Simpósio Multidisciplinar
Esse é mais um daqueles posts Off-Topic que não tem nada a ver com nada. Ou quase nada. Acho que a maioria de vocês já esta cansada de saber que este é meu último ano da universidade e que este ano estou todo enrolado com nosso Trabalho de Graduação (TG). Quem acompanha meu Twitter tem uma idéia de como isso tem tomado meu tempo. Você pode estar se perguntando: "O que faz este projeto?". Em uma palavra: "Nada". Estamos trabalhando a mais de um ano desenvolvendo hardware, firmware, driver, software e controle para que o projeto não faça nada. Na verdade, o objetivo é que não aconteça nada com um helimodelo em voo. Tá, tudo bem, desde o começo agora. Era uma vez um helimodelo
Alguns de vocês já sabem que além de programador retardado, também tento gastar o tempo que não tenho como helimodelista. Um helimodelo é um helicóptero em escala reduzida, equipado com motor à combustão ou mesmo elétrico. Com as funcionalidades de um helicóptero convencional, é capaz de realizar voos com as mesmas características e liberdade de movimentos. Na verdade, quanto à capacidade de voo, um helimodelo pode fazer mais que um helicóptero real. Não é incomum ver um helimodelo voando de cabeça para baixo. Um helimodelo é controlado por um sistema de rádio controle que determina os movimentos de servomotores instalados na aeronave. Cada servo tem seu papel específico dentro do helimodelo. Um controla o acelerador, outro a inclinação das pás do rotor de cauda e assim por diante. É fácil controlar um helimodelo?
Não mesmo. O rádio controle possui dois sticks, e assim, são 4 os movimentos que você deve comandar ao mesmo tempo, isso além das chaves adicionais. O grande problema é que para ganhar os reflexos necessários para fazer a correção leva um certo tempo de treino. Acredite, você não vai querer um helimodelo voando desgovernado perto de você. Além do risco de se ferir, é quase certo que o helimodelo acertará algo e se dividirá em vários pedaços. Posso dizer que este não é um equipamento barato e cada queda pode significar centenas de reais para colocar tudo para funcionar novamente. Para começar com esse hobby normalmente utilizamos um simulador.  Um simulador é muito parecido com um vídeo-game. Com ele você recebe um joystick especial que é uma réplica de um rádio controle, mas que faz interface USB com seu computador. Você utiliza tal joystick para controlar um helimodelo na tela do seu computador. Assim, a cada queda que o modelo sofrer, basta apertar um botão de reset no próprio controle para que você possa tentar novamente. Quando eu estava começando no hobby, todos me indicaram um simulador até que comprei um. Fiquei pensando: "Que dificil que nada... Pra cima de mim? Só se for pra esses coroas. Tenho mais de vinte anos de vídeo-game nas costas". Quando comecei a brincar com o simulador ví que não era tão fácil assim. Dezenas e dezenas de quedas. Os simuladores são realmente um excelente início. Eles conseguem reproduzir com grandes detalhes o comportamento de um modelo.  Depois do simulador, você normalmente faz aulas de voo. E para isso você contrata um instrutor. Mas o que pode fazer um instrutor além de lhe desejar boa sorte? O que acontece é que rádios controles podem ser ligados por um cabo de treinamento. Esse cabo permite que o instrutor possa controlar o helimodelo, e com o mudar de uma chave, o controle passa para a mão do aluno. Se o aluno perder o controle sobre o modelo, o instrutor pode tomar o controle de volta e evitar que seu helimodelo novinho se transforme num monte de lixo. Legal, mas e o projeto?
Nosso projeto tem como objetivo controlar um helimodelo em voo de forma a estabilizá-lo. Por isso digo que nosso projeto vai fazer nada. Houve professor que ainda disse: "Caramba! Vocês vão fazer tudo isso para fazer um helicóptero ficar parado?". Se você é um helimodelista, sabe que manter o helimodelo parado é o primeiro desafio de um piloto. Mesmo em ambientes sem vento, estabilizar um helimodelo requer uma boa quantidade de experiência. Nosso objetivo final seria descrever um plano de voo simples onde ele decole, estabilize no ar, faça alguns movimentos e finalmente pouse. Não queremos dar um passo maior que a perna. Vamos ver o que conseguimos até o dia da apresentação. Para detectar os movimentos do helimodelo, nós instalamos alguns sensores no helimodelo, os dados dos sensores são reunidos por um microcontrolador que está instalado numa placa também a bordo do helimodelo. Depois de reunidos, os dados são enviados à uma outra placa em solo através de um módulo ZigBee. Na foto abaixo pode-se ver nossa plaquinha. Essa placa em solo é um kit da Atmel que tem suporte a diversas interfaces, sendo a USB uma delas. Assim, escrevemos o firmware e o driver USB para fazer com que tais leituras agora fossem recebidas pelo nosso software de controle.  Para fazer o controle, utilizaremos o novo Toolkit de lógica Fuzzy que está disponível no novo LabVIEW 2009 da National Instruments. Para quem não conhece, o LabVIEW é uma das principais ferramentas de controle utilizadas pela engenharia moderna. É possível fazer programas de controle apenas desenhando e arrastando componentes sobre a tela, pode parecer contraditório um desenvolvedor de baixo nível dizer isso, mas toda essa abstração nos dá tempo para dedicar às coisas que realmente precisam de tempo. A ferramenta também dispõe de interfaces de I/O permitindo trabalhar com sensores e atuadores diretamente sobre o meio eletrônico.  Uma outra informação interessante é que o Toolkit de PID e Lógica Fuzzy foi completamente re-escrito e reformulado com a ajuda de um brasileiro. Isso mesmo, Bruno Cesar (na foto ao lado) trabalha na National Instruments Brasil e foi um dos responsáveis por esse desenvolvimento. Acha isso conhecidência? Então o que você me diria ao saber que ele também se formou na Universidade São Judas Tadeu? Bruno esteve semana passada no campus da Mooca dando uma palestra sobre o novo módulo de lógica Fuzzy. Isso nos deu mais certeza de que a lógica Fuzzy é a ideal para nosso problema de controle, já que ela é perfeita para lidar com problemas complexos onde não se tem o modelo matemático que descreva o comportamento de um helimodelo. A lógica Fuzzy se baseia na experiência de um operador para atuar sobre os controles. É bem interessante. O LabVIEW também nos permite que façamos chamadas à DLLs. Essa foi uma maneira simples que conseguimos para fazer com os dados que estavam no driver chagar ao software de controle. Assim, criamos uma DLL que abstraísse muitas das complicações de se interagir com um dispositivo USB. O LabVIEW apenas chama funções do tipo LeAmostra() que já retorna o dado pronto para o uso. Todo aquele código com CreateFile() e DeviceIoControl() ficou por conta da DLL, além de outras funções auxiliares. Agora vocês podem estar se perguntando: "Mas como o LabVIEW vai atuar sobre o helimodelo?". Lembra daquele cabo de treinamento? Nossa placa USB vai receber as ações de controle do LabVIEW através da mesma DLL. Para fazer com que tais comandos sejam aplicados sobre o helimodelo, fizemos com que nossa placa USB se comporte como um rádio controle, que utilizará o cabo de treinamento para aplicar o controle sobre o helimodelo. Obviamente ainda teremos um piloto segurando o rádio no papel de instrutor, principalmente para evitar acidentes, onde um mal funcionamento de nosso projeto poderia transformar meu helimodelo num monte de lixo. O Simpósio Multidisciplinar
 Desde que comecei a escrever sobre o projeto neste blog, algumas pessoas ficaram curiosas e disseram: "Me avise quando houver alguma apresentação". Bom, esta é a sua chance de dar uma olhada em alguns projetos. Nosso projeto foi aceito para ter um espaço neste evento que vai acontecer de 18 a 25 de setembro na Universidade São Judas Tadeu. Serão várias apresentações curtas de 15 cada. Os projetos ainda não estão terminados, e dessa forma, você ainda não verá nenhum helimodelo voando sozinho por lá, mas estaremos com nosso equipamento dispostos a responder algumas perguntas. O simpósio é aberto à visitação pública. Não é necessário ser aluno para participar, basta se inscrever gratuitamente no site da universidade e pronto. Se quiser discutir um pouco sobre sensores, microcontroladores, firmwares, drivers, controle ou ainda helimodelismo, mesmo que durante um café, é só aparecer. Bom, é isso aí. Já escrevi demais. Agora preciso voltar ao meu projeto. Até mais!
Notificando eventos à aplicação
Há algumas semanas, cá estava eu todo enrolado com meu projeto da faculdade. Com toda essa atividade, o que tenho comentado com meus amigos é que meu Twitter mais parece um cronograma. Mas em fim, em meio a tanta correria, recebi a seguinte dúvida do leitor Júlio César (Rio de Janeiro - RJ): "Como implementar a comunicação entre um driver e uma aplicação de modo que o driver inicie a comunicação? Ou seja, não quero que a aplicação envie uma mensagem ao driver, mas sim que o driver envie uma mensagem à aplicação."
Minha resposta curta, porém grossa, é que não existem meios de um driver simplesmente acordar numa manhã ensolarada, coçar a barriga enquanto se espreguiça e dizer a si mesmo: "Hoje vou fazer uma surpresa ao meu amigo notepad.exe. Vou mandar lhe um cartão postal da Kernel-lândia." Um modelo Cliente-Servidor
O Windows funciona num modelo Cliente-Servidor, onde o lado Servidor seria o Kernel, que atende às requisições de seus clientes, que nesse caso são as aplicações. Nenhuma atividade é iniciada pelo Kernel por vontade própria. Sempre são as aplicações, que utilizando a API nativa do sistema, solicitam notificações do sistema de uma série de eventos. "Mas Fernando, como ficam as notificações do plug-and-play às aplicações em user-mode?"
Na verdade elas são solicitadas pelas aplicações utilizando a rotina RegisterDeviceNotification(). Esse assunto é bem legal para se comentar num post futuro. Deixa eu anotar aqui na minha lista de posts a escrever. "Mas Fernando, quando o sistema inicia, as coisas não começam automagicamente?"
O Boot é um procedimento especial no qual o Kernel inicia apenas o Gerenciador de Sessão na User-lândia, também conhecido pelos mais chegados como Smss. O Smss é um processo nativo (que utiliza apenas API nativa) e é considerado um componente de confiança. Ele não utiliza API Windows porque o Subsistema Windows (Csrss) ainda não existe. Daí em diante ocorre uma série de inicializações originadas pelo Smss e seus processos filhos, mas vou deixar os detalhes sobre isso com o Lesma. Isso me fez lembrar que Csrss se extende por "Client Server Run-Time Subsystem". "Mas Fernando, e quanto aos serviços?"
Serviços são iniciados por um processo chamado Services.exe, que por sua vez também foi iniciado por outro componente durante o processo de Boot. "Mas Fernando, e quanto aos drivers de boot?"
A carga de drivers não é considerada uma notificação para o user-mode. "Mas Fernando, setembro chove?"
Bom, chega né? Vamos falar do que interessa agora. Operações de I/O pendentes
Já vimos em um outro post que uma aplicação pode solicitar serviços ao driver. Para dar a impressão de que o driver enviou uma notificação à aplicação, podemos utilizar uma operação que ficaria pendente até que o evento desejado ocorra. Tal como uma operação de leitura numa porta serial, que ficaria presa na chamada ReadFile() até que um ou mais caracteres fossem recebidos. Isso funciona razoavelmente bem, mas teríamos algumas complicações caso o evento nunca ocorra e sua aplicação precise sair porque deixou o feijão no fogo ou coisa assim. Dessa forma, teríamos que adotar uma solução multi-threaded, onde uma segunda thread avisaria à thread pendente de que é tarde demais, que não adianta mais esperar pelo evento, já era, miou, esquece, cai na real. Para as pessoas que sofrem de "thread-fobia", uma solução utilizando Overlapped I/O cairia como uma luva, mas não vou falar sobre isso hoje. Na verdade isso já está na minha lista, mas não vai ser hoje. Compartilhando um evento
A maneira que mais gosto de trabalhar é compartilhando um evento. Todos sabem o que é um evento? Pode parecer besteira, mas tem muita gente não sabe direito o que é um handle e quer programar o Kernel. Isso me preocupa um pouco. Que tipo de drivers essas pessoas podem gerar? Me permitam abrir um parenteses aqui para fazer uma pergunta: O que vocês acham de além de eu oferecer posts de drivers, eu oferecer posts sobre System Programming? Coisas como Processos, Threads, Objetos, Handles, Memória Virtual, Heaps, Dispatch Objects, Sincronismo e por aí vai. Me mandem e-mails com sugestões, que serão muito bem vindas. Voltando ao que interessa, se uma aplicação cria um evento e manda seu handle para o driver, este poderá sinalizar a existência de uma informação relevante à aplicação. Assim a aplicação pode esperar por este evento, e quando este for sinalizado, a aplicação faz o I/O para buscar tal informação usando os meios de comunicação que já vimos em outros posts. Image Notifier
Para exemplificar a recepção de eventos gerados por um driver, vamos ver hoje um driver que nos avisará sempre que uma imagem for mapeada em um processo. Primeiro vamos definir uma interface para essa comunicação. A aplicação precisará enviar o handle de um evento para o driver, isso também vai avisar o driver que a aplicação deseja receber notificações sobre o mapeamento de imagens. Para isso vamos definir nossas IOCTLs como já vimos neste outro post. //-f--> Este será o IOCTL para notificar o driver de que uma // aplicação está interessada nos eventos de carga de // imagens. Este IOCTL deverá levar o handle do evento // a ser sinalizado quando houver dados para a aplicação. #define IOCTL_IMG_START_NOTIFYING CTL_CODE(FILE_DEVICE_UNKNOWN, \ 0x800, \ METHOD_BUFFERED, \ FILE_ANY_ACCESS) //-f--> Sei que é besteira criar uma estrutura com um só membro, // mas isso além de ser mais didático, facilita para aquela // galera que vai fazer "Copy and Paste" do meu código para // outros projetos. Depois eles vão querer mandar mais dados // ao driver e vão se enrolar com isso. Aí já viu de quem é // a culpa: "Peguei esse código no blog daquela besta!". typedef struct _IMG_START_NOTIFYING { HANDLE hEvent; } IMG_START_NOTIFYING, *PIMG_START_NOTIFYING; //-f--> Este será o IOCTL que a aplicação lançará ao driver para // obter os detalhes sobre a carga de imagens num processo. #define IOCTL_IMG_GET_IMAGE_DETAIL CTL_CODE(FILE_DEVICE_UNKNOWN, \ 0x801, \ METHOD_BUFFERED, \ FILE_ANY_ACCESS) //-f--> Aqui vou definir um path máximo de 260 caracteres, mas // podem haver casos de paths mais longos. Não vou tratar // todos os casos e nem otimizar o transporte deste buffer // pegando apenas os bytes válidos. #define IMG_MAX_IMAGE_NAME 260 //-f--> Aqui segue path da imagem que o driver obterá // antes de notificar a aplicação. typedef struct _IMG_IMAGE_DETAIL { CHAR ImageName[IMG_MAX_IMAGE_NAME]; } IMG_IMAGE_DETAIL, *PIMG_IMAGE_DETAIL; //-f--> Aqui a aplicação diz que não está mais interessada nas // notificações sobre imagens. Isso fará com que o driver // libere a referência que fez ao handle. #define IOCTL_IMG_STOP_NOTIFYING CTL_CODE(FILE_DEVICE_UNKNOWN, \ 0x802, \ METHOD_BUFFERED, \ FILE_ANY_ACCESS)
Não vou colocar todo o código aqui no post, mas está tudo disponível no exemplo para download ao final deste post. Lembrem-se que nove em cada dez dentistas recomendam a leitura dos comentários para o melhor entendimento do exemplo. A aplicação basicamente criará um evento e enviará seu handle para o driver através de um IOCTL. //-f--> Cria o evento que será compartilhado. hNotificationEvt = CreateEvent(NULL, TRUE, FALSE, NULL); _ASSERT(hNotificationEvt); printf("Requesting device to start notifying.\n"); //-f--> Copiamos o handle do evento para a estrutura // que será enviada ao driver. Como sabemos, handles // são válidos apenas no contexto deste processo, // então estamos admitindo que nosso driver estará // no topo da device stack. StartNotifying.hEvent = hNotificationEvt; if (!DeviceIoControl(hDevice, IOCTL_IMG_START_NOTIFYING, &StartNotifying, sizeof(StartNotifying), NULL, 0, &dwBytes, NULL)) { //-f--> Respira fundo e abre o WinDbg... dwError = GetLastError(); printf("Error #%d on starting device notification.\n", dwError); __leave; }
Quando o driver receber este IOCTL, este irá adquirir uma referência ao objeto apontado pelo handle. Notem que para isso o driver utiliza a rotina ObReferenceObjectByHandle() do Object Manager, que além de incrementar o contador de referência do objeto, também certifica que o handle é do tipo de objeto que você espera receber. Isso evitaria que, por algum motivo, o handle de um outro objeto tenha sido passado no lugar do handle do evento. O resultado dessa chamada será um ponteiro para um evento recebido pelo driver. Como sabemos, objetos têm seu header num formato padrão, mas o corpo do objeto varia dependendo do seu tipo. Imagine que alguém enviasse um handle para uma thread no lugar de um handle para um evento, poderiamos usar as rotinas de evento para manipular uma thread e a chance de tudo ficar azul é alta. Por isso o uso do parâmetro ObjectType, apesar de opcional, é muito recomendado. //-f--> Obtém uma referência ao objeto nts = ObReferenceObjectByHandle(pStartNotifying->hEvent, EVENT_ALL_ACCESS, *ExEventObjectType, UserMode, (PVOID*)&g_pEvent, NULL);
"Fernando, isso é mesmo necessário? Minha aplicação é a única que vai usar esse driver, e ela sempre vai enviar um handle para evento."
Esse tipo de precaução evita que um programa engraçadinho envie qualquer coisa para seu driver produzindo uma tela azul propositalmente. "Fernando, na minha opinião você gosta mesmo é de complicar as coisas. Eu não poderia simplesmente fazer uma cópia do handle e usar as rotinas do tipo ZwSetEvent() que recebem o handle do evento como parâmetro?"
Veja bem, o handle é válido apenas dentro do processo que o obteve. No nosso caso, tal handle é válido apenas no contexto da nossa aplicação de teste. As notificações de imagens rodam em contexto arbitrário, ou seja, sabe Deus em qual contexto de processo. Por isso teremos que obter uma referência que seja válida em qualquer contexto. O ponteiro obtido pela rotina ObReferenceObjectByHandle() é válido em qualquer contexto, pois aponta para o próprio objeto que reside em System Space. Se você não sabe o que significa System Space, então dê uma passeada por este post. Bom, depois disso a aplicação vai ficar aguardando o evento ser sinalizado pelo driver. No código abaixo, dois eventos são monitorados, um deles é sinalizado pelo driver enquanto o outro é sinalizado pela própria aplicação no momento de encerrar sua atividade. //-f--> Aqui criamos um array de handles para a espera // por múltiplos objetos. hObjects[0] = hFinishEvt; hObjects[1] = hNotificationEvt; do { //-f--> Espera ou por um sinal do device indicando a // presença de dados no driver, ou um sinal da // thread primária dizendo aquela baboseira de // novela e tals. dwWait = WaitForMultipleObjects(2, hObjects, FALSE, INFINITE); switch(dwWait) { case WAIT_FAILED: //-f--> Pô Murphy, dá um tempo! dwError = GetLastError(); printf("Error #%d on waiting for device notification.\n", dwError); __leave; case WAIT_OBJECT_0 + 1: //-f--> Opa! O driver tem algo para nós, vamos buscar. if (GetImageDetail(hDevice) != ERROR_SUCCESS) __leave; break; } //-f--> Ficaremos nisso enquanto o evento de finalização // não for sinalizado pela thread primária. } while(dwWait != WAIT_OBJECT_0);
Quando o evento é sinalizado, a aplicação enviará um IOCTL para obter os dados do driver. Nossa aplicação de teste também imprime esse dado na tela por pura diversão. Vamos dar uma olhada no código do driver para saber como isso acontece. Durante a inicialização, o driver chama a rotina PsSetLoadImageNotifyRoutine() para registrar uma rotina de callback que é chamada sempre que uma imagem for mapeada para algum processo. //-f--> Registra rotina de callback para receber // as notificações de imagens mapeadas para // processos. nts = PsSetLoadImageNotifyRoutine(OnLoadImage); ASSERT(NT_SUCCESS(nts));
Nossa rotina de callback converte o path da imagem mapeada de Unicode para ANSI. Mais detalhes sobre conversão de strings neste post. Em seguida a rotina coloca esse path numa lista e seta o evento enviado pela aplicação. Se você ainda não sabe brincar de listas ligadas no kernel do Widows, então leia este post. VOID OnLoadImage(IN PUNICODE_STRING pusFullImageName, IN HANDLE hProcessId, IN PIMAGE_INFO pImageInfo) { PIMG_EVENT_NODE pNode; ANSI_STRING asImageName; NTSTATUS nts; //-f--> Vamos adquirir o controle das variáveis // compartilhadas por diferentes threads. nts = KeWaitForMutexObject(&g_EventMtx, UserRequest, KernelMode, FALSE, NULL); ASSERT(NT_SUCCESS(nts)); __try { //-f--> Verifica se a aplicação está interessada neste // evento. if (!g_pEvent) __leave; //-f--> Aloca um nó para a lista de paths de imagens pNode = (PIMG_EVENT_NODE)ExAllocatePoolWithTag(PagedPool, sizeof(IMG_EVENT_NODE), IMG_TAG); if (!pNode) { //-f--> Ops! ASSERT(FALSE); __leave; } //-f--> Inicializa uma ANSI_STRING para usar na conversão // do path da imagem. Vamos fornecer sempre um byte // a menos para nos reservar espaço para adicionar um // terminador nulo. RtlInitEmptyAnsiString(&asImageName, pNode->ImageDetail.ImageName, sizeof(pNode->ImageDetail.ImageName) - 1); //-f--> Faz a conversão sem alocação do resultado. nts = RtlUnicodeStringToAnsiString(&asImageName, pusFullImageName, FALSE); if (!NT_SUCCESS(nts)) { //-f--> Ops! ASSERT(FALSE); ExFreePool(pNode); __leave; } //-f--> Coloca o terminador nulo para que a aplicação de // teste possa contar com ele na hora de fazer o print. asImageName.Buffer[asImageName.Length] = 0; //-f--> Insere o nó na lista. InsertTailList(&g_ListHead, &pNode->Entry); //-f--> Setamos o evento informando a aplicação que existem // dados na lista a serem lidos. KeSetEvent(g_pEvent, IO_NO_INCREMENT, FALSE); } __finally { //-f--> Por fim, libera o mutex e corre pro abraço. KeReleaseMutex(&g_EventMtx, FALSE); } }
Quando o evento é sinalizado, a aplicação acorda de seu sono profundo e descobre que o driver tem dados para ela. Então ela envia um IOCTL para obter tais dados. Este IOCTL vai executar a rotina abaixo removendo o primeiro elemento da lista e verificar se ainda existem mais dados a serem coletados pela aplicação. Caso a lista esvazie nesta chamada, o driver reseta o evento para que a aplicação volte a dormir esperando pelos registros de novas imagens mapeadas. NTSTATUS OnGetImageDetail(PIMG_IMAGE_DETAIL pImageDetail) { NTSTATUS nts; PLIST_ENTRY pEntry; PIMG_EVENT_NODE pNode; //-f--> Adquire o mutex nts = KeWaitForMutexObject(&g_EventMtx, UserRequest, KernelMode, FALSE, NULL); ASSERT(NT_SUCCESS(nts)); //-f--> Verifica se a lista está vazia. Sempre // use esta rotina antes de tentar remover // um elemento da lista. if (!IsListEmpty(&g_ListHead)) { //-f--> Obtém o endereço do Entry pEntry = RemoveHeadList(&g_ListHead); //-f--> Obtém o endereço do nó pNode = CONTAINING_RECORD(pEntry, IMG_EVENT_NODE, Entry); //-f--> Copia para o buffer da aplicação. RtlCopyMemory(pImageDetail, &pNode->ImageDetail, sizeof(IMG_IMAGE_DETAIL)); //-f--> Libera o nó e balezia ExFreePool(pNode); nts = STATUS_SUCCESS; } else nts = STATUS_NO_MORE_ENTRIES; //-f--> Pode ser que nesta chamada a lista tenha // ficado vazia. Então verificamos novamente // e resetamos o evento para que a aplicação // não volte aqui. if (IsListEmpty(&g_ListHead)) KeResetEvent(g_pEvent); //-f--> Libera o mutex e pronto. KeReleaseMutex(&g_EventMtx, FALSE); return nts; }
O resultado de tanto bla-bla-bla
Depois que o driver for compilado, instalado e iniciado, poderemos executar nossa aplicação de teste e esperar que algo seja executado. Quando um processo é criado, tanto seu módulo como as DLLs que ele depende são mapeadas no sistema. Isso vai disparar nossa rotina de callback no driver e fazer a coisa toda funcionar. Se você não sabe como compilar, instalar e iniciar um driver, este post pode te ajudar. A imagem acima é o resultado da execução do notepad.exe enquanto nossa aplicação de teste esperava por eventos, mas qualquer outro processo poderia disparar tais eventos. Este post além de nos fornecer este exemplo de chamada invertida, também nos mostra como brincar com Mutex Objects, que foi a dúvida de outro leitor, Ismael Rocha (Brasília - DF). Agora deixa eu voltar para o meu projeto da faculdade. Até mais! Download ImgNotifier.zip
Strings no Kernel
Tem coisa mais besta que manipular strings? Creio que a resposta correta seria "Depende". Quando eu tinha terminado meu curso técnico de Informática Industrial em 1995, eu pensava que sabia muito de linguagem C. Afinal de contas eu já sabia manipular strings. Copiar, concatenar, inverter, fazer buscas por palavras... O que mais um programador deveria saber? Quando comecei a fazer meu estágio e a pegar programas do mundo real, descobri que eu não sabia nada. Mas uma coisa é certa, eu sabia manipular strings. Este post deve ajudar bastante os programadores C++ que manjam tudo de Templates, Smart Pointers, STL e coisas mágicas que abstraem a realidade facilitando a vida do programador. Com todos esses recursos de contadores de referências e sobrecarga de tudo que é operador, as coisas começam a ficar nebulosas e você começa a se perguntar: "Mas onde está o buffer mesmo?". Hoje vamos apenas dar uma passadinha de leve nas estruturas UNICODE_STRING, ANSI_STRING e algumas funções de conversão entre elas. Pode parecer besteira, mas se você não souber brincar de strings, pra quê aprender o resto? Vai tudo acabar em tela azul mesmo. E no prézinho...
Nós aprendemos com a tia da escolinha que strings são cadeias de caracteres. Assim poderíamos contar a história de nossas vidas apenas colocando um caractere na frente do outro. CHAR szExemplo[] = "Tava ruim lá na Bahia, profissão de bóia-fria\n" "Trabalhando noite e dia, num era isso que eu queria\n" "Eu vim-me embora pra \"Sum Paulo\",\n" "Eu vim no lombo dum jumento com pouco conhecimento\n" "Enfrentando chuva e vento e dando uns peido fedorento (vish)\n" "Até minha bunda fez um calo\n" "Chegando na capital, uns puta predião legal\n" "As mina pagando um pau, mas meu jumento tava mal\n" "Precisando reformar\n" "Fiz a pintura, importei quatro ferradura\n" "Troquei até dentadura e pra completar a belezura\n" "Eu instalei um Road-Star!";
Jumento Celestino / Mamonas Assassinas Duas coisas são obrigatórias que você saiba para que você possa continuar lendo este post. Uma delas é que estes caracteres precisam estar armazenados em algum lugar, seja em uma variável local, alocadas no heap ou mesmo no segmento de dados inicializados. A outra coisa é que strings normalmente são terminadas por um caractere NULL, mas a falta dele não descaracteriza uma string. Isso significa que podemos ter strings sem terminadores onde seu tamanho é indicado por uma outra variável. Deixo aqui um gancho para o Lesma explicar essas coisas aos meninos e meninas interessados. Uma outra característica importante é que nem sempre um caractere é igual a um Byte. Existem strings compostas por caracteres largos. Caracteres largos, ao contrário do que se pensa, não são caracteres sortudos, mas sim, caracteres formados por valores de 16 bits. Com strings formadas por tais caracteres pode-se expressar palavras em qualquer idioma. Isso explica como o Windows consegue lidar com nomes de arquivos alienígenas quando você tenta instalar os drivers do HiPhone que você comprou no China. Além disso, imagine que ocorra um erro durante o processo de criptografia do seu disco rídigo. Seria vital que uma mensagem de erro detalha lhe fosse exibida independente da nacionalidade do produto. Se a informação vai ajudar já é outro assunto. Quatro tipos de strings
Destes temos duas strings que já estamos acostumados a ver em user-Mode, ambas terminadas com caractere NULL. CHAR szString[] = "Uma string de CHAR"; WCHAR wzString[] = L"Uma string de WCHAR";
As outras duas strings são as que normalmente vemos em kernel-mode. typedef struct _UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; } UNICODE_STRING, *PUNICODE_STRING; typedef struct _STRING { USHORT Length; USHORT MaximumLength; PCHAR Buffer; } ANSI_STRING, *PANSI_STRING;
Estas strings são definidas por estruturas com três membros, os quais descrevo abaixo. O comportamento das rotinas e macros que às manipulam é muito similar. Por isso vou concentrar meus exemplos em UNICODE_STRING já que o sub-sistema Win32 converte tudo para unicode quando passa uma chamada para o kernel. Buffer: E um ponteiro para a região de memória onde os caracteres são armazenados. Length: Indica a quantidade de bytes válidos da string. Este tamanho é sempre expresso em bytes, mesmo que esta seja uma string de WCHAR. MaximumLength: Indica o tamanho máximo que esta string pode ter.
Para entender de como estes membros são interpretados, dê uma olhada no exemplo abaixo: void OsTresMembros(void) { WCHAR wsUmArray[200]; UNICODE_STRING usString; //--f-> Indico onde os caracteres desta string // serão armazenados. usString.Buffer = wsUmArray; //-f--> Ainda não escrevemos nada no buffer, // por isso, nada do que esteja no buffer // é válido. usString.Length = 0; //-f--> Embora o buffer não esteja inicializado // ele ainda está lá e pode conter uma string // de no máximo seu tamanho em bytes. usString.MaximumLength = sizeof(wsUmArray); }
Aqui vemos uma string unicode vazia, pois tem zero bytes válidos, mas sua capacidade de armazenar é de até 200 caracteres. Notem que os caracteres são armazenados em um array que está na pilha. Isso significa que nenhum leak de memória será causado com o termino desta rotina. Inicializando Strings
Podemos ainda utilizar algumas macros que fazem a inicialização destas estruturas. void InitString(void) { UNICODE_STRING usOutraConstante; UNICODE_STRING usVazia; WCHAR wzBuffer[30] = L"Isso não será considerado."; //-f--> Inicaliza uma string na sua criação. UNICODE_STRING usConstante = RTL_CONSTANT_STRING(L"Uma string."); //-f--> Inicializa uma string constante. RtlInitUnicodeString(&usOutraConstante, L"Uma outra string constante que não muda."); //-f--> Inicializa uma string vazia para uso posterior RtlInitEmptyUnicodeString(&usVazia, wzBuffer, sizeof(wzBuffer)); }
Reparem nos valores destas estruturas. kd> ?? usConstante struct _UNICODE_STRING "Uma string." +0x000 Length : 0x16 +0x002 MaximumLength : 0x18 +0x004 Buffer : 0xf8cd8600 "Uma string." kd> db 0xf8cd8600 L0x18 f8cd8600 55 00 6d 00 61 00 20 00-73 00 74 00 72 00 69 00 U.m.a. .s.t.r.i. f8cd8610 6e 00 67 00 2e 00 00 00 n.g.....
Embora a capacidade máxima desta string seja de 0x18 caracteres, somente 0x16 bytes são válidos. Isso porque a macro desconsidera o terminador NULL que o compilador C/C++ deixou de brinde. A macro RTL_CONSTANT_STRING() nos permite inicializar a string na sua criação, mas um outro notável recurso dela é que ela ferra o IntelliSence do Visual Studio (2008 pelo menos). Então se você gosta mesmo do IntelliSence, prefira utilizar a macro RtlInitUnicodeString(). kd> ?? usVazia struct _UNICODE_STRING "Isso não será considerado." +0x000 Length : 0 +0x002 MaximumLength : 0x3c +0x004 Buffer : 0xf8ae5c34 "Isso não será considerado."
Oops! Como pode uma string com zero bytes válidos ser exibida pelo WinDbg? Na verdade, o que acontece é que o WinDbg desmonta a estrutura UNICODE_STRING e mostra cada um dos mebros aqui. No caso, existe um array de WCHAR com valores bem comportados aqui. Não culpe o coitadinho. Você é que está mal acostumado com o Visual Studio. kd> db 0xf8ae5c34 L0x3c f8ae5c34 49 00 73 00 73 00 6f 00-20 00 6e 00 e3 00 6f 00 I.s.s.o. .n...o. f8ae5c44 20 00 73 00 65 00 72 00-e1 00 20 00 63 00 6f 00 .s.e.r... .c.o. f8ae5c54 6e 00 73 00 69 00 64 00-65 00 72 00 61 00 64 00 n.s.i.d.e.r.a.d. f8ae5c64 6f 00 2e 00 00 00 00 00-00 00 00 00 o...........
Mas estes dados só estão aí por acaso. Eles não são considerados como parte válida de uma string unicode. Isso atrapalha um pouco na hora de fazer o debug pois mesmo a janela de variáveis locais também mostra esse conteúdo inválido. A extensão !ustr mostra apenas os dados válidos de uma string unicode. kd> !ustr usConstante String(22,24) at f899fc6c: Uma string. kd> !ustr usVazia String(0,60) at f8ae5c1c:
O terminador é necessário?
 Não mesmo! Você pode bem observar que nos exemplos anteriores, a macro deixou o terminador de fora dos bytes válidos. Considerar o terminador como byte válido é um erro e pode gerar confusão. Imagine comparar duas strings distintas que carregam o mesmo conteúdo, mas uma delas considera o terminador como informação válida. Tais strings serão diferentes, já que possuem comprimento diferente. Creio que o importante mesmo é não contar com o terminador nas strings que você recebe de outros componentes. É fato que na maioria das vezes, o buffer possui um terminador, e que apesar de não ser considerado informação válida, o terminador ainda está lá. Nunca conte com isso a menos que haja alguma nota na documentação. Já vi pessoas que utilizarem o membro Buffer como parâmetro para uma chamada à rotina wcslen() por exemplo. Além do risco de obter a informação incorreta, ainda tem um plus de poder gerar uma tela azul. "Mas Fernando, eu já testei isso em vários sistemas operacionais e sempre funcionou." Isso não justifica nada, você não pode se apoiar em testes, mas em documentações. Quando seu produto se espalha no mercado, ele enfrenta muitos ambientes diferentes, com os mais diversos filtros, anti-virus, monitores e por aí vai. Não se pode ter certeza da implementação de qualquer software. O melhor que podemos esperar deles é que se apoiem na documentação. Manipulando Strings
Os membros da estrutura UNICODE_STRING são basicamente utilizados por rotinas de manipulação a fim de verificar se o buffer existente é suficiente para a operação desejada. Portanto, antes de utilizar uma string, certifique-se de esta foi inicializada corretamente. No caso de uma cópia de strings, a string de destino precisará ser inicilizada mesmo que vazia. void CopyString(void) { UNICODE_STRING usSource; UNICODE_STRING usTarget; WCHAR wzTarget[10]; //-f--> Aqui inicializamos nossa string de origem. RtlInitUnicodeString(&usSource, L"12345678901234567890"); //-f--> Aqui inicializamos nossa string de destino. RtlInitEmptyUnicodeString(&usTarget, wzTarget, sizeof(wzTarget)); //-f--> Realiza a cópia RtlCopyUnicodeString(&usTarget, &usSource); }
Aqui vemos o exemplo de uma cópia de string onde a string de origem é maior que a de destino. Nessa situação não teremos uma violação de acesso, mas o buffer de destino será preenchido completamente. Notem que a rotina não nos deixou o confortável terminador. kd> !ustr usTarget String(20,20) at f89a3c6c: 1234567890 kd> ?? usTarget struct _UNICODE_STRING "1234567890" +0x000 Length : 0x14 +0x002 MaximumLength : 0x14 +0x004 Buffer : 0xf89a3c54 "1234567890" kd> db 0xf89a3c54 L0x20 f89a3c54 31 00 32 00 33 00 34 00-35 00 36 00 37 00 38 00 1.2.3.4.5.6.7.8. f89a3c64 39 00 30 00 98 5d 5f 00-14 00 14 00 54 3c 9a f8 9.0..]_.....T<..
Diferentes da rotina RtlCopyUnicodeString(), algumas outras rotinas nos retornam STATUS_BUFFER_TOO_SMALL quando o buffer é insuficiente. VOID RtlCopyUnicodeString( IN OUT PUNICODE_STRING DestinationString, IN PCUNICODE_STRING SourceString ); NTSTATUS RtlAppendUnicodeStringToString( IN OUT PUNICODE_STRING Destination, IN PUNICODE_STRING Source );
Existe uma série de rotinas de manipulação de strings no WDK, mas sempre fica faltando alguma rotina se comparado com a ampla biblioteca de rotinas da biblioteca padrão C/C++. Rotinas como strrchr() por exemplo. Quando necessário teremos que construir uma versão que manipule estruturas UNICODE_STRING da mesma maneira. Aqui está uma lista básica de rotinas de string que o WDK suporta. Outras funções são listadas aqui, mas falaremos delas mais tarde. Mas onde está o buffer mesmo?
A estrutura UNICODE_STRING não armazena buffer, mas sim um ponteiro para ele. Dessa forma, a maneira de descartar uma string varia dependendo da maneira que você a obteve. Nos exemplos que vimos até agora, os bufferes utilizados estão em dados inicializados ou de arrays locais da função de exemplo. Existem rotinas que inicializam e alocam strings como forma de retornar a informação desejada. Nestes casos, é necessário liberar o buffer que você recebeu. É o caso das rotinas que fazem a conversão de ANSI_STRING para UNICODE_STRING e vice versa. São elas RtlUnicodeStringToAnsiString() e RtlAnsiStringToUnicodeString(). NTSTATUS RtlAnsiStringToUnicodeString( IN OUT PUNICODE_STRING DestinationString, IN PANSI_STRING SourceString, IN BOOLEAN AllocateDestinationString ); NTSTATUS RtlUnicodeStringToAnsiString( IN OUT PANSI_STRING DestinationString, IN PUNICODE_STRING SourceString, IN BOOLEAN AllocateDestinationString );
O exemplo abaixo faz uma conversão de ANSI para UNICODE com alocação do resultado, e em seguida, libera o buffer recebido. void ConvertString(void) { ANSI_STRING asString; UNICODE_STRING usString; //-f--> Aqui inicializamos nossa string de origem. RtlInitAnsiString(&asString, "Um exemplo simples."); //-f--> Neste caso não precisaremos inicializar a // string de destino. A rotina fará isso por nós. RtlAnsiStringToUnicodeString(&usString, &asString, TRUE); //-f--> Imprime a string resultante DbgPrint("String convertida: %wZ\n", &usString); //-f--> Liberamos o buffer alocado na conversão. RtlFreeUnicodeString(&usString); }
Você mesmo pode escrever uma rotina que gere UNICODE_STRINGs alocando o buffer dinamicamente. O buffer pode ser alocado dinamicamente utilizando ExAllocatePoolWithTag() ou uma de suas irmãs. No entanto, na hora de liberar o buffer desta string, utilize a função adequada, que neste exemplo seria ExFreePoolWithTag(). Não saia utilizando RtlFreeUnicodeString() a torto e a direito. Aprecie com moderação. Apenas utilize essa rotina para liberar strings que foram obtidas por funções como RtlAnsiStringToUnicodeString(), cuja documentação indica o uso de RtlFreeUnicodeString(). "..., the caller must deallocate the buffer by calling RtlFreeUnicodeString."
Safe Strings em Kernel
Uma parte das rotinas de manipulação de strings da biblioteca padrão do C/C++, tais como strcpy() e sprintf(), também estão disponíveis em kernel, mas a crescente preocupação com a seguraça na manipulação de buffers fez com que as funções seguras fossem disponibilizadas tanto para user-mode como para kernel-mode. Para ter detalhes sobre o uso destas funções consulte este link. Mais um driver de exemplo
Este outro post traz o exemplo de um driver que guarda uma lista de strings em memória. Já neste outro post, esse mesmo exemplo foi evoluido para que diferentes listas fossem mantidas sob diferentes contextos. Agora vou evoluir esse exemplo novamente. A aplicação continuará enviando strings com terminadores NULL durante a escrita, o driver criará ANSI_STRINGs a partir delas e às converterão em UNICODE_STRINGs antes de colocá-las na lista. A maior parte das modificações estão nas rotinas de leitura e escrita, então vou apenas exibir o código dessas rotinas aqui. De qualquer forma, todo o projeto incluindo o driver e uma aplicação de teste estão disponíveis para download ao final deste post. Vamos começar com a rotina de escrita que envia as strings ao driver. Como sempre, toda informação revelante estão nos comentários. /**** *** OnWrite ** ** A aplicação está enviando uma string. */ NTSTATUS OnWrite(IN PDEVICE_OBJECT pDeviceObj, IN PIRP pIrp) { PIO_STACK_LOCATION pStack; ANSI_STRING asString; PSTRING_LIST pStringList; PSTRING_REG pStringReg; ULONG ulBytes; KIRQL kIrql; NTSTATUS nts; //-f--> Obtemos a Stack Location corrente. pStack = IoGetCurrentIrpStackLocation(pIrp); //-f--> Obtém a ponta da lista. pStringList = (PSTRING_LIST)pStack->FileObject->FsContext; //-f--> O que temos no buffer de sistema aqui é um array // de bytes terminados com NULL. Vamos inicializar // uma ANSI_STRING com este buffer. RtlInitAnsiString(&asString, (PCSZ)pIrp->AssociatedIrp.SystemBuffer); //-f--> Aqui alocamos o nó que vai ser colocado na lista pStringReg = (PSTRING_REG) ExAllocatePoolWithTag(NonPagedPool, sizeof(STRING_REG), STR_LST_TAG); //-f--> Para fazer a conversão para UNICODE_STRING, vamos // solicitar que a rotina faça a alocação do bufer // resultante. Por essa razão, não precisaremos inicializar // a string de saída. nts = RtlAnsiStringToUnicodeString(&pStringReg->usString, &asString, TRUE); if (!NT_SUCCESS(nts)) { //-f--> Ops! Provavelmente não tivemos memória para isso. // Vamos sinalizar a falha e informar ao IoManager // que zero bytes foram copiados. ExFreePoolWithTag(pStringReg, STR_LST_TAG); pIrp->IoStatus.Information = 0; } else { //-f--> Vamos segurar o spinlock para evitar concorrência // no acesso à lista. KeAcquireSpinLock(&pStringList->SpinLock, &kIrql); //-f--> Insere o nó na lista. InsertTailList(&pStringList->ListHead, &pStringReg->Entry); //-f--> Libera o spinlock. KeReleaseSpinLock(&pStringList->SpinLock, kIrql); //-f--> Aqui informamos ao IoManager que todos os bytes // enviados pela aplicação foram recebidos com // sucesso pelo driver. pIrp->IoStatus.Information = pStack->Parameters.Write.Length; } //-f--> O campo de Information já foi preenchido, vamos apenas // copiar o status da operação e completar a IRP. pIrp->IoStatus.Status = nts; IoCompleteRequest(pIrp, IO_NO_INCREMENT); return nts; }
Agora vejamos a recuperação das strings na leitura. /**** *** OnRead ** ** A aplicação está querendo receber as strings eviadas ** por ela. */ NTSTATUS OnRead(IN PDEVICE_OBJECT pDeviceObj, IN PIRP pIrp) { ANSI_STRING asString; PIO_STACK_LOCATION pStack; PSTRING_LIST pStringList; PSTRING_REG pStringReg; PLIST_ENTRY pEntry; KIRQL kIrql; NTSTATUS nts; //-f--> Vamos deixar este campo com zero até que tenhamos // certeza de que a cópia foi feita paro o buffer da // aplicação pIrp->IoStatus.Information = 0; //-f--> Obtemos a Stack Location corrente. pStack = IoGetCurrentIrpStackLocation(pIrp); //-f--> Obtém a ponta da lista. pStringList = (PSTRING_LIST)pStack->FileObject->FsContext; //-f--> Vamos retornar para aplicação apenas um array de CHAR // com terminador NULL. As strings estão armexadas como // UNICODE_STRING. Vamos convertê-las para ANSI_STRING. // Aqui vamos inicializar a ANSI_STRING que receberá // o resultado da conversão de UNICODE_STRING. //-f--> Vamos oferecer Lengh-1 para reservar um byte para // o terminador null após conversão. RtlInitEmptyAnsiString(&asString, (PCHAR)pIrp->AssociatedIrp.SystemBuffer, (USHORT)pStack->Parameters.Read.Length - 1); //-f--> Aqui vamos adquirir o spinlock para evitar concorrência // no acesso à lista. KeAcquireSpinLock(&pStringList->SpinLock, &kIrql); //-f--> Verifica se a lista está vazia. if (IsListEmpty(&pStringList->ListHead)) { //-f--> Sinaliza erro na leitura e já libera o spinlock. nts = STATUS_NO_MORE_ENTRIES; KeReleaseSpinLock(&pStringList->SpinLock, kIrql); } else { //-f--> Remove o registro da lista e já libera o spinlock. pEntry = RemoveHeadList(&pStringList->ListHead); KeReleaseSpinLock(&pStringList->SpinLock, kIrql); pStringReg = CONTAINING_RECORD(pEntry, STRING_REG, Entry); //-f--> Aqui convertemos a string. Reparem que não solicitamos // a alocação do buffer. Estamos utilizando o buffer de // sistema para receber o resultado da conversão. Neste // caso a string de destino deve estar inicializada. nts = RtlUnicodeStringToAnsiString(&asString, &pStringReg->usString, FALSE); if (NT_SUCCESS(nts)) { //-f--> Aqui usamos aquele byte que reservamos e assim // o terminador null também é copiado pelo IoManager // do SystemBuffer para o buffer da aplicação asString.Buffer[asString.Length] = 0; //-f--> Precisamos informar ao IoManager a quantidade de // bytes que serão copiados para o buffer da aplicação. // Esse tamanho é o tamanho da string convertida mais // um byte ocupado pelo terminador NULL que colocamos. pIrp->IoStatus.Information = asString.Length+1; } //-f--> Neste exemplo, não estamos lidando com o caso de erro // na conversão, isso pode acontecer se a aplicação mandar // um buffer pequeno para a string. Caso algum erro ocorra // durante a conversão, vamos simplesmente descartar a string //-f--> Libera o buffer da string e em seguida o registro que ela // ocupava na lista. RtlFreeUnicodeString(&pStringReg->usString); ExFreePoolWithTag(pStringReg, STR_LST_TAG); } //-f--> O campo de Information já foi preenchido, vamos apenas // copiar o status da operação e completar a IRP. pIrp->IoStatus.Status = nts; IoCompleteRequest(pIrp, IO_NO_INCREMENT); return nts; }
Com este driver rodando, já posso pensar em alguns exemplos de filtros. Já venho pensando em exemplos de filtros há algum tempo, além de receber sujestões de post sobre esse assunto. O fato é que não tinhamos base para tal. O importante é termos um driver simples o suficiente para o fácil entendimento das coisas. Não adianta eu fazer um post com um filtro de disco rígido, um filtro de rede ou qualquer outro driver com plug-and-play e gerenciamento de energia. Estes precisariam de muito conhecimento acumulado e não caberiam num post. Enfim, mais uma vêz espero ter ajudado. E se alguma dúvida surgir é só me mandar um e-mail. Have fun! Download StringList.zip
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!
|
|