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.

Informações adicionais