Gerenciando paginação do driver

18 de November de 2008 - Fernando Roberto

Depois de tanto falar sobre memória virtual e paginação, recebi uma pergunta que coincidentemente tem tudo a ver com o assunto dos últimos posts. “Para que servem os pragma alloc_text que vemos nos exemplos do WDK?” (Thiago Cardoso, Recife-PE). Essa pergunta já deve ter passado pela cabeça de muitos que já deram uma olhada nos exemplos do WDK. Visto que todos os exemplos do WDK que conheço utilizam este pragma, até que demorou para alguém perguntar sobre isso. Mas enfim, vamos ao que interessa.

Como já vimos, páginas de memória podem estar tanto nos chips de RAM como em disco. Também já vimos que threads que estão em alta prioridade de execução não podem acessar dados que são pagináveis. Mas como saberiamos quais dados são pagináveis ou não?

Controlando Paginação de Dados

Quando alocamos memória dinamicamente, podemos escolher se a área de memória a ser alocada será paginável ou não. A função ExAllocatePool, e suas irmãs (ExAllocatePoolWithTag, ExAllocatePoolWithQuota, ExAllocatePoolWithQuotaTag e ExAllocatePoolWithTagPriority), recebem um parâmetro do tipo POOL_TYPE que define se a memória a ser alocada será paginável ou não.

typedef enum _POOL_TYPE {
  NonPagedPool,
  PagedPool,
  NonPagedPoolMustSucceed,
  DontUseThisType,
  NonPagedPoolCacheAligned,
  PagedPoolCacheAligned,
  NonPagedPoolCacheAlignedMustS
} POOL_TYPE;
 
 
PVOID 
  ExAllocatePool(
    IN POOL_TYPE  PoolType,
    IN SIZE_T  NumberOfBytes
    );

Você terá que gerenciar quais alocações serão acessadas por diferentes prioridades de execução. Por exemplo: Uma determinada lista ligada é consultada apenas por funções que rodam somente em IRQL baixa, logo, todos os seus elementos podem ser alocados em memória paginável (PagedPool). Por outro lado, uma lista ligada que é consultada por funções que rodam em IRQLs altas, deve ter seus elementos alocados em memória não paginável (NonPagedPool).

Legal Fernando, mas nem tudo é alocado dinamicamente. Como ficam as variáveis estáticas?

Por padrão, todas as variáveis globais são não pagináveis. Isso é ruim se seu driver possui muitas variáveis globais, o que exigiria mais memória não paginável para que seu driver pudesse ser carregado, e como também já foi visto, memória não paginável deve ser poupada. Felizmente, podemos definir que um conjunto de variáveis globais possa ser paginável, desde que estas sejam apenas acessadas por threads em baixa IRQL.

Dentro de um módulo, seja uma aplicação, uma DLL ou mesmo um driver, o controle de paginação da memória é aplicado nas sessões que os compõem. Para que um grupo de variáveis seja definido em uma sessão paginável, podemos utilizar o pragma data_seg. Porém, não é todo compilador que nos permite fazer isso. Para saber se o compilador que estamos utilizando suporta o uso deste pragma, contamos com os headers do WDK, que definem o símbolo ALLOC_DATA_PRAGMA quando o compilador oferece suporte a este recurso. Isso, obviamente, tem se tornado menos significante, já que o aconselhável é utilizar o compilador do próprio WDK, mas não custa nada prever que seu código possa ser compilado por algum outro compilador. Veja o exemplo abaixo que define tanto variáveis não pagináveis como pagináveis.

//-f--> Estas são variáveis definidas em uma sessão não paginável
PDEVICE_OBJECT  g_pControlDeviceObj;
PDRIVER_OBJECT  g_pDriverObj;
 
//-f--> Aqui eu verifico se o compilador que estou utilizando
//      suporta o uso do #pragma data_seg.
//      Se sim, eu abro a sessão PAGEDATA que é uma sessão de
//      dados paginável.
#ifdef ALLOC_DATA_PRAGMA
    #pragma data_seg("PAGEDATA")
#endif
 
//-f--> Todas as variáveis declaradas aqui serão pagináveis.
//      Assim, apenas threads que rodam em baixo nível de
//      prioridades poderão acessar tais variáveis
 
//-f--> Definimos um buffer gigante. Ainda bem que é paginável
UCHAR   g_Buffer[100000];
 
//-f--> Aqui marcamos o fim da sessão paginável. As variáveis
//      definidas após este #pragma serão não pagináveis.
#ifdef ALLOC_DATA_PRAGMA
    #pragma data_seg()
#endif

Controlando Paginação de Código

Memória é memória, seja para armazenar dados ou código. Podemos também controlar onde as funções que você escreve serão definidas. Assim, podemos colocar todas as funções que executam em baixa prioridade em sessões de código pagináveis. Aqui usaremos o pragma que originou a dúvida do Thiago, o pragma alloc_text.

Este pragma tem duas limitações. A primeira delas é que o pragma deve ser aplicado depois da declaração da função, mas antes da definição da mesma. A outra é que este pragma não é aplicável em funções C++, ou seja, métodos de classes ou funções com sobrecargas não terão esse luxo de ser pagináveis. Se você é como eu, que prefere utilizar a tipagem forte do C++ em drivers, mesmo que você só escreva simples funções, você deverá utilizar o modificador extern “C” nas declarações das funções.

Esse pragma não é obrigatoriamente suportado por todos os compiladores, e assim como o data_seg, os headers do WDK definem o símbolo ALLOC_PRAGMA para sinalizar que o compilador utilizado suporta esse recurso. Segue mais um exemplo.

//-f--> Normalmente esta declaração é feita em um arquivo de
//      header. Observe que se estivermos compilando em C++,
//      teremos que usar o extern "C" para poder definir a
//      sessão onde as funções aqui declaradas serão definidas.
#ifdef __cplusplus
extern "C"
{
#endif
    //-f--> Declara uma
    ULONG SumOne(IN ULONG ulParam);
 
    //-f--> Declara outra
    ULONG SumTwo(IN ULONG ulParam);
 
#ifdef __cplusplus
}
#endif
 
 
//-f--> A parte a seguir fica no mesmo módulo onde a função é
//      definida, e deve ficar antes da definição da função.
//      Repare que aqui testamos se este pragma é suportado, e
//      se sim, cada função deve receber seu pragma alloc_text
#ifdef ALLOC_PRAGMA
    #pragma alloc_text(PAGE, SumOne);
    #pragma alloc_text(PAGE, SumTwo);
#endif
 
 
//-f--> Depois disso, podemos definir as funções normalmente
 
ULONG SumOne(IN ULONG ulParam);
{
    //--f-> Função SumOne que foi escrita por alguém.
 
    //-f--> Esse comentário só tem graça em Inglês.
    //
    //      Function SumOne that has been written by someone
 
    PAGED_CODE();
 
    return ulParam + 1;
}
 
ULONG SumTwo(IN ULONG ulParam);
{
    //-f--> Função desgraçada. (que não tem graça nenhuma)
    PAGED_CODE();
 
    return SumOne(SomeOne(ulParam));
}

Fernando, para quê servem estas macros PAGED_CODE que você usou no exemplo?

Bom, lá vem histórinha. Uma coisa desconfortável é que problemas com paginação de memória vão ocorrer apenas quando a página que você está acessando estiver em disco. Isso significa que você pode escrever um driver com problema, testá-lo, e se por “sorte”, as páginas estiverem todas em RAM durante o teste, você não verá nenhum problema. Uma das coisas que ajuda bastante é essa macro PAGED_CODE. Em Checked Build, esta macro é traduzida para uma função que irá verificar se a prioridade corrente é baixa suficiente para executar código paginável. Caso não seja, uma exceção de breakpoint será lançada. Espero que você esteja com o depurador atachado para ver isso acontecer. Caso contrário, não se preocupe, uma linda tela azul irá aparecer e você vai acabar conectando o depurador mais cedo ou mais tarde. Quando compilado em Free Build, essa macro é traduzida para nada, evitando perda de performance. Concluindo, isso evita de você chamar uma função paginável em IRQL alta e tudo funcionar por “sorte”.

...
 
#elif DBG
 
#define PAGED_CODE() {                                                       \
    if (KeGetCurrentIrql() > APC_LEVEL) {                                    \
        KdPrint(("EX: Pageable code called at IRQL %d\n", KeGetCurrentIrql())); \
        NT_ASSERT(FALSE);                                                    \
    }                                                                        \
}
 
...
 
#else
 
#define PAGED_CODE()        NOP_FUNCTION;
 
...
 
#endif

Uma excelente maneira de pegar problemas de paginação de cógido é utilizar o Driver Verifier com a opção de Force IRQL Checking habilitada. Esta opção, além de verificar se você está chamando as funções da API nas prioridades corretas, também força a paginação de tudo que seja paginável em seu driver toda vez que a IRQL subir para DISPATCH_LEVEL ou superior. Isso acaba com aquela “sorte” de utilizar um recurso paginável em IRQL alta quando o recurso já estiver em RAM.

Sessão Descartável

Além da sessão de código paginável PAGE que vimos, uma outra sessão também é vista com grande frequência nos exemplos do WDK. A sessão INIT é descartada quando a chamada à função DriverEntry do seu driver retornar ao sistema, e se for o caso do seu driver, depois que qualquer função de reinicialização for terminada. Se você não sabe o que é uma função de reinicialização, então dê uma passada por este post. Assim, se você tem funções que são somente utilizadas durante a inicialização do seu driver (que neste caso significa: funções chamadas pela DriverEntry, ou funções chamadas por funções que foram chamadas pela DriverEntry, ou ainda funções chamadas por funções que foram chamadas por funções… Ah! Eu acho que você entendeu), vocé pode utilizar o mesmo procedimento para defini-las na sessão INIT da mesma maneira como foi feito anteriormente. Mas não custa deixar um exemplo.

//-f--> As funções aqui declaradas serão removidas da RAM quando
//      a chamada à função DriverEntry retornar. Não tente chamá-las
//      depois disso, porque estas já foram para o céu das funções
//      de inicialização.
 
#ifdef ALLOC_PRAGMA
    #pragma alloc_text(INIT, DriverEntry);
    #pragma alloc_text(INIT, FunctionCalledByDriverEntry);
    #pragma alloc_text(INIT, AnotherFunctionCalledByDriverEntry);
 
    #pragma alloc_text(PAGE, SumOne);
    #pragma alloc_text(PAGE, SumTwo);
#endif

Isso é especialmente útil em Legacy Drivers, que realizam muitos passos na inicialização. Legacy drivers, além de procurar o hardware que vão controlar, ainda precisam fazer a associação dos recursos disponíveis (portas, interrupções, canais de DMA e etc), e isso acaba consumido uma quantidade expressiva de código. Por outro lado, drivers de WDM recebem tudo mastigadinho do Plug-and-Play Manager. O hardware foi detectado e a associação de recursos já foi negociada. Coisa linda de Deus!

Paginável ou não paginável, eis a questão

Supondo que você tenha muitas funções que rodem em IRQL alta, e que por isso, devem estar em memória não paginável, isso faria com que uma grande quantidade de memória não paginável seja utilizada para manter tais funções, mesmo que ninguém esteja utilizando o driver. Também podemos definir nossas próprias sessões e assim torná-las pagináveis ou não pagináveis quando nos for conveniente. Isso permitiria que todas aquelas funções não pagináveis sejam pagináveis enquanto ninguém obtiver uma referência para nosso driver. Desta forma, quando recebermos um IRP_MJ_CREATE ou quando programarmos o hardware para disparar interrupções, podemos dizer ao sistema que agora precisaremos tornar a sessão onde tais funções foram definidas como não paginável.

Primeiro de tudo, teremos que criar nossas sessões do coração, e faremos isso utilizando o pragma alloc_text para definir sessões que devem ter seu nome no formato com PAGExxxx, onde xxxx seja um nome único no seu driver. Veja o exemplinho de desencargo de consciência.

//-f--> As funções aqui declaradas serão definidas em uma sessão
//      que pode ser não paginável quando nos for conveniente.
 
#ifdef ALLOC_PRAGMA
    #pragma alloc_text(PAGEABCD, FunctionOne);
    #pragma alloc_text(PAGEABCD, FunctionTwo);
#endif
 
 
//-f--> As funções aqui declaradas serão definidas em uma outra 
//      sessão que pode ser não paginável. Assim podemos definir
//      diferentes grupos de funções em diferentes sessões.
 
#ifdef ALLOC_PRAGMA
    #pragma alloc_text(PAGE1234, FunctionThree);
    #pragma alloc_text(PAGE1234, FunctionFour);
#endif

Agora que já definimos quais funções estarão definidas nessa sessão, podemos torná-la não paginável somente quando tais funções forem utilizadas. Para tal devemos utilizar a função MmLockPagableCodeSection.

PVOID 
  MmLockPagableCodeSection(
    IN PVOID  AddressWithinSection
    );

Para identificar qual sessão será marcada como não paginável, teremos que passar um endereço que esteja dentro da sessão. Um nome de função definida na sessão já resolve o problema, mas lembre-se que todas as funções dentro da mesma sessão serão marcadas como não paginável.

A função MmLockPagableCodeSection nos retorna um endereço opaco que pode ser utilizado como parâmetro para a chamada à função MmUnlockPagableImageSection, que marca a sessão novamente como paginável. Essa função é normalmente chamada antes do driver ser descarregado. Seguindo a linha do nosso exemplo, poderiamos chamar esta função ao receber um IRP_MJ_CLOSE. O mesmo endereço retornado por MmLockPagableCodeSection pode ser utilizado como parâmetro às chamadas à função MmLockPagableSectionByHandle para novamente tornar uma sessão não paginável, o que é muito mais rápido que a chamada à MmLockPagableCodeSection. Assim, devemos chamar MmLockPagableCodeSection pelo menos uma vez para obter o ponteiro opaco, e depois disso, podemos chamar MmLockPagableSectionByHandle e MmUnlockPagableImageSection.

VOID 
  MmLockPagableSectionByHandle(
    IN PVOID  ImageSectionHandle
    );
 
VOID 
  MmUnlockPagableImageSection(
    IN PVOID  ImageSectionHandle
    );

O mesmo pode ser feito em sessões customizadas que definem dados, mas devemos utilizar a função MmLockPagableDataSection para obter o ponteiro opaco que identifica a sessão.

Minha função pode ser paginável?

Fernando, se minha função é chamada em PASSIVE_LEVEL, então ela pode ser paginável?

Não é bem assim. Sua função, mesmo sendo chamada em PASSIVE_LEVEL, pode conter intervalos de código que precisam ser não pagináveis. Se você chamar funções que elevam a IRQL, tais como KeAcquireSpinLock, sua função não pode ser definida em sessão paginável.

Mas Fernando, acompanhe o meu raciocínio. Se a função foi chamada e está em execução no momento, não é obvio que a página à qual ela está contida está em RAM?

Você pode até não acreditar, mas uma função é composta por uma cadeia de bytes que podem estar nas fronteiras de páginas. Isso significa que o inicio da sua função pode estar no final de uma página, que de fato foi paginada para RAM quando a chamada foi feita, mas não sabemos onde uma nova página pode começar. Esta nova página pode estar em disco, e se for acessada em IRQL alta, a paginação causará uma tela azul. Veja o código abaixo para ter uma idéia do que estou falando e não esqueça de ler os comentários.

/****
***     FunctionCalledAtPassiveLevel
**
**      Rotina que é chamada em PASSIVE_LEVEL,
**      mas tem elevação de IRQL durante a chamada.
*/
 
PLIST_ENTRY
FunctionCalledAtPassiveLevel(VOID)
{
    KIRQL       Irql;
    PLIST_ENTRY pEntry = NULL;
 
    PAGED_CODE();
 
    //-f--> Aqui nossa IRQL vai para as alturas, se o codigo que
    //      vier depois desta chamada estiver na próxima página
    //      de memória que estiver em disco, então (CABUM !!!)
    KeAquireSpinLock(&g_SpinLock, &Irql);
 
 
    //-f-->    -------======= Fronteira de página =======-------
 
 
    //-f--> O código aqui é executado em DISPATCH_LEVEL, o que
    //      impede esta função de ser definida em uma sessão
    //      paginável.
 
    if (!IsListEmpty(&g_NonPagedList))
    {
        pEntry = RemoveHeadList(&g_NonPagedList);
    }
 
    //-f--> Tudo bem que você acesse apenas dados não pagináveis,
    //      mas o código que você usa para tal acesso também precisa
    //      estar em memória não paginável. Afinal, esse código está
    //      sendo executado em IRQL alta e recuperar uma página de
    //      código em disco resultaria em coisas horríveis.
 
    //-f--> Voltamos a PASSIVE_LEVEL
    KeReleaseSpinLock(Irql);
 
    return pEntry;
}

Se você possui funções que são grandes, mas que contém intervalos pontuais com elevação de IRQL, então separe tais intervalos em funções isoladas que podem ser definidas em sessões não pagináveis, permitindo assim que você defina sua grande e complexa funcão em uma sessão paginável.

Ufa! Eu ainda poderia escrever mais alguns comentários sobre este assunto, mas se eu já estou cansado de escrever, imagino como vocês estão. O assunto é tratado com todos detalhes na referência. Mas se tiverem qualquer dúvida a respeito, é só me enviar um e-mail e torcer pra eu saber responder.

Até mais!

3 Responses to “Gerenciando paginação do driver”

  1. Figueredo says:

    Olha rapaz, você adiantou a resposta pra uma outra dúvida, o significado da macro PAGED_CODE()! 🙂

  2. Anonymous says:

    Fernando, parabens pelos artigos. Muito bons.

    Deixa eu te perguntar uma coisa.

    Encontrei este codigo na web:
    http://groups.google.com/group/microsoft.public.development.device.drivers/browse_thread/thread/fc15553cc41ff917/f09e9bde4347eee0?lnk=raot

    É de um programa que registra os processos que são criados e que são terminados.

    Você teria como me dizer algum exemplo de código que permita saber qual é o processo que vai ser iniciado, mas antes dele ser executado? Ou seja, quando ele clicar no .exe, o driver vai saber quem é e por dentro do driver teria condicoes de permitir ou nao.

    Tem algo assim ?

    Obrigado

    • Olá Anonymous,

      Bloquear a execução de processos não é uma coisa tão simples assim. Na verdade, existem algumas alternativas. Já fiz isso pelo menos para duas empresas diferentes. É realmente divertido e não vai se resumir a algo com 15 linhas de código.

      1) Fazer um filtro de File System que detecte quando algum arquivo for aberto com o bit de FILE_EXECUTE em alto, ou seja, arquivo sendo aberto para execução. Pelo path do arquivo poderiamos mandar uma mensagem para algum programa em User Mode que avaliaria se determinado arquivo poderia ser executado. Mas isso não nos dá 100% de certeza de que o arquivo seria de fato executado. Também pegariamos DLLs, OCX, e qualquer coisa que deseja mapeada para execução. Se um programa qualquer simplesmente abrir um arquivo com todas as permissões, nosso evento já seria disparado.

      2) Fazer um hook da função ZwCreateProcess na tabela de APIs nativas do sistema. Isso fecharia um pouco mais nosso escopo em arquivos que seriam realmente excutados. Um problema é que esta API não recebe o path do arquivo a ser executado, mas sim um handle para a section do arquivo já mapeado para execução. Nem sempre se pode obter o nome do arquivo a partir da sua section, na verdade, as sections criadas para execução pelo Loader do Windows não coloca nomes nas sections, por isso você teria que interceptar as funções de criação de Sections para saber de quais arquivos elas vêm. Mas este ainda é o menor dos problemas. Esse negócio de interceptar a tabela da API nativa foi tão explorado que alguns anti-virus reclaram quando eles detectam que alguém fez isso, embora alguns anti-virus também façam esse jogo. O maior dos problemas nesta abordagem é que a partir do Windows XP (x64) a Microsoft criou uma coisa chamada Patch Guard, que detecta se a tabela foi violada e lança um BugCheck quando isso ocorre.

      Não estou dizendo que é impossível fazer isso, só é preciso ter um pouco de jogo de cintura para isso tudo funcionar, e mesmo que funcione, não apostaria minha vida que estariamos 100% seguros de incompatibilidades com anti-virus e proteções que futuras versões do Windows tragam. Se você perguntasse a alguém da lista OSR como fazer isso, eles provavelmente diriam que não há como fazer, já vi perguntas como estas por lá. Eles são puristas e acabam derrubando qualquer idéia com as regras sagradas da API e para quê elas foram criadas.

      Vale lembrar que soluções feitas em drivers são indiferentes com relação à sessões de usuários, contas de sistemas e coisas assim. Se você criar uma solução que bloqueie processos de serem executados, lembre-se que seu driver vai pegar todos os processos, mas todos mesmo, incluindo Winlogon.exe, Smss.exe, Csrss.exe, Lsass.exe e outros processos vitais que rodam antes mesmo do Windows descobrir que é um sistema operacional. Cuidado com estes caras.

      Espero ter ajudado de alguma forma.
      []s.

Deixe um comentário