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) 


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