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