Blog

  • Listas ligadas no DDK

    Desde o início dos meus estudos, sempre optei por estudar algo que unisse informática com eletrônica, e isso me levou a escolher o finado curso de Informática Industrial na ETE Jorge Street. Uma excelente escola e aprendi muito por lá. Entretanto só fui aprender o que é lista ligada em um ambiente profissional durante meu estágio. Anos mais tarde, a universidade me apresentou estas listas em forma de classes escritas em Java, onde lidamos com referências e o Garbage Collector. Mesmo quem trabalha com Visual C/C++, lida com classes e templates oferecidas pela MFC, ATL, WTL e STL, que acabam abstraindo a real implementação da lista ligada. Neste post vou falar um pouco sobre os recursos oferecidos pelo DDK em relação a este assunto.

    Mas já sou grandinho e sei construir listas ligadas em C/C++. Por que eu deveria utilizar os recursos do DDK?

    Se você vai apenas armazenar dados particulares, tais como uma lista de buffers a serem enviados para o dispositivo, então tudo bem, mas para lidar com estruturas do DDK, seria no mínimo interessante saber como elas são armazenadas em listas. Algumas situações exigem que você saiba utilizar as listas do DDK. Um exemplo disso é: Se você implementar o controle personalizado da fila de IRPs do seu driver, o campo pIrp->Tail.Overlay.ListEntry estará livre para ser utilizado enquanto tal IRP é mantida pelo seu driver.

    O mais simples destes recursos é a estrutura SINGLE_LIST_ENTRY que é definida no ntdef.h como exibido abaixo:

    typedef struct _SINGLE_LIST_ENTRY {
      struct _SINGLE_LIST_ENTRY *Next;
    } SINGLE_LIST_ENTRY, *PSINGLE_LIST_ENTRY;

    Uma variável do tipo SINGLE_LIST_ENTRY deve ser definida para ser a ponta de nossa lista. Esta variável deveria ter seu membro Next incializado com NULL antes de ser utilizada. As funções PushEntryList e PopEntryList são utilizadas respectivamente para adicionar e remover elementos da ponta da lista. Como a maioria das listas ligadas, os nós são referenciados pelo membro Next até que este seja NULL, indicando assim, o fim da cadeia de nós.

    VOID 
      PushEntryList(
        IN PSINGLE_LIST_ENTRY  ListHead,
        IN PSINGLE_LIST_ENTRY  Entry
        );
     
    PSINGLE_LIST_ENTRY 
      PopEntryList(
        IN PSINGLE_LIST_ENTRY  ListHead
        );

    Mas espere um pouco. Tudo isso é lindo, mas não está faltando nada? Como em qualquer estrutura de lista ligada, entre os membros de cada nó é preciso haver um que aponte para uma estrutura de mesmo tipo, que será o elo para o próximo nó da lista. No exemplo abaixo, o membro pNext é o responsável por isso.

    //-f--> Estrutura que define o nó da lista
    //      que será utilizada como exemplo
    typedef struct _MY_NODE
    {
        //-f--> Dados do seu driver definidos
        //      por você.
        UNICODE_STRING      usSomeData;
        ULONG               ulAnotherData;
        struct _MY_NODE     *pNext;
        PVOID               pMoreData;
     
    } MY_NODE, *PMY_NODE;

    Mas pelo que pudemos ver na estrutura do DDK, não existem membros úteis, ou seja, membros que contenham as informações que nos interessa armazenar, como os campos usSomeData ou pMoreData. Na estrutura oferecida pelo DDK, temos apenas o endereço do nó seguinte.

    Qual o interesse em guardar somente os nós?

    Na verdade, a estrutura SINGLE_LIST_ENTRY, assim como outras estruturas de listas do DDK, devem ser utilizadas em conjunto com a macro CONTAINING_RECORD. Esta macro nos retorna o endereço base da estrutura que tem um campo conhecido em um endereço conhecido. Bom, eu tentei reescrever esta frase umas três vezes, mas acho que você só irá entender quando ver o exemplo abaixo. Suponha que estamos utilizando o SINGLE_LIST_ENTRY em nosso exemplo anterior. Desta forma teríamos a seguinte estrutura:

    //-f--> Estrutura que define o nó da lista
    //      que será utilizada como exemplo
    typedef struct _MY_NODE
    {
        //-f--> Dados do seu driver definidos
        //      por você.
        UNICODE_STRING      usSomeData;
        ULONG               ulAnotherData;
     
        //-f--> Este membro não precisa ser necessariamente
        //      o primeiro ou o ultimo, ele pode estar em
        //      qualquer posição da sua estrutura.
        SINGLE_LIST_ENTRY   Entry;
     
        //-f--> Mais dados
        PVOID               pMoreData;
     
    } MY_NODE, *PMY_NODE;

    Nossa estrutura é basicamente a mesma, com exceção do membro pNext que agora foi mudado para utilizar a estrutura SINGLE_LIST_ENTRY. Reparem que o campo Entry não precisa estar nem no início e nem no final dos membros da nossa estrutura. O exemplo abaixo demonstra como incluir nós em listas formadas com SINGLE_LIST_ENTRY.

    NTSTATUS PrepareDataAndPush(VOID)
    {
        PMY_NODE            pMyNode;
     
        //-f--> Aqui obtemos o nó já alocado e com
        //      os campos devidamente preenchidos
        pMyNode = AllocateAndFillNode();
     
        //-f--> Testar nunca é demais
        ASSERT(pMyNode != NULL);
     
        //-f--> Para colocar o nó na lista, é necessário passar
        //      o endereço do membro do tipo SINGLE_LIST_ENTRY
        PushEntryList(&m_ListHead, &pMyNode->Entry);
     
        return STATUS_SUCCESS;
    }

    Na hora de incluir o nó na lista, devemos passar o endereço do membro do tipo SINGLE_LIST_ENTRY, como sugere o protótipo da função PushEntryList. Acompanhe o exemplo abaixo que demonstra como obter este nó utilizando a função PopEntryList. Esta função retorna o mesmo endereço passado na função PushEntryList, que é um PSINGLE_LIST_ENTRY. A partir deste endereço, conforme eu já havia comentado, podemos obter o endereço base de nossa estrutura utilizando a macro CONTAINING_RECORD. Veja o exemplo abaixo.

    NTSTATUS PopAndProcessNode(VOID)
    {
        PSINGLE_LIST_ENTRY  pEntry;
        PMY_NODE            pMyNode;
     
        //-f--> Aqui obtemos o nó da lista
        pEntry = PopEntryList(&m_ListHead);
     
        //-f--> Vamos verificar se realmente havia algo
        //      armazenado na lista
        if (!pEntry)
            return STATUS_NO_MORE_ENTRIES;
     
        //-f--> Agora vamos utilizar a macro CONTAINING_RECORD
        //      para obter o endereço base que precisamos
        pMyNode = (PMY_NODE) CONTAINING_RECORD(pEntry, MY_NODE, Entry);
     
        //-f--> Agora temos acesso a toda a estrutura
        ProcessAndFreeNode(pMyNode);
     
        return STATUS_SUCCESS;
    }

    Este mesmo estilo também é utilizado com a estrutura LIST_ENTRY, que é a estrutura de lista mais utilizada em drivers. Esta estrutura permite criar listas duplamente ligadas, e assim como o SINGLE_LIST_ENTRY, uma variável do tipo LIST_ENTRY é definida para ser a ponta de nossa lista. Sua inicialização é feita utilizando a função InitializeListHead, que inicializa os membros Blink e Flink com o endereço da ponta da lista.

    typedef struct _LIST_ENTRY {
      struct _LIST_ENTRY *Flink;
      struct _LIST_ENTRY *Blink;
    } LIST_ENTRY, *PLIST_ENTRY;

    O membro Flink aponta para o nó seguinte, enquanto que o membro Blink aponta para o nó anterior. Diferente da maioria das listas ligadas que conhecemos, suas pontas não são marcadas por NULL nos membros Blink ou Flink. Ao invés disso, suas pontas apontam para o endereço do nó designado como ponta de nossa lista. O exemplo abaixo demonstra como poderiamos varrer este tipo de lista em procura de um certo nó. Antes de realizar qualquer operação com as listas formadas por LIST_ENTRY, utilizamos a função IsListEmpty.

    //-f--> Procura por um nó através de uma lista
    //      formada por estruturas LIST_ENTRY
    BOOLEAN SearchEntry(PUNICODE_STRING  pusLookFor)
    {
        PLIST_ENTRY pEntry;
        PMY_NODE    pMyNode;
     
        //-f--> Antes de qualquer operação, é necessário
        //      verificar se existem nós na lista. Caso
        if (IsListEmpty(&m_ListHead))
            return FALSE;
     
        //-f--> Obtem o endereço do primeiro nó
        pEntry = m_ListHead.Flink;
     
        //-f--> Enquando pEntry não apontar para a ponta
        //      da lista, significa que ainda existem nós
        while(pEntry != &m_ListHead)
        {
            //-f--> Obtem o endereço base da estrutura
            pMyNode = (PMY_NODE) CONTAINING_RECORD(pEntry, MY_NODE, Entry);
     
            //-f--> Verifica se é o nó que estamos procurando
            if (RtlEqualUnicodeString(pusLookFor,
                                      &pMyNode->usSomeData,
                                      TRUE))
            {
                //-f--> Retorna TRUE sinalizando que o nó foi encontrado
                return TRUE;
            }
     
            //-f--> Obtem o endereço do próximo nó
            pEntry = pEntry->Flink;
        }
     
        //-f--> Se chegamos aqui, significa que não encontramos
        //      o nó desejado.
        return FALSE;
    }

    NOTA: O exemplo acima serve apenas para ilustrar a busca por um nó. Para implementar uma busca como essa em um ambiente de produção, é necessário ter em mente questões de sincronismo de acesso por multiplas threads e controle de referências.

    Para manipular listas formadas com LIST_ENTRY são utilizadas as funções InsertHeadList, InsertTailList, RemoveHeadList e por aí vai. Não vou listar todas as funções aqui, tenho certeza que a referência do DDK faz isso melhor que eu.

    Para ambos os tipos de listas, existem as funções ExInterlockedXxxList que recebem um Spin Lock a fim de sincronizar as alterações da lista. Lembre-se que caso você sincronize o acesso às listas utilizando Spin Locks, todos os nós devem residir em memória não paginada. Como alguns de vocês já devem saber, ao adquirir um Spin Lock, a IRQL da thread vai para DISPATCH_LEVEL. Nesta IRQL, o Memory Manager é incapaz de recuperar uma página de dados que foi paginada para disco, resultando em um BugCheck.

    É importante lembrar também que não se deve misturar os uso das funções ExInterlockedXxxList com as não sincronizadas. Se uma lista é acessada por várias threads, todos os acessos à mesma devem ser controlados.

    typedef struct _SLIST_ENTRY {
      struct _SLIST_ENTRY *Next;
    } SLIST_ENTRY, *PSLIST_ENTRY;

    A estrutura SLIST_ENTRY é uma alternativa para as listas do tipo SINGLE_LIST_ENTRY com acesso sincronizado. Esta versão se propôe ser mais eficiente utilizando as funções ExInterlockedPopEntryList e ExInterlockedPushEntryList. Diferente das outras estruturas aqui apresentadas, a ponta da lista é formada por uma estrutura diferente da estrutura utilizada nos nós, Esta estrutura é a SLIST_HEADER, que é uma estrutura opaca e que deve ser inicializada pela função ExInitializeSListHead.

    Um outro diferencial deste tipo de lista é que o DDK oferece a função ExQueryDepthSList que retorna a quantidade de nós armazenados na lista.

    Se o seu principal problema é performance, então o ideal seria utilizar o grupo de funções que trabalham com as Generic Tables. A ponta da lista é definida por uma estrutura opaca chamada RTL_GENERIC_TABLE. Esta estrutura juntamente com as funções de manipulação, tais como RtlGetElementGenericTable, criam arvores binárias com auto balanceamento. Que chique hein?

    Mas Generic Tables é um assunto a parte e que merece um post dedicado. Até a próxima!

  • Legal, mas o que é uma IRP?

    Meu amigo Lesma, assim que leu o título do meu último post, deu uma risada e disse que a maioria das pessoas que lessem isso perguntariam: “O que é uma IRP?”. Bom, pensando no que ele disse e levando em consideração a explicação nada simplificada que a referência do DDK nos oferece, vou dar uma descrição superficial da IRP, livrar minha consciência deste peso e poder dormir novamente.


    Vamos deixar de lado os assustadores e horripilantes diagramas do DDK para tentar ver as coisas de uma maneira um pouco mais simples. Depois disso, você poderá recorrer à documentação sagrada para reforçar os conceitos e tirar eventuais dúvidas sobre algo que você já saiba o que é, ou que pelo menos imagina.

    Vamos nos basear em um exemplo bem prático. Os drivers de File Systems por exemplo. O NTFS e o FAT são implementados como drivers de Kernel que recebem o nome File Systems Drivers. Imagino que muitos de vocês já tiveram a oportunidade de abrir e escrever em um arquivo.

    Para começar a conversar com um driver, temos inicialmente que criar uma conexão com ele, e isso é feito utilizando a função CreateFile. Do contrário que parece, essa função não é restrita à criação de arquivos, na verdade, o fato de abrir um arquivo é uma maneira específica de se abrir a conexão com o driver de File System (Detalhes sobre isso em uma próxima vez). A função CreateFile vai nos retornar um handle que será utilizado para interagir com o driver através de funções tais como ReadFile, WriteFile e DeviceIoControl, que por sinal, também não são restritas à operações com arquivos. Quando uma aplicação chama a função ReadFile o subsystema Win32 encaminha esta solicitação para a API nativa NtReadFile, que por sua vez faz a transição para Kernel Mode e chama o IoManager, este vai empacotar os parâmetros desta solicicação em uma estrutura chamada IRP (I/O Request Packet).

    O IoManager envia este pacote para o driver responsável passando pelos filtros que houverem instalados sobre ele. Um driver de anti-vírus é um exemplo perfeito para o senário que estamos falando aqui. Drivers de anti-vírus são implementados como File System Filters. Quando alguma aplicação escreve em um arquivo, a IRP de escrita passa pelo anti-vírus antes de chegar ao driver de File System, dando a oportunidade que o anti-vírus precisa para verificar se o que está sendo gravado contém a assinatura de algum vírus conhecido. No caso de uma leitura, a IRP passa pelo anti-vírus que instala uma CompletionRoutine nela, e desta forma ganha acesso aos dados quando a leitura for finalizada pelo driver final.

    A IRP é basicamente dividida em duas partes, sendo elas o Header e as Stack Locations. No Header temos informações gerais da IRP, tais como status, ponteiro para a thread à qual esta IRP pertence, endereços dos buffers do usuário, endereço da rotina de cancelamento desta IRP e por aí vai. Nas Stack Locations estão os parâmetros específicos da solicitação. No caso de uma leitura de arquivo, teremos o offset, o tamanho da leitura e o alinhamento.

    Dentro de uma IRP podem haver várias Stack Locations. Uma para cada device pertencente à cadeia de camadas que segue até chegar ao driver destino. Em outras palavras, seria uma Stack Location para o driver destino mais uma para cada filtro que estiver instalado sobre ele. A medida que a IRP vai descendo as camadas, cada driver repassa os parâmetros recebidos em sua Stack Location para a próxima, utilizando a função IoCopyCurrentIrpStackLocationToNext. Depois de copiados, esta camada pode fazer as alterações desejadas. Se não houver nenhuma alteração a ser feita nos parâmetros de uma camada para a outra, então pode-se utilizar a função IoSkipCurrentIrpStackLocation.

    As IRPs vão de uma camada para outra quando o filtro que a recebeu a encaminha para o device o qual está atachado, utilizando a função IoCallDriver. Quando esta IRP chega ao driver destino, o driver tem basicamente duas opções. Se a solicitação puder ser atendida imediatamente, o driver toma a ação desejada e finaliza a IRP utilizando a função IoCompleteRequest. Mas se for necessário comunicar com um dispositivo ou mesmo com outro driver, a IRP é posta em uma fila, marcada como pendente e só será finalizada quando todo processamento for feito.

    Voltando ao nosso exemplo, quando um driver de File System recebe uma IRP de escrita e dependendo de outras tantas condições, este driver coloca a IRP atual como pendente e cria uma nova IRP para os devices de Volume, onde estão as partições, que por sua vêz repassam solicitações para os devices de Storage. Desta forma fica fácil entender que drivers de disco não entendem nada de NTFS ou FAT. Drivers de storage podem possuir outros filtros de storage, como um RAID para espelhamento de discos entre outras coisas. Estas novas IRPs também são criadas pelo IoManager e todo o ciclo é refeito para cada nova solicitação.

    Visualizando IRPs

    O IrpTracker é uma ferramenta capaz de monitorar a atividade de IRPs de um determinado driver ou device. Veja a figura abaixo onde estou selecionando todo os devices do driver Kbdclass, responsável pela leitura de teclado. Não repare, minha máquina tem mesmo muitos teclados. Estive trabalhando em soluções anti key loggers atualmente. Selecionando todos os estes devices ficará bem visível o uso das IRPs para buscar as teclas que você pressiona.


    Para cada tecla que você bate no teclado, são emitidas quatro linhas no IrpTracker. Sendo que cada tecla batida representa dois movimentos, o de descida da tecla e o de liberação da mesma. Cada movimento é tratado com uma IRP que vem acompanhada de sua Completion, ou seja, uma linha é identificada como Call e outra como Comp, que representam respectivamente o envio da IRP e seu retorno. Reparem que neste exemplo, a linha Comp sempre vem antes da linha Call.

    Isso significa que a IRP teve sua completion antes de ser enviada para o driver do teclado?

    Na verdade, a IRP de leitura de teclado é do tipo que fica pendente até que uma tecla seja recebida. Assim, o sistema envia uma IRP para o driver que fica aguardando eventos do teclado. Quando este evento acontece, o driver recebe a tecla, completa a IRP e o sistema recebe sua completion. Logo em seguida o sistema lança uma nova IRP que aguardará o próximo evento. Desta forma teremos sempre pares linhas, sendo elas a Comp da IRP anterior e a Call da próxima IRP.

    Se você der um duplo clique em uma destas linhas, será possível ver os detalhes da cada campo da IRP como é exibido na figura abaixo.


    Estou preparando uma evolução do driver Useless.sys citado como ponto de partida em um post anterior. Essa evolução vai permitir que ele não seja tão Useless e que exemplifique o tratamento de IRPs na prática. Mas isso vai ficar para uma outra vez.

    Até lá…

  • De quem é essa IRP? (Process ID)

    Existem casos onde é necessário saber qual processo lançou determinada IRP. Isso é muito comum em Firewalls ou em outros programas de segurança, que interceptam operações de I/O para verificar em suas bases de dados se determinado processo tem ou não acesso a um determinado recurso ou serviço. Mas como posso saber qual é o processo dono daquela IRP?

    Bom, eu começo pensando que é muito fácil fazer isso. Como sabemos, o IoManager nos entrega as IRPs no contexto do processo que fez a requisição. Assim, conhecendo a API PsGetCurrentProcessId, podemos obter o ID do processo que lançou a IRP. Veja como é simples:

    /****
    ***     OnDispatchProc
    **
    **      Ô nominho genérico sem vergonha...
    */
    NTSTATUS OnDispatchProc(PDEVICE_OBJECT pDeviceObject,
                            PIRP           pIrp)
    {
        //-f--> Estou pensando que é fácil, não copiem isso.
        HANDLE hProcessID;
     
        //-f--> Obtém o ID do processo corrente.
        hProcessID = PsGetCurrentProcessId();
     
        ...
    }

    Viu como é simples pensar que está tudo certo e estar redondamente enganado?

    De fato, o IoManager entrega as IRPs no contexto do processo que está fazendo o I/O. Entretanto, o que aconteceria se um filtro de terceiro se atachar ao seu driver? Sim, isso é possível, e uma vez que tais drivers receberem estas IRPs, não é garantido que eles as repassarão para o nosso driver ainda no mesmo contexto. Vamos supor que escrevemos o driver de um dispositivo:

    • Uma aplicação solicita a escrita no dispositivo.
    • IoManager cria e envia a IRP para o nosso driver.
    • Um filtro atachado ao nosso device recebe a IRP.
    • O filtro realiza uma consulta assíncrona, possivelmente utilizando ExQueueWorkItem.
    • Enquanto a consulta não termina, o filtro marca a IRP como pendente e retorna STATUS_PENDING.
    • A operação assíncrona, neste caso, é realizada por uma thread de sistema, e ao final da consulta, o filtro encaminha a IRP para o driver abaixo dele, que no caso é o nosso driver.
    • Nosso driver então recebe a IRP no contexto de sistema e não no contexto do processo que originou a IRP.

    Quando o filtro mantém a IRP pendente e retorna da função de Dispatch, a thread que originou a IRP segue em frente e vai realizar outras tarefas. A thread original pode ainda retornar ao processo que iniciou toda esta operação, caso estejam sendo utilizadas as estruturas OVERLAPPED nas chamadas ao driver.

    A IRP agora será executada no contexto do processo que chamar IoCallDriver passando como parâmetro esta IRP que ficou pendente. Em nosso exemplo, o processo que vai fazer isso é o System. Utilizando o código acima, obteremos o PID do processo System no lugar do PID do processo que iniciou a IRP.

    Para corretamente obter a informação que estamos procurando, teremos que dar uma volta um pouco maior. Toda IRP, quando é criada, entra na lista de IRPs pendentes da thread que a criou. Para obter esta thread, utilizamos o campo pIrp->Tail.Overlay.Thread. Este campo possui um ponteiro para a estrutura ETHREAD da thread que criou esta IRP. Para chegar ao processo a partir da thread, utilizamos a API IoThreadToProcess. Veja o trecho abaixo.

    /****
    ***     OnDispatchProc
    **
    **      Ô nominho genérico sem vergonha...
    */
    NTSTATUS OnDispatchProc(PDEVICE_OBJECT pDeviceObject,
                            PIRP           pIrp)
    {
        PEPROCESS   pEProcess;
        PETHREAD    pEThread;
     
        //-f--> Aqui obtemos o ponteiro da thread que criou esta
        //      IRP.
        pEThread = pIrp->Tail.Overlay.Thread;
     
     
        //-f--> Agora obtemos o processo ao qual esta thread
        //      percente.
        pEProcess = IoThreadToProcess(pEThread);
     
        ...
    }

    Pronto, vejam que maravilha. Agora você tem em suas mãos a estrutura EPROCESS, que segundo a documentação da Microsoft, é uma estrutura opaca utilizada internamente pelo sistema operacional.

    “The EPROCESS structure is an opaque data structure used internally by the operating system.”

    E o que eu faço com isso agora?

    Embora eu tenha certeza que você já pensou em algo para eu fazer com esta estrutura, eu tenho uma sugestão bem melhor, e por que não dizer, bem mais apropriada. Mesmo porque podem ter crianças lendo isso. Apesar do EPROCESS não significar nada, ele ainda pode nos trazer alguma informação útil. Podemos obter o handle do processo identificado por essa estrutura e assim obter outras informações deste processo, tal como seu PID. Veja o exemplo abaixo.

    //-f--> ZwQueryInformationProcess from
    //      Windows NT/2000 Native API Reference
    //      ISBN-10: 1578701996 
    //      ISBN-13: 978-1578701995
     
    NTSTATUS
    NTAPI
    ZwQueryInformationProcess
    (
        IN  HANDLE            ProcessHandle,
        IN  PROCESSINFOCLASS  ProcessInformationClass,
        OUT PVOID             ProcessInformation,
        IN  ULONG             ProcessInformationLength,
        OUT PULONG            ReturnLength OPTIONAL
    );
     
     
    /****
    ***     MyGetProcessID
    **
    **      Obtém o ID de um processo a partir do seu EPROCESS
    **      Por favor, usem sua criatividade e dêem um nome melhor
    **      para esta função.
    */
     
    NTSTATUS
    MyGetProcessID(IN  PEPROCESS    pEProcess,
                   OUT PHANDLE      phProcessId)
    {
        NTSTATUS                    nts = STATUS_SUCCESS;
        HANDLE                      hProcess = NULL;
        PROCESS_BASIC_INFORMATION   ProcessInfo;
        ULONG                       ulSize;
     
        //-f--> Funções Zw são normalmente chamadas
        //      do User Mode, assim para chamá-las
        //      do Kernel, precisaremos no mínimo
        //      estar rodando em PASSIVE_LEVEL.
        ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL);
     
        __try
        {
            //-f--> Inicializa o parâmetro de saída
            *phProcessId = 0;
     
            //-f--> Obtemos um handle para o processo
            //      identificado pela estrutura EPROCESS
            nts = ObOpenObjectByPointer(pEProcess,
                                        OBJ_KERNEL_HANDLE,
                                        NULL,
                                        0,
                                        NULL,
                                        KernelMode,
                                        &hProcess);
            if (!NT_SUCCESS(nts))
            {
                ASSERT(FALSE);
                ExRaiseStatus(nts);
            }
     
            //-f--> Para utilizar esta API não documentada, basta
            //      declarar seu protótipo como é feito no início
            //      deste exemplo.
            nts = ZwQueryInformationProcess(hProcess,
                                            ProcessBasicInformation,
                                            &ProcessInfo,
                                            sizeof(ProcessInfo),
                                            &ulSize);
            if (NT_SUCCESS(nts))
            {
                ASSERT(FALSE);
                ExRaiseStatus(nts);
            }
     
            //-f--> Todos vivos até aqui, agora basta alimentar o
            //      parâmetro de saída com o que nos interessa
            *phProcessId = (HANDLE)ProcessInfo.UniqueProcessId;
        }
        __except(EXCEPTION_EXECUTE_HANDLER)
        {
            //-f--> Ops... Deu Mer(pii)
            nts = GetExceptionCode();
        }
     
        //-f--> Libera o handle do processo que obtivemos.
        //      Desta forma, seu gerente não fica te olhando
        //      torto quando, apesar dos processos terminarem,
        //      ainda houver acúmulo de estruturas em RAM.
        if (hProcess)
            ZwClose(hProcess);
     
        //-f--> E todos viveram felizes para sempre.
        //      (inclusive o seu gerente)
        return nts;
    }

    É possível obter inúmeras informações a partir do handle do processo. Existem até meios de obter o Path completo do processo a partir do seu handle, mas vamos deixar essa brincadeira para um próximo post.

    Até mais… 🙂

  • Prog2Svc – Serviço sem trabalho

    Hoje em dia, por mais contraditório que pareça, fazer um serviço não requer muito trabalho. Alguns cliques com o Wizard do Visual Studio 2005 e pronto, já teremos uma aplicação ATL capaz de fornecer interfaces COM e que seja de fato um serviço do Windows. Neste post vou falar sobre uma pequena ferramenta que estou oferecendo como brinde para aqueles que têm a paciência de ler este Blog.

    Na primeira empresa que trabalhei como programador, isso há uns 11 anos atrás, eu era responsável por manter o software de comunicação da rede de coletores de dados da Provectus. No início, um programa MFC era responsável por interagir com o driver que controlava a placa SS140, que era a interface que o PC tinha para fazer parte dessa rede de coletores. Logo que o programa se estabilizou, modificamos este software para torná-lo um serviço do Windows.

    Mas o que é um serviço?

    Um serviço é um módulo executável, que é registrado para que seja executado no sistema mesmo que ninguém faça logon no Windows. Hoje muitas aplicações e componentes do Windows são implementados como serviços. Um serviço não é apenas um executável comum cadastrado para ter seu início automatizado. Um serviço precisa fornecer uma interface com rotinas de CallBack para o sistema, a fim de responder aos comandos de inicialização, parada e suspensão da execução. Esta interface não é uma interface COM como alguns de vocês devem ter imaginado. Dê uma olhada na função StartServiceCtrlDispatcher para que você tenha uma idéia do tipo de interface que estou me referindo. Esta é apenas uma das funções necessárias para se construir um serviço. Dê uma passeada pela referência para saber os detalhes de como se implementa um serviço na unha. Serviços normalmente são executados em conta de sistema, mas podem opcionalmente utilizar uma conta de usuário pré-definida. Veja mais detalhes sobre serviços no site do MSDN.

    De volta aos anos 90, nos surgiu então a necessidade de fazer com que o Supervisor, um dos nossos principais programas na Provectus, trabalhasse como um serviço. O Supervisor era um programa gigante construído em Visual Basic 4.0. Ele era responsável por receber os comandos da rede de coletores, e a partir deles, executar Stored Procedures no SQL Server. O Visual Basic 4.0 ainda não implementava o operador AddressOf, que surgiu somente na versão 5.0. Este operador pode ser utilizado para se obter o ponteiro de funções escritas em VB. Isso possibilitou tais programas registrarem rotinas de CallBack no sistema. Enfim, com este operador, alguns quilos de paciência e uma arma apontada para a sua cabeça, seria possível construir um serviço em Visual Basic. Chegamos a cogitar a possibilidade de reescrever todo o Supervisor em C, para que fosse possível então transformá-lo em um serviço. Mas felizmente alguém teve um surto de sanidade e disse:

    “Porque você não faz um serviço vazio que simplesmente chama o Supervisor?”


    E não é que funciona mesmo? Esta necessidade de ter um programa qualquer se comportando com um serviço é mais comum do que se imagina, principalmente quando falamos de ambientes corporativos. Assim, logo apareceram ferramentas que tornaram isso possível, mas nada me impede de ter minha própria versão.

    O programa Prog2Svc é um serviço que faz exatamente isso. Executando este programa sem qualquer parâmetro, é exibida a mensagem abaixo, que informa quais os possíveis parâmetros a serem utilizados.

    Desta forma, para registrar a calculadora do Windows como um serviço, utilizaremos a seguinte linha de comando. Lembre-se que esta é uma operação que requer direito administrativo, sendo assim, no caso do Windows Vista, esta linha de comando deveria ser executada a partir de um Prompt de Comandos que foi iniciado como administrador.

    Prog2Svc -add Calculadora c:\Windows\System32\calc.exe

    Exibida a mensagem de sucesso, você já poderá visualizar seu serviço através gerenciador do Windows. Para ter acesso ao gerenciador de serviços, digite “services.msc” sem as aspas na janela Run… do Windows.

    Você pode iniciar seu serviço tanto utilizando gerenciador como utilizando o bom e velho comando “net start Calculadora” na janela Run….

    Quando iniciamos o serviço, não temos nenhum sinal de que ele esteja realmente funcionando. Isso porque o calc.exe foi executado em outro Desktop, mas podemos confirmar a sua execução utilizando o Process Explorer.

    O parâmetro adicional -interactive pode fazer com que seu novo serviço tenha interação com o Desktop. Nota: O Windows Vista merece uma atenção especial neste ponto. O parâmetro -auto configura o início automático do serviço quando o sistema é ligado. E por último, o parâmetro -silent, que faz com que a instalação seja feita de forma silenciosa, ou seja, nenhuma mensagem de sucesso ou erro será exibida. Nota: Neste caso, o sucesso ou a falha pode ser verificada pelo código de saída do Prog2Svc. Outra possibilidade é o uso de variáveis de ambiente no path da aplicação que será executada. Veja este outro exemplo mais completo.

    Prog2Svc -add -silent -auto BlocoDeNotas %SystemRoot%\System32\notepad.exe

    Para remover o serviço é muito simples. Veja o exemplo abaixo. Note que para remover serviços, também podemos contar com o modo silencioso.

    Prog2Svc -remove Calculadora
    Prog2Svc -remove -silent BlocoDeNotas

    Como o pseudo serviço termina?

    Quando solicitamos a parada do serviço, o programa Prog2Svc recebe uma notificação via uma rotina de CallBack. Neste momento, poderíamos dar uma voadora no peito do processo, mas como isso não é de bom tom, uma rotina identifica a janela principal do processo que foi criado, e envia uma mensagem de WM_CLOSE para esta janela. Depois disso, são aguardados 30 segundos para que o processo tenha a oportunidade de finalizar suas tarefas e desalocar recursos do sistema. Caso este tempo expire e o processo ainda esteja rodando, bem, é como meu amigo Thiago sempre diz: “Só a violência constrói”, e o processo é derrubado via TerminateProcess.

    Se você ainda não conseguiu imaginar como essa ferramenta lhe poderia ser útil, pense que você poderia criar um serviço que execute o Command Prompt, e assim, ter uma janela de comandos rodando em conta de sistema. Isso ajudaria a fazer testes e descobrir o que é possível fazer com privilégio desta conta.

    Prog2Svc -add -interactive SysCmd %SystemRoot%\System32\cmd.exe

    Para instalar este programa basta colocar uma cópia deste executável do diretório System32. Tecnicamente, seria possível colocá-lo em qualquer diretório, mas lembre-se de que a pasta onde ele ficará deveria ser acessível por uma conta de sistema. Assim, não o coloque em pastas como “Meus Documentos” ou outra pasta pessoal.

    Como sabemos que a internet não é o lugar mais seguro de se obter um executável, certifique-se de que o programa que você está baixando possui uma assinatura válida.

    Have fun!

    • Versão: 1.0.1.0
    • Windows: 2000 XP 2003 Vista

    Prog2Svc.exe – x86 (58.7 KB)
    Prog2Svc.exe – x64 (59.2 KB)
    Prog2Svc.exe – IA64 (113 KB)

  • Step into Kernel (SoftIce)

    Enfim de volta das férias. Já faziam aproximadamente seis anos que eu não tirava férias. Viajar, conhecer novos lugares e simplesmente relaxar foram minhas principais tarefas durante estes 20 dias. De volta ao trabalho, assim que cheguei na empresa, fui supreendido por ela. Eu nem acredito que mandaram aquela velha embora. Trocamos olhares e é óbvio que pintou um clima. Cheguei mais perto para poder sentir seu cheiro. Eu a olhava de cima a baixo. Simplemente perfeita e até uma injustiça deixa-la de canto. Não demorou e ela já estava sob o meu controle. Eu tinha que provar tudo aquilo. Afinal, haviam tantas opções: Café, cappuccino, chocolate, chá, com ou sem açúcar. Mas foi tudo ilusão. “Ô cafézim ruím dos inferno”. Meus amigos, as aparências enganam. Como pode uma máquina tão bonita fazer um café tão ruím? Confesso que sinto saudades da velha. Não estou querendo dizer que o café é ruinzinho porque o café é horrível! Agora estou buscando maneiras alternativas para manter meu vício. Talvez existam adesivos de cafeína para vender.

    Não se preocupem, este não será mais um post Off-Topic sobre máquinas de café trocadas e o impacto causado por elas. Neste post, que comecei no ano passado e só terminei agora, vou dar os passos necessários para utilizar o SoftICE como depurador de sistema.

    Eu ainda era novo na Scua e estava desenvolvendo uma GINA para o nosso produto de segurança. Para quem não sabe, GINA é aquela telinha de Logon que aparece na plataforma NT. A GINA é uma DLL que é carregada pelo processo Winlogon.exe e que tem a função de fazer a interface com o usuário para realizar o logon na estação. A MsGina.dll é a GINA original do sistema, mas podemos construir nossa própria GINA a fim de proporcionar maneiras alternativas de autenticação. Um exemplo prático disso é o produto Finger True vendido pela Scua. Este produto altera a GINA do sistema para que, com ajuda de um sensor biométrico, seja exigida a impressão digital do usuário na hora de fazer o Logon no sistema. Depurar uma GINA requer alguns passos atípicos, já que para testá-la é necessário fazer Logon, Logoff e coisas assim. Também é possível depurá-la utilizando o Debug Remoto do Visual Studio, mas na época, depurar a GINA foi a necessidade que tive para aprender a utilizar o SoftICE.

    Na Scua haviam duas equipes de desenvolvimento, uma de drivers de Kernel e outra de aplicações. Eu ainda trabalhava na equipe de aplicações, quando em uma reunião de área, a equipe de Kernel me apresentou o SoftICE como uma alternativa para resolver os problemas de Debug que eu estava enfrentando. Eu estava utilizando técnicas como arquivos de Log e MessageBox para depurar a GINA. Vocês podem imaginar os MessageBox com as típicas mensagens de “Passei na rotina X”, “Retornando da rotina Y”. Depois que aprendi a utilizar o SoftICE, ele passou a resolver muitas coisas para mim. Mesmo porque, outras GINAS tinham que ser desenvolvidas, e com o tempo, passei a desenvolver drivers de Kernel.

    A maior parte da equipe de aplicações tinha um certo pânico de usar o SoftICE. Uma situação bem semelhante à descrita em meu post anterior. Eles diziam: “Ah não, usar o SoftICE é muito complicado. Vou continuar com os MessageBox. Caso eu não consiga, então utilizarei o esse Debugger de Kernel”. Acho que foi só uma questão de costume, o SoftICE não é nenhum bicho de sete cabeças e nem difícil de utilizar. Eu achava engraçado quando uma parte da equipe ficava travada tentando resolver um determinado problema, aquele tipo de bug estranho que ninguém conseguia achar. Eu passava por perto e perguntava: “Porque vocês não usam logo o SoftICE?”, “Não não, acho que não é para tanto.”, diziam eles. Uma hora eu também era chamado para ajudar a descobrir o problema, e na maioria das vezes, em meia hora de Debug eu pegava o problema que eles já perseguiam à dias. Não estou dizendo que sou o super gênio do Debug, ou que eles fossem técnicamente incapazes. Estou dizendo que foi só uma questão de utilizar ferramenta certa. Você poderia cavar um túnel com uma colher de cozinha, mas com uma pá as coisas ficariam bem mais fáceis.

    O SoftICE faz parte de um pacote de desenvolvimento chamado Driver Studio que era vendido pela Compuware. Para quem ainda não sabe, a Compuware descontinuou o SoftICE e não podemos contar com uma versão desta ferramenta para o Windows Vista. Com o SoftICE é possível fazer Debug de Kernel utilizando apenas uma máquina, mas o produto ainda possui recursos de Debug remoto através de portas seriais ou ainda placas de rede. Desta forma, também são adotados os nomes Host para a máquina onde se faz o Debug e Target para a máquina que sofre o Debug (Similar ao WinDbg). Aqui adotaremos o nome Host para identificar a máquina onde temos o compilador e a instalação tipo Host do SoftICE. Não estaremos fazendo Debug remoto neste post.

    Muito resumidamente, podemos dizer que o SoftICE trabalha da seguinte maneira. Na máquina Host deverá ser gerado um arquivo de símbolos, conhecido como NMS. Este arquivo é copiado juntamente com o driver a ser testado para a máquina Target. Nela, carregamos os símbolos e depuramos o driver. Vamos descrever estes passos com mais detalhes a seguir.

    Instalando na máquina Host

    Instalar o SoftICE na máquina de desenvolvimento é necessário para que possamos gerar o arquivo NMS. Não se preocupe, você não vai testar seu driver em sua máquina de desenvolvimento. Pelo menos não em seu juízo perfeito. Durante a instalação, você passará pelas típicas telas de setup até chegar na tela exibida acima, onde selecionamos o tipo de instalação desejado. Como vamos utilizar apenas a parte Host da instalação, selecione essa opção e passe para a tela seguinte. Selecione somente a opção “SoftICE Host Components”, de modo que fique como exibida na tela abaixo. O restante desta instalação é next, next…

    Instalando na máquina Target

    Utilizaremos a máquina de teste de forma que não seja necessário haver duas máquinas para fazer o Debug, e assim, exploraremos este recurso que é normalmente utilizado em campo. Para isso selecione a opção “Full Instalation” na primeira tela. Em seguida selecione apenas os ítens “SoftICE Host Components” e “SoftICE Target Components”, de forma que fique como exibido abaixo.

    Neste caso, ao final da cópia e registro dos arquivos, teremos que configurar algumas propriedades do depurador. O primeiro grupo de configurações se refere à inicialização. Vamos selecionar a inicialização manual neste primeiro momento. Todas estas configurações podem ser alteradas depois que o produto foi instalado. A tela de configuração deve ficar como exibida abaixo.

    O próximo grupo será o de configurações gerais. Neste item vamos mudar o campo “Initialization” para que fique com a configuração como exibida abaixo. Estes são comandos que são executados no momento em que o depurador é iniciado. Cada comando é separado por ponto e vírgula. Estes comandos farão as seguintes alterações:

    • LINES 60; Mudar a quantidade total de linhas para 60
    • WC 30; Mudar o tamanho da janela de código para 30 linhas
    • WL 10; Mudar o tamanho da janela de variaveis locais para 10 linhas
    • X; E finalmente o comando que segue com a execução normal do sistema

    A falta do comando “X” fará com que o sistema fique paralizado na inicialização do depurador. Isso nos dá a oportunidade de colocar breakpoints inicias e só depois continuariamos com a execução do sistema com a tecla F5, que é a tecla de atalho para o comando “X”.

    Agora vamos configurar o vídeo. Neste momento você me pergunta: “Mas como assim configurar o vídeo? O Windows não implementa uma camada de abstração chamada GDI?”

    O SoftICE é um depurador de Kernel do sistema. A GDI faz parte do sistema. Como seria depurar um driver de vídeo se o próprio depurador utilizasse tal driver? O depurador deveria influenciar o mínimo possível no sistema depurado. É por esse motivo que o WinDbg precisa de um cabo serial e de outra máquina inteira para tornar o Debug de Kernel possível. Analogamente, o SoftICE, depois de carregado, não pode utilizar nenhum recurso do sistema. Isso inclui drivers de vídeo, disco, teclado, mouse e assim por diante. Para que o SoftICE possa ter um lugar no vídeo enquanto depurando o sistema, ele acessa a memória de vídeo diretamente e desenha “na mão” toda a sua interface. É por isso que temos que configurar o vídeo.

    Esta configuração pode ser escolhida entre duas opções principais. A primeira é a “Universal Vídeo Driver”, onde a interface aparecerá em uma “janela”. Esta “janela” na verdade é o resultado da escrita em memória de vídeo que nos dá a impressão de ser uma janela. Este não é o tipo de janela que estamos acostumados a ver e arrastar com o mouse ou mesmo sofrer a ação do ALT+TAB. Lembre-se, quando o SoftICE exibe sua interface, todo o sistema está congelado. A outra opção é a de tela cheia, onde o SoftICE alterna o modo do vídeo para modo texto para exibir sua interface.

    Se você estiver instalando o SoftICE em uma máquina virtual, alguns passos são necessários:

    • Não instalar ferramentas de aceleração de vídeo tais como VmTools.
    • Utilizar o driver padrão VGA do Windows e configurá-lo para que fique em 640×480 com 16 cores. (nojento mas necessário)
    • Selecionar a opção de “Tela cheia” no SoftICE.

    Se estes passos não forem seguidos, a máquina ficará congelada quando solicitado, mas a tela do depurador não aparecerá. Admitindo que estamos instalando em uma máquina real, selecione a opção de vídeo como aparece abaixo.

    O restante é next, next…

    Gerando o arquivo NMS

    Na máquina Host, depois de escrever e compilar o seu driver (se você ainda não fez isso leia este outro post), agora você deve rodar a aplicação “SoftICE Symbol Loader” que foi instalada. Esta aplicação faz a tradução dos símbolos e gera o arquivo NMS. Selecione a opção Open… do menu File e aponte o driver que você vai depurar. Depois de aberto, o programa deve colocar o nome do driver carregado no título da janela. Eu numerei os três botões que precisaremos para esta operação.

    • Certifique-se que o botão 2 esteja pressionado. Isso vai fazer com que todo o código fonte do driver seja anexado ao arquivo NMS resultante.
    • Certifique-se também de que o botão 3 esteja liberado. Isso vai evitar que o programa fique solicitando por fontes que ele não encontrar.
    • Clique no botão 1 que fará a tradução dos simbolos e irá gerar o arquivo de extensão NMS no diretório local.

    Iniciando o SoftICE

    Depois de instalar seu driver normalmente na máquina Target, estaremos prontos para iniciar o Debug do nosso driver. Lembrando que tanto nosso driver de exemplo quanto o SoftICE estão configurados com Start manual. Para iniciar o SoftICE você pode clicar no ícone “Start SoftICE” que foi instalado no seu menu Iniciar ou simplesmente executar o comando “net start ntice” na janela “Run…” do Windows. Para se certificar de que o depurador foi carregado, pressione as teclas CTRL+D para que o sistema congele e seja apresentada a tela abaixo.

    Não, isso não é um CRASH DUMP da sua placa de vídeo que acabou de explodir. Senhores, é com prazer que vos apresento o SoftICE. Agora você entende o pânico da turma de aplicações? Não se preocupe, ele é feio mas não morde. Pressionando F5 o sistema volta a execução normal.

    Agora teremos que carregar o arquivo NMS no depurador. Isso é feito facilmente simplesmente dando um duplo clique sobre o arquivo de extensão NMS. Este arquivo pode estar em qualquer diretório da máquina. O único momento em que este arquivo é utilizado é quando você o carrega no depurador. Para verificar se o arquivo foi carregado pelo SoftICE, utilizamos o comando “table”, que lista todas as tabelas de símbolos carregadas. Várias tabelas podem ser carregadas simultaneamente, mas somente uma fica ativa por vez. O comando “file *” lista todos os arquivos de uma determinada tabela. Para abrir um arquivo em específico, coloque o nome do arquivo na frente do comando. Veja no exemplo abaixo.

    Com o arquivo já aberto na janela de código, pressione ALT+C para que o cursor vá para o código fonte. Se esta seqüência de teclas não estiver funcionando, pressione ESC e em seguida ALT+C. Repare que você só consegue caminhar com o cursor para cima e para baixo. As tentativas de mover o curor para os lados faz com que a janela de comandos ganhe o foco novamente. Coloque o cursor sobre a linha que você deseja inserir o breakpoint e pressione F9. Neste momento a linha deveria ganhar destaque como mostra a imagem abaixo.

    Agora vamos pressionar F5 para que o sistema volte ao funcionamento normal. No momento em que o driver for iniciado, nosso breakpoint irá interromper esta ação e exibir a interface do SoftICE. A linha em destaque mostra qual será a próxima instução a ser executada. Pressionando as teclas ALT+L, a janela de variáveis locais ganha foco. Você pode navegar entre as variáveis movendo o cursor para cima e para baixo. Pressione a tecla ENTER sobre as estruturas que começam com “+” para que a estrutura seja expandida. A imagem abaixo mostra a janela de registradores seguida da janela de variáveis locais.

    Vou deixar uma pequena tabela de comandos aqui, mas a referência completa está no PDF “SoftICE Command Reference”.

    • F5 : Run
    • F9 : Breakpoint
    • F8 : Step into
    • F10 : Step over
    • F12 : Step out
    • F7 : Run to cursor
    • WL : Janela de variáveis locais
    • WW : Janela de Watch
    • WD : Janela de memória
    • F3 : Alterna entre fonte C/Assembly/Mixed

    É óbvio que existem inúmeros detalhes e técnicas de utilização, mas vamos deixar isso para os manuais que vêm com o produto.

    Nossa, que post longo! Pensou que ia ser fácil não é? Até a próxima…

  • Wow!! DDKBuild fora de Vista

    A cerca de um ano atrás, eu estava lendo o Windows Internals quando cheguei na parte em que o livro fala sobre o Wow64, então pensei comigo mesmo: “Estou até vendo, isso ainda vai dar pano para a manga.”

    Essa semana instalei uma cópia do Vista x64 em minha máquina para fazer alguns testes e tentar adiantar os problemas que eu teria quando ele fosse lançado. Para não dizer que não tive nenhum problema, tive que configurar os atalhos para alguns programas com o modo de compatibilidade setado para Windows XP SP2. Isso porque estes programas exibiam a mensagem: “Este programa não é compatível com Windows 6.0”

    Instalado o Visual Studio 2005, o DDK do Windows 2003 Server e o DDKBuild, tudo estava funcionando muito bem. Um projeto era criado, compilado e depurado no Visual Studio sem problemas. Também compilei um dos exemplos do DDK na linha de comando e também tudo OK. Porém, na hora de compilar um dos exemplos que já disponibilizei no site utilizando o DDKBuild, obtive a seguinte mensagem de erro:

    'ddkbuild' is not recognized as an internal or external command

    Executando o DDKBuild no prompt de comandos tudo funcionava, mas o mesmo erro sempre era apresentado a partir do Visual Studio. Depois de arrancar algumas mechas de cabelo, tentei abrir o arquivo DDKBuild.cmd a partir do Visual Studio, ou seja, menu File, Open…, diretório System32 e foi quando vi que o arquivo não estava no diretório System32 como deveria, pelo menos não para o Visual Studio.

    Demorou mas a ficha caiu. O Visual Studio é um processo 32 bits e caiu nas regras de redirecionamento de File System do Wow64 (Windows-On-Windows 64). Para quem não sabe, para manter compatibilidade com o passado, as plataformas 64 bits ainda utilizam o diretório C:\Windows\System32 para colocar as DLLs de sistema, mesmo sendo elas de 64 bits.

    Mas o que acontece com as DLLs de sistema de 32 bits? Os processos de 32 bits ainda precisam destas DLLs para serem carregados. Quando um processo 32 bits tenta acessar o diretório C:\Windows\System32, seu acesso é redirecionado pelo Wow64 para a pasta C:\Windows\SysWow64 (C:\Windows\System32\SysWow64 no caso do Windows XP x64). Os acessos ao registro também sofrem redirecionamentos e em alguns casos sofrem espelhamentos de 32 bits para 64 bits e vice-versa. Confira os detalhes do Wow64 no site do MSDN.

    Depois que a ficha caiu, ficou ridículo resolver o problema. Simplesmente coloquei uma cópia do DDKBuild.cmd no diretório C:\Windows\SysWow64 e todos viveram felizes para sempre.

  • Walter Oney para Gerentes

    O fato de escrever programas um pouco fora do comum traz algumas conseqüências. Alguns dos seus amigos de trabalho, aqueles que às vezes sentam ao seu lado, não entendem nada do que você está fazendo (e vice-versa). Outros são vistos sussurrando: “Cuidado! É aquele cara ali que programa drivers.”. Essa dificuldade que as outras pessoas têm de entender o que estamos escrevendo, acaba atingindo também os gerentes, diretores e assim por diante. Não que eles fossem culpados disso. Afinal de contas, se eles se interessassem pela área técnica a esse ponto, provavelmente não seriam gerentes, diretores ou seja lá o que for. Isso não é uma regra, mas pelo menos nas empresas onde trabalhei, a minoria dos meus superiores entendiam a complexidade daqueles problemas que acabavam atrasando a entrega de um produto.

    A pior das situações é quando um superior pensa que é simples resolver as coisas. Afinal de contas, quando ele programava em COBOL, não havia um problema que ele não resolvesse, e que você provalvelmente estaria fazendo corpo mole ou simplesmente se achando um super astro por resolver os problemas das telas azuis. Eu particularmente acho lindo quando eles perguntam: “Mas você já não resolveu esse problema da tela azul?”, como se houvesse apenas um problema cuja conseqüência é uma tela azul.

    Bom, desta forma acho que a opinião de uma reconhecida autoridade no assunto poderia ter alguma influência sobre a opinião destas pessoas. No primeiro capítulo do seu livro Programming The Microsoft Windows Driver Model (2º Edition), Walter Oney dá algumas dicas sobre como uma empresa poderia encarar o desenvolvimento de drivers a fim de obter resultados mais próximos dos esperados. Separei alguns trechos que seguem logo abaixo.

    “A triste realidade é que programar WDM é muito difícil, e somente programadores experientes (e caros!) são capazes de fazê-lo bem.”

    “O desenvolvimento dos drivers deve iniciar assim que houver uma especificação razoavelmente definida de como o hardware vai trabalhar. Você deveria esperar por modificações na especificação em virtude às desagradáveis descobertas durante o desenvolvimento do driver, …”

    “Você também deve esperar que programar drivers leve mais tempo e mais custo que o imaginado inicialmente. Todo programa está sujeito a uma maior demanda de tempo e custo. Uma demanda adicional viria da dificuldade de comunicação entre as pessoas de hardware e software, das ambiguidades na especificação e na documentação do DDK,…”

    “Dê atenção em saber como os usuários finais irão instalar seu driver. A maioria dos vendedores de hardware preferem entregar uma instalação customizada em um CD-ROM, e escrever o instalador é um processo longo que pode consumir um programador experiente por várias semanas…”

    “O arquivo executável provavelmente incluirá mensagens de texto em um resource especial com múltiplas línguas, e seria uma boa idéia ter uma pessoa treinada para compô-las. (Eu não estou dizendo que seu programador de drivers não possa fazer isto, mas ele pode não ser a melhor escolha.)”

    “Finalmente, não trate seus drivers como detalhes sem importância. Ter um bom driver com um suave instalador é no mínimo tão importante quanto a aparência exterior do produto. … Então, uma má escolha por um desenvolvimento barato de driver poderia ter um dramático efeito negativo em poucos anos. Este aviso é especialmente importante para fabricantes de hardware de países em desevolvimento, onde gerentes têm a tendência a procurar por qualquer corte de custos. Eu sugiro que o desenvolvimento de drivers seja em um lugar onde ter decisões baseadas em custos seja inapropriado.”

    Talvez eles nunca leiam isso, mas com certeza nos ajuda a respirar fundo e continuar programando. Agora volta a trabalhar porque você não é pago pra ficar navegando na Internet! (Essa também é ótima)… 😀

  • Step into Kernel (Serial)

    Ganhei meu primeiro computador aos 13 anos de idade, mas foi só depois de 7 anos, quando comecei meu estágio, que descobri que era possível depurar os programas que eu escrevia. Até lá, meus métodos de depuração sempre foram coisas como imprimir o valor da variável na tela. Tudo bem que nesse tempo eu não desenvolvia programas complexos o suficiente para ficar sem saída, e com o passar do tempo, acho que me acostumei a trabalhar sem esse luxo todo de breakpoint e tal.

    Quando fui apresentado a um depurador, nem dei o devido valor para aquilo. Eu pensava comigo mesmo: “É só colocar um printf e beleza. Não preciso de toda essa parafernalha.”. Na época utilizávamos o CodeView, um depurador 16 bits que roda em DOS, que é exibido na tela abaixo.

    Não demorou muito para eu começar a depender do depurador para fazer as coisas mais simples, como escovar os dentes por exemplo. Na verdade, algumas situações como escrever código para coletores de dados, escrever firmwares para hardwares que não tinham display, situações nas quais não podíamos contar nem com uma porta serial para saber o que se passava com o software, e a inesquecível situação onde havíamos colocado um ocsiloscópio em um determinado pino do processador para capturar sinais de vida do nosso software, enfim, situações onde daríamos um braço para ter um simples breakpoint me mostraram que o depurador não é perfumaria.

    O engraçado é que hoje em dia, na faculdade, a grande maioria dos alunos da minha turma não sabe depurar software. Eu já tentei explicar para meus amigos o tamanho da vantagem de utilizar um depurador, mas eles acabam tendo a mesma reação que tive anos atrás: “Ah não Fernando, isso é muito complicado. Só de olhar a gente acaba encontrando o erro.”. Por mim tudo bem…

    Bom, a gente está aqui pra beber ou pra conversar? Vamos depurar o driver gerado pelo tradicional post Getting Started. Desta vez, vamos fazer como manda a tradição. Vamos precisar de duas máquinas, um cabo serial do tipo Null Modem e por último e não menos importante, uma cópia do Windbg. Não, não adianta ler esta frase de novo, você não entendeu errado, você vai precisar realmente de duas máquinas. Não sei porque, algumas pessoas apresentam uma certa resistência em acreditar nisso. Bom, suponho que superamos essa fase e podemos continuar com o post.

    Se você ainda não tem uma cópia do Windbg, você poderá baixa-lo gratuitamente da página do Windows Debugging Tools no site da Microsoft.

    Vamos tomar esta imagem ao lado emprestada do help do Windbg, que mostra como devemos ter os micros para realizar o Debug de Kernel.

    A máquina nomeada HOST será onde teremos o WinDbg rodando. Esta máquina é, na maioria das vezes, a máquina onde o driver foi desenvolvido. Vamos partir do princípio que esta é a máquina onde o driver foi desenvolvido e adiar os detalhes de como configurar o diretório de símbolos e fontes para um outro post. A máquina nomeada TARGET é onde teremos nosso driver carregado pelo sistema. O sistema operacional da máquina TARGET não precisa necessariamente ser Checked Build, nem é necessário instalar nenhum kit adicional ao sistema operacional. Tudo o que precisamos é do seu driver e de uma porta serial. Todos os Windows da plataforma NT (Windows NT, Windows 2000, Windows XP, Windows 2003 Server e Windows Vista) já trazem seu depurador nativo desde a sua instalação. Na verdade, essa é uma excelente característica no uso do Windbg. Imagine aquele tipo de problema que só se manifesta na máquina do cliente. Você não vai querer instalar nada agressivo ou mesmo que mude o cenário de forma que o problema não se manifeste mais. Acredite, isso não é tão raro assim. Nestes casos, basta habilitar o Debug e pronto.

    Para habilitar o Debug na máquina TARGET, precisaremos editar alguns parâmetros de inicialização que estão no arquivo Boot.ini localizado na pasta raiz do sistema. Este arquivo está com os atributos de arquivo de sistema, arquivo oculto e também como somente leitura, talvez seja necessário configurar o Windows Explorer para que seja possível ver estes arquivos. Utilizando o Windows Explorer, vá até a pasta raiz, remova a propriedade de somente leitura do arquivo Boot.ini, em seguia, abra este arquivo utilizando um editor de texto como o Bloco de Notas por exemplo. O conteúdo deste arquivo é semelhante ao demonstrado baixo.

    Na sessão Operating Systems você terá que duplicar a linha do sistema que você vai querer depurar. Adicione ao final desta linha os parâmetros /debugport=com1 /baudrate=115200 que irão configurar o modo Debug. O primeiro parâmetro seleciona a porta COM a ser utilizada, enquanto o segundo seleciona a velocidade da comunicação. O protocolo serial não é o meio mais rápido de comunicação entre as máquinas HOST e TARGET, e em algumas situações chega a ser desagradável ter que esperar as interações entre o WinDbg e o sistema depurado. Existem outros meios de comunicação que podem ser utilizados caso seu computador não tenha uma porta serial disponível ou caso você queria ter mais velocidade e conforto no Debug. Veja mais detalhes no post Serial Killers.

    O resultado final do nosso arquivo Boot.ini deveria ser algo semelhante ao exibido abaixo. Vou pular o inicio as linhas que decrevem o sistema operacional para a melhor visualização colocando “…”.

    [boot loader]
    timeout=30
    default=multi(0)disk(0)rdisk(0)partition(2)\WINDOWS
    [operating systems]
    multi(0)... /NoExecute=OptIn
    multi(0)... /NoExecute=OptIn /debugport=com1 /baudrate=115200

    Feitas estas alterações, você pode salvar o arquivo e restaurar os atributos originais do arquivo. No Windows XP e Windows 2003, os mesmos passos acima descritos poderiam ser feitos com a ajuda de uma ferramenta chamada Bootcfg.exe. A partir do Windows Vista este processo é bem diferente e vou descreve-los em um post futuro.

    De volta à máquina HOST, inicie o WinDbg e selecione a opção Kernel Debug… do menu File. Deverá ser exibida uma janela que configura o meio de comunicação. Faça isso de forma que fique compatível com o que foi configurado no arquivo Boot.ini da máquina TARGET. Neste caso, estamos utilizando a porta serial COM1 e baud rate de 115200. Clicando em OK o WinDbg vai abrir a porta serial da máquina HOST e vai esperar até que a máquina TARGET esteja pronta. Neste ponto, teremos a seguinte mensagem na janela de comandos do WinDbg.

    Enquanto a máquina HOST aguarda, certifique-se de que seu driver está instalado na máquina TARGET e a reinicie. Depois de reiniciada, quando sistema for iniciar a sua carga, o Loader do sistema irá exibir as opções encontradas no arquivo Boot.ini editadas por você. Selecione a opção onde aparece [debugger enabled] como mostra abaixo.

    Selecionada esta opção, as máquinas HOST e TARGET deveriam se conectar e na janela Command seria exibiria uma saíba semelhante à mostrada abaixo.

    Microsoft (R) Windows Debugger  Version 6.6.0007.5
    Copyright (c) Microsoft Corporation. All rights reserved.
     
    Opened \\.\com1
    Waiting to reconnect...
    Connected to Windows XP 2600 x86 compatible target, ptr64 FALSE
    Kernel Debugger connection established.
    Symbol search path is: *** Invalid ***
    ****************************************************************************
    * Symbol loading may be unreliable without a symbol search path.           *
    * Use .symfix to have the debugger choose a symbol path.                   *
    * After setting your symbol path, use .reload to refresh symbol locations. *
    ****************************************************************************
    Executable search path is:
    *********************************************************************
    * Symbols can not be loaded because symbol path is not initialized. *
    *                                                                   *
    * The Symbol Path can be set by:                                    *
    *   using the _NT_SYMBOL_PATH environment variable.                 *
    *   using the -y  argument when starting the debugger. *
    *   using .sympath and .sympath+                                    *
    *********************************************************************
    *** ERROR: Symbol file could not be found.  Defaulted to export symbols for
    ntkrnlpa.exe -
    Windows XP Kernel Version 2600 UP Free x86 compatible
    Built by: 2600.xpsp_sp2_gdr.050301-1519
    Kernel base = 0x804d7000 PsLoadedModuleList = 0x805531a0
     

    Muito bem, até aqui sabemos que as máquinas estão conectadas. Pressionando as teclas Ctrl + Break no Windbg, faremos com que o sistema fique congelado na máquina TARGET e que o controle seja passado ao depurador. O depurador estará pronto para receber seus comandos quando aparecer um prompt de comandos indicado pelas letras KD no canto inferior esquerdo da janela Command.

    Apenas para fazer um simples teste, digite o comando “lm” e tecle enter. Este comando lista os módulos carregados pelo sistema. Note que o driver Useless ainda não está nesta lista. Isso acontece porque o driver exemplo está configurado para ter sua execução iniciada manualmente, desta forma, o driver ainda não foi carregado pelo sistema. Em seguida, selecione o ítem Open Source File… do menu File e abra o arquivo Useless.c. Clique sobre o nome da função DriverEntry e pressione F9. Embora não haja nenhum sinal visível disso, colocamos um breakpoint nesta função. O fato de não ter nenhuma alteração visual se deve ao fato de que nosso módulo ainda não está carregado. Podemos nos certificar de que o breakpoint está instalado listando os breakpoints com o comando “bl”.

    kd> bl
     0 e f9fb3430     0001 (0001) Useless!DriverEntry

    Agora vamos lançar o comando “g” que fará com que o sistema volte a execução normal.

    Depois que o sistema for carregado, vamos iniciar o driver através do comando net start Useless. Quando o driver for iniciado, o entry point DriverEntry será executado e por conseqüência teremos a execução interrompida pelo nosso breakpoint. A máquina TARGET ficará congelada enquanto o depurador detém o controle sobre o sistema. Repare que agora que nosso módulo está carregado, os breakpoints são facilmente identificados pelas linhas com a cor de fundo vermelha.

    Bom, os primeiros passos já foram dados, os comandos e detalhes de como depurar o Kernel de Windows estariam fora do escopo deste post. Mas uma boa dica de como obter kilos de detalhes sobre este assunto é o livro Windows 2000 Kernel Debugging. O livro não é um dos meus preferidos, ele dá uma boa introdução ao assunto além de falar sobre como utilizar e construir Debug Extensions, mas não chega a ter as técnicas ninjas utilizadas pelos verdadeiros mestres do Kernel Debugging. Uma outra excelente fonte de detalhes sobre este assunto é a lista de discussão Windows Debugger Users List dedicada ao Windbg. Isso tudo sem nos esquecermos do Help.

    Este foi um post inicial sobre o assunto, aguardem por posts futuros onde darei os passos de como utilizar o Windbg com a VMware e de como depurar o Kernel do Windows com apenas uma máquina utilizando o SoftIce (que Deus o tenha).

    Have fun… 😉

  • Off-topic HP 50G

    Finalmente o período de provas semestrais terminou. Já posso voltar à vida normal, me re-integrar à sociedade, e porque não, escrever posts. Para os que não sabiam, estou cursando Engenharia da Computação na Universidade São Judas Tadeu. Apesar dos meus trinta anos de idade, ainda estou no terceiro ano devido à uma vida um tanto agitada que um programador pode ter. Tenho certeza que muitos de vocês sabem exatamente do que estou falando. Tive que interromper meu curso diversas vezes, ou por falta de grana, ou por excesso de trabalho. Enfim, estas últimas semanas estive correndo atrás do prejuízo e não pude compor um post decente. Assim, vão ter que se contentar com algumas curiosidades sobre minha nova melhor amiga, a HP 50G.

    Costumo dizer que programador não é nome de profissão, e sim nome de doença. “Programador: Pessoa que sofre de programação”. Um dos sintomas que considero mais determinantes no programador é a grande capacidade de associar coisas diversas com programação, mesmo sem que se queira. Um exemplo? Fácil!!! Quando eu estava cursando Informática Industrial na ETE, havia um professor que falava pausadamente, cerca de 3 segundos entre uma frase e outra. Ele parecia um andróide. Eu costumava imaginar que aquela característica se dava ao fato de que o professor tinha pouca RAM, dessa forma, levava um certo tempo para carregar uma nova frase da HD para ser processada pelo sintetizador de voz. Na época dizíamos que se prestássemos atenção, poderiamos ver um led piscando dentro da orelha dele durante aquelas pausas. Felizmente aquilo poderia ser facilmente resolvido se houvesse uma segunda thread que colocasse a próxima frase na RAM à medida que a frase atual fosse processada pela thread principal. Não sei o que vocês pensam sobre isso, mas para mim parece doença. :-S

    Estudando para as provas de Cálculo Numérico Computacional e armado com uma HP 50G, foi inevitável programar a HP para automatizar alguns passos. Os exercícios desta matéria são compostos basicamente por contas simples, mas são longos e cheios de regrinhas. Portador de programação desde os 13 anos de idade, baixei os manuais e mãos à obra. Quando compramos a HP, vem um “Quick Start” de umas 120 páginas, mas é possível baixar PDF do manual completo de 918 páginas em português.

    Como era de se esperar, encontrei muitos sites e tutoriais sobre o assunto, vários em português mesmo. O site hpcalc.org foi um grande colaborador. Na página de programação pode-se encontrar muita coisa. E o melhor, é tudo free.

    Podemos programar a HP em UserRPL ou SystemRPL. Programar em SystemRPL nos dá um ganho de performance quase 10 vezes superior que em UserRPL. Entretanto, tudo na vida em um preço. O trecho abaixo foi retirado em um dos inúmeros PDFs que encontrei.

    Estranho! Eu podia jurar que já li algo parecido em algum lugar.

    Como não consegui baixar uma versão do WinDbg que depurasse a HP, resolvi programar em UserRPL mesmo. A linguagem é basicamente composta pela seqüência de teclas que seriam pressionadas durante o uso normal da calculadora, porém, podemos ainda contar com blocos de execução, laços de repetição(for, while), execução condicional (if, else), execução de sub-programas e outras tantas coisas que não tive paciência de ler. É divertido poder misturar coisas como IF e ELSE com comandos que fazem operações com matrizes como se fossem tão simples quanto somar 1 em 1.

    A HP pode ser programada nela mesma, ou seja, utilizando seu próprio teclado. Mas ficar escrevendo sobre aquele tecladinho é similiar a ficar jogando Decathlon no Atari. Funciona, mas você sabe o que vai acabar acontecendo. Outra coisa super desconfortável é tentar entender o que está errado no seu programa olhando o fonte (que não esta indentado) em uma telinha de LCD. Uma das facilidades é um cabo USB que conecta a HP em seu micro.

    Para ter acesso aos diretórios e variáveis armazenados na calculadora, você terá que fazer o download do software que faz esta conexão. Mas se você possuir uma máquina x64 (mesmo que seja da HP), não haverão drivers disponíveis para realizar esta conexão. Se este for o seu caso, você pode utilizar uma máquina virtual que esteja rodando um sistema operacional de 32 bits. Não é que funciona mesmo!

    Quando conectado, é exibido uma espécie de “Explorer” onde você pode navegar nas pastas e editar variáveis e programas. Com um double-click sobre os programas, um excelente ambiente de desenvolvimento chamado “Notepad” é aberto com o seu programa. Nem pense em identar seu programa. Quando o este é enviado para a HP, ele assume uma identação dela, que obviamente não é a mesma que gostariamos de ter.

    Pois é, foi no mínimo interessante aprender mais essa. Realmente valeu a pena gastar essa grana comprando algo que eu possa programar e poder sustentar meu vício. Mas ainda não para por aí. Se você der uma olhada na quantidade e variedade de aplicações para download da hpcalc.org, você terá uma idéia do que mais ela pode fazer além de calcular o determinante de uma matriz. Se estiver curioso, você pode baixar um emulador da calculadora em seu PC e fazer um Test Drive.

  • Kernel + Visual Studio 2005

    Programador de driver ou software de baixo nível está acostumado a compilar seus projetos na linha de comando, utilizar editores de texto como o Notepad ou mesmo o Norton Editor para codificar seus drivers. É como meu amigo Thiago diz: “Faca nos dentes e sangue nos olhos”. Utilizar o comando TYPE | MORE para ver os arquivos de erros de compilação, mostra o quanto somos seres superiores, dominates da tecnologia, que não sub-utilizamos nosso cérebro, que não precisamos de ferramentas supérfluas como interface gráfica, sintaxe colorida, intellisence ou mesmo o auto-complete e que estamos no topo da cadeia alimentar.

    Se você realmente acredita nisso, então é melhor abandonar este post por aqui mesmo. Hoje vou defender o uso das ferramentas que ajudam, e muito, na hora de codificar um driver e ter que lidar com funções com grande número de parâmetros e intermináveis estruturas.

    Utilizar ou não o compilador do Visual Studio para gerar drivers é mais uma das discussões que tendem ao infinito, tal como o uso de C++ no desenvolvimento para Kernel Mode. De um lado estão aqueles que defendem que o ambiente quase nunca está perfeitamente configurado para compilar drivers. Já li posts de verdadeiras autoridades no assunto falando sobre quantos dias foram necessários para encontrar um bug que foi gerado por uma otimização do compilador ou mesmo um define incorreto que foi gerado pelo Wizard. Estes que dizem que utilizar o Visual Studio como ambiente de desenvolvimento requer um conhecimento mínimo de cada parâmetro utilizado na chamada para o cl.exe, que o ideal mesmo é utilizar o bom e velho Build que trata os arquivos sources, dirs e makefile e ter a absoluta certeza de que o ambiente configurado pelos atalhos do DDK está correto. Do outo lado estão os que não abrem mão da comodidade de ter um ambiente integrado, de poder pressionar apenas uma tecla para cair na linha do arquivo em que o erro foi detectado, de utilizar sintaxe colorida, intellisence, auto-complete e outras tantas vantagens que o Visual Studio oferece.

    Já faço isso a algum tempo e lembro-me de quando o DDK resolveu trazer um compilador embitudo no Kit a partir do Windows XP. O principal objetivo desta mudança no DDK foi tentar separar o ambiente de desenvolvimento de drivers do ambiente de aplicações. Esse vínculo já criou muitos problemas. O DDK do Windows NT 4.0 era baseado no VC 4.2, mas com o surgimento do VC 5.0 e posteriormente do VC 6.0, o formato da tabela de símbolos foi alterado e o Windbg parou de funcionar. Nesta época eu utilizava muito mais o SoftIce que o WinDbg para depurar drivers e continuei utilizando o compilador do Visual Studio sem nem perceber este problema. Só tive problemas com símbolos quando o Visual Studio passou para a versão 7.0. O VToolsD até hoje não oferece suporte ao novo formato de símbolos. Conclusão, até hoje eu utilizo VC 6.0 para compilar VXDs. Não que seja impossível utilizar o VC 7.0 para isso, só não conseguiremos depura-lo.

    Mas foi quando fizemos o upgrade do VS2003 para o VS2005 que mudanças mais drásticas fizeram com que minha vida se tornasse um pouco mais azul. Foi a partir dessa época que comecei a utilizar o melhor dos dois mundos, ou seja, a confiança de ter um ambiente corretamente configurado e toda a comodidade das ferramentas que o editor do VS2005 nos oferece.

    Como fazer isso? Ok, vamos utilizar o projeto criado no post Getting Started como ponto de partida. Inicialmente precisaremos baixar o arquivo DDKBUILD.CMD da OSR Online e grave-o em um diretório que esteja no PATH da sua máquina. Este CMD irá precisar da variavel de ambiente WNETBASE configurada com o diretório base da sua instalação do DDK. Este arquivo é um meio de fazer um build externo com o Visual Studio. Para fazer isso, crie um projeto de Makefile no Visual Studio como mostra a seguir.

    Depois de entrar com o nome do seu novo projeto e a pasta onde ele será criado, é só clicar em “OK”. Mesmo sem configurar nada, clique em “Finish” da janela seguinte. Criado o projeto, adicione os arquivos no Solution Explorer. Em seguida, vamos editar as propriedades do projeto de forma que fique como ilustrado abaixo. Note que no campo “Include Search Path” estou assumindo que a instalação do DDK foi realizada no diretório C:\WINDDK\3790. Este campo não tem nenhuma influência sobre o processo de compilação, ele apenas informa ao intellisence quais os diretórios devem ser utilizados para servirem de fontes para sua base de dados.

    A partir deste ponto, já podemos compilar o projeto como se fosse um projeto qualquer de User Mode. As imagens abaixo dispensam comentários. Imagino que não é necessário dizer que não é possível depurar drivers utilizando o ambiente do Visual Studio. Lembre-se que depuração de Kernel exige depuradores especiais para isso como comentei em meu último post.

    Uma janela especialmente interessante que foi adicionada ao Visual Studio 2005, foi a “Code Definition Window”, que exibe o lugar onde os símbolos foram definidos conforme você os escreve na janela de código. Veja o exemplo disso quando escrevo uma chamada para IoSetCompletionRoutine.

    Enfim, dêem uma olhada nas opções oferecidas pelo DDKBUILD. Você pode listar suas opções simplesmente abrindo uma janela de prompt e executando-o sem qualquer parâmetro. Observe que podemos compilar drivers utilizando inclusive o PREfast.

    É isso aê… Have fun ! 😉