Archive for July, 2009

Strings no Kernel

7 de July de 2009

Tem coisa mais besta que manipular strings? Creio que a resposta correta seria “Depende“. Quando eu tinha terminado meu curso técnico de Informática Industrial em 1995, eu pensava que sabia muito de linguagem C. Afinal de contas eu já sabia manipular strings. Copiar, concatenar, inverter, fazer buscas por palavras… O que mais um programador deveria saber? Quando comecei a fazer meu estágio e a pegar programas do mundo real, descobri que eu não sabia nada. Mas uma coisa é certa, eu sabia manipular strings. Este post deve ajudar bastante os programadores C++ que manjam tudo de Templates, Smart Pointers, STL e coisas mágicas que abstraem a realidade facilitando a vida do programador. Com todos esses recursos de contadores de referências e sobrecarga de tudo que é operador, as coisas começam a ficar nebulosas e você começa a se perguntar: “Mas onde está o buffer mesmo?”. Hoje vamos apenas dar uma passadinha de leve nas estruturas UNICODE_STRING, ANSI_STRING e algumas funções de conversão entre elas. Pode parecer besteira, mas se você não souber brincar de strings, pra quê aprender o resto? Vai tudo acabar em tela azul mesmo.

E no prézinho…

Nós aprendemos com a tia da escolinha que strings são cadeias de caracteres. Assim poderíamos contar a história de nossas vidas apenas colocando um caractere na frente do outro.

CHAR    szExemplo[] = "Tava ruim lá na Bahia, profissão de bóia-fria\n"
                      "Trabalhando noite e dia, num era isso que eu queria\n"
                      "Eu vim-me embora pra \"Sum Paulo\",\n"
                      "Eu vim no lombo dum jumento com pouco conhecimento\n"
                      "Enfrentando chuva e vento e dando uns peido fedorento (vish)\n"
                      "Até minha bunda fez um calo\n"
                      "Chegando na capital, uns puta predião legal\n"
                      "As mina pagando um pau, mas meu jumento tava mal\n"
                      "Precisando reformar\n"
                      "Fiz a pintura, importei quatro ferradura\n"
                      "Troquei até dentadura e pra completar a belezura\n"
                      "Eu instalei um Road-Star!";

Jumento Celestino / Mamonas Assassinas

Duas coisas são obrigatórias que você saiba para que você possa continuar lendo este post. Uma delas é que estes caracteres precisam estar armazenados em algum lugar, seja em uma variável local, alocadas no heap ou mesmo no segmento de dados inicializados. A outra coisa é que strings normalmente são terminadas por um caractere NULL, mas a falta dele não descaracteriza uma string. Isso significa que podemos ter strings sem terminadores onde seu tamanho é indicado por uma outra variável. Deixo aqui um gancho para o Lesma explicar essas coisas aos meninos e meninas interessados.

Uma outra característica importante é que nem sempre um caractere é igual a um Byte. Existem strings compostas por caracteres largos. Caracteres largos, ao contrário do que se pensa, não são caracteres sortudos, mas sim, caracteres formados por valores de 16 bits. Com strings formadas por tais caracteres pode-se expressar palavras em qualquer idioma. Isso explica como o Windows consegue lidar com nomes de arquivos alienígenas quando você tenta instalar os drivers do HiPhone que você comprou no China.

Além disso, imagine que ocorra um erro durante o processo de criptografia do seu disco rídigo. Seria vital que uma mensagem de erro detalha lhe fosse exibida independente da nacionalidade do produto. Se a informação vai ajudar já é outro assunto.

Quatro tipos de strings

Destes temos duas strings que já estamos acostumados a ver em user-Mode, ambas terminadas com caractere NULL.

CHAR    szString[] = "Uma string de CHAR";
WCHAR   wzString[] = L"Uma string de WCHAR";

As outras duas strings são as que normalmente vemos em kernel-mode.

typedef struct _UNICODE_STRING {
  USHORT  Length;
  USHORT  MaximumLength;
  PWSTR  Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
 
typedef struct _STRING {
  USHORT  Length;
  USHORT  MaximumLength;
  PCHAR  Buffer;
} ANSI_STRING, *PANSI_STRING;

Estas strings são definidas por estruturas com três membros, os quais descrevo abaixo. O comportamento das rotinas e macros que às manipulam é muito similar. Por isso vou concentrar meus exemplos em UNICODE_STRING já que o sub-sistema Win32 converte tudo para unicode quando passa uma chamada para o kernel.

  • Buffer: E um ponteiro para a região de memória onde os caracteres são armazenados.

  • Length: Indica a quantidade de bytes válidos da string. Este tamanho é sempre expresso em bytes, mesmo que esta seja uma string de WCHAR.

  • MaximumLength: Indica o tamanho máximo que esta string pode ter.

Para entender de como estes membros são interpretados, dê uma olhada no exemplo abaixo:

void OsTresMembros(void)
{
    WCHAR           wsUmArray[200];
    UNICODE_STRING  usString;
 
    //--f-> Indico onde os caracteres desta string
    //      serão armazenados.
    usString.Buffer = wsUmArray;
 
    //-f--> Ainda não escrevemos nada no buffer,
    //      por isso, nada do que esteja no buffer
    //      é válido.
    usString.Length = 0;
 
    //-f--> Embora o buffer não esteja inicializado
    //      ele ainda está lá e pode conter uma string
    //      de no máximo seu tamanho em bytes.
    usString.MaximumLength = sizeof(wsUmArray);
}

Aqui vemos uma string unicode vazia, pois tem zero bytes válidos, mas sua capacidade de armazenar é de até 200 caracteres. Notem que os caracteres são armazenados em um array que está na pilha. Isso significa que nenhum leak de memória será causado com o termino desta rotina.

Inicializando Strings

Podemos ainda utilizar algumas macros que fazem a inicialização destas estruturas.

void InitString(void)
{
    UNICODE_STRING  usOutraConstante;
    UNICODE_STRING  usVazia;
    WCHAR           wzBuffer[30] = L"Isso não será considerado.";
 
    //-f--> Inicaliza uma string na sua criação.
    UNICODE_STRING  usConstante = RTL_CONSTANT_STRING(L"Uma string.");
 
    //-f--> Inicializa uma string constante.
    RtlInitUnicodeString(&usOutraConstante,
                         L"Uma outra string constante que não muda.");
 
    //-f--> Inicializa uma string vazia para uso posterior
    RtlInitEmptyUnicodeString(&usVazia,
                              wzBuffer,
                              sizeof(wzBuffer));
}

Reparem nos valores destas estruturas.

kd> ?? usConstante
struct _UNICODE_STRING
 "Uma string."
   +0x000 Length           : 0x16
   +0x002 MaximumLength    : 0x18
   +0x004 Buffer           : 0xf8cd8600  "Uma string."
 
kd> db 0xf8cd8600 L0x18
f8cd8600  55 00 6d 00 61 00 20 00-73 00 74 00 72 00 69 00  U.m.a. .s.t.r.i.
f8cd8610  6e 00 67 00 2e 00 00 00                          n.g.....

Embora a capacidade máxima desta string seja de 0x18 caracteres, somente 0x16 bytes são válidos. Isso porque a macro desconsidera o terminador NULL que o compilador C/C++ deixou de brinde.

A macro RTL_CONSTANT_STRING() nos permite inicializar a string na sua criação, mas um outro notável recurso dela é que ela ferra o IntelliSence do Visual Studio (2008 pelo menos). Então se você gosta mesmo do IntelliSence, prefira utilizar a macro RtlInitUnicodeString().

kd> ?? usVazia
struct _UNICODE_STRING
 "Isso não será considerado."
   +0x000 Length           : 0
   +0x002 MaximumLength    : 0x3c
   +0x004 Buffer           : 0xf8ae5c34  "Isso não será considerado."

Oops! Como pode uma string com zero bytes válidos ser exibida pelo WinDbg? Na verdade, o que acontece é que o WinDbg desmonta a estrutura UNICODE_STRING e mostra cada um dos mebros aqui. No caso, existe um array de WCHAR com valores bem comportados aqui. Não culpe o coitadinho. Você é que está mal acostumado com o Visual Studio.

kd> db 0xf8ae5c34 L0x3c
f8ae5c34  49 00 73 00 73 00 6f 00-20 00 6e 00 e3 00 6f 00  I.s.s.o. .n...o.
f8ae5c44  20 00 73 00 65 00 72 00-e1 00 20 00 63 00 6f 00   .s.e.r... .c.o.
f8ae5c54  6e 00 73 00 69 00 64 00-65 00 72 00 61 00 64 00  n.s.i.d.e.r.a.d.
f8ae5c64  6f 00 2e 00 00 00 00 00-00 00 00 00              o...........

Mas estes dados só estão aí por acaso. Eles não são considerados como parte válida de uma string unicode. Isso atrapalha um pouco na hora de fazer o debug pois mesmo a janela de variáveis locais também mostra esse conteúdo inválido.


A extensão !ustr mostra apenas os dados válidos de uma string unicode.

kd> !ustr usConstante
String(22,24) at f899fc6c: Uma string.
 
kd> !ustr usVazia
String(0,60) at f8ae5c1c:

O terminador é necessário?

Não mesmo! Você pode bem observar que nos exemplos anteriores, a macro deixou o terminador de fora dos bytes válidos. Considerar o terminador como byte válido é um erro e pode gerar confusão. Imagine comparar duas strings distintas que carregam o mesmo conteúdo, mas uma delas considera o terminador como informação válida. Tais strings serão diferentes, já que possuem comprimento diferente.

Creio que o importante mesmo é não contar com o terminador nas strings que você recebe de outros componentes. É fato que na maioria das vezes, o buffer possui um terminador, e que apesar de não ser considerado informação válida, o terminador ainda está lá. Nunca conte com isso a menos que haja alguma nota na documentação. Já vi pessoas que utilizarem o membro Buffer como parâmetro para uma chamada à rotina wcslen() por exemplo. Além do risco de obter a informação incorreta, ainda tem um plus de poder gerar uma tela azul.

“Mas Fernando, eu já testei isso em vários sistemas operacionais e sempre funcionou.”

Isso não justifica nada, você não pode se apoiar em testes, mas em documentações. Quando seu produto se espalha no mercado, ele enfrenta muitos ambientes diferentes, com os mais diversos filtros, anti-virus, monitores e por aí vai. Não se pode ter certeza da implementação de qualquer software. O melhor que podemos esperar deles é que se apoiem na documentação.

Manipulando Strings

Os membros da estrutura UNICODE_STRING são basicamente utilizados por rotinas de manipulação a fim de verificar se o buffer existente é suficiente para a operação desejada. Portanto, antes de utilizar uma string, certifique-se de esta foi inicializada corretamente. No caso de uma cópia de strings, a string de destino precisará ser inicilizada mesmo que vazia.

void CopyString(void)
{
    UNICODE_STRING  usSource;
    UNICODE_STRING  usTarget;
    WCHAR           wzTarget[10];
 
    //-f--> Aqui inicializamos nossa string de origem.
    RtlInitUnicodeString(&usSource,
                         L"12345678901234567890");
 
    //-f--> Aqui inicializamos nossa string de destino.
    RtlInitEmptyUnicodeString(&usTarget,
                              wzTarget,
                              sizeof(wzTarget));
 
    //-f--> Realiza a cópia
    RtlCopyUnicodeString(&usTarget,
                         &usSource);
}

Aqui vemos o exemplo de uma cópia de string onde a string de origem é maior que a de destino. Nessa situação não teremos uma violação de acesso, mas o buffer de destino será preenchido completamente. Notem que a rotina não nos deixou o confortável terminador.

kd> !ustr usTarget
String(20,20) at f89a3c6c: 1234567890
 
kd> ?? usTarget
struct _UNICODE_STRING
 "1234567890"
   +0x000 Length           : 0x14
   +0x002 MaximumLength    : 0x14
   +0x004 Buffer           : 0xf89a3c54  "1234567890"
 
kd> db 0xf89a3c54 L0x20
f89a3c54  31 00 32 00 33 00 34 00-35 00 36 00 37 00 38 00  1.2.3.4.5.6.7.8.
f89a3c64  39 00 30 00 98 5d 5f 00-14 00 14 00 54 3c 9a f8  9.0..]_.....T<..

Diferentes da rotina RtlCopyUnicodeString(), algumas outras rotinas nos retornam STATUS_BUFFER_TOO_SMALL quando o buffer é insuficiente.

VOID 
  RtlCopyUnicodeString(
    IN OUT PUNICODE_STRING  DestinationString,
    IN PCUNICODE_STRING  SourceString
    );
 
NTSTATUS 
  RtlAppendUnicodeStringToString(
    IN OUT PUNICODE_STRING  Destination,
    IN PUNICODE_STRING  Source
    );

Existe uma série de rotinas de manipulação de strings no WDK, mas sempre fica faltando alguma rotina se comparado com a ampla biblioteca de rotinas da biblioteca padrão C/C++. Rotinas como strrchr() por exemplo. Quando necessário teremos que construir uma versão que manipule estruturas UNICODE_STRING da mesma maneira. Aqui está uma lista básica de rotinas de string que o WDK suporta. Outras funções são listadas aqui, mas falaremos delas mais tarde.

Mas onde está o buffer mesmo?

A estrutura UNICODE_STRING não armazena buffer, mas sim um ponteiro para ele. Dessa forma, a maneira de descartar uma string varia dependendo da maneira que você a obteve. Nos exemplos que vimos até agora, os bufferes utilizados estão em dados inicializados ou de arrays locais da função de exemplo. Existem rotinas que inicializam e alocam strings como forma de retornar a informação desejada. Nestes casos, é necessário liberar o buffer que você recebeu. É o caso das rotinas que fazem a conversão de ANSI_STRING para UNICODE_STRING e vice versa. São elas RtlUnicodeStringToAnsiString() e RtlAnsiStringToUnicodeString().

NTSTATUS 
  RtlAnsiStringToUnicodeString(
    IN OUT PUNICODE_STRING  DestinationString,
    IN PANSI_STRING  SourceString,
    IN BOOLEAN  AllocateDestinationString
    );
 
NTSTATUS 
  RtlUnicodeStringToAnsiString(
    IN OUT PANSI_STRING  DestinationString,
    IN PUNICODE_STRING  SourceString,
    IN BOOLEAN  AllocateDestinationString
    );

O exemplo abaixo faz uma conversão de ANSI para UNICODE com alocação do resultado, e em seguida, libera o buffer recebido.

void ConvertString(void)
{
    ANSI_STRING     asString;
    UNICODE_STRING  usString;
 
    //-f--> Aqui inicializamos nossa string de origem.
    RtlInitAnsiString(&asString,
                      "Um exemplo simples.");
 
    //-f--> Neste caso não precisaremos inicializar a
    //     string de destino. A rotina fará isso por nós.
    RtlAnsiStringToUnicodeString(&usString,
                                 &asString,
                                 TRUE);
 
    //-f--> Imprime a string resultante
    DbgPrint("String convertida: %wZ\n",
             &usString);
 
    //-f--> Liberamos o buffer alocado na conversão.
    RtlFreeUnicodeString(&usString);
}

Você mesmo pode escrever uma rotina que gere UNICODE_STRINGs alocando o buffer dinamicamente. O buffer pode ser alocado dinamicamente utilizando ExAllocatePoolWithTag() ou uma de suas irmãs. No entanto, na hora de liberar o buffer desta string, utilize a função adequada, que neste exemplo seria ExFreePoolWithTag(). Não saia utilizando RtlFreeUnicodeString() a torto e a direito. Aprecie com moderação. Apenas utilize essa rotina para liberar strings que foram obtidas por funções como RtlAnsiStringToUnicodeString(), cuja documentação indica o uso de RtlFreeUnicodeString().

"..., the caller must deallocate the buffer by calling RtlFreeUnicodeString."

Safe Strings em Kernel

Uma parte das rotinas de manipulação de strings da biblioteca padrão do C/C++, tais como strcpy() e sprintf(), também estão disponíveis em kernel, mas a crescente preocupação com a seguraça na manipulação de buffers fez com que as funções seguras fossem disponibilizadas tanto para user-mode como para kernel-mode. Para ter detalhes sobre o uso destas funções consulte este link.

Mais um driver de exemplo

Este outro post traz o exemplo de um driver que guarda uma lista de strings em memória. Já neste outro post, esse mesmo exemplo foi evoluido para que diferentes listas fossem mantidas sob diferentes contextos. Agora vou evoluir esse exemplo novamente. A aplicação continuará enviando strings com terminadores NULL durante a escrita, o driver criará ANSI_STRINGs a partir delas e às converterão em UNICODE_STRINGs antes de colocá-las na lista.

A maior parte das modificações estão nas rotinas de leitura e escrita, então vou apenas exibir o código dessas rotinas aqui. De qualquer forma, todo o projeto incluindo o driver e uma aplicação de teste estão disponíveis para download ao final deste post. Vamos começar com a rotina de escrita que envia as strings ao driver. Como sempre, toda informação revelante estão nos comentários.

/****
***     OnWrite
**
**      A aplicação está enviando uma string.
*/
NTSTATUS
OnWrite(IN PDEVICE_OBJECT  pDeviceObj,
        IN PIRP            pIrp)
{
    PIO_STACK_LOCATION  pStack;
    ANSI_STRING         asString;
    PSTRING_LIST        pStringList;
    PSTRING_REG         pStringReg;
    ULONG               ulBytes;
    KIRQL               kIrql;
    NTSTATUS            nts;
 
    //-f--> Obtemos a Stack Location corrente.
    pStack = IoGetCurrentIrpStackLocation(pIrp);
 
    //-f--> Obtém a ponta da lista.
    pStringList = (PSTRING_LIST)pStack->FileObject->FsContext;
 
    //-f--> O que temos no buffer de sistema aqui é um array
    //      de bytes terminados com NULL. Vamos inicializar
    //      uma ANSI_STRING com este buffer.
    RtlInitAnsiString(&asString,
                      (PCSZ)pIrp->AssociatedIrp.SystemBuffer);
 
    //-f--> Aqui alocamos o nó que vai ser colocado na lista
    pStringReg = (PSTRING_REG) ExAllocatePoolWithTag(NonPagedPool,
                                                     sizeof(STRING_REG),
                                                     STR_LST_TAG);
 
    //-f--> Para fazer a conversão para UNICODE_STRING, vamos
    //      solicitar que a rotina faça a alocação do bufer
    //      resultante. Por essa razão, não precisaremos inicializar
    //      a string de saída.
    nts = RtlAnsiStringToUnicodeString(&pStringReg->usString,
                                       &asString,
                                       TRUE);
    if (!NT_SUCCESS(nts))
    {
        //-f--> Ops! Provavelmente não tivemos memória para isso.
        //      Vamos sinalizar a falha e informar ao IoManager
        //      que zero bytes foram copiados.
        ExFreePoolWithTag(pStringReg, STR_LST_TAG);
        pIrp->IoStatus.Information = 0;
    }
    else
    {
        //-f--> Vamos segurar o spinlock para evitar concorrência
        //      no acesso à lista.
        KeAcquireSpinLock(&pStringList->SpinLock,
                          &kIrql);
 
        //-f--> Insere o nó na lista.
        InsertTailList(&pStringList->ListHead,
                       &pStringReg->Entry);
 
        //-f--> Libera o spinlock.
        KeReleaseSpinLock(&pStringList->SpinLock,
                          kIrql);
 
        //-f--> Aqui informamos ao IoManager que todos os bytes
        //      enviados pela aplicação foram recebidos com
        //      sucesso pelo driver.
        pIrp->IoStatus.Information = pStack->Parameters.Write.Length;
    }
 
    //-f--> O campo de Information já foi preenchido, vamos apenas
    //      copiar o status da operação e completar a IRP.
    pIrp->IoStatus.Status = nts;
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    return nts;
}

Agora vejamos a recuperação das strings na leitura.

/****
***     OnRead
**
**      A aplicação está querendo receber as strings eviadas
**      por ela.
*/
NTSTATUS
OnRead(IN PDEVICE_OBJECT  pDeviceObj,
       IN PIRP            pIrp)
{
    ANSI_STRING         asString;
    PIO_STACK_LOCATION  pStack;
    PSTRING_LIST        pStringList;
    PSTRING_REG         pStringReg;
    PLIST_ENTRY         pEntry;
    KIRQL               kIrql;
    NTSTATUS            nts;
 
    //-f--> Vamos deixar este campo com zero até que tenhamos
    //      certeza de que a cópia foi feita paro o buffer da
    //      aplicação
    pIrp->IoStatus.Information = 0;
 
    //-f--> Obtemos a Stack Location corrente.
    pStack = IoGetCurrentIrpStackLocation(pIrp);
 
    //-f--> Obtém a ponta da lista.
    pStringList = (PSTRING_LIST)pStack->FileObject->FsContext;
 
    //-f--> Vamos retornar para aplicação apenas um array de CHAR
    //      com terminador NULL. As strings estão armexadas como
    //      UNICODE_STRING. Vamos convertê-las para ANSI_STRING.
    //      Aqui vamos inicializar a ANSI_STRING que receberá
    //      o resultado da conversão de UNICODE_STRING.
 
    //-f--> Vamos oferecer Lengh-1 para reservar um byte para
    //      o terminador null após conversão.
    RtlInitEmptyAnsiString(&asString,
                           (PCHAR)pIrp->AssociatedIrp.SystemBuffer,
                           (USHORT)pStack->Parameters.Read.Length - 1);
 
    //-f--> Aqui vamos adquirir o spinlock para evitar concorrência
    //      no acesso à lista.
    KeAcquireSpinLock(&pStringList->SpinLock,
                      &kIrql);
 
    //-f--> Verifica se a lista está vazia.
    if (IsListEmpty(&pStringList->ListHead))
    {
        //-f--> Sinaliza erro na leitura e já libera o spinlock.
        nts = STATUS_NO_MORE_ENTRIES;
        KeReleaseSpinLock(&pStringList->SpinLock,
                          kIrql);
    }
    else
    {
        //-f--> Remove o registro da lista e já libera o spinlock.
        pEntry = RemoveHeadList(&pStringList->ListHead);
        KeReleaseSpinLock(&pStringList->SpinLock,
                          kIrql);
 
        pStringReg = CONTAINING_RECORD(pEntry,
                                       STRING_REG,
                                       Entry);
 
        //-f--> Aqui convertemos a string. Reparem que não solicitamos
        //      a alocação do buffer. Estamos utilizando o buffer de
        //      sistema para receber o resultado da conversão. Neste
        //      caso a string de destino deve estar inicializada.
        nts = RtlUnicodeStringToAnsiString(&asString,
                                           &pStringReg->usString,
                                           FALSE);
        if (NT_SUCCESS(nts))
        {
            //-f--> Aqui usamos aquele byte que reservamos e assim
            //      o terminador null também é copiado pelo IoManager
            //      do SystemBuffer para o buffer da aplicação
            asString.Buffer[asString.Length] = 0;
 
            //-f--> Precisamos informar ao IoManager a quantidade de
            //      bytes que serão copiados para o buffer da aplicação.
            //      Esse tamanho é o tamanho da string convertida mais
            //      um byte ocupado pelo terminador NULL que colocamos.
            pIrp->IoStatus.Information = asString.Length+1;
        }
 
        //-f--> Neste exemplo, não estamos lidando com o caso de erro
        //      na conversão, isso pode acontecer se a aplicação mandar
        //      um buffer pequeno para a string. Caso algum erro ocorra
        //      durante a conversão, vamos simplesmente descartar a string
 
        //-f--> Libera o buffer da string e em seguida o registro que ela
        //      ocupava na lista.
        RtlFreeUnicodeString(&pStringReg->usString);
        ExFreePoolWithTag(pStringReg, STR_LST_TAG);
    }
 
    //-f--> O campo de Information já foi preenchido, vamos apenas
    //      copiar o status da operação e completar a IRP.
    pIrp->IoStatus.Status = nts;
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    return nts;
}

Com este driver rodando, já posso pensar em alguns exemplos de filtros. Já venho pensando em exemplos de filtros há algum tempo, além de receber sujestões de post sobre esse assunto. O fato é que não tinhamos base para tal. O importante é termos um driver simples o suficiente para o fácil entendimento das coisas. Não adianta eu fazer um post com um filtro de disco rígido, um filtro de rede ou qualquer outro driver com plug-and-play e gerenciamento de energia. Estes precisariam de muito conhecimento acumulado e não caberiam num post.

Enfim, mais uma vêz espero ter ajudado. E se alguma dúvida surgir é só me mandar um e-mail.

Have fun!

StringList.zip