A maioria de vocês já deve conhecer o tratamento de exceção da linguagem C. Sabendo que a Run Time C em Kernel não oferece suporte ao tratamento de exceções do C++, o tratamento de exceções do C nos cai como uma luva. Exceções nem sempre significam que um erro crítico ocorreu, mas independente disso, tratá-las é essencial. Uma exceção não manipulada em Kernel significa tela azul. Em se tratando de uma exceção inesperada, tal como um Access Violation, conseguimos prevenir o sistema de terminar em azul utilizando manipuladores de exceção. Isso é ótimo, parabéns, mais uma vida salva, mas não podemos nos esquecer que houve uma exceção inesperada. Esse post não vai falar como utilizar manipuladores de exceção em detalhes, mas vai falar sobre como podemos reportar eventos como esses ao administrador do sistema.
Uma típica manipulação de exceção
Para os desavisados de plantão, vou apenas dar uma breve descrição sobre manipulação de exceção no fonte abaixo. Detalhes estão na referência. O fonte a seguir foi retirado de um dos exemplos disponíveis neste blog. Esta função consegue nos salvar de uma impiedosa tela azul no caso de o mané que chamar esta função passar uma string inválida como fonte desta duplicação.
/****
*** DupString
**
** Recebe uma Unicode String e a duplica.
** A string resultante deve ser liberada
** utilizando a função RtlFreeUnicodeString
*/
NTSTATUS
DupString(IN PUNICODE_STRING pusSource,
OUT PUNICODE_STRING pusTarget)
{
NTSTATUS nts = STATUS_SUCCESS;
__try
{
__try
{
//-f--> Inicializa destino
pusTarget->Buffer = NULL;
//-f--> Aloca buffer de destino
if (!(pusTarget->Buffer = (PWSTR)ExAllocatePool(PagedPool,
pusSource->Length)))
ExRaiseStatus(STATUS_INSUFFICIENT_RESOURCES);
//-f--> Copia buffer
RtlCopyMemory(pusTarget->Buffer,
pusSource->Buffer,
pusSource->Length);
//-f--> Copia tamanhos
pusTarget->MaximumLength = pusTarget->Length = pusSource->Length;
}
__finally
{
//-f--> Se algo deu errado, desalocamos buffer de destino
if (AbnormalTermination())
if (pusTarget->Buffer)
RtlFreeUnicodeString(pusTarget);
}
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
nts = GetExceptionCode();
DbgPrint("DupString >> An exception occourred at "__FUNCTION__
" with status 0x%08x.\n", nts);
ASSERT(FALSE);
}
return nts;
}
Repare que existem dois blocos de execução. O mais externo, que vai lidar com exceções não manipuladas, e o mais interno, que vai lidar com finalizações. O manipulador do bloco de exceções vai executar o bloco __except quando alguma exceção for lançada de dentro do bloco __try. Isso nos dará a oportunidade de tentar descobrir o que ocorreu e em alguns casos até solicitar que a instrução que causou a exceção seja re-executada. O bloco de finalização __finally é executado quando a linha de execução sair do bloco principal, seja pelo simples fim do bloco, por um return, por um goto para fora do bloco, ou mesmo uma exceção que foi lançada. O bloco __finally é sempre executado. Isso nos dá a oportunidade de liberar qualquer recurso que tenha ficado pendente durante a execução do bloco. Juntando tudo, podemos imaginar a seguinte situação: Vamos supor que a função RtlCopyMemory lance uma exceção, neste caso, o bloco __except será executado, mas para que isto ocorra, o fluxo de execução vai sair do bloco mais interno também, causando assim a execução do bloco __finally.
Dentro de cada manipulador existe uma chamada de função especial. Dentro do __except temos a GetExceptionCode, que nos retorna o código da exceção que foi lançada. Esta função só pode ser chamada dentro deste contexto. Note também que dento do bloco __finally existe a chamada à função AbnormalTermination que indica que o bloco não foi executado até o seu final. Em nosso exemplo, utilizamos esta função para determinar que algo errado aconteceu e que é necessário liberar o buffer que seria retornado à função chamadora. Enfim, dê uma olhada na referência para uma completa descrição destes recursos.
O principal ponto a notar aqui, é o ASSERT ao final do bloco __except. Caso este fonte seja compilado em Checked, o ASSERT causará um prompt no depurador. Caso não haja um depurador conectado ao sistema, então uma tela azul será exibida. Isso porque esta macro lançará a exceção de break para o depurador, mas como o depurador não estará lá para manipular esta exceção, então já viu. Quando compilado em Free, a macro ASSERT é traduzida para nada, nossa função vai manipular a exceção e graciosamente retornar o código da exceção à função chamadora. Que lindo, mas quando o administrador vai ficar sabendo?
Um sinal de vida
A maneira padrão de deixar este sinal é escrevendo no registro de eventos do sistema. O que é isso? Este link pode lhe dar informações detalhadas sobre o logs de eventos do sistema. Para escrever um registro nesta lista a partir do Kernel, temos inicialmente que alocar o buffer que vai carregar toda as informações do registro utilizando a função IoAllocateErrorLogEntry.
PVOID
IoAllocateErrorLogEntry(
IN PVOID IoObject,
IN UCHAR EntrySize
);
O ponteiro retornado por esta função, apesar de ser do tipo PVOID, aponta para uma estrutura do tipo IO_ERROR_LOG_PACKET, que possui tamanho variável dependendo das informações que a compõe. Essa estrutura é utilizada para empacotar todos os dados do evento que será logado.
typedef struct _IO_ERROR_LOG_PACKET
{
UCHAR MajorFunctionCode;
UCHAR RetryCount;
USHORT DumpDataSize;
USHORT NumberOfStrings;
USHORT StringOffset;
USHORT EventCategory;
NTSTATUS ErrorCode;
ULONG UniqueErrorValue;
NTSTATUS FinalStatus;
ULONG SequenceNumber;
ULONG IoControlCode;
LARGE_INTEGER DeviceOffset;
ULONG DumpData[1];
} IO_ERROR_LOG_PACKET, *PIO_ERROR_LOG_PACKET;
Embora esta estrutura tenha três quilos de membros, você não vai precisar preencher a maioria deles. Feito isso, o passo seguinte é passar a estrutura para a função IoWriteErrorLogEntry e pronto. Como disse um amigo meu: “É mel na sopa”. Provavelmente ele misturou a frase “É mel na chupeta” com algo que leve sopa. Ainda não consegui descobrir o que seria. Alguma dica?
VOID
IoWriteErrorLogEntry(
IN PVOID ElEntry
);
Vai pensando que é fácil
Então é só alocar a estrutura, inicializá-la, chamar a função de escrita e correr para o abraço? Engraçada esta pergunta. Há pouco tempo atrás estive em Porto Alegre, e lá me disseram que quando uma explicação é iniciada por “É só…”, o trabalho real é cinco vezes maior que o explicado. Vamos começar escrevendo a tão famosa frase “Hello World” para ver o tamanho da encrenca.
Muito bem… Onde é que passo a mensagem mesmo? Ah tá, isso mesmo, no arquivo de mensagens. Diferente dos arquivos de log que normalmente fazemos em aplicações, as mensagens não são passadas diretamente para a função de escrita. Ao invés disso, o membro ErrorCode da estrutura IO_ERROR_LOG_PACKET recebe o identificador da mensagem a ser exibida. Quando a função IoWriteErrorLogEntry é chamada, esta coloca o pacote em uma lista em memória. Mais tarde, este pacote é gravado no arquivo de log de eventos do sistema. O EventViewer, software utilizado para visualizar tais eventos, obtém o identificador da mensagem contido no pacote. Com esse identificador, ele localiza a string que vai estar armazenada em um módulo em separado. Este módulo pode ser o próprio driver ou uma DLL. Assim, a string é lida desse módulo e exibida ao usuário. Ufa!
Hã? Quem? Quando? Onde? Vamos nos basear no driver de exemplo mais simples que temos para adicionar estes recursos. Vamos começar compondo o módulo de mensagens escrevendo um arquivo texto de extensão .mc. Este arquivo texto vai ser compilado pelo Message Compiler. Esta compilação irá gerar o arquivo de recurso que conterá as strings junto com o arquivo de header que fará referência a alguns símbolos criados. Veja o modelo de arquivo de mensagem abaixo:
MessageIdTypedef = NTSTATUS
SeverityNames =
(
Success = 0x0:STATUS_SEVERITY_SUCCESS
Informational = 0x1:STATUS_SEVERITY_INFORMATIONAL
Warning = 0x2:STATUS_SEVERITY_WARNING
Error = 0x3:STATUS_SEVERITY_ERROR
)
FacilityNames =
(
System = 0x0
DriverEntryLogs = 0x2A:DRIVERENTRY_FACILITY_CODE
)
LanguageNames =
(
Portuguese = 0x0416:msg00001
English = 0x0409:msg00002
)
MessageId = 0x0001
Facility = DriverEntryLogs
Severity = Informational
SymbolicName = EVT_HELLO_MESSAGE
Language = Portuguese
"Ola mundo!"
.
Language = English
"Hello world!"
.
Repare que podemos ter a mesma mensagem em várias línguas. Nossa que chique hein? Salve este arquivo com o nome de LogMsgs.mc. Pode ser o nome de sua preferência, mas lembre-se que este nome vai se repetir nos arquivos gerados. Para compilar este arquivo, basta colocá-lo na lista de fontes a serem compilados que fica no arquivo sources do seu projeto como mostra abaixo. Se você decidir que as mensagens deverão ser contidas dentro do próprio driver, então é necessário adicionar o arquivo LogMsgs.rc que vai ser gerado pelo Message Compiler.
TARGETNAME=Useless
TARGETPATH=obj
TARGETTYPE=DRIVER
SOURCES=LogMsgs.mc\
Useless.c \
LogMsgs.rc
Depois de compilado, os arquivos LogMsgs.rc e LogMsgs.h serão gerados. O arquivo de header define os códigos de erros definidos no arquivo de mensagens e deve ser incluído nos fontes que farão as chamadas para a criação do log. Observe um trecho deste arquivo que foi gerado automagicamente.
//
// MessageId: EVT_HELLO_MESSAGE
//
// MessageText:
//
// "Ola mundo!"
//
#define EVT_HELLO_MESSAGE ((NTSTATUS)0x402A0001L)
Finalmente já podemos escrever as linhas de código que utilizarão toda essa parafernália que criamos.
#include
#include "LogMsgs.h"
/****
*** DriverEntry
**
** Ponto de entrada do nosso driver, tudo começa aqui,
** depois vai enrolando, enrolando, ...
*/
NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject,
IN PUNICODE_STRING pusRegistryPath)
{
PIO_ERROR_LOG_PACKET pLogPacket;
//-f--> Aloca a entrada para o evento
pLogPacket = IoAllocateErrorLogEntry(pDriverObject,
sizeof(IO_ERROR_LOG_PACKET));
//-f--> Inicializa toda a estrutura
RtlZeroMemory(pLogPacket, sizeof(IO_ERROR_LOG_PACKET));
//-f--> Coloca a mensagem desejada
pLogPacket->ErrorCode = EVT_HELLO_MESSAGE;
//-f--> Envia a entrada para a lista de eventos
IoWriteErrorLogEntry(pLogPacket);
//-f--> Ufa, conseguimos chegar até aqui, isso merece
// um retorno de sucesso para o sistema.
return STATUS_SUCCESS;
}
Tudo pronto agora?
Do ponto de vista do driver já está tudo pronto. O driver já pode ser compilado, carregado e assim criar a entrada no log carregando o número da mensagem a ser exibida pelo EventViewer. Depois de pôr o driver para rodar, vamos abrir o EventViewer e localizar a entrada gerada pelo nosso driver, e assim, ver a mensagem.
Ops! Ainda temos que informar ao EventViewer o módulo responsável por armazenar as strings. Em nosso caso, as mensagens estão no próprio driver. Para isso temos que adicionar uma chave com nome do nosso driver dentro da chave de registro do EventLog. Dentro desta chave, ainda termos que criar dois valores informando o path do módulo que conterá as mensagens e quais tipos de eventos nosso driver pode gerar. Observe com atenção o caminho do registo na figura abaixo.
Assim que entramos com os valores acima descritos no registro, é só reabrir o mesmo evento para que a mensagem seja exibida como se deve. Lembre-se que o driver não precisa enviar o evento novamente para que a mensagem seja exibida corretamente. O driver apenas manda uma entrada com o identificador da mensagem. Isso já foi feito independente do módulo de mensagem ter sido configurado. Na figura abaixo podemos observar as mensagens em português e em inglês, dependendo da língua do sistema operacional. Olha que bunitim!
Como este post está ficando maior que eu esperava (pra variar), vou dividi-lo em partes. Ainda pretendo comentar sobre como inserir parâmetros nas entradas e esclarecer a dúvida que alguns de vocês podem estar se fazendo neste instante: “Se a entrada do log fica inicialmente em uma lista ligada, e só depois de um tempo é que efetivamente vai para disco, as entradas podem ser perdidas se a máquina cair neste intervalo?”.
Será que o culpado é mesmo o mordomo? Será que a Matrix está dentro de outra Matrix? Tostines vende mais porque é fresquinho, ou é fresquinho porque vende mais? Não percam!
Até mais…