Archive for October, 2006

Serial Killers

24 de October de 2006

Não há mais dúvidas, estão mesmo acabando com as portas seriais em notebooks e desktops. Neste post vou falar sobre alguns problemas que encontramos quando é necessário depurar aquele driver que só dá problema naquela máquina que não tem portas seriais. Quais as alternativas que temos em relação a isso?

Este mês chegaram micros novos aqui na empresa. Nestas máquinas, colocamos algumas versões beta de nossos produtos. Por algum motivo, somente nas máquinas novas, um dos nossos drivers não estava funcionando como deveria. Conforme o cita o código de ética do programador, a culpa é do estagiário que fazia os testes até que se prove o contrário. Depois de algumas tentativas frustradas de descobrir o que estava acontecendo, não tive dúvidas, vamos depurar. Para não ter que deslocar a vítima até minha mesa para conectar o cabo serial, resolvi instalar o SoftIce. Mas a vida é uma caixinha de surpresas e obtivemos uma bela BSOD quando iniciamos o SoftIce. Wow!!! Isso é raro, mas acontece. Nestes casos não temos muita escolha, vamos ter que utilizar o Windbg mesmo. Os micros que chegaram são do modelo Dimension 5150 da Dell. Dê uma olhada na parte traseira do gabinete na foto ao lado e responda rapidamente: Onde está a porta serial? Pois é, não tem mesmo. Consultando o site do fabricante, verificamos que o micro dispõe de 8 portas USB, mas nada de porta paralela e nada de porta serial.

Neste momento, fizemos uma roda e falamos todos juntos: “Oh Deus, o que faremos agora?”. Alguns dos problemas que acontecem em máquinas reais são reproduzidos em máquinas virtuais, principalmente se o driver que você está desenvolvendo é um filtro ou um driver que não lide diretamente com o hardware. Para fazer um teste, instalamos a VMware em uma das máquinas novas e felizmente o problema foi reproduzido. Essa é uma daquelas ocasiões onde ficamos felizes pelo problema aparecer. Daí em diante, foi só utilizar o velho truque da porta serial virtual na máquina virtual e redirecionar os dados para um named pipe. Isso até poderia virar um post.;-)

Bom, com o debugger conectado, break-point setado, não precisou de muito tempo para encontrar o problema, gerar uma nova versão, testar, funcionar e viver feliz para sempre. Mas nem sempre a história se resolve com estes poucos passos. Algumas máquinas virtuais não dão suporte a dispositivos USB. Mesmo a VMware que oferece este recurso, ainda não dá suporte a dispositivos de interface humana (HID) como teclados e mouses USB. Isso sem falar dos problemas psico-esotéricos que envolvem race conditions e/ou uma boa ação conjunta com Murphy. O que podemos fazer nestes casos além de sentar e chorar?

Felizmente a tecnologia a serviço da humanidade previu situações como estas. Nestes casos temos tradicionalmente duas alternativas:

1) Descrever o bug que você quer eliminar em uma folha de papel. É recomendado que esta descrição tenha trechos como “Sai bug estranho sai…”. Costurar a folha de papel dentro da boca de um sapo e com o pensamento positivo na resolução do problema, atire o sapo em um rio sem que você veja onde ele caiu. Depois de sete dias, dê um “Rebuild All” em seu projeto (Um reboot antes do build é recomendado). Caso o problema persista, repita a operação. Formatar a máquina ajuda a eliminar os maus fluídos e a espantar os espíritos atormentados que assombram o seu código. Consulte uma benzedeira de software para obter melhores resultados.

2) Utilizar uma placa FireWire e fazer a conexão do Windbg utilizando um cabo IEEE 1394. Esta opção só estará disponível se a máquina a ser depurada for um Windows XP ou superior. Estas placas são espetadas em seu barramento PCI ou ainda PCMCIA. Ainda não cheguei a utilizar este tipo de conexão para depurar uma máquina, mas é seguramente bem rápido e confortável sabendo que os dados são transmitidos a uma taxa de até 400Mbps.

Não posso utilizar um daqueles famosos conversores USB para portas seriais que são vendidos na Santa Ifigênia? Mesmo que não seja necessário instalar um driver no seu sistema, estes adaptadores utilizam drivers para funcionar. Toda a pilha USB é montada para que estes dispositivos funcionem. Em uma máquina que está sendo depurada em Kernel, todo o código que trata a comunicação serial ou firewire está hard coded no loader do sistema e trabalha com endereços fixos padrões. Estes adaptadores podem ser utilizados sem problemas pela máquina que fará o Debug, mas não pelas que irão sofrer o Debug.

E se eu utilizar uma placa multi-serial PCI ? Isso vai depender de quais endereços de I/O e interrupções a placa vai oferecer. Normalmente estas placas utilizam endereços diferentes dos endereços padrões a fim de não criar nenhuma incompatibilidade com as portas que possivelmente pré-existam na máquina. Mas se a placa oferecer a opção de utilizar os endereços padrões, não haverá problemas.

O Windows Vista trará suporte a Debug de Kernel via porta USB 2.0, entretanto algumas condições serão exigidas. O cabo USB deverá ser especial para este fim, a interface USB do computador sendo depurado deverá oferecer suporte a Debug de Kernel, e por último e não menos importante, a controladora USB do computador que fará o Debug deverá ser compatível com Extended Host Controller Interface(EHCI).

Caso você se interesse em adquirir interfaces IEEE 1394, lembre-se que você deverá ter pelo menos duas interfaces deste tipo para que haja o link entre a máquina depuradora e a máquina depurada. Embora eu tenha visto alguns comentários perdidos na Internet sobre cabos conversores USB x FireWire (para utilizar na máquina depuradora), até onde pude ver são eletricamente impossíveis de serem construídos. Isso porque todo o protocolo é diferente. Enquanto o USB é um protocolo “Master x Slave”, o FireWire é “Peer to Peer”. Seria necessário algum hardware inteligente no meio do caminho para fazer toda a tradução.

Estamos adquirindo estas interfaces FireWire para PCMCIA aqui na Open a fim de eliminar o problema de depurar notebooks de clientes que nasceram aleijados de portas seriais. Creio que em pouco tempo poderemos abrir mão do sapo e tudo mais. Farei algumas comparações com o bom e velho cabo serial e darei alguma notícia sobre este assunto.



Driver plus plus

10 de October de 2006

Desenvolver software de baixo nível é muito bom. Eu pessoalmente adoro o tipo de trabalho que realizo. Tenho amigos muito próximos que se maravilham com a beleza do C++, como as coisas parecem se encaixar perfeitamente em meio ao STL, iterators e templates. Meu negócio definitivamente é outro. Apesar de utilizar C/C++, algumas pontinhas de Assembly e gostar muito destas linguagens, não me dedico em estudar as entranhas do C++. Meu lado prático de resolver os problemas me leva a fazer algumas coisas que fariam o Lesma querer me bater, mas felizmente ele é uma pessoa calma. 😛

Depois que meu irmão (Kabloc) leu meu post Getting Started…, ele me perguntou se não poderiamos utilizar C++ no lugar de C para desenvolver drivers. Neste post vou resumir os problemas enfrentados para se ter uma Tela Azul orientada a objetos. Não vou opinar se C++ é melhor ou pior que C para escrever drivers. Essa é uma discussão que parece não ter fim. Se você participa de alguma lista de discussão de drivers, sabe do que estou falando. Eu pessoalmente prefiro sair criando classes, namespaces e templates que resolvam de maneira modular os meus problemas. Isso me parece ser mais confortável. De fato podemos sentir uma certa tendência do desenvolvimento Kernel Mode para a programação orientada a objetos. Este é um aspecto cada vez mais presente com as DDIs (device-driver interfaces), que utilizam o modelo COM para comunicação entre camadas no Kernel, e a chegada do WDF. Vale lembrar que utilizar o modelo do COM não significa utilizar o COM, mas apenas o conceito de interfaces derivadas da tão conhecida IUnknown para implementar contadores de referência e intercâmbio de interfaces.

Primeiro de tudo, vamos nos basear no projeto exemplo criado em meu post anterior para criar um driver que faça uso de uma classe. Como manda a tradição, vamos começar pensando que é fácil e simplesmente usar a extensão CPP no arquivo fonte abaixo e ver no que dá.

#include 
 
//-f--> Uma classe com um nome bem original
class MyClass
{
public:
    MyClass()
    {
        DbgPrint("Construtor %p\n", this);
    }
 
    ~MyClass()
    {
        DbgPrint("Destrutor %p\n", this);
    }
 
    VOID SayHello(VOID)
    {
        DbgPrint("Hello from %p\n", this);
    }
};
 
 
/****
***
**      DriverEntry 
*/
 
NTSTATUS DriverEntry(IN PDRIVER_OBJECT  pDriverObject,
                     IN PUNICODE_STRING pusRegistryPath)
{
    MyClass Instance;
 
    //-f--> Um sinal de vida
    DbgPrint("DriverEntry called\n");
 
    //-f--> Hello...
    Instance.SayHello();
 
    //-f--> Até aqui tudo bem
    return STATUS_SUCCESS;
}

Round #1…

Compiling - krnclass.cpp for i386
...\ntddk.h(2152) : error C2220: warning treated as error - no object
                    file generated
...\ntddk.h(2152) : error C4162: '_ReturnAddress' : no function with
                    C linkage found
...\ntddk.h(6889) : error C4162: '_InterlockedExchange' : no function
                    with C linkage found
...\ntddk.h(6915) : error C4162: '_InterlockedIncrement' : no function
                    with C linkage found
...\ntddk.h(6928) : error C4162: '_InterlockedDecrement' : no function
                    with C linkage found
...\ntddk.h(6942) : error C4162: '_InterlockedExchangeAdd' : no function
                    with C linkage found
...\ntddk.h(6972) : error C4162: '_InterlockedCompareExchange' : no
                    function with C linkage found
...\ntddk.h(7024) : error C4162: '_InterlockedOr' : no function with C
                    linkage found
...\ntddk.h(7034) : error C4162: '_InterlockedAnd' : no function with C
                    linkage found
...\ntddk.h(7044) : error C4162: '_InterlockedXor' : no function with C
                    linkage found

Vamos encontrar no ntddk.h algumas definições que precisam ser compiladas em C e não em C++. Este problema também vai acontecer com a função de entrada do driver DriverEntry. O DDK espera encontrar este símbolo exportado como C. Mas isso ainda é mamão com açúcar, basta mudar a maneira como incluímos este header e como declaramos do nosso entry point para que fique como mostra abaixo.

extern "C"
{
    #include 
}
 
...
 
extern "C"
NTSTATUS DriverEntry(IN PDRIVER_OBJECT  pDriverObject,
                     IN PUNICODE_STRING pusRegistryPath)
{

Agora sim, compilou direitinho. Carregando este driver poderemos ver as seguintes mensagens no DebugMon.

Pronto? Já temos um driver em C++? Ainda não. Já podemos desfrutar de alguns conceitos como classes, templates e sobrecargas por exemplo, mas vejamos o que acontece quando tentamos utilizar o operador new.

extern "C"
NTSTATUS DriverEntry(IN PDRIVER_OBJECT  pDriverObject,
                     IN PUNICODE_STRING pusRegistryPath)
{
    MyClass *pInstance = NULL;
 
    //-f--> Criando uma instância dinamicamente
    pInstance = new MyClass();
 
    //-f--> Telazulfobia
    if (!pInstance)
        return STATUS_NO_MEMORY;
 
    //-f--> Hello from heap
    pInstance->SayHello();
 
    //-f--> Liberando
    delete pInstance;
 
    //-f--> Até aqui tudo bem
    return STATUS_SUCCESS;
}
 

Round #2…

Linking Executable - objchk_wxp_x86\i386\krnclass.sys for i386
krnclass.obj : error LNK2019: unresolved external symbol "void * __cdecl
               operator new(unsigned int)" (??2@YAPAXI@Z) referenced in
               function _DriverEntry@8
krnclass.obj : error LNK2019: unresolved external symbol "void __cdecl
               operator delete(void *)" (??3@YAXPAX@Z) referenced in
               function "public: void * __thiscall MyClass::`scalar
               deleting destructor'(unsigned int)" (??_GMyClass@@QAEPAXI@Z)
objchk_wxp_x86\i386\krnclass.sys : error LNK1120: 2 unresolved externals

Ops! Para resolver estas dependências, teremos que sobrecarregar os operadores new e delete para que as alocações sejam feitas através do ExAllocatePool. Vamos passar um parâmetro adicional ao operador new para que possamos escolher entre NonPagedPool, PagedPool and so on…

typedef unsigned int size_t;
 
//-f--> Sobrecarga do operador new
__inline void* __cdecl operator new(size_t size,
                                    POOL_TYPE pool)
{
    return ExAllocatePool(pool, size);
}
 
//-f--> Sobrecarga do operador delete
__inline void __cdecl operator delete(void *pVoid)
{
    ExFreePool(pVoid);
}
 
...
 
    //-f--> Devemos especificar o pool type aqui
    pInstance = new(NonPagedPool) MyClass();
 
...

Agora já compila novamente e as alocações de memória são realizadas de maneira que se deve, entretanto existe um outro problema que acontecerá quando você tentar criar objetos estáticos ou globais. Vamos demonstrar isso criando uma instância global e mais uma vez compilar este exemplo.

Round #3…

Linking Executable - objchk_wxp_x86\i386\krnclass.sys for i386
krnclass.obj : error LNK2019: unresolved external symbol _atexit
               referenced in function _$E1
objchk_wxp_x86\i386\krnclass.sys : error LNK1120: 1 unresolved externals

Como não temos suporte a C++ em Kernel, não temos a run time que implementa toda a inicialização dos objetos estáticos chamando todos os construtores e destrutores globais. Este é um processo um tanto complicado e que vai nos exigir um cadim de conceito. Existe um artigo muito bom do Matt Pietrek que descreve este processo. Parece que isso vai nos dar um belo trabalho, mas felizmente no site da Hollis Technology Solutions existe um MSI que traz uma implementação da run time do C++ para Kernel. O pacote é Open Source, e assim traz todo o código fonte (incluindo um exemplo) e as bibliotecas desta implementação.

Esta implementação da run time é bem mais ampla que a descrita neste post, e teremos o suficiente para brincar bastante com C++ em Kernel. Para os malloqueiros de plantão, ela traz inclusive as definições do malloc e free. A única coisa desconfortável que encontrei neste pacote é a necessidade de substituir a definição do nosso ponto de entrada DriverEntry para CPP_DRIVER_ENTRY.

#include 
 
...
 
/****
***
**      DriverEntry 
*/
CPP_DRIVER_ENTRY(IN PDRIVER_OBJECT  pDriverObject,
                 IN PUNICODE_STRING pusRegistryPath)
{
 
...

Vou deixar um exemplo de como utilizar este pacote para download admitindo que você fez a instalação do MSI em C:\Library\HtsCpp. Este exemplo também poderá nos servir de base para futuros posts que venham a utilizar algum recurso de orientação a objeto. De qualquer forma, segue o preview de como deve ficar seu sources.

TARGETNAME=KrnClass
TARGETPATH=obj
TARGETTYPE=DRIVER
 
!if "$(DDKBUILDENV)" == "fre"
BUILD_CONFIG=libFre
!else
BUILD_CONFIG=libChk
!endif
 
TARGETLIBS=C:\Library\HtsCpp\$(BUILD_CONFIG)\i386\HtsCpp.lib
 
INCLUDES=C:\Library\HtsCpp\sys\inc;
 
SOURCES=KrnClass.cpp

Até mais mais! 🙂

KrnClass.zip

RtlGetModuleBase & RtlGetProcAddress

2 de October de 2006

No post ExAllocatePool(WithoutTag) falei um pouco sobre os conflitos de utilizar APIs novas em drivers e acabar tornando-os incompatíveis com sistemas legados. Nossa proposta inicial era ter um único binário que pudesse ser executado tando em Windows NT como em sistemas mais recentes. Chegamos a uma solução que pode não ser considerada a ideal, onde utilizamos sempre as funções antigas mesmo em sistemas que suporte APIs mais novas.

Pensando em resolver este tipo de limitação, o DDK nos trouxe a MmGetSystemRoutineAddress. Analogamente à GetProcAddress exportada pela Kernel32.dll, a MmGetSystemRoutineAddress obtém o endereço de uma função dinamicamente a partir de seu nome de exportação. Mas o mundo ainda não estará a salvo enquanto o Windows NT sobreviver. Essa nova função é implementada somente a partir do Windows 2000.

Já que não temos uma alternativa para Windows NT, vamos fazer uma. Com um ou dois conceitos sobre PE, implementamos uma versão desta função. A estrutura do PE carrega alguns quilos de regras, mas para nossa necessidade podemos implementar uma versão light. Se você quer ter detalhes das regras adotadas pelo PE, você pode dar uma olhada no artigo An In-Depth Look into the Win32 Portable Executable File Format por Matt Pietrek.

Bom, agora que já lemos todo o artigo e já sabemos tudo sobre PE, segue protótipo do algoritmo básico de como caminhar pela estrutura do PE procurando por uma determinada função exportada pelo nome. Esta função não é fornecida pelo DDK do Windows NT e está definida no código fonte exemplo disponível para download.

NTSTATUS RtlGetProcAddress(IN PVOID     pBaseAddress,
                           IN LPCSTR    pszFunctionName,
                           IN NTPROC    *pProcedure);

Repare que os parâmetros de entrada são o endereço base, o nome da função desejada e por fim o endereço onde será armazenado o endereço desta função. Mas onde é que vou conseguir esse tal de endereço base? Lembre-se de que as funções que estamos procurando são exportadas pela ntoskrnl.lib e que são implementadas no módulo ntoskrnl.exe. Para nos certificar de que a função que procuramos é realmente exportada por este módulo, utilize o “Dependecy Walker” para visualizar a tabela de exportação deste módulo. Como já comentei antes, algumas funções são implementadas como macros, desta forma, sua definição estará em um arquivo de header e não na tabela de exportação.

Para obtermos o endereço base do módulo que exporta estas funções, vamos utilizar a ZwQuerySystemInformation, que embora não seja documentada pela Microsoft, já existem várias publicações que comentam a respeito dela. Desta forma definiremos a RtlGetModuleBase que terá o comportamento similar à bem conhecida GetModuleHandle. Esta função também está definida no exemplo para download.

NTSTATUS
NTAPI
ZwQuerySystemInformation(IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
                         IN OUT PVOID SystemInformation,
                         IN ULONG SystemInformationLength,
                         OUT PULONG ReturnLength OPTIONAL)

Existem muitas funções e definições não documentadas que podem ser de extrema utilidade. O livro Windows NT/2000 Native Api Reference por Gary Nebbett é excelente para fazer uso destas funções. Ele traz protótipos, enums, estruturas utilizadas em chamadas, descrições do que cada função faz e de cada parâmetro. Aqui em nosso exemplo, vamos utilizar apenas as declarações abaixo para obter informações sobre os módulos carregados no address space do sistema.

typedef struct _SYSTEM_MODULE_INFORMATION   // Information Class 11
{
    ULONG Reserved[2];
    PVOID Base;
    ULONG Size;
    ULONG Flags;
    USHORT Index;
    USHORT Unknown;
    USHORT LoadCount;
    USHORT ModuleNameOffset;
    CHAR ImageName[256];
 
} SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;

O membro “Base” da estrutura acima nos traz o endereço base do módulo que é onde o PE está. No código de exemplo está a definição da nossa rotina RtlGetModuleBase que tem o seguinte protótipo.

NTSTATUS RtlGetModuleBase(IN LPCSTR     pszModuleName,
                          OUT PVOID*    ppBaseAddress);

Com a união dos seus poderes, agora poderemos saber se uma determinada API é implementada no sistema corrente e obter seu endereço. Assim é perfeitamente possível ter um único binário que possa rodar tanto em Windows NT utilizando o ExFreePool, como em sistemas posteriores utilizando o ExFreePoolWithTag. Abaixo segue um exemplo bem básico como sempre. É claro que podemos criar uma única rotina de alocação que faria todo o trabalho sujo.

#include "GetProcAddr.h"
 
//-f--> Tipo para o ponteiro de função ExFreePoolWithTag
typedef VOID (NTAPI* PF_EX_FREE_POOL_WITH_TAG)
(
    IN PVOID  P,
    IN ULONG  Tag 
);
 
 
VOID OnDriverUnload(PDRIVER_OBJECT     pDriverObj)
{
    //-f--> Esta rotina está aqui apenas para permitir
    //      que o driver seja terminado, mesmo vazia.
}
 
 
/****
***
**           Era uma vez um Driver...
**
*/
NTSTATUS DriverEntry(PDRIVER_OBJECT     pDriverObj,
                     PUNICODE_STRING    pusRegistryPath)
{
    NTSTATUS                    nts;
    PVOID                       pBaseAddress, pTemp;
    PF_EX_FREE_POOL_WITH_TAG    pfExFreePoolWithTag;
 
    //-f--> Seta rotina de finalização
    pDriverObj->DriverUnload = OnDriverUnload;
 
    //-f--> Obtem o endereço base
    nts = RtlGetModuleBase("ntoskrnl.exe",
                           &pBaseAddress);
 
    //-f--> Testar retorno não mata ninguem, mas a falta
    //      pode matar seu sistema
    if (!NT_SUCCESS(nts))
        return nts;
 
    //-f--> Alocando memória para teste do Free
    pTemp = ExAllocatePoolWithTag(NonPagedPool,
                                  10,
                                  'tseT');
 
    //-f--> Obtem o endereço da API
    nts = RtlGetProcAddress(pBaseAddress,
                            "ExFreePoolWithTag",
                            (NTPROC*)&pfExFreePoolWithTag);
 
    if (NT_SUCCESS(nts))
    {
        //-f--> Se o sistema implementa esta API, então
        //      obteremos sucesso e poderemos chama-la
        pfExFreePoolWithTag(pTemp,
                            'tseT');
    }
    else
    {
        //-f--> Quem não tem cão, caça com gato.
        ExFreePool(pTemp);
    }
 
    return STATUS_SUCCESS;
}

Have fun! 🙂

ExGetProc.zip