Step into Kernel (VMware 7+WinDbg)

4 de September de 2010 - Fernando Roberto

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 - Fernando Roberto

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 - Fernando Roberto

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

Mapeando Arquivos em Memória

26 de June de 2010 - Fernando Roberto

Depois de ilustrar algumas das características do Memory Manager sendo um provedor de serviços ao Cache Manager no post anterior, hoje vou demonstrar que meras aplicações User-Mode também podem utilizar tais serviços. Mapeando arquivos em memória a aplicação ganha um intervalo de endereços virtuais que contém o conteúdo do arquivo. O acesso ao conteúdo do arquivo se dá simplesmente desreferenciando um ponteiro, sem a necessidade de chamar as funções ReadFile() ou WriteFile().

Quer uma necessidade para isso? Imagine que sua aplicação precise fazer a busca por uma determinada string em um arquivo, digamos “DriverEntry”. Em um desevolvimento “arroz com feijão”, o handle do arquivo é obtido através da chamada à função CreateFile(), e um buffer recebe o conteúdo parcial do arquivo, vamos supor 200 bytes. Uma simples função de busca da API poderia fazer tal busca no buffer.

Essa solução seria perfeita se não houvesse a possibilidade de a palavra buscada cair nas extremidades do buffer tal como ilustrado abaixo.

Um algoritmo mais espertinho teria que ser utilizado para identificar o prefixo e continuar a busca na próxima leitura do arquivo.

Este é apenas um simples exemplo, mas que ilustra com clareza uma das vantagens de se mapear arquivos. Se houvesse uma simples função que recebesse o path de um arquivo e nos retornasse um ponteiro para o conteúdo dele,  a busca seria bem simples.

Arquivos mapeados em memória também podem facilitar a escrita em seu conteúdo. Escrevendo no ponteiro recebido por tal mapeamento, o Memory Manager vai se encarregar de fazer o I/O necessário para que este novo conteúdo chegue ao disco.

Uma simples função de mapeamento

Aqui vou exemplificar o uso das rotinas que mapeiam um arquivo em memória. Os comentários seguem na explicação.

/****
***     MapFileToMemory
**
**      Rotina que recebe o path de um arquivo que será
**      mapeado em memória para leitura. Um endereço é
**      retornado à rotina chamadora bem como o tamanho
**      do arquivo.
*/
 
DWORD MapFileToMemory(LPCTSTR   tzFileName,
                      LPVOID*   ppMemory,
                      LPDWORD   pdwSize)
{
    HANDLE  hFile = NULL,
            hMapping = NULL;
    DWORD   dwError = ERROR_SUCCESS;
 
    __try
    {
        __try
        {
            //-f--> Zera variáveis de saída.
            *pdwSize = NULL;
            *ppMemory = NULL;
 
            //-f--> Aqui abrimos o arquivo a ser mapeado
            hFile = CreateFile(tzFileName,
                               GENERIC_READ,
                               FILE_SHARE_READ | FILE_SHARE_DELETE,
                               NULL,
                               OPEN_EXISTING,
                               FILE_ATTRIBUTE_NORMAL,
                               NULL);
 
            //-f--> Vericamos se o arquivo foi aberto, senão
            //      o homem do saco vem e nos leva.
            if (hFile == INVALID_HANDLE_VALUE)
                RaiseException(GetLastError(),
                               0,
                               0,
                               NULL);
 
            //-f--> Embora o tamanho do arquivo não seja necessário
            //      nesta função, vamos aproveitar que temos o handle
            //      do arquivo em mãos para obter essa informação para
            //      a rotina chamadora que vai precisar dela.
            *pdwSize = GetFileSize(hFile,
                                   NULL);
 
            //-f--> Aqui criamos um mapeamento do arquivo.
            //      Em kernel seria o equivalente a se criar uma
            //      section do arquivo.
            hMapping = CreateFileMapping(hFile,
                                         NULL,
                                         PAGE_READONLY,
                                         0,
                                         0,
                                         NULL);
 
            //-f--> Prevenindo o homem do saco.
            if (!hMapping)
                RaiseException(GetLastError(),
                               0,
                               0,
                               NULL);
 
            //-f--> Aqui sim o mapeamento é feito e ganhamos o
            //      intervalo de endereços que conterá o conteúdo
            //      do arquivo.
            *ppMemory = MapViewOfFile(hMapping,
                                      FILE_MAP_READ,
                                      0,
                                      0,
                                      0);
 
            //-f--> A mesma treta do saco que já comentei.
            if (!*ppMemory)
                RaiseException(GetLastError(),
                               0,
                               0,
                               NULL);
        }
        __finally
        {
            //-f--> Aqui é onde faremos toda a faxina
            //      fechando os handles que foram abertos.
            if (hFile)
                CloseHandle(hFile);
 
            if (hMapping)
                CloseHandle(hMapping);
        }
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        //-f--> Oops! Alguma coisa não saiu como foi ensaiado.
        //      Encontre um culpado e faça de conta que não é com você.
        dwError = GetExceptionCode();
    }
 
    return dwError;
}
 

Este exemplo é realmente bem simples, mas sinta-se à vontade para adicionar parâmetros que tornem essa função mais flexível e complexa.

“Fernando, mesmo que o arquivo tenha sido mapeado com succeso, você fecha o handles do arquivo e do mapeamento. Isso não deveria liberar as refências que este programa tem com o arquivo?”

Na verdade, depois de criarmos o mapeamento de arquivo utilizando a rotina CreateFileMapping() que recebe o handle do arquivo, uma referência extra já foi feita ao arquivo e assim já poderíamos fechar o handle dele se quisessemos. O mesmo acontece com a chamada da rotina MapViewOfFile(), que recebe o handle do mapeamento, e que por sua vez possui uma referência indireta ao arquivo mapeado. Ou seja, depois de tudo mapeado podemos fechar todos os handles e deixar as referências indiretas tomarem conta disso.

No próximo código fonte veremos um exemplo simples de utilização dessa função.

/****
***     _tmain
**
**      Simples utilização da função de mapeamento de arquivo.
**      Só pra não dizer que não fiz tudim tudim...
*/
 
int _tmain(int argc, _TCHAR* argv[])
{
    PBYTE   pBuffer;
    DWORD   dwError, dwSize, i;
 
    //-f--> Passa o nome do arquivo e obtém o ponteiro
    //      com seu conteúdo mapeado. Simples assim...
    dwError = MapFileToMemory(_T("C:\\Temp\\Test.txt"),
                              (LPVOID*)&pBuffer,
                              &dwSize);
 
    //-f--> Testar erro nunca é demais.
    if (dwError == ERROR_SUCCESS)
    {
        //-f--> Momentos de suspense antes de tocar o endereço.
        printf("Hit any key to access the buffer at 0x%p...\n", pBuffer);
        _getch();
 
        //-f--> Imprime cada caractere armzenado no arquivo.
        //      "Olha mamãe! Sem o ReadFile()!"
        for (i=0; i
            printf("%c", pBuffer[i]);
 
        //-f--> Aqui o mapeamento é desfeito.
        UnmapViewOfFile(pBuffer);
    }
 
    return dwError;
}
 

Ao final deste exemplo podemos observar a chamada à rotina UnmapViewOfFile(), que recebe simplesmente o ponteiro base do mapeamento do arquivo. Com essa chamada, todas as referências internas são desfeitas e o arquivo finalmente é fechado.

Testando o brinquedo

Para que possamos fazer um teste besta, crie um arquivo texto utilizando o Notepad.exe.

Rodando a aplicação de teste temos a saída como ilustrada abaixo.

Agora na câmera lenta do replay

Com o WinDbg podemos observar o exato momento em que a aplicação acessa o intervalo de endereços referente ao conteúdo do arquivo. Para isso vamos colocar um breakpoint na rotina de leitura de arquivo do driver Ntfs.sys, desta forma poderemos ver a requisição do Memóry Manager ser atendida. Para que isso aconteça, o arquivo texto não pode estar no cache do sistema, então se você já rodou a aplicação de teste ao menos uma vez, você deverá reiniciar o sistema.

Caso você ainda não tenha utilizado o WinDbg e não sabe como conectá-lo ao sistema, então leia este post para um quick start. Depois de conectar o WinDbg ao Kernel do sistema, vá até o diretório onde está a aplicação de teste e a execute, mas ainda não pressione qualquer tecla deixando-a parada como mostra a seguir:

Depois disso pressione Ctrl+Break no WinDbg para que você adquira o controle sobre o sistema depurado, que neste momento vai permanecer congelado.

Para colocar o tal breakpoint na rotina de leitura de arquivo do Ntfs.sys, precisaremos saber onde está essa rotina dentro do driver. Podemos obter essa informação utilizando a extenção !drvobj do WinDbg como exibido abaixo.

kd> !drvobj \FileSystem\Ntfs 2
Driver object (843fd650) is for:
 \FileSystem\Ntfs
DriverEntry:   828f5b75    Ntfs!GsDriverEntry
DriverStartIo: 00000000    
DriverUnload:  00000000    
AddDevice:     00000000    
 
Dispatch routines:
[00] IRP_MJ_CREATE                      8289400a    Ntfs!NtfsFsdCreate
[01] IRP_MJ_CREATE_NAMED_PIPE           8165a013    nt!IopInvalidDeviceRequest
[02] IRP_MJ_CLOSE                       82896fcf    Ntfs!NtfsFsdClose
[03] IRP_MJ_READ                        82818514    Ntfs!NtfsFsdRead
[04] IRP_MJ_WRITE                       82815638    Ntfs!NtfsFsdWrite
[05] IRP_MJ_QUERY_INFORMATION           82895a88    Ntfs!NtfsFsdDispatchWait
[06] IRP_MJ_SET_INFORMATION             8281e950    Ntfs!NtfsFsdSetInformation
[07] IRP_MJ_QUERY_EA                    82895a88    Ntfs!NtfsFsdDispatchWait
[08] IRP_MJ_SET_EA                      82895a88    Ntfs!NtfsFsdDispatchWait
[09] IRP_MJ_FLUSH_BUFFERS               82884349    Ntfs!NtfsFsdFlushBuffers
[0a] IRP_MJ_QUERY_VOLUME_INFORMATION    828b5fc6    Ntfs!NtfsFsdDispatch
[0b] IRP_MJ_SET_VOLUME_INFORMATION      828b5fc6    Ntfs!NtfsFsdDispatch
[0c] IRP_MJ_DIRECTORY_CONTROL           828b5d41    Ntfs!NtfsFsdDirectoryControl
[0d] IRP_MJ_FILE_SYSTEM_CONTROL         8289970e    Ntfs!NtfsFsdFileSystemControl
[0e] IRP_MJ_DEVICE_CONTROL              82879466    Ntfs!NtfsFsdDeviceControl
[0f] IRP_MJ_INTERNAL_DEVICE_CONTROL     8165a013    nt!IopInvalidDeviceRequest
[10] IRP_MJ_SHUTDOWN                    8282b36b    Ntfs!NtfsFsdShutdown
[11] IRP_MJ_LOCK_CONTROL                82823b7a    Ntfs!NtfsFsdLockControl
[12] IRP_MJ_CLEANUP                     828a1d42    Ntfs!NtfsFsdCleanup
[13] IRP_MJ_CREATE_MAILSLOT             8165a013    nt!IopInvalidDeviceRequest
[14] IRP_MJ_QUERY_SECURITY              828b5fc6    Ntfs!NtfsFsdDispatch
[15] IRP_MJ_SET_SECURITY                828b5fc6    Ntfs!NtfsFsdDispatch
[16] IRP_MJ_POWER                       8165a013    nt!IopInvalidDeviceRequest
[17] IRP_MJ_SYSTEM_CONTROL              8165a013    nt!IopInvalidDeviceRequest
[18] IRP_MJ_DEVICE_CHANGE               8165a013    nt!IopInvalidDeviceRequest
[19] IRP_MJ_QUERY_QUOTA                 82895a88    Ntfs!NtfsFsdDispatchWait
[1a] IRP_MJ_SET_QUOTA                   82895a88    Ntfs!NtfsFsdDispatchWait
[1b] IRP_MJ_PNP                         8286137b    Ntfs!NtfsFsdPnp
 
Fast I/O routines:
FastIoCheckIfPossible                   8288187b    Ntfs!NtfsFastIoCheckIfPossible
FastIoRead                              82880c38    Ntfs!NtfsCopyReadA
FastIoWrite                             82881f53    Ntfs!NtfsCopyWriteA
FastIoQueryBasicInfo                    82888c3a    Ntfs!NtfsFastQueryBasicInfo
FastIoQueryStandardInfo                 82888aa6    Ntfs!NtfsFastQueryStdInfo
FastIoLock                              8287bf41    Ntfs!NtfsFastLock
FastIoUnlockSingle                      8287bd75    Ntfs!NtfsFastUnlockSingle
FastIoUnlockAll                         828cd7b3    Ntfs!NtfsFastUnlockAll
FastIoUnlockAllByKey                    828cd958    Ntfs!NtfsFastUnlockAllByKey
ReleaseFileForNtCreateSection           8281e904    Ntfs!NtfsReleaseForCreateSection
FastIoQueryNetworkOpenInfo              8287ad84    Ntfs!NtfsFastQueryNetworkOpenInfo
AcquireForModWrite                      8280c892    Ntfs!NtfsAcquireFileForModWrite
MdlRead                                 828cd0d8    Ntfs!NtfsMdlReadA
MdlReadComplete                         81650af6    nt!FsRtlMdlReadCompleteDev
PrepareMdlWrite                         828cd31f    Ntfs!NtfsPrepareMdlWriteA
MdlWriteComplete                        817f5a9a    nt!FsRtlMdlWriteCompleteDev
FastIoQueryOpen                         82874d03    Ntfs!NtfsNetworkOpenCreate
AcquireForCcFlush                       8281ab35    Ntfs!NtfsAcquireFileForCcFlush
ReleaseForCcFlush                       8281aa9c    Ntfs!NtfsReleaseFileForCcFlush
 

Como você deve estar imaginando, a rotina de leitura do Ntfs é utilizada com muita frequência, o que faria este breakpoint parar muitas vezes sem ter a menor relação com o nosso teste. Para limitar o escopo do breakpoint, vamos fazer com que ele se aplique somente à thread que fará a solicicação que estamos esperando.

Como já expliquei no post anterior, quando o endereço de memória é obtido, o arquivo ainda não foi lido. Quando a aplicação desreferenciar este ponteiro buscando os dados, um page fault será gerado e o Memory Manager vai tomar o controle sobre a thread por meio de um trap de sistema. Essa é a thread que será utilizada para realizar a leitura do arquivo que vai abastecer a página de memória à pedido do Memory Manager. Esta é a razão pela qual nosso programa de teste espera uma tecla ser pressionada antes de acessar o buffer. Isso nos dá a oportunidade de obter a identificação da thread que está aguardando esse evento.

Utilizamos a extenção !process para localizar o nosso programa de teste e também listará suas threads, que em nosso caso é uma única.

kd> !process 0 2 MapFile.exe
PROCESS 84bf6d90  SessionId: 1  Cid: 0b60    Peb: 7ffdf000  ParentCid: 0b40
    DirBase: 1f09b4c0  ObjectTable: 8e77ccd0  HandleCount:   5.
    Image: MapFile.exe
 
        THREAD 84f6cb50  Cid 0b60.0b64  Teb: 7ffde000 Win32Thread: 00000000 WAIT: (WrLpcReply) ...
            84f6cd64  Semaphore Limit 0x1
 
kd> bp /1 /t 84f6cb50 Ntfs!NtfsFsdRead
kd> g
 

Depois de colocado o breakpoint, podemos liberar a execução do sistema e teclar algo na aplicação de teste. Isso fará com que nosso breakpoint interrompa o sistema bem como pretendíamos. Olhando para a pilha de chamadas que temos no momento, podemos evidenciar a execução do trap que foi gerado pela aplicação de teste. Este trap está sendo atendido pelo Memory Manager.

Breakpoint 0 hit
Ntfs!NtfsFsdRead:
82818514 6a40
 
kd> kb
ChildEBP RetAddr  Args to Child
8f395b6c 816f00c3 84438020 84f40290 84f40290 Ntfs!NtfsFsdRead
8f395b84 821a3ba7 84437730 84f40290 00000000 nt!IofCallDriver+0x63
8f395ba8 821a3d64 8f395bc8 84437730 00000000 fltmgr!FltpLegacyProcessingAfterPreCallbacksCompleted+0x251
8f395be0 816f00c3 84437730 84f40290 00000000 fltmgr!FltpDispatch+0xc2
8f395bf8 8167bf2e 84f6cb50 8443a18c 8443a158 nt!IofCallDriver+0x63
8f395c14 816b8d51 00000043 84f6cb50 8443a198 nt!IoPageRead+0x172
8f395cd0 816db03f 00020000 90825810 00000000 nt!MiDispatchFault+0xd18
8f395d4c 8168ebf4 00000000 00020000 00000001 nt!MmAccessFault+0x1fb7
8f395d4c 0018d972 00000000 00020000 00000001 nt!KiTrap0E+0xdc
0015fbc0 0018ec36 00000001 00281a28 00281a78 MapFile!wmain+0x72
0015fc0c 0018eb0f 0015fc20 77554911 7ffdf000 MapFile!__tmainCRTStartup+0x116
0015fc14 77554911 7ffdf000 0015fc60 77ace4b6 MapFile!wmainCRTStartup+0xf
0015fc20 77ace4b6 7ffdf000 77a26775 00000000 kernel32!BaseThreadInitThunk+0xe
0015fc60 77ace489 0018b532 7ffdf000 00000000 ntdll!__RtlUserThreadStart+0x23
0015fc78 00000000 0018b532 7ffdf000 00000000 ntdll!_RtlUserThreadStart+0x1b
 

Como conhecemos o protótipo que uma rotina de dispatch precisa ter, sabemos que o segundo parâmetro da rotina NtfsFsdRead() é o endereço da IRP que o driver recebeu. Utilizando a extensão !irp podemos obter detalhes sobre a IRP. Isso nos permite conhecer o FileObject ao qual está destinado essa solicitação.

kd> !irp 84f40290 
Irp is active with 8 stacks 8 is current (= 0x84f403fc)
 Mdl=8443a1d8: No System Buffer: Thread 84f6cb50:  Irp stack trace.  
     cmd  flg cl Device   File     Completion-Context
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    
 
            Args: 00000000 00000000 00000000 00000000
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    
 
            Args: 00000000 00000000 00000000 00000000
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    
 
            Args: 00000000 00000000 00000000 00000000
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    
 
            Args: 00000000 00000000 00000000 00000000
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    
 
            Args: 00000000 00000000 00000000 00000000
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    
 
            Args: 00000000 00000000 00000000 00000000
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    
 
            Args: 00000000 00000000 00000000 00000000
>[  3, 0]   0  0 84438020 84bd0028 00000000-00000000    
           \FileSystem\Ntfs
            Args: 00001000 00000000 00000000 00000000
 

Com o endereço do FileObject em mãos, a extensão !fileobj nos mostrará mais detalhes sobre esse objeto. Assim podemos verificar que de fato o arquivo a ser lido é o nosso arquivo texto que foi mapeado.

kd> !fileobj 84bd0028 
 
\Temp\Test.txt
 
Device Object: 0x84439030   \Driver\volmgr
Vpb: 0x84436e28
Access: Read SharedRead SharedDelete 
 
Flags:  0x44042
    Synchronous IO
    Cache Supported
    Cleanup Complete
    Handle Created
 
FsContext: 0x92a4cd80    FsContext2: 0x92a4ced8
CurrentByteOffset: 0
Cache Data:
  Section Object Pointers: 84d24e74
  Shared Cache Map: 00000000
 

Sabendo que estamos no contexto da thread que fez o acesso, podemos dar uma espiadinha no endereço acessado antes do page fault ser atendido. Esse endereço foi exibido na saída da aplicação de teste antes o breakpoint interromper a execução do sistema.

kd> db 0x00020000
00020000  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
00020010  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
00020020  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
00020030  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
00020040  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
00020050  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
00020060  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
00020070  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
 

“Fernando, por que aparecem sinais de interrogação? Tudo bem que o conteúdo do arquivo ainda não foi copiado para o buffer da aplicação, mas não deveríamos ver lixo ou mesmo zeros?”

Olha, essa sua pergunta foi realmente muito boa, acho que eu mesmo não poderia ter pensado em uma pergunta melhor. Na verdade a resposta para essa pergunta está vinculada àquela reposta tosca do post anterior. O que acontece é que um intervalo de endereços foi reservado para conter as páginas de memória com o conteúdo do arquivo. Como nenhum acesso ainda foi feito, o esse endereço virtual ainda não aponta para nenhuma página física de memória. Sem essa página física não se pode determinar seus dados. Podemos verificar isso utilizando a extenção !vtop do WinDbg, que faz a tradução de endereços virtuais para endereços físicos.

kd> !vtop 0 0x00020000
X86VtoP: Virt 00020000, pagedir 1f09b4c0
X86VtoP: PAE PDPE 1f09b4c0 - 000000001876d801
X86VtoP: PAE PDE 1876d000 - 0000000018908867
X86VtoP: PAE PTE 18908100 - ffffffff00000420
X86VtoP: Virt ffffffff, pagedir 1f09b4c0
X86VtoP: PAE PDPE 1f09b4d8 - 00000000187b0801
X86VtoP: PAE PDE 187b0ff8 - 0000000000128063
X86VtoP: PAE PTE 128ff8 - 0000000000000000
X86VtoP: PAE zero PTE
Virtual address 20000 translation fails, error 0x8007001E.
 
kd> !error 0x8007001E
Error code: (HRESULT) 0x8007001e (2147942430) - The system cannot read from the specified device.
 

A tentativa de tradução desse endereço virtual resulta em um erro. Vamos tentar fazer essa tradução novamente depois que o page fault for atendito. Vamos liberar a execução do sistema até o endereço de retorno para a rotina MmAccessFault(). Este endereço foi obtido na pilha de chamadas da thread e foi destacado nos resultados do comando kd já ilustrado acima.

kd> ga 8168ebf4 
 
nt!KiTrap0E+0xdc:
8168ebf4 85c0            test    eax,eax
 
kd> !vtop 0 0x00020000
X86VtoP: Virt 00020000, pagedir 1f09b4c0
X86VtoP: PAE PDPE 1f09b4c0 - 000000001876d801
X86VtoP: PAE PDE 1876d000 - 0000000018908867
X86VtoP: PAE PTE 18908100 - 8000000018844025
X86VtoP: PAE Mapped phys 18844000
Virtual address 20000 translates to physical address 18844000.
 

Aqui o page fault já foi antendido e o controle será devolvido à aplicação. Neste ponto o Memory Manager realizou as tarefas necessárias para que esse endereço virtual agora pudesse ser traduzido para uma página física. Repetindo a mesma tentativa de tradução que falhou anteriormente, teremos a seguinte saída.

kd> db 0x00020000
00020000  54 65 73 74 65 20 64 65-20 6d 61 70 65 61 6d 65  Teste de mapeame
00020010  6e 74 6f 20 64 65 20 61-72 71 75 69 76 6f 2e 2e  nto de arquivo..
00020020  2e 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00020030  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00020040  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00020050  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00020060  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00020070  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

Liberando a execução do sistema, a aplicação fará o acesso ao buffer e teremos a mesma saída exibida anteriormente. Vale lembrar que para repetir essa experiência, é necessário reiniciar o sistema, pois o conteúdo do arquivo texto agora está no cache do sistema. Isso significa que o page fault não ocorrerá novamente até que esta página seja descartada pelo Cache Manager. Tal evento depende de muitos fatores e pode não acontecer até que a máquina desligue.

Enfim, este post além de trazer uma simples função de mapeamento de arquivo, também traz o mesmo blá-blá-blá técnico de sempre. Espero que tenham gostado.
Até mais! 😉

MapFile.zip

Ponteiro perdido no Kernel pode corromper arquivos?

9 de June de 2010 - Fernando Roberto

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 - Fernando Roberto

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

Último post pelo Blogger

28 de April de 2010 - Fernando Roberto

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

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

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

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

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

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

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

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

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

Sexto Encontro de Programadores C/C++

26 de February de 2010 - Fernando Roberto

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

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

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

Lidando com Memória Virtual em Drivers

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

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

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

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

Download dos slides

Escrevendo Filtros

20 de January de 2010 - Fernando Roberto

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 - Fernando Roberto

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!