Archive for the ‘Kernel Development’ Category

Step into Kernel (VMware 7+WinDbg)

4 de September de 2010

A primeira impressão, que alguns leitores mais antigos podem estar tendo agora, é a de que eu estou ficando realmente sem tempo de escrever novos posts, e por isso, esse novo post seria apenas uma repetição do post Step into Kernel (WMware+WinDbg). Na verdade estou tão surpreso quanto alguns de vocês. Neste post vou falar sobre um pequeno detalhe que vai fazer diferença na hora de fazer debug de Kernel utilizando uma máquina virtual criada com a nova versão da VMware.

O Sintoma

Neste último final de semana, apesar do feriado, do sol e da imensa vontade de pôr o pé na estrada,  tive que ficar em casa resolvendo algumas tretas pessoais. Uma delas envolvia fazer debug de Kernel numa máquina virtual. Nada de incomum nisso até agora. Afinal de contas, fazer debug de Kernel em máquinas virtuais é arroz com feijão para quem trabalha desenvolvendo drivers.

Criei uma VM nova e instalei o Windows nela, realizei as configurações TARGET e HOST exatamente como descrevo naquele outro post, mas para minha surpresa, por algum motivo o WinDbg não conseguia se conectar ao sistema TARGET, permanecendo sempre com a mesma mensagem “Waiting to reconnect…”. Mesmo usando o Ctrl+Alt+D no WinDbg para ver informações internas do depurador, tudo o que eu tinha na saída do WinDbg era a mensagem exibida abaixo.

 
Microsoft (R) Windows Debugger Version 6.12.0002.633 AMD64
Copyright (c) Microsoft Corporation. All rights reserved.
 
Opened \\.\pipe\com_1
Waiting to reconnect...
>>>> ReadAll(0x800703E3,1,0) fails.
>>>> ReadAll(0x800703E3,1,0) fails.
>>>> ReadAll(0x800703E3,1,0) fails.
>>>> ReadAll(0x800703E3,1,0) fails.
>>>> ReadAll(0x800703E3,1,0) fails.
>>>> ReadAll(0x800703E3,1,0) fails.
SYNCTARGET: Timeout.
>>>> ReadAll(0x800703E3,1,0) fails.
Throttle 0x10 write to 0x1
>>>> ReadAll(0x800703E3,1,0) fails.
>>>> ReadAll(0x800703E3,1,0) fails.
>>>> ReadAll(0x800703E3,1,0) fails.

Depois de verificar todos os passos minuciosamente várias vezes, eu não conseguia entender o que estava acontecendo.

A Causa

Ao criar uma nova porta serial na máquina virtual, a qual seria redirecionada para um named pipe na máquina real, reparei que seu nome estava fora do comum. Apesar de esta ser supostamente a única porta serial do sistema, a interface de edição de configurações da VMware mostrava seu nome como “Serial Port 2” como mostra a figura abaixo.

Desconfiei disso e tentei recriar a porta serial achando que seria um bug na inteface da VMware, mas o mesmo nome apareceu. Então nada melhor que abrir o arquivo de configuração da VM no bom e velho Notepad. Foi então que descobri que apesar de não haver uma porta serial além da que eu estava criando na interface de edição da VMware, no arquivo de configuração de fato haviam duas portas seriais como mostro abaixo.

.encoding = "windows-1252"
config.version = "8"
virtualHW.version = "7"
scsi0.present = "TRUE"
scsi0.virtualDev = "lsisas1068"
memsize = "1024"
mem.hotadd = "TRUE"
scsi0:0.present = "TRUE"
scsi0:0.fileName = "Windows 7 x64.vmdk"
ide1:0.present = "TRUE"
ide1:0.autodetect = "TRUE"
ide1:0.deviceType = "cdrom-raw"
floppy0.startConnected = "FALSE"
floppy0.fileName = ""
floppy0.autodetect = "TRUE"
ethernet0.present = "TRUE"
ethernet0.connectionType = "nat"
ethernet0.virtualDev = "e1000"
ethernet0.wakeOnPcktRcv = "FALSE"
ethernet0.addressType = "generated"
usb.present = "TRUE"
ehci.present = "TRUE"
sound.present = "TRUE"
sound.fileName = "-1"
sound.autodetect = "TRUE"
mks.enable3d = "TRUE"
serial0.present = "TRUE"
serial0.fileType = "thinprint"
pciBridge0.present = "TRUE"
pciBridge4.present = "TRUE"
pciBridge4.virtualDev = "pcieRootPort"
pciBridge4.functions = "8"
pciBridge5.present = "TRUE"
pciBridge5.virtualDev = "pcieRootPort"
pciBridge5.functions = "8"
pciBridge6.present = "TRUE"
pciBridge6.virtualDev = "pcieRootPort"
pciBridge6.functions = "8"
pciBridge7.present = "TRUE"
pciBridge7.virtualDev = "pcieRootPort"
pciBridge7.functions = "8"
vmci0.present = "TRUE"
roamingVM.exitBehavior = "go"
displayName = "Windows 7 x64"
guestOS = "windows7-64"
nvram = "Windows 7 x64.nvram"
virtualHW.productCompatibility = "hosted"
printers.enabled = "TRUE"
extendedConfigFile = "Windows 7 x64.vmxf"
ide1:0.startConnected = "TRUE"
ethernet0.generatedAddress = "00:0c:29:91:aa:62"
tools.syncTime = "FALSE"
uuid.location = "56 4d a8 8a e8 f9 f0 a8-62 de e8 25 40 91 aa 62"
uuid.bios = "56 4d a8 8a e8 f9 f0 a8-62 de e8 25 40 91 aa 62"
cleanShutdown = "FALSE"
replay.supported = "FALSE"
replay.filename = ""
scsi0:0.redo = ""
pciBridge0.pciSlotNumber = "17"
pciBridge4.pciSlotNumber = "21"
pciBridge5.pciSlotNumber = "22"
pciBridge6.pciSlotNumber = "23"
pciBridge7.pciSlotNumber = "24"
scsi0.pciSlotNumber = "160"
usb.pciSlotNumber = "32"
ethernet0.pciSlotNumber = "33"
sound.pciSlotNumber = "34"
ehci.pciSlotNumber = "35"
vmci0.pciSlotNumber = "36"
scsi0.sasWWID = "50 05 05 6a e8 f9 f0 a0"
vmotion.checkpointFBSize = "134217728"
usb:0.present = "TRUE"
usb:1.present = "TRUE"
ethernet0.generatedAddressOffset = "0"
vmci0.id = "1083288162"
usb:1.deviceType = "hub"
usb:0.deviceType = "mouse"
ide1:0.fileName = "auto detect"
unity.wasCapable = "FALSE"
serial1.yieldOnMsrRead = "TRUE"
serial1.fileName = "\\.\pipe\com_1"
serial1.pipe.endPoint = "server"
 

A propriedade “filetype” com o valor “thiprint” na porta serial zero me fez prestar atenção neste novo hardware que fazia parte das configurações da minha máquina virtual e eu nem tinha reparado.

O fato de essa impressora utilizar a porta COM1 da máquina virtual como interface de hardware, fazia com que qualquer nova porta serial utilizasse a COM2, que seria a próxima porta vaga. Agora ficou simples de resolver.

A Solução

Existem duas maneiras de resolver esse pequeno contratempo. A primeira, que minha opinião é mais simples e tosco, é simplesmente remover a impressora antes de adicionar a porta serial que será utilizada como interface de debug. Isso fará com que a primeira porta serial seja realmente utilizada como porta serial. Caso você necessite utilizar a impressora, então adicione e impressora após a adição da porta serial. Isso fará com que a impressora utilize a primeira porta serial disponível, que no caso é a COM2. A janela de configuração de hardware da VMware deve ficar como exibida na figura abaixo.

Reparem que o nome da porta serial não leva mais o número 2 enquanto que na configuração da impressora diz que a porta utilizada é a serial 2.

A segunda solução, como uma alternativa à primeira, é simplesmente configurar o lado TARGET para que ele utilize a porta serial 2. No exemplo da figura abaixo, eu mudo a configuração de um Windows 7 utilizando a ferramenta BcdEdit. Já comentei dessa ferramenta neste post, que fala sobre como fazer debug de Kernel através de portas USB utilizando o Windows Vista.

Depois de tudo resolvido, o depurador agora conecta ao sistema TARGET e agora vem a parte mais fácil; depurar o problema. 😉

Até mais!

O que ler para escrever drivers para Windows

27 de August de 2010

Uma pergunta que sempre me fazem é sobre quais livros é necessário se ler para que se possa desenvolver drivers para Windows. Este é um assunto que eu julgava muito óbvio para se ter um post a respeito, mas com o tempo fui percebendo que este assunto não é tão óbvio para os que estão partindo do zero. Para alguns, escrever drivers é algo inalcançável, que por mais que eles leiam ou tentem aprender, nunca será informação suficiente para começar a meter a mão na massa. Para outros, escrever drivers não é algo assim tão complicado, que da mesma forma que ele aprendeu a escrever aplicações com Visual Basic, alguns cliques seriam mais que suficientes para gerar o driver que ele precisa. Na tentativa de dar uma ideia do caminho e das dificuldades a serem enfrentadas nesta nova empreitada, vou listar alguns livros que considero ser um bom caminho a seguir para se escrever drivers.

Não ensinam isso na faculdade

Entre as tantas vezes que essa pergunta me foi feita, uma vez me perguntaram se isso é ensinado no curso de Engenharia de Computação, que era o curso que eu fazia na época. Não sei se todos aqui sabem, mas Engenharia de Computação é um curso que fica entre a Ciência da Computação e a Engenharia Eletrônica. Tentando reunir os principais assuntos de ambas as graduações, o curso não dá tanta ênfase em software nem em eletrônica. Apesar de estudar hardware, microprocessadores, linguagem C e sistemas operacionais, não chegamos nem perto do assunto drivers. Sabemos que eles existem e qual sua função, mas nada que chegue nem perto do exemplo mais simples desse blog.

Pô Fernando, como driver é software, então o curso de Ciência da Computação deve ensinar drivers.

Na verdade, os assuntos vistos em Ciência da Computação tratam de soluções mais alto nível, com mais detalhes em algoritmos específicos para problemas complexos. O fato é que esse curso fica mais distante do desenvolvimento de software básico. No mestrado isso fica ainda mais raro na área de software.

Driver é software de baixo nível, mas não exagere

Tenho um amigo que sempre gostou de estudar tudo nos mínimos detalhes. Tudo bem se a pessoa é um apaixonado por um determinado assunto e quer entendê-lo completamente, mas se o objetivo é escrever drivers, creio que seja melhor estudar apenas o necessário para um início rápido, e depois ir se aprofundando em cada assunto conforme a necessidade. Já vi algumas pessoas dizerem que antes de estudar a linguagem C, gostariam de estudar o completamente a linguagem assembly, ou mesmo começar a estudar o datasheet do processador Intel para ter o completo domínio sobre o assunto. Calma lá gente!

Eu aprendi a programar a linguagem C durante meu colegial técnico, eu não conheço as principais referências sobre o assunto, mas o livro que li é simples e, com relação à linguagem C, é abrangente o suficiente para programar. Por isso recomendo o livro Treinamento em Linguagem C. Você não vai precisar comprar verdadeiras bíblias sobre o assunto, que discutem qual a melhor forma de se compilar um determinado algoritmo.

A grande maioria dos drivers que vi nestes quase 10 anos de experiência em Kernel são escritos em C, e não em C++. A linguagem C é transparente e confiável. Não estou dizendo que não se deve confiar no C++, mas que com o tempo você olha para o código em linguagem C e consegue imaginar como seria o assembly gerado por ele. Isso ajuda bastante quando você precisa depurar sistemas onde os fontes não batem perfeitamente com a versão que você tem, ou mesmo na tentativa de entender situações onde não se tem o fonte. O C++ tem suas armadilhas com sobrecarga de operadores, templates e classes. Olhando para o fonte fica difícil imaginar o que realmente vai acontecer quando aquela linha for executada. Eu mesmo já desenvolvi drivers em C++ e já dei curso numa empresa onde Walter Oney deixou seus fontes de um driver todo em C++. Acho que é uma escolha pessoal, mas o fato é que você não precisa estudar C++ para escrever drivers.

Também já ouvi dizer “Primeiro vou aprender C++ completamente para depois começar a estudar o Kernel”. Acho que não preciso dizer que isso é completamente desnecessário. Sem falar que ainda não sei se é humanamente possível aprender C++ completamente.

Preciso ler Charles Petzold?

No curso que ofereço sobre desenvolvimento de drivers, um dos pré-requisitos é API básica do Windows. Quando digo básica, estou me referindo à manipulação de objetos de sistema, tais como arquivos, eventos, threads, processos, memória virtual e coisas do tipo. Não queria adentrar no universo sobre loop de mensagens, MFC, WTL e inúmeros conceitos de User-Mode que não serão utilizados em Kernel.

Um excelente livro sobre a arquitetura interna do sistema é descrita com muitos detalhes no livro Windows Internals 5th Edition. O único problema é que o livro tem mais de mil páginas e isso é um verdadeiro pé no freio para quem está empolgadão para sair escrevendo drivers. Uma alternativa viável é a temporária substituição desse livro pelo Inside Windows NT 2nd Editon. Um livro tem a metade do tamanho do Windows Internals e que traz os conceitos mais importantes sobre o sistema e que ainda são empregados no Windows 7. Quando você estiver mais a vontade, então poderá ler o Windows Internals, e quem sabe, até tirar uma certificação sobre o assunto.

Modelo Legacy

Depois de entender um pouco sobre a arquitetura do Windows, você já poderá começar a estudar o assunto chave. Mas para isso, você deverá escolher que tipo de driver você pretente desenvolver. O modelo mais antigo é o Legacy, que apesar de ter iniciado no Windows NT 3.51, ainda roda sobre o Windows 7. Este modelo não faz interações com o Plug-And-Play Manager, mas ainda é o modelo empregado no desenvolvimento de File Systems até hoje. Todas as regras usadas no modelo Legacy são também utilizadas no modelo WDM. Dessa forma, estudar o modelo Legacy é uma ótima introdução para estudar WDM ou drivers File System. O livro que recomendo para desenvolvimento Legacy é o Windows NT Device Driver Development. Estudar o modelo Legacy dá a oportunidade de o leitor se aprofundar em conceitos básicos e essenciais no desenvolvimento em Kernel Mode. Por ser um modelo mais simples, o cérebro do leitor consegue absorver com mais qualidade questões como: manipulação de IRPs, memória virtual, níveis de execução de um thread, mecânicas de sincronismo, objetos do sistema e por aí vai.

O leitor que estudar o modelo Legacy estará bem mais preparado para lidar como modelo WDM, que possui regras próprias que serão aplicadas utilizando os conceitos já adquiridos no modelo Legacy. O mesmo se pode dizer com relação ao desenvolvimento de drivers de File System. Apesar de os drivers de File System serem desenvolvidos em Legacy, não significa que ao aprender o modelo Legacy, aprende-se File Systems automagicamente. Este também é um assunto que requer um estudo dedicado.

Windows Driver Model (WDM)

Se o seu modelo alvo é WDM, então um bom livro para começar é o Windows 2000 Device Driver Book. Isso porque o grande problema dos livros de desenvolvimento de drivers é que muita teoria é necessária para se fazer um driver. No modelo WDM a coisa fica ainda mais carregada. Este não é o melhor livro de WDM que conheço, e já vi comentários em listas de discussão dizendo que este livro é conhecido pelos seus erros. O fato é que ele faz uma apresentação simplificada, e o melhor de tudo é que o autor cria um driver básico no início do livro e depois vai colocando mais conceitos em prática.  Com isso o leitor vai escrever um driver de exemplo antes de morrer louco com tanta teoria.

Depois de ter uma base em WDM, então você já estará pronto para ler um livro de gente grande, e não estou me referindo às revistas de nudez. O livro Programming the Windows Driver Model 2nd Edition é realmente muito bom. Eu mesmo já o li umas três vezes, mas como eu comentei antes, muitas páginas são necessárias para que o leitor possa começar a ver as coisas como um todo. Toda essa teoria cansa e confunde um leitor que está começando no assunto.

Drivers de File System

Se você é um dos poucos seres vivos desse planeta que precisam desenvolver ou dar suporte a drivers de File System, então estudar o modelo Legacy foi tranquilo. Alguns me perguntam: “O que tem de tão complicado em escrever drivers de File System?” O problema é que além dos conceitos do modelo Legacy, você precisa lidar com os próprios conceitos utilizados na integração dos drivers de File System com outros componentes do sistema. Estou falando dos seus inseparáveis amigos Memory Manager e Cache Manager. Várias estruturas e regras são impostas pelo sistema para que este componente possa cumprir seu papel de forma estável e eficiente. A falta de livros sobre o assunto adiciona alguma adrenalina ao processo de aprendizado. O livro Windows NT File Systems Internals é o único livro que trata do assunto para Windows. Espera-se que a OSR, empresa que atualmente detém os direitos deste livro, possa lançar uma versão atualizada dele, mas como o público alvo é restrito, talvez não haja incentivo bastante para a concretização disso.

Escrever drivers de File System é complicado e pode-se até imaginar que esse é o tipo de driver mais complicado que existe de se escrever, mas segundo a OSR e outras autoridades no assunto, essa posiçãp de driver mais complexo é ocupada pelos filtros de File System. Isso porque além de ter de conhecer as regras que drivers que File System precisam seguir, os filtros ainda não recebem todas as notificações que um o próprio driver recebe. Escrever esse tipo de filtro é estar entre duas caixas pretas que conversam com alta intensidade e intimidade, e ainda agregar valor a esse serviço. Quando fiz meu curso de Drivers de File System na OSR, Tony Mason disse que as coisas ficam realmente interessantes com a chegada do NTFS transacional.

Diante de tanta dificuldade, a Microsoft criou a classe de drivers chamada MiniFilters, que é uma espécie de Miniport driver para filtros de File System. Infelizmente ainda não existem livros que trate deste assunto. Você vai ter que encarar esse tópico no site do MSDN. O detalhe aqui é que minifilter drivers são módulos carregados pelo Filter Manager, que do ponto de vista do sistema, é mais um filtro de File System Legacy. Desta forma, conhecer o modelo Legacy é uma mão na roda na hora de depurar isso tudo.

Windows Driver Foundation (WDF)

Que o modelo WDM é complexo e trabalhoso, acho que todos concordamos. A fim de evitar tanta complexidade na hora de escrever drivers, a Microsoft criou um novo modelo que se apoia no WDM para implementar todos os comportamentos padrão de um driver por default. Isso significa que você pode escrever um driver de dispositivo e deixar que o framework faça a maior parte do trabalho com relação ao plug-and-play, gerenciamento de energia e inúmeros conceitos que são iguais para a grande maioria dos drivers. Caso você queira um comportamento especial em uma determinada situação, basta registrar a rotina de callback e ser feliz (ou não). O WDF traz dois frameworks, o Kernel-Mode Driver Framework (KMDF) e o User-Mode Driver Framework (UMDF). Desenvolver drivers em KMDF traz algumas poucas restrições em relação a WDM, mas vale a pena escrever um driver novo em WDF. Drivers em User-Mode trazem maiores restrições, principalmente ao barramento que seu dispositivo está e quão rápido o driver precisa ser. O livro Developing Drivers with the Windows Driver Foundation ainda é o único que trata deste assunto. Mais uma vez, muita teoria é apresentada até que um exemplo prático apareça. Outra característica interessante sobre este livro é que ambos os frameworks  são apresentados simultaneamente. Assim, você aprende a desenvolver drivers em KMDF e UMDF ao mesmo tempo. Dependendo do leitor, isso deve até dar um barato.

Se o que você quer mesmo é desenvolver driver em User-Mode, então vamos repensar a questão do C++. Como um driver UMDF deve ser implementado em COM, alguns recursos do C++ agora são requisitos básico para um desenvolvimento confortável. Mais uma vez vou recomendar um livro da Viviane, o Treinamento em Liguagem C++, que sob meu ponto de vista é suficiente para escrever drivers com relação à linguagem. Depois você pode se tornar um mestre ninja com Boost, STL e outras coisas que eu nem imagino.

Depois do C++, uma boa dose de COM vai bem. O livro que meu amigo Strauss me empresou, que foi direto ao ponto de forma clara e simples é o Essential COM do Don Box.

Toda essa base teórica lhe dará bagagem para que você possa escolher um tipo de driver e se aprofundar nele. Mesmo dentro do assunto Kernel, ainda existem inúmeras especialidades de drivers. Não pense que você poderá aprender este assunto completamente antes de sair desenvolvendo, então escolha logo o seu livro e coloque a mão na massa.

Até mais! 😉

Sétimo Encontro de Programadores C/C++

20 de July de 2010

Como alguns de vocês já sabem, no próximo dia 14 de agosto acontecerá o sétimo encontro de programadores do grupo C/C++ Brasil. Mais uma vez, será um grande prazer participar do evento como palestrante e ter a oportunidade de falar um pouco sobre o desenvolvimento de drivers (pra variar). 😉

Minha primeira participação nesse evento foi em sua quarta edição, onde fiz um overview sobre arquitetura e desenvolvimento de drivers para Windows. Os slides desta palestra podem ser baixados a partir deste post. Apesar de o feedback ter sido muito bom, recebi algumas sugestões sobre mostrar mais código fonte. O desenvolvimento de drivers requer muito conceito, tanto sobre o sistema operacional quanto sobre as  regras impostas pelo I/O Manager e seus inseparáveis amigos. Assim, imaginei que mostrar código fonte sobre estruturas e regras não conhecidas pela maioria dos participantes da palestra soaria como “Nana neném que a Cuca vem…”. Mas não tem jeito, programador gosta mesmo é de código fonte.

Na sexta edição foi que tive minha segunda participação. Desta vez foquei nas caraterísticas de funcionamento e utilização da memória virtual por drivers, dei mais ênfase aos nomes das rotinas que drivers utilizam para obter os serviços do Kernel. Pois é, embora tenha melhorado ainda não foi suficiente. O resultado do feedback mais uma vez revelou que faltou exemplos práticos e código fonte pra galera.

Nesta sétima edição vou praticamente escrever, compilar, instalar, executar e depurar um driver de exemplo. Tudo alí na hora. A proposta desta palestra é demostrar como dar os estes primeiros passos para se ter um driver compilado bem alí em seu próprio notebook.

Embora essa informação também esteja na página no evento, seguem os tópicos que serão apresentados por mim:

  • O que é o Windows Driver Kit
  • Device Drivers e Software Drivers
  • WDK e os arquivos de um projeto
  • A rotina DriverEntry e DriverUnload
  • Hands on: Escrevendo e compilando um driver
  • Testando drivers em máquinas vituais
  • Hands on: Instalando o driver exemplo
  • Depurando o Kernel do Windows
  • Hands on: Depurando seu driver
  • A Tela Azul da Morte (BSOD)
  • Hands on: A primeira Tela Azul a gente nunca esquece
  • Gerando crash dumps
  • Hands on: Analisando seu crash dump
  • Dúvidas, pânico e depressão

Parece muito? Pois é, para que isso seja possível no tempo que disponho será necessário que já se tenha o ambiente preparado. Você não vai querer ver uma palestra de uma hora que vai explicar de onde baixar as ferramentas, como instalá-las e como preparar uma máquina virtual que sirva de ambiente de testes para seu novo driver. Se tudo isso for feito no dia do evento, mais uma vez vou ficar devendo o código fonte e provavelmente serei enforcado em praça pública. Eu não sei sobre vocês, mas eu não gostaria que isso acontecesse.

Baixando e Instalando o WDK

O primeiro passo dessa preparação é baixar o kit de desenvolvimento de drivers da Microsoft, o Windows Driver Kit (WDK). Esse kit contém os headers e bibliotecas necessárias para que seu driver possa utilizar as rotinas exportadas pelo Kerne, permitindo a interação do seu driver com o restante do sistema operacional. Além dos headers e bibliotecas, o kit também traz o compilador e depurador de sistema, fazendo desse kit a única ferramenta necessário para que se tenha um driver compilado e depurado.

Ainda não falei dos vários e vários exemplos de drivers que o kit traz, bem como toda a documentação necessária para a utilização das rotinas exportadas pelo Kernel. Essa documentação não serve apenas de referência, mas também como um guia de construção de drivers. Embora a documentação se proponha a servir de guia de construção de drivers, ainda recomendo fortemente a leitura de livros dedicados a essa atividade. Os livros conseguem expor as idéias com mais clareza abordando os assuntos de forma gradativa.

O fato de haver um compilador nesse kit não significa que exista um ambiente de desenvolvimento específico para drivers. O que estou querendo dizer é que não existe uma versão do Visual Studio específica para drivers, ou mesmo um plug-in que fosse instalado no Visual Studio que agregasse tais funcionalidades ao ambiente. No caso mais básico, os fontes poderiam ser editados no bom e velho Notepad. Isso não nos impede de utilizar nosso editor do coração para escrever drivers, como mostra este outro post. De fato existem algumas ferramentas com esse propósito como o VisualDDK, mas não é nada oficial.

Baixar o WDK é fácil e indolor. E quando digo indolor, estou me referendo também no sentido financeiro da idéia. O primeiro passo é baixar o WDK que está disponível no Microsoft Download Center. Seguindo as instruções, você baixará um ISO de aproximadamente 620MB que conterá toda a instalação.

O ISO pode ser queimado em uma mídia de CD, ou você pode simplesmente usar um desses aplicativos que cria uma unidade de disco virtual a partir de um arquivo de imagem. Se você não tem nenhum aplicativo desses à mão, você pode  baixar e instalar gratuitamente o OsrVitrtualCdAndDisk da OSR.

Depois de inserir o CD, execute o aplicativo KitSetup.exe que estará na pasta raíz do CD. Fazendo isso você verá a splash screen do Windows Driver Kit 7.1.0 como exibida abaixo.

Em seguida a janela exibida abaixo.

Selecione os itens como exibido acima, deixando de selecionar os itens “Device Simulation Framework” e “Windows Device Testing Framework”. Em seguida clique em OK para ver a próxima janela como exibida na figura abaixo.

Pelo menos para a palestra, simplesmente aceite o caminho de instalação sugerido, isso vaip evitar contratempos durante a palestra. Clicando OK nesta janela, apenas aceite os termos da instalação e clique em OK na janela exibida abaixo para finalizar a instalação do WDK.

Aguarde a cópia dos arquivos terminar e a instalação se encerrará.

Dando uma compiladinha, só pra…

Depois de instalar o WDK você pode se certificar de que tudo está certo simplesmente compilado um dos exemplos instalados pelo próprio kit. Para fazer isso, abra o prompt de comando de build como demonstrado na figura abaixo.

Este prompt de comando é diferente do convencional por já estar devidamente configurado com as variáveis de ambiente necessárias para a compilação de drivers para Windows XP. Mais detalhes sobre isso na palestra. Com este prompt de comando, execute os passos indicados na figura a seguir.

Certifique-se de que o exemplo foi compilado verificando a saída do compilador como destacado na figura acima. Caso você tenha algum problema em seguir estes passos, mande-me um e-mail que a gente sai na porrada e tudo bem.

Baixando e Instalando o Virtual PC

Ter o ambiente de desenvolvimento configurado é essencial para o desenvolvimento de drivers, mas de nada adiantaria se você não tiver um ambiente de teste. Não caia na besteira de utilizar sua própria máquina de desenvolvimento como ambiente de teste. Tente pelo menos usar a máquina de desenvolvimento de um estagiário ou sei lá. Pense que você não vai escrever uma aplicação que, na pior das hipóteses, resultará no sonoro PAM. Dependendo do tipo de driver e do tamanho da orelhada que você aprontar, sua máquina pode ficar inutilizada, não iniciar mais ou mesmo ter dados corrompidos.

Depurar um driver de Kernel requer condições especiais. A rigor são necessárias duas máquinas para essa tarefa. Isso mesmo, duas máquinas. O que você está pensando? Que vai poder colocar um break-point em seu driver, interromper sua execução e dar um ALT+TAB para dar uma olhadinha na janela de variáveis locais? Nem pensar amigão. Quando um break-point interrope o sistema operacional, o Windows inteiro para. O mouse não se mexe, a janela não repinta o scheduler não trabalha, nada nada. Por essa razão é que precisamos de uma segunda máquia. Das suas máquinas, uma delas será sua vítima, a máquina na qual rodará seu driver e que será completamente congelada quando necessário. A outra será sua máquina de desenvolvimento. Nela estarão os códigos fontes, os símbolos e o ambiente de depuração. É em sua máquina de desenvolvimento que a janelinha de variáveis locais vai aparecer.

Hoje em dia podemos recorrer à virtualização para economizar tempo e espaço em nossas mesas. Utilizando uma máquina virtual podemos ter o ambiente que precisamos para dar continuidade à palestra e testar o driver que será desenvolvido. Máquinas virtuais ajudam em muitos casos, principalmente nos casos de desenvolvimento de drivers que não lidam com hardware diretamente. Este é o caso de drivers de anti-virus, firewall, file systems, alguns tipos de drivers USB e outros. Por outro lado, se você por exemplo tiver uma placa PCI para fazer port I/O, manipular interrupções ou mesmo fazer DMA, uma VM não vai ajudar muito, mas este é outro caso.

O Virtual PC é uma ferramenta gratuita da Microsoft e que pode ser baixada a partir deste link. Executando o arquivo baixado, teremos a seguinte janela como listrado na figura abaixo. Apenas clique em Next para a próxima janela.

Aceite os termos do produto e clique Next para dar continuidade a instalação.

Apenas aceite o diretório de destino sugerido e clique Next.

Depois de aguardar a cópia dos arquivos, finalize a instalação.

Criando uma nova VM

Este não é um blog especializado em máquinas virtuais, então nem tentem me mandar e-mails com dúvidas cabeludas sobre esse assunto. Entretanto, posso ajudar na criação da máquina virtual que usaremos na palestra. Afinal de contas teremos que destruir alguma coisa, caso contrário ficaria meio sem graça.

Nesta tela inicial exibida acima, apenas clique Next para continuar com a instalação.

Aqui selecione a opção de criação de uma nova máquina virtual e depois clique Next para continuar.

Neste passo você dá o nome da sua máquina virtual. Fique a vontade para colocar o nome que achar mais adequado. Zé, Jão e Toin são algumas opções válidas, mas por simples clareza vou preferir “Windows XP” mesmo.

Neste passo selecionamos o sistema operacional que será instalado nessa nova máquina. Isso serve para que a VM possa instalar as ferramentas certas de integração. Resumindo, selecione Windows XP e clique Next para continuar.

Apenas aceite a quantidade de memória recomendada pelo Wizard e clique Next.

Como vamos fazer uma nova instalação do Windows, precisaremos de uma nova HD que será formatada pela instalação do Windows. Selecione a opção indicada na figura acima e clique Next.

Bom, aqui você determina onde o arquivo que representa o disco rígido será criado. Sem muitas firulas apenas aceite o caminho sugerido e clique Next para continuar.

Finalize a criação da sua nova máquina virtual clicando em Finish e pronto.

Ufa! Agora sua máquina virtual está pronta para receber um novo sistema operacional.

Deste ponto em diante você pode instalar uma cópia do seu Windows XP. Outras versões do Windows poderiam ser instaladas, mas a fim de evitar diferenças nas configurações de debug durante a palestra, vamos combinar de instalar o Windows XP mesmo.

Caso você não tenha uma cópia do Windows XP para instalar, não há muito que eu possa fazer para te ajudar. O máximo que posso fazer é te dar o seguinte conselho: Vá até a igreja de Santa Ifigênia, que fica na avenida Santa Ifigênia bem no centro de São Paulo, e acenda uma vela para a santa. Aproveitando que você vai estar por lá, dê uma passeada pelo comercio local. Tenho certeza que você receberá uma luz em seu caminho.

Agora vou para por aqui, caso contrário eu não vou ter muito que falar no dia da palestra. Aproveite essa oportunidade para aprender os conceitos básicos (e bota básico nisso) sobre como gerar seu primeiro driver, sua primeira tela azul e por fim fazer seu primeiro crash analysis. Pense que você um dia vai poder contar isso para os seus netos. Embora isso não ajude muito, eles não vão fazer a menor ideia do que você está falando e vão acabar te internando de qualquer maneira.

Espero vê-los por lá. T+!

Download dos slides

Ponteiro perdido no Kernel pode corromper arquivos?

9 de June de 2010

A nova versão de um driver pode implementar aquela funcionalidade nova que você tanto esperava. Afinal de contas, o time de desenvolvimento de drivers anda sempre muito ocupado, e para conseguir alguma coisa nova é sempre um parto. O único problema é que de vez em outra uma tela azul acontece. Os mais desesperados podem até querer usar o novo driver a qualquer custo, mesmo que uma telinha azul apareça com uma frequência aceitável. Aí surge a perguntinha:

Fernando, existe algum problema se eu for usando esse driver novo até que uma correção para essa tela azul saia?

O principal problema aqui é que não temos a menor idéia do que está causando a tela azul. Esse tipo de classificação (sem a menor idéia) inclui um ponteiro que pode sair escrevendo onde não se deve. Além de sair atropelando estruturas vitais do sistema operacional, esse ponteiro bomba pode também corromper arquivos. Então, mais uma perguntinha:

Mas Fernando, esse driver nem faz manipulação de arquivos. Como é que um ponteiro perdido pode abrir um arquivo e ainda gerar uma escrita em disco para corrompê-lo?

Na verdade, isso não é assim tão difícil. Você já ouviu falar do Cache Manager?

Quem é esse tal de Cache Manager?

Vi uma definição simples de Cache Manager em uma palestra do Plugfest. Vamos ver se consigo reproduzí-la aqui. Vocês desenvolvedores provavelmente já fizeram cache do conteúdo de algum arquivo em uma aplicação para não ter que acessar o tal arquivo toda vez que precisar da informação contida nele, certo? Errado? Tá tá, então do começo hoje.

Lembra lá no prézinho quando a tia ensinou que se você fizer acesso com frequência a um arquivo, você pode manter uma cópia dele em memória e evitar de fazer tanto I/O para ganhar performance? Nesse caso uma área de memória, também conhecida como cache, é carregada com o conteúdo do arquivo. Depois disso, os vários acessos de leitura ao arquivo são substituídos por leituras no cache. Quando uma escrita acontece, o cache é atualizado e a escrita também vai para o arquivo. Em casos onde a escrita é frequente, o cache é atualizado a cada escrita enquanto o arquivo recebe várias modificações de uma vez em intervalos definidos.

O cache implementado na aplicação perde o sentido se um determinado arquivo é compartilhado por mais de uma aplicação, o conteúdo do cache da aplicação “A” tem que ser o mesmo do cache da aplicação “B”, caso contrário, as alterações feitas pela aplicação “A” não seriam vistas pela aplicação “B” e vice versa, sem falar que as atualizações seriam perdidas sem o correto sincronismo que isso exigiria.

Por essa razão é que existe um cache centralizado no sistema. Um módulo no Kernel que mantém páginas de memória contendo o conteúdo dos arquivos recentemente manipulados. Quando um arquivo é aberto, ele é registrado pelo seu respectivo driver de File System no Cache Manager. Mas o Cache Manager não faz tudo sozinho. Na verdade ele faz parte de uma gangue nas “quebrada” que garante a otimização de acessos a arquivos no sistema. Para isso o Cache Manager conta com a ajuda de seus fiéis companheiros, o Vitual Memory Manager e os File System Drivers.

Pulando algumas toneladas de detalhes, digamos que quando uma solicitação de leitura chega a um driver de file system, este a encaminha ao Cache Manager, este então vai satisfazer tal operação apenas copiando o conteúdo desejado do arquivo que já estaria em páginas de memória. Copiar os dados da memória é muito mais rápido que fazer todo o ritual para obter os mesmos dados do disco, mas para isso os dados já deveriam estar carregados na memória.

Fernando, o Cache Manager coloca todo o arquivo na memória?

Quer uma resposta tosca? Sim e Não ;-). O Cache Manager na verdade mapeia o arquivo aberto em memória, e para isso ele conta com as características mais básicas de memória virtual discutidas neste outro post. Um intervalo de endereços é reservado no sistema, mas tais endereços ainda não se refletem em espaços nos chips de memória, ou seja, existe um endereço de memória, mas seu contetúdo ainda está em disco. O Memory Manager protege esses endereços contra acessos que podem ocorrem a eles. Quando um acesso de leitura é realizado nestes endereços, um page fault ocorre e o Memory Manager então precisa recuperar os dados do arquivo que estão no disco e colocá-os em memória. Para fazer isso o Memory Manager vai criar uma solicitação de leitura no I/O Manager para que uma IRP possa ser entregue ao respectivo driver de file system do arquivo em questão.

Pára tudo “perlamor” de Deus! Fernando, você disse logo alí em cima que quando uma solicitação de leitura chega ao driver de file system, esta é encaminhada ao Cache Manager que vai copiar os dados já contidos na memória a fim de antender a solicitação. Mas agora você está dizendo que para carregar tais páginas de memória o Cache Manager troca uma idéia com o Memory Manager, que por sua vêz vai criar uma solicitação de leitura para os drivers de file system. Isso não lhe parece um pouco recursivo?

Eu diria completamente recursivo, mas lembre-se que isso vai ocorrer apenas quando o arquivo ainda não foi lido por nenhum processo, e portanto ainda não está no cache do sistema. Para diferenciar uma solicitação da outra, drivers de file system precisam verificar a flag IRP_NOCACHE nas solicitações que recebem. Quando as solicitações vêm de uma aplicação, estas não carregam a flag IRP_NOCACHE, e desta forma podem ser atendidas pelo Cache Manager, por outro lado quando o Memory Manager precisa suprir as páginas de memória do Cache Manager, tais solicitações precisam ignorar o conteúdo do cache, e por isso carregam a flag IRP_NOCACHE. Paga facilitar o entendimento de toda essa  máquina, observem os passos enumerados de uma leitura de arquivo que ainda não está no cache.

  1. Uma aplicação faz uma solicitação de leitura de um arquivo.
  2. I/O Manager cria uma IRP e a encaminha ao seu respectivo driver de file system.
  3. O driver de file system verifica a ausência da flag IRP_NOCACHE e solicita a cópia dos dados desse arquivo do cache para o buffer da aplicação.
  4. O Cache Manager tenta fazer a cópia acessando as páginas que foram mapeadas do arquivo. Com isso um page fault é gerado por esse acesso e é atendido pelo Memory Manager.
  5. O Memory Manager cria uma nova IRP para atender a necessidade de abastecer o Cache Manager. Essa solicitação é encaminhada recursivamente ao driver de file system.
  6. Dessa vez o driver verifica a presença da flag IRP_NOCACHE e então cria as solicitações que serão atendidas pelos drivers de disco ou de rede.
  7. As solicitações de leitura de mídia são atendidas.
  8. As páginas de memória são abastecidas e o page fault é satisteito.
  9. Memory Manager re-executa a tentativa de leitura do Cache Manager que gerou o page fault, mas desta vez a o acesso de leitura será bem sucedido, pois os dados agora estão no endereço de memória mapeado.
  10. O Cache Manager completa a cópia dos dados para o buffer da aplicação.
  11. O driver de file system completa a solicitação de leitura.
  12. Os dados são retornados para a aplicação que fez a solicitação inicial.

Atenção agora meninos e meninas: A sequência descrita acima ilustra o caso onde o Cache Manager ainda precisa carregar o arquivo em memória. As próximas tentativas de leituras são satisfeitas diretamente pelo Cache Manager, que não vai gerar um page fault. Não vão me matar de vergonha dizendo por aí que o sistema operacional sempre faz toda a sequência para cada leitura de arquivo.

Mais uma vez as regras básicas de memória virtual são aplicadas aqui para que conforme as páginas de memória vão deixando de ser acessadas com tanta frequência, elas perdem lugar nos chips de memória, e assim, se mais tarde forem acessadas novamente, um novo page fault será gerado.

Interessante ver como esses componentes, o I/O Manager, Cache Manager, Virtual Memory Manager, File System Drivers, sem falar dos filtros que ainda podem existir, todos trabalhando juntos como caixas pretas, cada um com seu papel e sem conhecer o funcionamento interno do outro, interagindo entre si apenas através de suas interfaces públicas. Óbviamente que para a felicidade de alguns e talvêz tristeza de outros, não coloquei todos os detalhes aqui, mas podem ser encontrados no conhecido livro da galinha preta.

Mas voltando ao assunto…

De maneira análoga, as escritas também utilizam essa mesma mecânica que envolve mapeamento de arquivos. O simples fato de escrever no intervalo de endereços que é mantido pelo Cache Manager vai fazer com que tal página seja marcada como modificada, e mais tarde o Memory Manager vai querer atualizar essa página em disco. Dessa forma podemos resumir que solicitações de escrita chegam aos drivers de File System e são encaminhadas ao Cache Manager, que vai simplesmente escrever nas páginas referentes ao conteúdo do arquivo e completar a solicitação. Page faults e threads de sistema vão se encarregar de atualizar o que for preciso no momento mais adequado. O importante a notar aqui é pensarmos no Cache Manager como um simples consumidor dos serviços do Memory Manager, tudo que ele precisa fazer é ler ou escrever em páginas de memória, e é aqui que o título do post começa a fazer sentido.

Nada impede um ponteiro retardado de escrever em páginas de memória que fazem referência ao conteúdo de arquivos. Se isso acontece, o restante do sistema vai se encarregar de atualizar as barbaridades desse ponteiro em disco corrompendo o arquivo. É fácil notar que nem é necessário tantos passos para isso acontecer.

  1. Um driver inexperiente come aquele pedaço de pizza que ficou esquecido no micro-ondas e fica bem loco. Depois de dar vexame, falar o que não devia, chorar e dizer que te considera pra caramba, o driver escreve em páginas de memória referente a um arquivo de dados. Tipo um daqueles do SQL que eu nem imagino a extensão.
  2. O coitado do Memory Manager faz seu trabalho para garantir o leitinho das crianças como se nada de errado tivesse acontecido.
  3. O driver de file system vai de embalo e consolida a completa falta de noção do driver, que numa hora dessas já está abraçado com o vaso sanitário.
  4. Esse passo não é ilustrado na sequência acima mas pode ser explicado nesse site.

Nessa hora você vai torcer para que seu driver novinho em folha escreva sobre alguma estrutura vital do sistema para que uma tela azul possa conter a atividade desse inconsequente. Por esse motivo é que o sistema está cheio de testes e verificações para garantir que os dados do usuário não sejam perdidos. Melhor ver uma tela azul do que ter consequências muito piores. Lembre-se do sábio Morphy:

Nada é tão ruim que não possa ser piorado

Ainda existem muitas outras características interessantes sobre o Cache Manager que eu gostaria de descrever aqui, como por exemplo a “Falha de escrita retardada”, mas esse post já está ficando muito grande.


Até mais… 😉

Prevenindo Execução de Processos

17 de May de 2010

Durante esse longo período em que estive distante de novos posts no blog, algumas coisas aconteceram e que mereceram um lugarzinho aqui na em minha listinha de posts a escrever. Uma delas foi a longa discussão que aconteceu na lista do grupo de C/C++. Ela falava sobre quais as passos a serem seguidos para se escrever um driver que faria um pouco de tudo sobre serviços de segurança. Um dos ítens que foi especialmente discutido foi a idéia de se escrever um driver que pudesse impedir que um determinado processo fosse executado. Já vou logo dizendo que não vou me envolver se isso resolve ou não um problema de sergurança. Não estou aqui pra discutir isso, e para ser bem sincero, eu nem quero. Neste post vou demonstrar de uma maneira bem simples como podemos evitar a execução de um processo.

Rastreando o tempo de vida dos Processos

Antes de sair colocando os dois pés no peito de um processo para que ele caia, vamos primeiro apenas ver como monitorar seu tempo de vida. Isso é facilmente feito chamando a rotina PsSetCreateProcessNotifyRoutine() que está disponível desde quando o arco-iris era preto e branco. Embora a documentação diga que está disponível desde o Windows 2000, já conheço essa rotina de outros carnavais e sei que está por aí pelo menos desde o finado Windows NT 3.51. Nossa, estou ficando velho. Mas voltando ao assunto, essa rotina registra uma função de callback que notifica nosso driver sobre o ínico e o término dos processos no sistema. Isso é especialmente útil se um determinado driver quer manter informações relacionadas aos processos, e assim, saber quando um processo foi encerrado é fundamental para liberar os recursos utilizados tais informações.

NTSTATUS PsSetCreateProcessNotifyRoutine(
  __in  PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine,
  __in  BOOLEAN Remove
);

A função de callback registrada por essa rotina tem a assinatura como exibido abaixo:

VOID
(*PCREATE_PROCESS_NOTIFY_ROUTINE) (
    IN HANDLE  ParentId,
    IN HANDLE  ProcessId,
    IN BOOLEAN  Create
    );

Bem simples não? O primeiro parâmetro é o ProcessId do processo pai nessa criação. Isso significa que, se por exemplo você iniciar o Notepad a partir do “Executar…” no menu Iniciar do Windows, teremos o Explorer.exe como processo pai do novo processo Notepad.exe. O segundo parâmetro é o ProcessId do processo sendo iniciado ou encerrado no momento da chamada. Por último e não menos importante, temos a flag que vai indicar se esta é uma notificação de início ou de término de um processo.

Uma coisa importante a notar aqui é sobre o ProcessId do processo pai. Esse parâmetro é confiável nas notificações de inicio de processo, mas nem tanto quando se trata do término. Isso ocorre porque quando um processo está sendo iniciado, seu processo pai ainda está lá, firme e forte, mas quando um processo termina, apesar de o ParentId trazer o mesmo valor da notificação de ínicio de processo, esse dado não tem mais validade. Deixa eu dar um exemplo pra ficar mais fácil.

  1. Processo1(32) cria Processo2(57), recebemos chamada: CreateProcessNotifyRoutine(32, 57, TRUE);
  2. Processo1(32) termina.
  3. Processo3(32) é criado e granha Id igual a 32.
  4. Processo2(57) termina e recebemos a chamada: CreateProcessNotifyRoutine(32, 57, FALSE);

Na notificação de término do Processo2 que ocorreu no passo 4,  o processo cujo Id é 32 agora é o Processo3, que por sinal, não é realmente o processo pai do processo2. Então ao reunir informações sobre um processo, o faça durante sua inicialização, mantenha estes dados em uma lista e depois as remova quando o processo terminar.

Registrar a função de callback é muito simples, mas o importante mesmo é remover esse registro quando o driver for descarregado. Consegue imaginar o que aconteceria se uma dessas notificações fosse entregue a um driver que não está mais na memória? Bom, eu consigo.

Obtendo o caminho da imagem de um processo

É provável que você queira obter mais informações sobre os processos envolvidos nessas notificações, uma dessas informações é o caminho do arquivo que está sendo executado. Pode-se obter essa informação utilizando o Id dos processos que recebemos na notificação de novo processo. Para isso, teremos que utilizar a rotina quase documentada ZwQueryInformationProcess(). Essa é uma API nativa que existe desde sempre mas que nunca foi documentada oficialmente. Para utilizá-la basta declarar sua assinatura como mostra abaixo.

NTSTATUS
ZwQueryInformationProcess(IN HANDLE ProcessHandle,
                          IN PROCESSINFOCLASS ProcessInformationClass,
                          OUT PVOID ProcessInformation,
                          IN ULONG ProcessInformationLength,
                          OUT PULONG ReturnLength OPTIONAL);

Se quiser saber mais sobre APIs nativas não documentadas, esse link é uma super mão na roda, mas nada como este livro.

O código abaixo utiliza essa API para obter a imagem de um processo a partir do seu Pid. Reparem que ZwQueryInformationProcess() pede um handle para o processo sobre o qual se quer obter informações. Para se obter esse handle precisaremos primeiro obter a estrutura EPROCESS que representa um processo em Kernel-Mode. Faremos isso utilizando a função PsLookupProcessByProcessId() que nos retornará um ponteiro para essa estrutura.

NTSTATUS PsLookupProcessByProcessId(
  __in   HANDLE ProcessId,
  __out  PEPROCESS *Process
);

Apesar de opaca, essa estrutura vai nos possibilitar obter o handle para processo representado por ela utilizando agora a função ObOpenObjectByPointer() do Object Manager.

NTSTATUS ObOpenObjectByPointer(
  __in      PVOID Object,
  __in      ULONG HandleAttributes,
  __in_opt  PACCESS_STATE PassedAccessState,
  __in      ACCESS_MASK DesiredAccess,
  __in_opt  POBJECT_TYPE ObjectType,
  __in      KPROCESSOR_MODE AccessMode,
  __out     PHANDLE Handle
);

Acho que tudo vai ficar mais fácil de ser entendido com o fonte abaixo. Afinal, uma linha de código vale mais que mil palavras. A função a seguir obtém a estrutura EPROCESS de um processo, em seguida obtém o handle para ele, com esse handle obtém-se as informações que queremos do processo. Tá, tá, tá… Segue o fonte, mas não esqueça de ler os comentários.

/****
***     GetProcessImageName
**
**      Retorna um PUNICODE_STRING contendo o caminho
**      da imagem utilizada pelo processo cujo Pid foi
**      fornecido como parâmetro.
*/
NTSTATUS
GetProcessImageName(HANDLE           hProcessId,
                    PUNICODE_STRING* ppusImageName)
{
    NTSTATUS        nts;
    PUNICODE_STRING pusImageName = NULL;
    ULONG           ulSize;
    HANDLE          hProcess;
    PEPROCESS       pEProcess;
 
    //-f--> Primeiro de tudo, zera a variável de saída.
    *ppusImageName = NULL;
 
    //-f--> Aqui obtemos a estrutura que representa um processo
    //     (EPROCESS) a partir do seu Pid;
    nts = PsLookupProcessByProcessId(hProcessId,
                                     &pEProcess);
    if (!NT_SUCCESS(nts))
        return nts;
 
    //-f--> Agora obtemos um handle para este objeto.
    nts = ObOpenObjectByPointer(pEProcess,
                                OBJ_KERNEL_HANDLE,
                                NULL,
                                0,
                                *PsProcessType,
                                KernelMode,
                                &hProcess);
 
    //-f--> Independente do handle ter sido ou não obtido,
    //      teremos que desfazer a referência que obtivemos
    //      para o EPROCESS.
    ObDereferenceObject(pEProcess);
    if (!NT_SUCCESS(nts))
        return nts;
 
    //-f--> Agora que temos o handle para o processo, podemos
    //      obter informações a respeito dele, nesse caso,
    //      vamos obter o caminho da imagem do processo.
    nts = ZwQueryInformationProcess(hProcess,
                                    ProcessImageFileName,
                                    NULL,
                                    0,
                                    &ulSize);
 
    //-f--> Para obter o tamanho certo, passamos zero na primeira
    //      tentativa, isso vai nos retornar um erro e a quantidade
    //      de bytes necessários para obter essa informação.
    if (nts != STATUS_INFO_LENGTH_MISMATCH)
        return nts;
 
    //-f--> O tamanho retornado inclui o tamanho da estrutura UNICODE_STRING,
    //      então tudo é alocado de uma única vez.
    pusImageName = (PUNICODE_STRING) ExAllocatePoolWithTag(PagedPool,
                                                           ulSize,
                                                           TRACER_TAG);
    //-f--> Oops! Fecha o Photo Shop e tenta de novo.
    if (!pusImageName)
        return STATUS_INSUFFICIENT_RESOURCES;
 
    //-f--> Agora oferecemos o buffer alocado com o tamanho certo.
    //      O que pode sair errado? (TUDO!)
    nts = ZwQueryInformationProcess(hProcess,
                                    ProcessImageFileName,
                                    pusImageName,
                                    ulSize,
                                    &ulSize);
    if (!NT_SUCCESS(nts))
    {
        //-f--> Oops! Algo deu errado.
        ExFreePoolWithTag(pusImageName,
                          TRACER_TAG);
    }
    else
    {
        //-f--> Tudo bem até aqui. A rotina chamadora fica encarregada
        //      de liberar a memória alocado aqui.
        *ppusImageName = pusImageName;
    }
 
    return nts;
}
 

Com essa rotina fica fácil escrever a seguinte função de callback que vai nos mostrar as informações básicas sobre os processos iniciados e terminados.

/****
***     OnCreateProcess
**
**      Função de callback que será registrada caso este
**      driver esteja rodando em Windows Vista ou anterior.
*/
VOID
OnCreateProcess(HANDLE  hParentId,
                HANDLE  hProcessId,
                BOOLEAN bCreate)
{
    //-f--> Aqui verificamos se o evento trata de uma criação
    //      ou término de um processo.
    if (bCreate)
    {
        NTSTATUS        nts;
        PUNICODE_STRING pusImageName = NULL;
 
        //-f--> Obtém o caminho da imagem que foi usada por
        //      este processo.
        nts = GetProcessImageName(hProcessId,
                                  &pusImageName);
 
        if (NT_SUCCESS(nts))
        {
            //-f--> Caso o caminho tenha sido obtido com sucesso,
            //      registra a notificação de início de processo.
            DbgPrint("[Process Tracer] Action = Starting\n"
                     "                 Process Id = 0x%x\n"
                     "                 Parent Id = 0x%x\n"
                     "                 Image name = %wZ\n\n",
                     hProcessId,
                     hParentId,
                     pusImageName);
 
            //-f--> Libera os recursos alocados.
            ExFreePool(pusImageName);
        }
    }
    else
    {
        //-f--> Registra o evento de término de processo.
        DbgPrint("[Process Tracer] Action = Finishing\n"
                 "                 Process Id = 0x%x\n"
                 "                 Parent Id = 0x%x\n\n",
                 hProcessId,
                 hParentId);
    }
}
 

Com estas funções trabalhando em um Windows XP, teremos a seguinte saída no depurador.

[Process Tracer] Action = Starting
                 Process Id = 0x35c
                 Parent Id = 0x694
                 Image name = \Device\HarddiskVolume1\WINDOWS\system32\notepad.exe
 
kd> !process 0x35c 0
Searching for Process with Cid == 35c
Cid handle table at e1003000 with 380 entries in use
 
PROCESS 82100020  SessionId: 0  Cid: 035c    Peb: 7ffd7000  ParentCid: 0694
    DirBase: 08840400  ObjectTable: e10d2400  HandleCount:  41.
    Image: notepad.exe
 
kd> !process 0x694 0
Searching for Process with Cid == 694
Cid handle table at e1003000 with 380 entries in use
 
PROCESS 821cc228  SessionId: 0  Cid: 0694    Peb: 7ffd6000  ParentCid: 0640
    DirBase: 08840200  ObjectTable: e18df2a0  HandleCount: 365.
    Image: explorer.exe
 
kd> g
[Process Tracer] Action = Finishing
                 Process Id = 0x35c
                 Parent Id = 0x694
 

Logo no início eu inicio o processo Notepad a partir do menu “Executar…” como eu comentei logo no início. Depois disso eu utilizo a extensão !process do WinDbg para obter informações mínimas sobre os processos envolvidos nesta notificação. Em seguida eu encerro o Notepad dando origem a última mensagem demonstrada acima.

Uma nova API no Windows Vista SP1

Tudo bem, isso é muito legal, mas estamos aqui para falar sobre como previnir que um certo processo seja executado. Impedir a execução de processo no Windows Vista virou coisa de criança com a nova rotiva PsSetCreateProcessNotifyRoutineEx() que tem sua assinatura listada logo abaixo:

NTSTATUS PsSetCreateProcessNotifyRoutineEx(
  __in  PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,
  __in  BOOLEAN Remove
);

Essa rotina registra uma função de calback que também notifica seu driver a sobre o início e término dos processos no sistema. O primeiro parâmetro indica a rotina de callback a ser registrada, enquanto que o segundo parâmetro indica se a rotina deve ser registrada ou removida.  Muito semelhante à sua irmã mais velha PsSetCreateProcessNotifyRoutine(). A rotina de callback deve ter a seguinte assinatura:

VOID CreateProcessNotifyEx(
  __inout   PEPROCESS Process,
  __in      HANDLE ProcessId,
  __in_opt  PPS_CREATE_NOTIFY_INFO CreateInfo
);

Fica fácil perceber que os parâmetros dessa função de callback mudaram bastante. Para saber se o evento se trata da criação ou do término de processo, basta verificar parâmetro CreateInfo. Se esse for não nulo, então se trata de um novo processo sendo executado, caso contrário do seu término. Vamos dar uma olhada nessa estrutura:

typedef struct _PS_CREATE_NOTIFY_INFO {
  SIZE_T              Size;
  union {
    ULONG  Flags;
    struct {
      ULONG FileOpenNameAvailable  :1;
      ULONG Reserved  :31;
    } ;
  } ;
  HANDLE              ParentProcessId;
  CLIENT_ID           CreatingThreadId;
  struct _FILE_OBJECT *FileObject;
  PCUNICODE_STRING    ImageFileName;
  PCUNICODE_STRING    CommandLine;
  NTSTATUS            CreationStatus;
} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;

Pois é, parace que a vida ficou bem mais fácil para quem quer buscar maiores detalhes sobre os processos envolvidos na notificação. O grande atrativo dessa nova versão é que podemos previnir a criação de um processo apenas modificando o campo CreationStatus. Apenas para exemplificar essa facilidade, eu escrevi a função de callback abaixo. Sempre leia os comentários.

/****
***     OnCreateProcessEx
**
**      Função de callback que será registrada caso este
**      driver esteja rodando em Windows Vista SP1 ou superior.
*/
VOID
OnCreateProcessEx(PEPROCESS                 pEProcess,
                  HANDLE                    hProcessId,
                  PPS_CREATE_NOTIFY_INFO    pCreateInfo)
{
    //-f--> Aqui verificamos se o evento trata de uma criação
    //      ou término de um processo.
    if (pCreateInfo)
    {
        UNICODE_STRING  usBlockingApp;
 
        //-f--> Como se trata apenas de um exemplo, estou colocando o path
        //      do arquivo hard coded aqui, mas lembre-se que referências às
        //      imagens de arquivos podem utilizar HarddiskVolume1 ou outras
        //      variações que mudam dependendo de muitas coisas.
        RtlInitUnicodeString(&usBlockingApp,
                             L"\\??\\C:\\Windows\\System32\\Notepad.exe");
 
        //-f--> Comparando a imagem do processo que acabada de ser criado
        //      com o caminho que usei acima.
        if (RtlEqualUnicodeString(&usBlockingApp,
                                  pCreateInfo->ImageFileName,
                                  TRUE))
        {
            //-f--> Tudo bem, agora é apertar o maluco e chegar apavorando:
            //      "Perdeu praybooy!"
            DbgPrint("[Process Tracer] Action = Blocking\n"
                     "                 Process Id = 0x%x\n"
                     "                 Parent Id = 0x%x\n"
                     "                 Image name = %wZ\n\n",
                     hProcessId,
                     pCreateInfo->ParentProcessId,
                     pCreateInfo->ImageFileName);
 
            //-f--> Muda o status da criação do processo fazendo com que
            //      ela não prossiga.
            pCreateInfo->CreationStatus = STATUS_ACCESS_DENIED;
        }
        else
        {
            //-f--> Não é o nosso "homem", deixa o processo inciar normalmente.
            DbgPrint("[Process Tracer] Action = Starting\n"
                     "                 Process Id = 0x%x\n"
                     "                 Parent Id = 0x%x\n"
                     "                 Image name = %wZ\n\n",
                     hProcessId,
                     pCreateInfo->ParentProcessId,
                     pCreateInfo->ImageFileName);
        }
    }
    else
    {
        //-f--> Aqui apenas registramos a notificação do término de
        //      um processo.
        DbgPrint("[Process Tracer] Action = Finishing\n"
                 "                 Process Id = 0x%x\n\n",
                 hProcessId);
    }
}
 

Repare que o caminho para o arquivo que estou impedindo sua criação está hard-coded no fonte de exemplo. Esse caminho pode ter sua sintaxe diferente para o mesmo arquivo dependendo de como o processo é criado ou em qual versão do Windows estamos rodando. Por essa razão, caso queria rodar esse teste na sua casa, verifique se a sintaxe está como utilizei aqui, caso contrário acerte e recompile o exemplo.

Um driver, duas opções

Esta nova API está disponível apenas para Windows Vista SP1 e posteriores, mas é provável que você queria ter um driver que seja capaz de ainda rodar em versões anteriores do Windows mesmo que o sistema não tenha suporte a essa rotina. Como você já deve saber, simplesmente chamar a rotina PsSetCreateProcessNotifyRoutineEx() no seu driver vai criar uma dependência estática e seu driver não será capaz de ser carregado em versões mais antigas do Windows.

Para previnir essa dependência estática tendo um único binário que possa ser carregado tanto em versões mais antigas como nas mais novas, utilizando a versão mais nova dessa rotina, usaremos a função MmGetSystemRoutineAddress(), que é a irmã Kernel-Mode da bem conhecida GetProcAddress() em User-Mode. O driver de exemplo disponível para download no final deste post possui essas caracteristicas justamente para demonstrar como isso pode der feito. Obviamente que rodando o driver em Windows XP não teremos o suporte do sistema operacional para interromper um processo, e teremos que recorrer a técnicas alternativas para obter o mesmo resultado.

A função DriverEntry para este driver fica da seguinte maneira:

/****
***     DriverEntry
**
**      Ponto de entrada do driver. Se ainda estiver pensando
**      é fácil, não se preocupe, você acabará mudando de opinião.
*/
NTSTATUS
DriverEntry(PDRIVER_OBJECT     pDriverObj,
            PUNICODE_STRING    pusRegistryPath)
{
    UNICODE_STRING  usSystemRoutine;
    NTSTATUS        nts;
 
    //-f--> Registra nossa função de finalização para que
    //      nosso driver possa ser descarregado.
    pDriverObj->DriverUnload = OnDriverUnload;
 
    //-f--> Inicia o nome da rotina que tentaremos buscar dinamicamente.
    RtlInitUnicodeString(&usSystemRoutine,
                         L"PsSetCreateProcessNotifyRoutineEx");
 
    //-f--> Aqui verificamos de o sistema já tem suporte a PsSetCreateProcessNotifyRoutineEx
    //      Exatamente como a tia ensinou GetProcessAddress() lá no prézinho.
    *(PVOID*)&pfPsSetCreateProcessNotifyRoutineEx = MmGetSystemRoutineAddress(&usSystemRoutine);
 
    if (pfPsSetCreateProcessNotifyRoutineEx)
    {
        //-f--> Se o estivermos em Windows Vista SP1 ou superior, teremos o endereço
        //      desta rotina, e portanto, vamos nos registrar com ela.
        nts = pfPsSetCreateProcessNotifyRoutineEx(OnCreateProcessEx,
                                                  FALSE);
    }
    else
    {
        //-f--> Puuts! Estamos rodando em algum 386. Vamos nos registrar com
        //      essa rotina da década de 90.
        nts = PsSetCreateProcessNotifyRoutine(OnCreateProcess,
                                              FALSE);
    }
 
    ASSERT(NT_SUCCESS(nts));
    return nts;
}

Testando a PsSetCreateProcessNotifyRoutineEx()

Até aqui você deve estar dando pulos de alegria imaginando que aquele seu driver de dominar o mundo finalmente vai funcionar com grande facilidade utilzando essa nova API, mas o caso é que essa rotina não é para qualquer um. Isso porque apenas driver digitalmente assinados podem chamar essa rotina nova sem receber o retorno STATUS_ACCESS_DENIED.

Mas Fernando, como vou poder testar o seu driver de exemplo? Não tenho certificado nem nada!

Esta rotina inicialmente verifica de o módulo onde seu driver está definido tem o bit de verificação de integridade setado. Para setar esse bit só de brincadeirinha, adicione a opção /INTEGRITYCHECK nas opções do linker do seu projeto. O arquivo sources do projeto de exemplo está da seguinte forma:

TARGETNAME=ProcessTracer
TARGETTYPE=DRIVER
 
SOURCES=ProcessTracer.cpp
 
LINKER_FLAGS=/INTEGRITYCHECK
 

Isso fará com que o sistema verifique a assinatura do seu driver, mas como seu driver não está assinado, você ainda receberá o mesmo erro. Para finalmente ver isso funcionar sem ter mesmo um certificado, você terá que desabilitar e verificacão de integridade de código para drivers no Windows Vista.

Tudo bem, sem pânico. Inicie o Windows Vista e pressione F8 logo que o Boot iniciar, em seguida selecione a opção abaixo no menú que aparecerá como mostra abaixo:

Com estas duas modificações é possível testar o driver de exemplo e ter uma saída no depurador como a ilustrada abaixo:

[Process Tracer] Action = Blocking
                 Process Id = 0x3a4
                 Parent Id = 0xd98
                 Image name = \??\C:\Windows\system32\notepad.exe

Neste caso, mais uma vez, tentei iniciar o Notepad pelo menú “Executar…”, mas desta vez a saída que obtive foi a exibida abaixo:

Ufa! Mais um post gigante para a coleção. Espero ter ajudado e até mais!

ProcessTracer.zip

Escrevendo Filtros

20 de January de 2010

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

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

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

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

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

Escrevendo a DriverEntry

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

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

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

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

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

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

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

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

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

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

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

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

VOID 
  ObDereferenceObject(
    IN PVOID  Object
    );

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

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

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

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

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

"O que vem a ser um device extension?"

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

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

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

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

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

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

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

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

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

Na verdade existe, a IoAttachDeviceToDeviceStack().

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

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


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


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


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


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

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

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

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

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

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

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

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

Escrevendo a OnDriverUnload

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

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

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

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

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

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

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

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

Escrevendo Dispatch Routines

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

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

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

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

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

Testando o Filtro

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

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

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

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

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

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

Até mais!

StringFilter.zip

Drivers de Boot no Windows

17 de December de 2009

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!

Notificando eventos à aplicação

18 de August de 2009

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

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

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

Um modelo Cliente-Servidor

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

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

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

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

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

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

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

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

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

“Mas Fernando, setembro chove?”

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

Operações de I/O pendentes

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

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

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

Compartilhando um evento

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

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

Image Notifier

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

O resultado de tanto bla-bla-bla

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


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

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

ImgNotifier.zip

Strings no Kernel

7 de July de 2009

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

E no prézinho…

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

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

Jumento Celestino / Mamonas Assassinas

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

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

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

Quatro tipos de strings

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

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

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

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

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

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

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

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

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

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

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

Inicializando Strings

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

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

Reparem nos valores destas estruturas.

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

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

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

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

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

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

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


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

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

O terminador é necessário?

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

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

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

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

Manipulando Strings

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

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

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

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

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

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

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

Mas onde está o buffer mesmo?

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

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

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

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

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

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

Safe Strings em Kernel

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

Mais um driver de exemplo

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

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

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

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

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

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

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

Have fun!

StringList.zip

Levando a tela azul pra casa

18 de June de 2009

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!