Archive for February, 2007

Sincronismo x Performance

28 de February de 2007

Não é à toa que meus últimos posts estão trazendo assuntos referentes a listas ligadas e performance. Nas últimas semanas, fui contratado por uma empresa de segurança para dar uma olhada em um de seus filtros de File System, a fim de diminuir o atraso causado por eles. Neste post vou falar sobre sincronismo e contenção de CPU.

Acho que não deve ser novidade para muitos aqui que uma lista ligada, ou qualquer outro recurso, quando compartilhado entre várias threads, deve implementar algum sincronismo de acesso, evitando assim, que uma thread leia dados inválidos em conseqüência de uma alteração realizada por outra thread.

Mas eu não tenho dois processadores, para quê sincronismo?

É verdade que computadores com um processador, em um dado instante, executam apenas uma thread. Assim, nunca teremos duas threads sendo executadas ao mesmo tempo. Mas é importante lembrar que threads são executadas em pequenas fatias de tempo que são determinadas pelo scheduler, levando em consideração o tamanho do quantum e a prioridade de cada thread. Existem meios de evitar que uma thread seja interrompida pelo scheduler do Windows, mas em condições normais de temperatura e pressão, não sabemos quando uma thread será interrompida para que outra passe a ser executada.

Suponha que a thread A esteja varrendo uma lista à procura de um determinado nó. Esta thread chega ao registro R e é interrompida para que a thread B possa ser executada. A thread B remove o mesmo registro R da lista e o desaloca da RAM. Quando a thread A é retomada e consulta os campos do finado registro R, esta acessará dados inválidos e tornará os passos seguintes imprevisíveis. Se bem que é bem previsível que algo azul deva acontecer.

Existem vários mecanismos de sincronismo que podemos utilizar, mas neste post, vou comentar especificamente sobre os envolvidos nestas semanas. Mas além destes, sou obrigado a falar sobre o mais exótico que já vi em anos de experiência, que foi por mim apelidado de “Mutex, pero no mutcho”. Esta era uma classe derivada da classe VMutex do VToolsD. O método enter() desta classe tentava insanamente adquirir o mutex algumas milhares de vezes em um loop. Se depois destas milhares de interações, o mutex ainda não fosse adquirido, então a thread se dava por satisfeita e acessava o recurso de qualquer maneira. Será que o sistema estava tendo algum problema de Dead Lock? De qualquer forma, foi um prazer corrigir isso anos atrás. Tem coisas que a gente só acredita vendo.

Conforme eu já comentei num outro post, filtro de File System é uma camada posta sobre os drivers FASTFAT, NTFS, CDFS, Network Redirectors e quaisquer outros drivers que implementem a interface de sistema de arquivos. Ou seja, todas as operações referentes a arquivos, inclusive os acessos com File Mapping passam pelos filtros de File System.

Se você não está só interessado em saber a respeito de filtros de File System, e sim interessado em trabalhar com eles, então você tem a obrigação de ler o Windows NT File System Internals de Rajeev Nagar. Este livro é a única referência reconhecidamente abrangente o suficiente sobre este assunto. Publicado inicialmente em 1997 pela O’Reilly, este livro ainda é a ferramenta obrigatória para o desenvolvimento referente a File Systems mesmo para o Windows Vista. Sua publicação foi interrompida durante alguns anos, e durante essa época, eu mesmo cheguei a ver o preço em mais de U$ 200.00 por único um exemplar usado na Amazon. Hoje a OSR detém os direitos do livro e atualmente está trabalhando em uma edição atualizada, que deve trazer assuntos tais como o Shadow Copy, o NTFS transacional, Filter Manager, Mini-Redirectors e a desmontagem forçada de volumes. Entretanto este é um trabalho que ainda levará algum tempo, então em 2005, a versão original foi reimpressa para suprir essa necessidade enquanto a nova edição é composta.

O filtro que tenho trabalhado mantem várias listas que devem ser consultadas a cada acesso interceptado. O mecanismo utilizado para sincronizar o acesso a estas listas era o Spin Lock. Uma escolha óbvia, mas inadequada para este cenário, onde a atividade é muito intensa. O grupo de listas que é consultado na IRP_MJ_READ é o mesmo utilizado na IRP_MJ_WRITE e em outros eventos. O resultado disso é a contenção de CPU. Ou seja, quando uma thread adquire um Spin Lock para realizar uma consulta na lista, todas as outras threads que precisam consultar a mesma lista precisam sentar e esperar até que o Spin Lock seja liberado (mesmo em casos onde temos mais de um processador). Sabendo que estas listas não são pequenas, adivinha se ficava lento…

Assim como a fome mundial, o câncer de colo do útero, o exame de próstata e outros males que atingem a humanidade, algo devia ser feito a respeito. Por isso, numa incansável luta contra as forças do mal foi criado o ERESOURCE. (Cantos angelicais e uma névoa de gelo seco se dissipa no chão)

Utilizando o ERESOURCE

ERESOURCE é o meio mais adequado e nativamente utilizado para sincronizar acessos às estruturas que fazem parte de sistemas de arquivos, tais como o FCB (File Control Block), que além de outras informações, mantém o tamanho atual do arquivo. Com o ERESOURCE, várias threads podem ter acesso à mesma lista ligada ao mesmo tempo para leitura. Admitindo que nenhuma das threads vai alterar dados da lista, então todos os acessos poderiam ser realizados simultaneamente. Em contrapartida, se necessário for, uma thread pode ganhar acesso exclusivo à lista a fim de fazer alguma alteração.

Para utilizar o ERESOURSE, você precisa declarar uma variável do tipo ERESOURCE, que deve residir em memória não paginada e alinhada em 8 bytes, e inicializá-la utilizando a função ExInitializeResourceLite. Para ter acesso compartihado (somente leitura) às listas, você deve utilizar a função ExAcquireResourceSharedLite. Esta função verifica a existência de alguma thread com acesso exclusivo sobre o recurso controlado. Caso haja, a função pode, dependendo de um parâmetro, retornar falha ou aguardar até que o recurso seja liberado. Para ter acesso exclusivo, utilize a função ExAcquireResourceExclusiveLite, que análogamente verifica a existência de threads com acesso compartilhado sobre o recurso, e opcionalmente, aguarda até que todas as threads com acesso compartilhado liberem o recurso para que o acesso exclusivo seja dado. Finalmente, para liberar o acesso adquirido, seja exclusivo ou compartilhado, utilize a função ExReleaseResourceLite.

Um ponto interessante a ser notado é que a entrega de Kernel APC precisa ser desabilitada para as threads que adquirirem o ERESOURCE. Não conheço o real motivo desta necessidade, mas no mínimo evita que a thread que detém o acesso ao recurso controlado seja terminada por uma outra thread, isso sabendo que o TerminateThread é implementado via Kernel APC. Desta forma, a maneira mais comum de utilizar essas funções é como demonstrado abaixo.

    //-f--> Obtem acesso compartilhado (read only)
    KeEnterCriticalRegion();
    ExAcquireResourceSharedLite(&m_Resource, TRUE);
 
    //-f--> Realiza consultas ao recurso
 
    //-f--> Libera o recurso
    ExReleaseResourceLite(&m_Resource);
    KeLeaveCriticalRegion();

Um ponto negativo é que a maioria das funções que lidam com ERESOURCE, diferente dos Spin Locks, devem ser chamadas em IRQL < DISPATCH_LEVEL. Desta forma, talvez sejam necessárias algumas manobras com as IRPs e suas CompletionsRoutines para lidar com isso.

Feitas as modificações, o sistema ganhou muito em performance nos testes que fiz. O ganho será ainda maior à medida que tivermos mais operações em paralelo, e obviamente, em micros com mais de um processador.

E todos viveram felizes para sempre…

Listas ligadas no DDK

24 de February de 2007

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?

12 de February de 2007

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)

5 de February de 2007

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

2 de February de 2007

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)