Archive for November, 2008

Gerenciando paginação do driver

18 de November de 2008

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!