A Aldeia Numaboa ancestral ainda está disponível para visitação. É a versão mais antiga da Aldeia que eu não quis simplesmente descartar depois de mais de 10 milhões de pageviews. Como diz a Sirley, nossa cozinheira e filósofa de plantão: "Misericórdia, ai que dó!"

Se você tiver curiosidade, o endereço é numaboa.net.br.

Leia mais...

Informática Numaboa - Tutoriais e Programação

Assembly - Lidando com exceções

Seg

22

Jun

2009


19:53

(1 voto de 5.00) 


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:

inicio: ; ponto de entrada do programa push OFFSET manip_final call SetUnhandledExceptionFilter ... ... ; código coberto pelo manipulador final ... call ExitProcess ; -------------------------------------------------------- manip_final: ... ... ; código para a "gentil" mensagem de despedida ... mov eax, -1 ; (eax=-1 Restaura contexto e continua) ret

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:

push OFFSET manipulador push FS:[0] ; endereço da próxima estrutura "ERR" mov FS:[0], esp ; passar para FS:[0] o endereço de "ERR" ... ... ; Código coberto pelo manipulador ... pop FS:[0] ; restaurar o endereço de "ERR" em FS:[0] add esp, 4h ; descartar o resto da estrutura "ERR" ; -------------------------------------------------------------------- manipulador: ... ... ; código do manipulador de exceção ... mov eax, 1 ; eax=1 vai para o próximo manipulador ret ; eax=0 restaura contexto e continua execução

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 PilhaUso da pilha pela função C ...
Manipulador de Exceções C
Dados Locais da Função C
2ª Moldura da PilhaEndereç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 PilhaEndereç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:

  1. A caminhada é iniciada pelo manipulador e não pelo sistema.
  2. 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.
  3. 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:

PUSH Valor de Retorno PUSH pRegistroDeExceção PUSH ADDR MarcadorDoCodigo PUSH UltimaMolduraDaPilha CALL RtlUnwind

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.

info 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):

MOV D[EBX+4],2h ; faz a flag de exceção EH_UNWINDING FS MOV EDI,[0] ; pega o endereço do 1° manipulador thread específico L2: CMP D[EDI],-1 ; vê se é o último JZ >L3 ; sim, então termina PUSH EDI,EBX ; push estrutura ERR, EXCEPTION_RECORD CALL [EDI+4] ; chama manipulador para desarme ADD ESP,8h ; remove os dois parâmetros PUSHados MOV EDI,[EDI] ; pega ponteiro para a próxima estrutura ERR JMP L2 ; e processa o próximo se não tiver terminado L3: ; marcador do código quando terminar

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).

Informações adicionais