Informática Numaboa - Tutoriais e Programação
Assembly - Lidando com exceções
Seg 22 Jun 2009 19:53 |
- Detalhes
- Categoria: Assembly Numaboa (antigo oiciliS)
- Atualização: Segunda, 22 Junho 2009 22:47
- Autor: vovó Vicki
- Acessos: 11862
Os dois tipos de manipuladores de exceção
Com certeza você vai se surpreender com que facilidade é possível associar manipuladores de exceção aos seus programas. Existem dois tipos de manipuladores de exceção: os thread-específicos e os finais.
Os manipuladores finais
O manipulador de exceção "final" é chamado pelo sistema quando o seu programa estiver "marcado para morrer". Este manipulador é específico do processo e não está vinculado ao thread que causou a exceção. Este tipo de manipulador geralmente é inserido no thread principal, logo depois do ponto de entrada do programa, através de uma chamada à função da API denominada SetUnhandledExceptionFilter, cuja tradução literal é "configure um filtro de exceções não tratadas". Colocado neste nível, o manipulador cobre o programa a partir deste ponto até o fim do mesmo. Não há a necessidade de remover este manipulador quando o programa termina - o windows faz isto automaticamente. Por exemplo:
Existe um (e apenas um) manipulador final ativo. Se a função SetUnhandledExceptionFilter for chamada uma segunda vez, o endereço do manipulador final é alterado para o novo valor e a versão anterior é descartada.
Os manipuladores thread-específicos
Este tipo de manipulador é usado para vigiar áreas específicas de código. É acionado alterando-se o valor mantido pelo sistema em FS:[0]. Cada thread do programa tem um valor diferente no registrador de segmento FS, de modo que o manipulador é sempre específico para cada thread. O manipulador será acionado se ocorrer uma exceção durante a execução do código protegido pelo manipulador.
O valor em FS é um seletor de 16 bits que aponta para o "Thread Information Block", uma estrutura que contém as informações de cada thread. O primeiro dword do Thread Information Block aponta para uma estrutura que passaremos a chamar de estrutura "ERR". Esta estrutura "ERR" possui no mínimo 2 dwords:
1° dword + 0 | Ponteiro para a próxima estrutura "ERR" |
---|---|
2° dword + 4 | Ponteiro para o manipulador de exceção particular |
De posse destas informações, criar um manipulador de exceções thread-específico é muito fácil. Exemplo:
Encadeamento de manipuladores de exceção thread-específicos: no código acima pode-se observar que o 2° dword da estrutura ERR, que é o endereço do manipulador, é colocado na pilha em primeiro lugar. Depois o 1° dword da estrutura ERR subsequente é colocado na pilha através da instrução PUSH FS:[0]. Suponha que o código protegido por este manipulador tenha chamado outras funções que necessitem ter suas próprias proteções individuais. Neste caso, você pode criar outra estrutura ERR com um manipulador para proteger este código exatamente da mesma maneira. Isto é denominado encadeamento (chaining). Na prática isto significa que, quando ocorrer uma exceção, o sistema irá percorrer a cadeia de manipuladores chamando inicialmente o manipulador de exceção mais atual, aquele estabelecido logo antes do código onde a exceção ocorreu. Se este manipulador não lidar com a exceção (retornando EAX=1), então o sistema chama o anterior da cadeia. Como cada estrutura ERR contém o endereço do manipulador anterior, pode-se estabelecer qualquer quantidade de manipuladores deste tipo. Cada manipulador poderá proteger ou lidar com tipos particulares de exceção, dependendo do código que você lhes atribuir. A pilha é usada para manter as estruturas ERR, evitando que sejam sobre-escritas. Entretanto, nada impede o uso de outras partes da memória para guardar estruturas ERR - depende do gosto de cada um.
Desdobramento da pilha (stack unwind)
Agora vamos dar uma olhada no chamado "stack unwind", que pode ser traduzido como "desdobramento da pilha", para acabar com este "mistério". Um "desdobramento da pilha" soa um tanto dramático mas, na prática, consiste em simplesmente chamar os manipuladores de exceção cujos dados locais estejam localizados mais abaixo na pilha e depois (provavelmente) continuar a execução a partir de uma outra moldura (frame). Em outras palavras, o programa é preparado para ignorar o conteúdo da pilha entre estas duas posições.
Caso você não saiba o que é uma moldura de pilha, revise o conceito lendo o texto Tratamento de erros.
3ª Moldura da Pilha | Uso da pilha pela função C ... |
Manipulador de Exceções C | |
Dados Locais da Função C | |
2ª Moldura da Pilha | Endereço de retorno da Função C |
Uso da pilha pela função B ... | |
Manipulador de Exceções B | |
Dados Locais da Função B | |
1ª Moldura da Pilha | Endereço de retorno da Função B |
Uso da pilha pela função A ... | |
Manipulador de Exceções A | |
Dados Locais da Função A | |
Endereço de retorno da Função A | |
... |
Neste caso, quando cada uma das funções é chamada, são PUSHadas coisas na pilha: em primeiro lugar o endereço de retorno, depois os dados locais e finalmente o manipulador de exceções (esta é a estrutura "ERR" mencionada anteriormente).
Agora suponha que tenha ocorrido uma exceção na Função C. Como vimos, o sistema iniciará uma caminhada pela cadeia de manipuladores. O manipulador 3 será o primeiro a ser chamado. Imagine que o manipulador 3 não trate a exceção (retornando EAX=1), então o manipulador 2 será chamado. Se o manipulador 2 também retornar EAX=1, então o manipulador 1 será chamado. Se o manipulador 1 tratar a exceção, ele precisará "desarmar" os dados locais nas molduras da pilha criadas pelas Funções B e C. Isto é feito através do Desdobramento.
O desdobramento simplesmente repete a caminhada na cadeia de manipuladores chamando inicialmente o manipulador 3, depois o 2 e finalmente o 1.
As diferenças entre este tipo de caminhada pela cadeia de manipuladores e a caminhada iniciada pelo sistema quando a exceção ocorreu pela primeira vez são as seguintes:
- A caminhada é iniciada pelo manipulador e não pelo sistema.
- A flag de exceção no registro EXCEPTION_RECORD deveria receber o valor 2h (EH_UNWINDING). Este valor indica ao manipulador thread específico que ele está sendo chamado por outro manipulador situado mais adiante na cadeia e que deve desarmá-lo usando dados locais. Não deve fazer nada além disso e precisa retornar EAX=1.
- A caminhada termina imediatamente antes do chamador. No exemplo do diagrama, se o manipulador 1 iniciar a caminhada, o último manipulador a ser chamado durante o desdobramento será o manipulador 2. Não existe a necessidade do manipulador 1 ser chamado por ele mesmo porque ele tem acesso aos seus próprios dados locais para desarmar-se.
Como é feito o desdobramento
O manipulador pode iniciar um desdobramento usando a função da API RtlUnwind ou, como veremos adiante, usando o código que você escrever. Esta função pode ser chamada da seguinte forma:
Valor de Retorno contém um valor de retorno depois do desdobramento, o qual, provavelmente, nem será usado.
pRegistroDeExceção é um ponteiro para o registro de exceção, o qual é uma das estruturas enviadas ao manipulador responsável pela área onde ocorreu a exeção.
MarcadorDoCodigo é o local a partir do qual a execução deve continuar depois do desdobramento e, tipicamente, é o endereço do código imediatamente após a chamada a RtlUnwind. Se não for especificado, a função da API funciona normalmente, porém é melhor não brincar com este tipo de função e garantir que funcione adequadamente.
UltimaMolduraDaPilha é a moldura da pilha na qual o desdobramento deve parar. Normalmente é o endereço da pilha da estrutura ERR que contém o endereço do manipulador que iniciou o desdobramento.
Observação: Diferentemente de outras funções da API, não deixe para RtlUnwind preservar os registradores EBX, ESI ou EDI – se você for usar esta função, o correto é preservá-los fazendo um PUSH antes do primeiro parâmetro e restaurá-los com POP após MarcadorDoCodigo.
Código próprio de Desdobramento
O código a seguir simula o desdobramento (onde ebx guarda o endereço da estrutura EXCEPTION_RECORD enviada ao manipulador):
Neste caso cada manipulador é chamado com a flag de exceção 2h até que o último manipulador seja alcançado (o sistema possui o valor -1 na última estrutura ERR).
O código acima não checa valores corrompidos em [EDI] e em [EDI+4]. O primeiro é um endereço da pilha e poderia ser checado verificando se está acima da base da pilha do thread indicada em FS:[8] e abaixo do topo da pilha do thread indicada em FS:[4]. O segundo é um endereço do código de modo que é possível checar se está situado entre dois marcadores de código, um no começo do seu código e outro no fim do mesmo. Alternativamente é possível checar se [EDI] e [EDI+4] podem ser lidos chamando a função da API IsBadReadPtr.
Desdobrar pelo manipulador final e depois continuar
Não é apenas um manipulador thread específico que pode iniciar um desdobramento de pilha. O desdobramento também pode ser realizado pelo manipulador final chamando RtlUnwind ou através de um código próprio que faça o desdobramento retornando posteriormente EAX=-1. (Veja "Continuar a execução depois de chamar o manipulador final").
Desdobramento final e depois terminar
Se um manipulador final estiver instalado e ele retornar EAX=0 ou EAX=1, o sistema fará com que o processo termine. Entretanto, antes do término acontece uma coisa interessante. O sistema faz um desdobramento final voltando ao primeiro manipulador da cadeia (ou seja, o manipulador que contém o código no qual ocorreu a exeção). Esta é a última oportunidade que seu manipulador tem de executar o código de desarme necessário em cada moldura da pilha. Você pode ver claramente este desdobramento final ocorrendo se configurar o programa demo Except.exe para permitir que a exceção vá até o manipulador final e pressionar F3 ou F5 quando alcançar este ponto. O mesmo acontece com o programa mais simples, Except1.exe (os dois programas estão disponíveis na seção de downloads da Aldeia em Tutoriais / Assembly Numaboa ou no final deste artigo).