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) 


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.

Informações adicionais