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) 


Maluco beleza Exceções são "pecados" que os programas ou o hardware cometem. O código "mal comportado" que gera uma exceção pode comprometer o funcionamento do sistema operacional o qual, para se precaver, geralmente interrompe o programa faltoso e apresenta uma mensagem de erro do tipo

Mensagem de erro

Ao invés de delegar ao sistema a função de monitorar exceções, podemos gerenciá-las por conta própria, tornando nossos aplicativos mais robustos. Veja como fazer isto.

Entendendo a manipulação de exceções

A idéia básica na manipulação de exceções, também denominada Manipulação Estruturada de Exceções ou SEH , é a de codificar uma ou várias rotinas callback no aplicativo. Estas rotinas são denominadas genericamente de manipuladores de exceção (exception handlers). Se ocorrer uma exceção, o sistema, ao invés de tratá-la, chamará a rotina callback e a responsabilidade de tratá-la ficará por conta do aplicativo. O que se espera é que o manipulador de exceções seja capaz de resolver e corrigir a exceção, mantendo a execução do aplicativo na mesma área de código onde a exceção ocorreu ou numa "área segura". Em resumo, deve dar a impressão de que nenhuma anomalia tenha ocorrido - nada de caixa de mensagens.

Além disto, o tratamento de erros pode realizar uma bela faxina: incluir o fechamento de manipuladores, fechamento de arquivos temporários, liberação de modelos de contexto, liberação de áreas de memória, informação a outros threads, ajuste de pilha ou o fechamento do thread "pecador". Durante o processo, o manipulador de exceções pode registrar em arquivo todas as fases do tratamento do erro, possibilitando uma análise posterior.

Caso não seja possível realizar um tratamento adequado, o manipulador de exceções ainda pode fechar o aplicativo de forma mais elegante que a famigerada "janelinha de erro" após realizar o máximo de faxina, preservar o máximo de dados e, caso você queira, pedindo as devidas desculpas pelo transtorno.

O que é possível fazer

Primeiro a boa notícia: existem as mais diversas aplicações para um manipulador de exceções. Dentre elas, destacam-se as seguintes:

  • Durante o desenvolvimento de um programa pode interceptar e registrar erros e funcionar como uma alternativa de debug.
  • Quando usarmos código escrito por terceiros que ainda não tenham sido adequadamente testado.
  • Quando realizarmos leitura ou escrita em áreas de memória que possam ter mudado sem aviso prévio. Por exemplo, quando fuçamos em áreas de memória do sistema (que deveriam estar sob a tutela do sistema) ou em áreas de memória que possam ser fechadas por outros processos ou threads.
  • Usando ponteiros de arquivos que possam estar corrompidos ou em formatos incorretos. Neste caso, um manipulador de exceções é muito mais rápido do que o uso das funções da API IsBadReadPtr ou IsBadWritePtr, onde cada novo ponteiro precisa ser testado antes de ser usado.
  • Como um interceptador geral de erros involuntários.

O que não é possível prevenir

Agora, a má notícia: existem erros que geram exceções irrecuperáveis. O tipo mais comum, não levando em consideração a divisão por zero (código de exceção 0C0000094h), que pode ser facilmente evitada através de codificação de proteção, é a tentativa de leitura ou escrita num endereço de memória inválido (código de exceção 0C0000005h). As origens deste erro são diversas:

  • Valores errados do registrador de índice quando se endereça a memória.
  • Loops contínuos inesperados que envolvam acesso à memória.
  • PUSH e POP descasados, de modo que, retornando de uma chamada, a continuidade da execução ocorre a partir de um lugar errado.
  • Corrupção inesperada de arquivos de dados de entrada.

Em todos estes casos o imponderável é o fator determinante do erro. Não nos resta outra alternativa a não ser fazer o máximo de faxina e encerrar o aplicativo com as devidas desculpas. Existem outras causas de interrupção de aplicativos, porém não estão associadas a exceções. As mais comuns são:

  • Recursos insuficiente do sistema.
  • Loops contínuos que não envolvam acesso de memória.

O resultado, nestes casos, é que o programa não pode responder as mensagens do sistema e dá a impressão de estar parado (congelado ou pendurado). Como o programa roda no seu espaço de endereçamento virtual próprio, outros programas não são afetados. O sistema, porém, fica mais lento.

Alguns erros são tão graves que o sistema nem consegue redirecionar o tratamento da exceção para o manipulador. Neste caso, ou aparece a "janelinha de erro" ou... a tela azul do GPF mostrando um "erro fatal". Neste caso de desastre total, na maioria das vezes, o único remédio é fazer um reboot.

Como o sistema trata as exceções

Para poder criar manipuladores de exceção, é óbvio que precisamos conhecer a rotina de manipulação de exceções do sistema.

  1. Em primeiro lugar, o Windows decide se a exceção deve ser tratada pelo manipulador de exceções do programa. Se for o caso, verifica se o programa está sendo "debugado". Se sim, o sistema notifica o debugger suspendendo a execução do programa e enviando um EXCEPTION_DEBUG_EVENT (valor 01h).
  2. Se o programa não estiver sendo "debugado", ou se a exceção não for compartilhada com o debugger, o sistema envia a exceção ao seu manipulador de exceções thread-específico - se é que existe um manipulador. Um manipulador thread-específico é instalado em tempo de execução e apontado pelo primeiro dword no Bloco de Informações de Thread (Thread Information Block), cujo endereço está em FS:[0].
  3. O manipulador de exceções thread-específico pode tentar resolver a exceção ou então então deixá-la para outros manipuladores hierarquicamente superiores, se é que há mais manipuladores instalados.
  4. Eventualmente, se nenhum dos manipuladores thread-específicos tratar a exceção e se o programa estiver sendo debugado, o sistema suspenderá novamente a execução do programa e notificará o debugger.
  5. Se o programa não estiver sendo debugado ou se a exceção ainda não tiver sido tratada pelo debugger, o sistema chamará seu manipulador final se este estiver instalado. Este será um manipulador final instalado pelo aplicativo em tempo de execução e utilizando a função da API SetUnhandledExceptionFilter.
  6. Se, após retornar do seu manipulador final, a exceção ainda não tiver sido tratada adequadamente, então o manipulador final do sistema será acionado. Opcionalmente ele mostrará a caixa de mensagem que informa o fechamento do sistema. Dependendo da configuração do registro (registry), esta caixa de diálogo pode oferecer a opção de associar um debugger ao programa. Se não existir esta opção ou se o debugger não puder ser acionado, o programa é condenado e o sistema chamará ExitProcess para terminar o programa.
  7. Entretanto, antes de terminar o programa, o sistema efetuará um "alinhamento final" da pilha para o thread no qual a exceção ocorreu.

Se tudo o que foi dito é uma grande novidade para você, não se preocupe. Com o tempo e um pouco de prática a coisa fica bem mais fácil. Só não é possível escapar desta teoria toda... coisas da vida.

As vantagens de se usar Assembly no tratamento de exceções

O win32 tem apenas umas poucas funções na API para o tratamento de erros. Isto nos força a escrever a maior parte do código para um manipulador de exceções. Os programadores de "C" podem lançar mão de várias facilidades oferecidas pelos compiladores, incluindo no seu código fonte declarações como _try, _except, _finally, _catch e _throw. Uma desvantagem importante em depender do código gerado por compiladores "C" é que o tamanho do executável final pode ser aumentado, e muito! Além disto, a maioria dos programadores em "C" não tem idéia do tipo de código que é produzido pelo compilador para manipular as exceções e, para fazer um tratamento de erros eficaz, é preciso ter flexibilidade, saber o que se está fazendo e ter um controle absoluto do processo. Isto porque as exceções podem ser interceptadas e tratadas de várias maneiras e em vários níveis diferentes do código. Usando Assembly, você poderá produzir um código enxuto, confiável, flexível e que atenda especificamente o seu aplicativo.

Aplicativos multi-threaded necessitam de um tratamento ainda mais cuidadoso e a linguagem Assembly oferece um modo simples e versátil de adicionar manipuladores de exceção a programas deste tipo.

Obter informações a respeito do tratamento de exceções em baixo nível não é nada fácil. Os exemplos do SDK (Software Development Kit) do win32, ao invés de explorarem o uso da sua estrutura básica, mostram somente como utilizar declarações do compilador "C".

As informações para este texto foram obtidas usando um programa teste, de um debugger e desassemblando código produzido por compiladores "C". O programa except.exe (que será criado neste tutorial) demonstra as técnicas descritas a seguir.


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


A informação enviada aos manipuladores

A informação enviada aos manipuladores (handlers) precisa ser suficientemente clara para que estes possam tentar corrigir a exceção, fazer logs de erros ou avisar o usuário. Como veremos adiante, esta informação, quando os manipuladores são chamados, é enviada pelo próprio sistema através da pilha. Adicionalmente você pode enviar suas próprias informações para os manipuladores aumentando a estrutura ERR para que possa conter mais informações.

Informação enviada ao manipulador final

O manipulador final está documentado no Windows Software Development Kit (SDK) como API "UnhandledExceptionFilter". Ele recebe apenas um parâmetro, um ponteiro para a estrutura EXCEPTION_POINTERS. Esta estrutura é a seguinte:

EXCEPTION_POINTERS +0 Ponteiro para a próxima estrutura EXCEPTION_RECORD
+4 Ponteiro para a estrutura do registro CONTEXT

A estrutura EXCEPTION_RECORD contém os seguintes campos:

EXCEPTION_RECORD +0 Código de exceção (ExceptionCode)
+4 Flag de exceção (ExceptionFlag)
+8 Registro de exceção aninhado (NestedExceptionRecord)
+C Endereço da exceção (ExceptionAddress)
+10 Parâmetros numéricos (NumberParameters)
+14 Dados adicionais (AdditionalData)

onde

  1. ExceptionCode dá o tipo de exceção que ocorreu. Existem muitos deles listados nos arquivos SDK e de cabeçalho (header) mas, na prática, os tipos que você geralmente encontra são:
    • C0000005h – Violação de escrita ou leitura da memória
    • C0000094h – Divisão por zero
    • C0000095h – Overflow de divisão
    • C00000FDh – A pilha ultrapassou o tamanho máximo disponível
    • 80000001h – Violação de uma página guard na memória gerada por Virtual Alloc
  • O seguinte apenas ocorre quando se lida com exceções:
    • C0000025h – Uma exceção não-continuável – o manipulador não deve tentar tratá-la
    • C0000026h – Código de exceção usado pelo sistema durante o tratamento de exceções. Este código pode ser utilizado se o sistema encontrar um retorno inesperado de um manipulador. Também é usado se um Registro de Exceção (ExceptionRecord) não for fornecido numa chamada RtlUnwind.
  • Para debugar são usados os seguintes:
    • 80000003h – Ocorreu uma parada (breakpoint) porque foi encontrado um INT3 no código.
    • 80000004h – Passo único (single step) durante o debugging.

Os códigos de exceção seguem as seguintes regras:

Bits 31-30 Bit 29 Bit 28 Bits 27-0
0=sucesso
1=Informação
2=Alerta
3=erro
0=Microsoft
1=Aplicação
Reservado
Precisa ser zero
Código

Um código de erro personalizado típico enviado por RaiseException poderia ser, por exemplo, E0000100h (erro, applicação, código=100h). Se necessário, este é um modo rápido de sair do código e ir direto ao seu próprio manipulador.

  1. A ExceptionFlag dá instruções ao manipulador. Os valores podem ser:
    • 0 - uma exceção continuável (pode ser reparada).
    • 1 - exceção não-continuável (não pode ser reparada).
    • 2 - a pilha está se desdobrando - não tente reparar.
  2. O NestedExceptionRecord aponta para um outra estrutura EXCEPTION_RECORD se o próprio manipulador tenha causado uma nova exceção.
  3. ExceptionAddress é o endereço no código onde ocorreu a exceção.
  4. NumberParameters é o número de dwords que devem ser seguidos na AdditionalInformation.
  5. AdditionalInformation é um array de dwords com mais informações. Tanto pode ser informação enviada pela aplicação quando chamou RaiseException ou, se o código de exceção for C0000005h, será o seguinte: 1º dword - 0=violação de leitura, 1=violação de escrita; 2º dword - endereço da violação de acesso.

A segunda parte da estrutura EXCEPTION_POINTERS que é enviada ao manipulador final aponta para a estrutura do registro CONTEXT, a qual contém os valores de todos os registros, específicos do processador, no momento em que ocorreu a exceção. WINNT.H contém as estruturas CONTEXT para vários processadores. Seu programa pode determinar o tipo de processador que está em uso chamando GetSystemInfo. A estrutura CONTEXT é a seguinte para o IA32 (Intel 386 e posteriores):

+0 (usado quando se chama GetThreadContext)
REGISTRADORES DE DEBUG
+4 registrador debug número 0
+8 registrador debug número 1
+C registrador debug número 2
+10 registrador debug número 3
+14 registrador debug número 6
+18 registrador debug número 7
PONTO FLUTUANTE / REGISTRADORES MMX
+1C ControlWord
+20 StatusWord
+24 TagWord
+28 ErrorOffset
+2C ErrorSelector
+30 DataOffset
+34 DataSelector
+38 Registradores FP x 8 (10 bytes cada)
+88 CrONpxState
REGISTRADORES DE SEGMENTO
+8C Registrador gs
+90 Registrador fs
+94 Registrador es
+98 Registrador ds
REGISTRADORES COMUNS
+9C Registrador edi
+A0 Registrador esi
+A4 Registrador ebx
+A8 Registrador edx
+AC Registrador ecx
+B0 Registrador eax
REGISTRADORES DE CONTROLE
+B4 Registrador ebp
+B8 Registrador eip
+BC Registrador cs
+C0 Registrador eflags
+C4 Registrador esp
+C8 Registrador ss

Informação enviada ao manipulador thread-específico

Quando o manipulador thread-específico é chamado, ESP aponta para as seguintes três estruturas:

ESP +4 Ponteiro para a estrutura EXCEPTION_RECORD
ESP +8 Ponteiro para uma estrutura ERR própria
ESP +C Ponteiro para a estrutura registro CONTEXT

Diferentemente de outros CALLBACKs no Windows, quando um manipulador thread-específico é chamado, é usada a convenção de chamada C (o chamador precisa remover os argumentos da pilha) e não a convenção de chamada PASCAL (a função remove os argumentos da pilha). Pode-se verificar esta peculiaridade no código do Kernel32 usado para fazer a chamada:

PUSH Param, CONTEXT record, ERR, EXCEPTION_RECORD CALL HANDLER ADD ESP,10h

Na prática, o primeiro argumento, Param, parece não conter informações relevantes.

As estruturas EXCEPTION_RECORD e registro CONTEXT já foram descritas acima. A estrutura ERR é a estrutura que você criou na pilha quando o manipulador foi constituído e precisa conter o ponteiro para a próxima estrutura ERR, além do endereço do código do manipulador que está sendo instalado (reveja como criar um manipulador no tópico "Tipos de Manipuladores"). O ponteiro para a estrutura ERR passada ao manipulador thread-específico está no topo desta estrutura. Portanto, é possível aumentar a estrutura ERR de modo que o manipulador possa receber informações adicionais.

Num arranjo típico, a estrutura ERR poderia ser a seguinte, onde [ESP=8h] aponta para o topo desta estrutura quando o manipulador é chamado:

ERR +0 Ponteiro para a próxima estrutura ERR
ERR +4 Ponteiro para o manipulador de exceção próprio
ERR +8 Endereço no código de um "lugar seguro" para o manipulador
ERR +C Informação para o manipulador
ERR +10 Área para as flags
ERR +14 Valor de EBP no "lugar seguro"

Como veremos adiante ("Seguindo a execução a partir de um lugar seguro"), os campos +8 e +14 podem ser utilizados pelo manipulador para fazer a recuperação desta exceção.

Fornecendo acesso a dados locais

Agora vamos analisar a melhor posição para a estrutura ERR na pilha, relativa à moldura da pilha, e que pode muito bem conter variáveis de dados locais. Isto é importante por que o manipulador eventualmente precisa acessar estes dados locais para poder realizar a "limpeza" convenientemente. Aqui está um código típico que pode ser usado para criar um manipulador thread-específico onde haja dados locais:

MINHAFUNCAO: ; ponto de entrada do procedimento PUSH EBP ; salva ebp (usado para endereçar o ; quadro da pilha MOV EBP,ESP ; usa EBP como ponteiro para o quadro ; da pilha SUB ESP,40h ; faz 16 dwords na pilha para ; dados locais ;******** dados locais agora em [EBP-4] a [EBP-40h] ;********** instala manipulador e sua estrutura ERR PUSH EBP ; ERR+14h salva ebp ; (ebp em lugar seguro) PUSH 0 ; ERR+10h área para flags PUSH 0 ; ERR+0Ch informação para o manipulador PUSH ADDR LUGAR_SEGURO ; ERR+8h novo eip em lugar seguro PUSH ADDR HANDLER ; ERR+4h endereço do manipulador FS PUSH [0] ; ERR+0h manter próximo ERR na cadeia FS MOV [0],ESP ; apontar para ERR recém-criado na pilha ... ; ... ; código protegido vem aqui ... ; JMP >L10 ; fim normal se não ocorrer ; nenhuma exceção LUGAR_SEGURO: ; manipulador ajusta eip/esp/ebp ; para aqui L10: ; FS POP [0] ; restaura próxima ERR na cadeia MOV ESP,EBP POP EBP RET ;***************** HANDLER: RET

Usando este código, quando o manipulador é chamado, [ESP+8h] está apontando para o topo da estrutura ERR (isto é, ERR+0h) e o seguinte encontra-se na pilha:

. pilha +ve
ERR +0 Ponteiro para a próxima estrutura ERR
ERR +4 Ponteiro para o manipulador de exceção próprio
ERR +8 Endereço no código de "lugar seguro" para o manipulador
ERR +C Informação para o manipulador
ERR +10 Área para as flags
ERR +14 Valor de EBP no "lugar seguro"
ERR +18 Dados locais
ERR +1C Dados locais
ERR +20 Dados locais
mais dados locais

Fica fácil perceber que, quando o manipulador recebe um ponteiro para a estrutura ERR, ele também pode achar o endereço dos dados locais na pilha. Isto é possível por que o manipulador conhece o tamanho da estrutura ERR e a posição dos dados locais na pilha. Se o campo EBP for usado em ERR+14h, como no exemplo acima, ele também poderia ser usado como um ponteiro para os dados locais.


Recuperando-se e reparando uma exceção

Continuando a partir de um lugar seguro

Você precisa continuar a execução num lugar do código que não cause mais problemas. A coisa mais importante que deve ser observada é que, se o seu programa foi escrito para trabalhar dentro do framework do Windows, seu objetivo é o de retornar ao sistema o mais rápido possível e de forma controlada de modo que se possa esperar pelo próximo evento do sistema. Se a exceção ocorreu durante uma chamada do sistema a um procedimento de uma janela, então um bom lugar seguro será próximo do ponto de saída do procedimento da janela para que o sistema retome o controle de maneira "limpa". Desta forma, o sistema terá a impressão de que sua aplicação retornou do procedimento da janela da forma usual.

Entretanto, se a exceção ocorrer num trecho de código onde não existe um procedimento de janela, então será preciso exercer um controle maior. Por exemplo, um thread estabelecido para determinadas tarefas provavelmente precisará ser terminado, avisando o thread principal que a tarefa não pode ser completada.

Outra consideração importante é a facilidade de colocar os valores corretos de EIP, ESP e EBP no lugar seguro. Veremos a seguir que isto não é nada complicado.

São tantas as possibilidades que podem ser exploradas que seria inútil tentar mencioná-las todas. O lugar seguro exato depende da natureza do seu código e do uso que você está fazendo da manipulação de exceções. Entretanto, dê novamente uma olhada no código acima referente a MINHAFUNCAO. Você pode ver o marcador de código "LUGAR_SEGURO". Isto é um endereço no código a partir do qual a execução poderia continuar com segurança com o manipulador tendo feito toda a "faxina" necessária.

No exemplo de código, para que a execução continue com sucesso, é preciso lembrar que, apesar de LUGAR_SEGURO estar dentro da mesma moldura de pilha da exceção ocorrida, os valores de ESP e EBP precisam ser cuidadosamente estabelecidos pelo manipulador antes que a execução continue a partir de EIP. Portanto, estes três registradores precisam ser estabelecidos pelas seguintes razões:

  • ESP - para que a instrução FS POP [0] esteja habilitada a trabalhar e, se necessário, para POP de outros valores.
  • EBP - para assegurar que os dados locais possam ser endereçados dentro do manipulador e para restaurar o valor de retorno correto de MINHAFUNCAO em ESP.
  • EIP - para forçar que a execução continue a partir de LUGAR_SEGURO.

Agora é possível perceber que cada um destes valores pode ser rapidamente obtido dentro da função de manipulação. O valor correto de ESP é, de fato, exatamente o mesmo que o do topo da própria estrutura ERR (dado por [ESP+8h] quando o manipulador é chamado). O valor correto de EBP pode ser obtido de ERR+14h porque foi PUSHado para a pilha quando a estrutura ERR foi montada. E o endereço correto do código do LUGAR_SEGURO que deve ser passado para EIP está em ERR+8h.

Neste ponto estamos prontos para observar, caso ocorra um erro, como o manipulador pode garantir que a execução continue a partir do lugar seguro ao invés de permitir que o processo termine.

HANDLER: PUSH EBP MOV EBP,ESP ;** agora [EBP+8]=ponteiro para EXCEPTION_RECORD ;** [EBP+0Ch]=ponteiro da estrutura ERR para o registro CONTEXT ;** [EBP+10h]=ponteiro; salva registradores requeridos pelo ; Windows para ter o registro da exceção em ebx PUSH EBX,EDI,ESI MOV EBX,[EBP+8] TEST D[EBX+4],1h ; ve se é uma exceção não-continuável JNZ >L5 ; sim, precisa ser tratada TEST D[EBX+4],2h ; verifica se é EH_UNWINDING JZ >L2 ; não ... ... ; limpa código enquanto faz desdobramento ... JMP >L5 ; precisa retornar 1 para ir ao próximo ; manipulador L2: PUSH 0 ; valor de retorno (não usado) PUSH [EBP+8h] ; ponteiro para este registro de exceção PUSH ADDR UN23 ; endereço do código para RtlUnwind retornar PUSH [EBP+0Ch] ; ponteiro para esta estrutura ERR CALL RtlUnwind UN23: MOV ESI,[EBP+10h] ; obtém registro de contexto em esi MOV EDX,[EBP+0Ch] ; obtém ponteiro para a estrutura ERR MOV [ESI+0C4h],EDX ; usa como novo esp MOV EAX,[EDX+8] ; obtém lugar seguro dado na estrutura ERR MOV [ESI+0B8h],EAX ; insere novo eip MOV EAX,[EDX+14h] ; obtém ebp no lugar seguro de ERR MOV [ESI+0B4h],EAX ; insere novo ebp XOR EAX,EAX ; recarrega contexto ; e retorna eax=0 ao sistema JMP >L6 L5: MOV EAX,1 ; vai para o próximo manipulador ; retorna eax=1 L6: ; retorno normal (sem argumentos) POP ESI,EDI,EBX MOV ESP,EBP POP EBP RET

Reparando a exceção

No exemplo acima você viu que o contexto carregado com o novo EIP, EBP e ESP faz com que a execução continue a partir de um lugar seguro. Também é possível, usando o mesmo método de substituir os valores de alguns registradores do contexto, "consertar" a exceção e permitir que a execução continue a partir de um local próximo do código faltoso para que a atual tarefa possa ser realizada.

Um exemplo típico seria a divisão por zero, que pode ser reparada por um manipulador que substitua o valor do divisor por 1 e depois retorne EAX=0 (se for um manipulador thread-específico) fazendo com que o sistema recarregue o contexto e continue a execução.

No caso de violações de memória, você pode utilizar o fato de que o endereço da violação de memória é passado como o segundo dword no campo additional information do registro da exceção. O manipulador pode usar este valor, passá-lo para VirtualAlloc e abrir mais memória a partir deste ponto. Se obtiver sucesso, o manipulador pode então recarregar o contexto (não modificado) e retornar EAX=0 para continuar a execução (caso seja um manipulador thread-específico).

Continuando a execução após a chamada de um manipulador final

Se quiser, você pode lidar com exceções no manipulador final. Você ainda deve se lembrar que o manipulador final é chamado pelo sistema quando o processo está prestes a terminar.

Os retornos do manipulador final em EAX não são os mesmos dos manipuladores thread-específicos. Se o retorno for EAX=0, o processo termina sem a caixa de mensagem; se o retorno for EAX=1, a caixa de mensagem é mostrada.

Existe um terceiro código de retorno, o EAX=-1. Este é descrito no SDK como "EXCEPTION_CONTINUE_EXECUTION". Este retorno tem o mesmo efeito do EAX=0 de um manipulador thread-específico, isto é, recarrega o registro de contexto no processador e continua a execução a partir do EIP do contexto. É claro que o manipulador final pode alterar o registro de contexto antes de retornar ao sistema, da mesma forma que um manipulador thread-específico. Deste modo, um manipulador final pode recuperar uma exceção continuando a execução a partir de um lugar seguro apropriado ou pode tentar reparar a exceção. Apesar disto, perde-se alguma flexibilidade.

Em primeiro lugar, não é possível aninhar manipuladores finais. Só é possível ter um manipulador final ativo estabelecido por SetUnhandledExceptionFilter. Você pode, se quiser, mudar o endereço do manipulador final à medida que diferentes porções do seu código sejam processadas. SetUnhandledExceptionFilter retorna o endereço do manipulador final que está sendo substituído de modo que seria possível fazer o seguinte:

PUSH ADDR FINAL_HANDLER CALL SetUnhandledExceptionFilter PUSH EAX ; guarda endereço do manipulador anterior ... ... ; este é o código sendo guardado ... CALL SetUnhandledExceptionFilter ; restaura manipulador anterior

Observe que, no momento da segunda chamada a SetUnhandledExceptionFilter, o endereço do manipulador anterior já está na pilha devido à instrução PUSH EAX.

Outra dificuldade em usar o manipulador final é que a informação que lhe é enviada limita-se ao registro de exceção e ao registro de contexto. Portanto, será preciso manter na memória estática o endereço do código do lugar seguro, além dos valores de ESP e EBP do lugar seguro. Isto pode ser facilmente implementado em tempo de execução. Por exemplo, quando se usa a mensagem WM_COMMAND dentro de um procedimento de janela

PROCESS_COMMAND: ; chamado em uMsg=111h (WM_COMMAND) MOV EBPLUGAR_SEGURO,EBP ; mantém ebp num lugar seguro MOV ESPLUGAR_SEGURO,ESP ; mantém esp num lugar seguro ... ... ; código protegido aqui ... LUGAR_SEGURO: ; marcador para o lugar seguro XOR EAX,EAX ; retorna eax=0=mensagem processada RET

No exemplo acima, para reparar a exceção a partir de um lugar seguro, o manipulador iria inserir os valores de EBPLUGAR_SEGURO em CONTEXT+0B4h (ebp), ESPLUGAR_SEGURO em CONTEXT+0C4h (esp), ADDR LUGAR_SEGURO em CONTEXT+0B8h (eip) e depois retornar -1.

Note que, num desdobramento de pilha forçado pelo sistema devido a uma saída fatal, apenas são chamados os manipuladores "thread-específicos" (se houver algum) e não o manipulador final. Se não existirem manipuladores "thread-específicos", o manipulador final terá que administrar toda a "limpeza" antes de retornar ao sistema.


Passo-a-passo com uma flag no manipulador

Você pode fazer um testador passo-a-passo para o seu programa durante o desenvolvimento usando a propriedade dos manipuladores de poder armar uma flag no contexto dos registradores antes de retornar ao sistema. Você pode determinar que o manipulador mostre os resultados na tela ou então que os armazene num arquivo. Isto pode ser útil se você suspeitar que os resultados estejam sendo alterados durante o processo de debug ou então se você precisar observar rapidamente como um detrminado trecho de código responde a várias entradas. Insira o seguinte fragmento de código onde o passo-a-passo deve começar:

MOV D[SSCOUNT],5 INT 3

SSCOUNT é um símbolo de dados e contém o número de passos que o manipulador deve dar antes de retornar à sua operação normal. A INT3 provoca uma exceção 80000003h assim que seu manipulador for chamado.

O código no seu programa em desenvolvimento deveria ser protegido por um manipulador thread-específico por um código do tipo:

SS_HANDLER: PUSH EBP MOV EBP,ESP PUSH EBX,EDI,ESI ; como exigido pelo Windows, salva os registradores MOV EBX,[EBP+8] ; pega registro de exceção em ebx TEST D[EBX+4],01h ; vê se é uma exceção não-continuável JNZ >L14 ; sim TEST D[EBX+4],02h ; vê se EH_UNWINDING JNZ >L14 ; sim MOV ESI,[EBP+10h] ; pega registro do contexto em esi MOV EAX,[EBX] ; pega ExceptionCode CMP EAX,80000004h ; vê se está aqui porque flag está ; armada JZ >L10 ; sim CMP EAX,80000003h ; vê se é INT3 para o passo-a-passo JNZ >L14 ; não L10: DEC D[SSCOUNT] ; pára se atingiu o número de passos JZ >L12 OR D[ESI+0C0h],100h ; arma a flag no contexto L12: ... ... ; código para mostrar na tela ... XOR EAX,EAX ; eax=0 recarrega contexto e retorna ; ao sistema JMP >L17 L14: MOV EAX,1 ; eax=1 sistema vai para o próximo ; manipulador L17: POP ESI,EDI,EBX MOV ESP,EBP POP EBP RET

Aqui, a primeira chamada ao manipulador é causada pela INT3 (o sistema estrilou um bocado quando tentei usar INT1). Recebendo esta exceção, que poderia ter vindo apenas do fragmento de código inserido no código-a-ser-testado, o manipulador arma a flag no contexto antes de retornar. Isto faz com que uma exceção 80000004h alcance o manipulador na próxima instrução. Note que, com estas exceções, eip já está na próxima instrução, isto é, uma além da INT3 ou além da instrução executada com a flag armada. Tudo o que é preciso fazer no manipulador para continuar o passo-a-passo é armar a flag novamente e retornar ao sistema. (Agradeço ao G.W.Wilhelm, Jr da IBM pela idéia)

Tratamento de exceções em aplicações multi-thread

Quando se trata de aplicações multi-thread, o sistema oferece pouca ou nenhuma ajuda no tratamento das exceções. Você vai ter que planejar proteções para falhas prováveis e organizar seus threads de acordo.

As regras que se aplicam ao tratamento de exceções pelo sistema, no contexto de aplicações multi-thread, são:

  1. Apenas um tipo 1 (manipulador final) pode existir a qualquer tempo para cada processo. Se um novo manipulador chamar SetUnhandledExceptionFilter, isto simplesmente substituirá o manipulador final - não existe uma cadeia de manipuladores finais como há para os manipuladores tipo 2 (thread-específicos). Portanto, o uso mais simples de um manipulador final provavelmente ainda é o melhor numa aplicação multi-thread - estabelecê-lo no thread principal o mais cedo possível após o ponto de entrada do programa.
  2. O manipulador final será chamado pelo sistema se o processo está para ser terminado, independentemente de qual thread tenha causado a exceção.
  3. Entretanto, um desdobramento final (imediatamente antes do término) só vai ocorrer nos manipuladores thread-específicos estabelecidos para o thread que tenha causado a exceção. Mesmo que outros threads (inocentes) possuam uma janela e um loop de mensagem, o sistema não irá avisá-los de que o processo está para ser encerrado (nenhuma mensagem especial lhes será enviada além das mensagens usuais provenientes da perda de foco de outras janelas).
  4. Portanto, os outros manipulaodres (inocentes) não podem esperar um desdobramento final se o processo estiver para terminar. Continuarão sem saber que o término é iminente.
  5. Se, o que é provável, estes outros threads inocentes precisarem ser desarmados, você precisará informá-los a partir do manipulador final. O manipulador final precisará esperar até que estes outros threads tenham completado a "limpeza" para poder retornar ao sistema.
  6. O modo como os threads inocentes são informados sobre o término iminente do programa depende da acuidade do seu código. Se o thread inocente possuir uma janela e um loop de mensagem, então o manipulador final pode usar SendMessage para esta janela para enviar uma mensagem definida pela aplicação (precisa ser 400h ou acima) para informar este thread de que deve terminar com elegância.
    Se não existir uma janela e um loop de mensagem, o manipulador final poderia armar uma variável flag pública, rastreada de tempos em tempos pelo outro thread. Alternativamente, você poderia usar SetThreadContext para forçar o thread a executar determinado código de encerramento fazendo com que eip aponte para este código. Este método não funcionará se o thread estiver numa API, por exemplo, esperando o retorno de uma GetMessage. Neste caso, você também precisará enviar uma mensagem para garantir que o thread retorne da API e que o novo contexto seja configurado.
  7. RaiseException só funciona no thread chamador, de modo que não pode ser usado para fazer a comunicação entre threads fazendo com que um thread inocente execute seu próprio código manipulador.
  8. Como é que o manipulador final sabe quando pode prosseguir após informar os outros threads de que o programa está para terminar? SendMessage não retorna enquanto o destinatário não tiver voltado do seu procedimento de janela e o manipulador final não pode esperar por este retorno. Alternativamente, ele poderia inscrever uma flag que esperasse por uma resposta de um outro thread que ele acabou de desobstruir (observe que você precisa chamar a API Sleep no loop de inscrição para evitar a sobrecarga do sistema). Ou, ainda melhor, o manipulador final poderia esperar até que o outro thread termine (isto pode ser feito usando a API WaitForSingleObject ou WaitForMultipleObjects se houver mais de um thread). Alternativamente poderiam ser usadas as APIs Event ou Semaphore.
  9. Para um exemplo de como estes procedimentos poderiam funcionar na prática, imagine que um thread secundário tenha a função de reorganizar uma base de dados e depois escrevê-la em disco. Pode estar no meio da tarefa quando o thread principal causa uma exceção, entrando no seu manipulador final. Neste caso você poderia abortar a tarefa do thread secundário, forçando seu desdobramento e seu término elegante, deixando os dados originais no disco ou, alternativamente, você poderia permitir que ele terminasse a tarefa e depois avisasse o manipulador que terminou, de modo que o manipulador possa voltar ao sistema. Você teria que impedir que o thread secundário iniciasse novas tarefas assim que seu manipulador fosse chamado. Neste caso o manipulador precisa armar uma flag que será testada pelo thread secundário antes de iniciar qualquer tarefa ou então usar as APIs Event.
  10. Se a comunicação entre threads é difícil, existe uma outra maneira de um thread acessar a pilha de outro thread e, deste modo, causar um desdobramento. Neste caso faz-se uso do fato de que, apesar de cada thread possuir sua própria pilha, a memória reservada para esta pilha está no espaço de endereços do próprio processo. Você pode confirmar este fato observando uma aplicação multi-thread usando um debugger. Quando você se movimentar entre threads, os valores de ESP e EBP mudarão, mas eles estarão dentro do espaço de endereços do próprio processo. O valor de FS também será diferente dependendo do thread e apontará para o Thread Information Block de cada thread. Assim, se você seguir os seguintes passos, um thread pode acessar a pilha e causar o desdobramento de outro:
    a. Guarde numa variável estática o valor do registrador FS de cada novo thread que for criado.
    b. Retorne as variáveis estáticas para zero quando cada um dos threads for fechado.
    c. O manipulador que precisar desdobrar outros threads deveria analisar cada uma das variáveis estáticas e, para as com valor diferente de zero (ou seja, o thread está ativo na hora da exceção), os manipuladores devem ser chamados com a flag de exceção 2 (EH_UNWINDING) e uma flag de usuário, digamos 400h, para mostrar que o manipulador thread-específico está sendo chamado pelo seu manipulador final. Você não pode chamar um manipulador thread-específico num thread diferente usando RtlUnwind (que é thread-específico), mas pode fazê-lo usando o seguinte código (onde ebx contém o endereço do EXCEPTION_RECORD):
    MOV D[EBX+4],402h ; faz a flag de exceção EH_UNWINDING+400h L1: PUSH ES MOV AX,[FS_VALUE] ; pega o valor de FS do thread que deve ser desdobrado MOV ES,AX ES MOV EDI,[0] ; pega o endereço do primeiro manipulador thread-específico POP ES 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 o manipulador para rodar o código de desobstrução ADD ESP,8h ; remove os dois parâmetros PUSHados MOV EDI,[EDI] ; pega o ponteiro para a próxima estrutura ERR JMP L2 ; e faz o próximo se não estiver no fim L3: ; marcador para quando terminar ; agora volta para L1 com um novo FS_VALUE até que todos os threads ; tenham sido processados Aqui você vê que o Thread Information Block de cada thread inocente é lido usando o registrador ES, o qual, temporariamente, recebe o valor do registrador FS do thread.
    Ao invés de usar FS para achar o Thread Information Block, você pode usar o seguinte código para obter um endereço linear de 32 bits. Neste código, LDT_ENTRY é uma estrutura de 2 dwords, AX contém o valor de 16 bits do seletor (FS_VALUE) que deve ser convertido e hThread é qualquer manipulador de thread válido:
    AND EAX,0FFFFh PUSH ADDR LDT_ENTRY,EAX,[hThread] CALL GetThreadSelectorEntry OR EAX,EAX ; vê se falhou JZ >L300 ; sim, então retorne zero MOV EAX,ADDR LDT_ENTRY MOV DH,[EAX+7] ; pega base alta MOV DL,[EAX+4] ; pega meio da base SHL EDX,16D ; desloca para o topo de edx MOV DX,[EAX+2] ; e pega a base baixa OR EDX,EDX ; agora edx=endereço linear de 32 bits L300: ; retorna nz se sucesso A razão da importância (usando a flag 400h) de informar o manipulador chamado de que está sendo chamado por outro thread (o manipulador final) é que o thread chamado ainda está rodando porque a exceção ocorreu num thread diferente. É claro que o manipulador, nesta circunstância, precisa suspender o thread para que a tarefa de desobstrução possa ser realizada pelo thread chamador. O thread inocente, então, recebe um lugar seguro para ir antes de chamar ResumeThread. Tudo isto precisa ser feito antes que o manipulador final receba permissão para voltar ao sistema porque, no retorno, o sistema simplesmente irá terminar todos os threads na força bruta.

O programa exept1

Este programa é um exemplo simples de como a manipulação de exceções pode ser usada na prática em programas para Windows escritos em Assembly. O código fonte está no arquivo Except1.asm, escrito com a sintaxe GoAsm. Apesar de ser um programa GDI Windows, baseia-se somente em caixas de mensagem, motivo pelo qual não existem loops de mensagem.

O programa possui dois manipuladores de exceção, um final e um thread-específico. O manipulador final é criado primeiro, depois é chamado um procedimento, cujo código está protegido pelo manipulador thread-específico. Dentro deste procedimento ocorre uma exceção e o manipulador thread-específico é chamado. Dentro do manipulador, pergunta-se ao usuário se o manipulador deve ou não engolir a exceção. Se o usuário decidir que o manipulador deve engolir a exceção, o programa está preparado para continuar rodando, mas termina normalmente. Se o usuário decidir que a exceção não deve ser engolida pelo manipulador, então o manipulador final é acionado (durante o fechamento do programa). Na vida real, este manipulador seria responsável pelos logs e registros, pelo fechamento de manipuladores de arquivo, liberação de memória, etc. Mas, antes que o programa finalmente termine, algo muito interessante acontece. O sistema chama o manipulador de exceções thread-específico caso mais desobstruções sejam necessárias nesta moldura de pilha particular que usa dados locais. Este é o desdobramento feito pelo sistema. Todos estes eventos são anunciados por várias caixas de mensagem que aparecem na tela.

O programa exept2

exept2

Este é um programa mais complexo com a intenção de demonstrar o conteúdo deste artigo em maiores detalhes.

O código fonte do Except2.exe (Except2.asm e Except2.RC) também é fornecido e está na sintaxe GoAsm.

A janela principal é um diálogo modal. Um manipulador final é criado bem no início do processo. Quando o botão "Cause Exception" é clicado, primeiro o procedimento de diálogo é chamado com o comando e depois duas rotinas adicionais são chamadas - a última rotina causa uma exceção do tipo escolhido através dos botões de rádio. Quando a execução passa por este código, 3 manipuladores de exceção thread-específicos são criados.

Se possível, a exceção é reparada in situ, ou então o programa se recupera dentro do manipulador escolhido a partir de um lugar seguro. Se for permitido que a exceção vá até o manipulador final, você pode sair pressionando F3 ou F5. Ou, se você perssionar F7, o manipulador final tentará fazer a recuperação desta exceção.

Você pode acompanhar os eventos à medida que ocorrerem por que cada um dos manipuladores apresenta várias mensagens na listbox. Ocorre um pequeno delay entre cada mensagem para que você possa acompanhar com mais facilidade o que está ocorrendo, ou você pode rolar as mensagens para analisá-las melhor.

Quando o programa estiver para terminar acontece algo interessante. O sistema provoca um desdobramento final com a flag de exceção armada com 2h. As mensagens enviadas à listbox tornam-se ainda mais lentas por que o programa está prestes a terminar!

Você verá que o mesmo tipo de desdobramento ocorre se você especificar que a execução deve continuar a partir de um "lugar seguro" (safe-place) ou se F7 for pressionada quando o manipulador final estiver no comando. Este desdobramento é iniciado pelo próprio manipulador.

Agradecimentos

Agradeço ao autor, Jeremy Gordon, pela excelência do texto. Espero que minha tradução ajude os interessados que tenham dificuldade com o inglês (dificuldade com o texto todo mundo vai ter wink)

Faça o download do artigo original acompanhado dos programas exemplo (57 Kb).

mfx brokerсковорода биол купитьотзывы ооо полигонпланшетытамплієрисалтовка харьковвозрождение

Informações adicionais