Jump to content

Um pouco sobre Excepções


Rick Santos

Recommended Posts

Posted

Neste tópico irei falar um pouco sobre excepções, espero que seja util para quem desconhece este assunto. (Mais informações podem ser encontradas no manual da Intel, Volume 3, Capítulo 6).

Excepções podem ser consideradas um tipo de interrupção gerada pela CPU após algum acontecimento inesperado de execução. Elas podem causar um grande custo de performace.

Certos aspetos que irei falar aqui encontram-se melhor explicados em livros e documentação oficial, como tal, este conteúdo é apenas uma introdução.

Uma ultima dica é observar o código fonte do relacionado ao que falo aqui, nele encontram-se todas as estruturas, etc...

Existem 3 tipos principais de Excepções (pelo menos conhecidos :P), estes são (por ordem de "gravidade"):

 

Faults/Faltas (Em Português é comum chama-las "Falhas"): Simplesmente acontecimentos inesperados que continuam a permitir o fluxo de execução normal sem causar grandes custos de processamento (Mas é sempre preferível evita-las :P).

Traps/Armadilhas (Nunca vi nenhuma tradução em Português para esta por isso pode ser armadilhas B|): São um tipo de interrupção (como todas as outras) e são reportadas imediatamente após à instrução que gerou a excepção.

Aborts/Abortos - Excepções de nível grave, onde muitas das vezes torna-se impossível continuar a operação do sistema dando-se assim o término do processo que gerou o aborto, e por vezes do sistema operativo em si, sendo necessário o reboot da máquina. OBS: Alguns Abortos ao afetarem o Kernel podem gerar algo chamado Kernel Panic (normalmente originado a partir de um "fatal error") que no caso do Windows, pode dar-se o famoso "Blue Screen of Death" :ph34r:.

PS: Por convenção, a nível de arquitetura, excepções tal como interrupções podem ser representadas por "excepção/interrupção + #", como demonstrado a baixo.

Algumas excepções como General Protection Fault (GP#) , Page Fault (PF#) e Double Fault (DF#) (entre muitas outras) podem colocar um código de erro específico da determinada excepção na Stack (Pilha) pertencente ao processo que gerou o "erro". Normalmente este código de erro é empilhado após o empilhamento de diveros registadores, na ordem:  SS, RSP, CS, RIP (SS é empilhado apenas por questões de optimização, segundo consta xD). Estes registadores e códigos de erro devem-se manter empilhados até ao momento do tratador de interrupção encontrar alguma instrução do tipo IRET que recuperá todos os conteúdos que terão sido colocados na pilha antes do Kernel passar o controle de execução para o tratador de excepção corresponde à mesma. Por fim após os valores "desempilhados", o tratador ainda tem a função se direcionar o controle de execução para CS:RIP (Par de registadores para o qual a CPU executa código). 

------------------------------------------------------------------------------------------------------------------------------------------------------------

Aqui irei fazer uma breve introdução ao tratamento de excepções no modo x64 do Linux.

A primeira coisa a entender é que uma excepção, em certos aspectos, é idêntica a uma interrupção, e como tal, necessita de ser tratada a partir de de uma determinada rotina que é escolhida a partir do tratador de excepção (Exception Handler). OBS: Caso exista, por exemplo uma excepção devido a uma divisão por zero na CPU, o exception handler pode mandar um signal (sinal) chamado SIGFPE ao processo que causou o "erro". Isto é, o processo interrompe o fluxo de execução atual e atende o signal a partir de uma rotina de tratamento de sinais, caso tal não seja possível pode se dar o aborto do processo, sendo assim encerrado. Dependendo da tratador de excepção escolhido o sinal enviado ao processo pode ser diferente, por vezes sim ou nao tratável...

(Para uma boa leitura sobre sinais recomendo o livro: Richard W. Stevens (“Advanced Programming in the UNIX Environment, Third
Edition”))

De uma forma bastante resumida o ciclo de tratamento de excepções pode ser divido em 3 passos:

1º - O contexto de determinados registadores são guardados na pilha (Dependo da excepção pode ser colocado também um código de erro após estes).

2º - A excepção é tratada a partir de uma função escrita em C.

3º - A "saída" do tratador de interrupção (exception handler), dá se a partir da função "ret_from_exception(), que termina todas as "excepções", excepto as emuladas como int 0x80 (interrupção de software).

-----------------------------------------------------------------------------------------------------------------------------------------------------------

Primeirante a IDT (Interrupt Descriptor Table) deve-se encontrar inicializada com um exception handler para excepção conhecida. Isto é trabalho da função "trap_init()", que tem como "função" :P colocar todas a rotinas de tratamento de excepção nas entradas da IDT que se referem às próprias excepções e também, em alguns casos, às NMI (Non Maskable Interrupt).

O primeiro ponto durante o ciclo de tratamento de excepção, como referido acima, consiste no empilhamento do contexto de certos registadores, cujo a ordem é: SS, RSP, CS, RIP.

Esta "preparação" antes do tratamento da excepção propriamente dito, é feita de um arquivo assembly presente em "arch/x86/entry/entry_64.S" que o seu código fonte também pode ser visto  aqui: 

https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/entry/entry_64.S

Nele está definido uma Macro chamada "idtentry" que indica o "Entry Point" das excepções na IDT. Esta Macro tem como função preparar o pré-tratamento de excepções a partir da preservamento dos registadores na pilha. Idtentry irá "alocar espaço para os registadores na pilha, como definido na estrutura "pt_regs" de "/entry_64.S", por fim também poderá colocar o código de erro respetivo à excepção na pilha, caso este código não exista o seletor colocado no registador de segmentos CS é "checado" (assim, acessando o descriptor correspondente) e dá-se a troca de estado de código, do user-space para o kernel-space ou vice-versa, dependendo do valor de DPL (Descriptor Privilege Level) do descritor apontado por CS (Existe também uma cópia de DPL em SS, mas não nos interessa por agora).

Após tudo isto, o idtentry terá de fazer uma chamada ao exception handler respetivo, assim fazendo com que o Kernel direcione o controle de execução para ele.

Por fim, já depois de o exception handler ter terminado as suas "tarefas", a macro idtentry ainda tem a função de "desempilhar" os registadores da pilha executando a instrução IRET, como descrito mais acima no texto.

Todos os Handlers presentes na IDT (Interrupt Descriptor Table) são definidos em "arch/x86/kernel/traps.c" a partir de um macro chamado "DO_ERROR", neste arquivo para além de muitas outras coisas também está indicado qual o signal que pode ou não ser enviado pelo handler ao processo que gerou a excepção. A descrição de estruturas adiantes torna-se bastante extenso, recomendo aos leitores procurarem sobre tal em documentação oficial, etc...

 

DO_ERROR(X86_TRAP_DE, SIGFPE, "divide error", divide_error)

DO_ERROR(X86_TRAP_OF, SIGSEGV, "overflow", overflow)

DO_ERROR(X86_TRAP_UD, SIGILL, "invalid opcode", invalid_op)

DO_ERROR(X86_TRAP_OLD_MF, SIGFPE, "coprocessor segment overrun", coprocessor_segment_overrun)

DO_ERROR(X86_TRAP_TS, SIGSEGV, "invalid TSS", invalid_TSS)

DO_ERROR(X86_TRAP_NP, SIGBUS, "segment not present", segment_not_present)

DO_ERROR(X86_TRAP_SS, SIGBUS, "stack segment", stack_segment)

DO_ERROR(X86_TRAP_AC, SIGBUS, "alignment check", alignment_check)

                            //Número                       //Sinal        //String identificadora   // Ponto de Entrada do tratador de excepção

                                 do vetor                         que será             da excepção

                                 da interrupção            enviado

                                                                         ao processo

Segue o código de fonte de "/traps.c": https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/kernel/traps.c

-----------------------------------------------------------------------------------------------------------------------------------------------------------

Agora para terminar (mais suave xD), vou colocar em baixo alguns (nem perto de todos :P) tipos de excepções, referindo o seu grupo e fazendo uma breve descrição sobre elas. (As traduções para Português são hilárias xD e nunca vi o uso delas :P).

------------------------------------------------------------------------------------------------------------------------------------------------------------

Faults/Falhas:

Divide-by-Zero Error/Divisão por Zero - A CPU lança esta excepção ao dividir algum número por zero (matemáticamente impossível), a partir de instruções de divisão (como DIV ou IDIV, por exemplo). RIP, que terá sido salvo na pilha com visto anteriormente, irá conter o apontar (conter o endereço) para a instrução que gerou a excepção. OBS: Certas vezes Software Developers "abusam" desta excepção com o intuito de testar o funcionamento dos tratadores da mesma (exception handlers). 

Invalid Opcode/Código de operação inválido - Esta excepção é lançada pela CPU na execução de uma instrução não existente (não definida na CPU), em certos casos também pode ser gerada devido a uma instrução exceder os 15 bytes de tamanho. RIP salvo na pilha irá apontar para a instrução que causou a excepção.

Device Not Available/Dispositivo não disponível - (Apesar de ter um nome que se refere a dispositivos nada tem a ver com eles, estranhamente... 9_9) Esta excepção é gerada quando é executada algum tipo de instrução estendida não estando elas habilitadas (isto é se certas as flags do registador de controle CR0 (flags as quais habilitam instruções como FPU/MMX/SSE) estiverem a zero). Novamente o salvo RIP apontará para a instrução que causou a excepção.

Segment not Present/Segmento não Presente - Esta excepção é gerada quando é tentado o acesso ou o carregamento de um segmento de memória cujo o descriptor contém o campo "P (Present) = 0", que significa que tal segmento não se encontra "presente". Ao contrário das excepções descritas acima, esta coloca um código de erro na pilha (da maneira como foi descrito a meio do tópico) que é um indíce de um seletor de segmentos (Segment Selector Index). O RIP salvo na pilha, apontará para a instrução que gerou a excepção (mais uma vez :P).

General Protection Fault/Falta de proteção Geral - (Talvez uma das excepções mais famosas)  Esta excepção pode ser gerada a partir de dezenas (sem exagero) de casos. Alguns deles podem ser: O erro de algum segmento (seja por parte de não correspondência de Privilégios, tipo, limites ou leitura/escrita); A execução de uma instrução priviligiada enquanto CPL (Current Privilege level) do processo em questão é diferente de zero (significa que o código não se encontra em Kernel-Mode); Acessar a partir de Seletores algum tipo de "NULL Descriptor", como por exemplo a primeira entrada de GDT (Global Descriptor Table); Escrever o valor 1 em registadores reservados; Escrever um valor de diferente de zero (0x0) no registador EOIR (End of Interruption Register), presente na LAPIC de cada processador lógico (Caso a CPU em questão faça uso desta)................... Quando a excepção GP# é gerada a partir de algum problema relacionado com um segmento de memória, é empilhado um código de erro na pilha que é um seletor de segmentos (Segment Selector Index), por outro lado apenas é colocado o valor 0 na pilha. O RIP apontará para a instrução que gerou a excepção.

Page Fault/Falta da Página - Esta excepção é gerada devido a algum acontecimento inesperado relacionado com uma página. Uma página é uma região de memória (criada pelo Kernel) de 4 KiB ou KB que permite o uso de memória virtual. As páginas devem ser mapeadas atráves de uma hierarquia de tabelas cujo sua informação estão fora do escopo deste tópico, pode ser encontrada muito boa informação nos manuais da Intel sobre Paginação e Memória Virtual. Alguns acontecimentos que dão origem à excepção Page Fault são: Referir ou Acessar uma página que não foi mapeada (logo não se encontra presente na memória);  Escrever numa página Read-Only (Esta informação encontra-se na tabela da página em questão); O RIP apontar para uma página marcada como não executável (bit "XD" ativado na tabela da página); Tentar acessar uma página quando o nível de privilégio atual de execução do processo (CPL) é maior do que o da página, ou seja menos priviligiado; Escrever em bits reservados da página (normalmente os 12 inferiores para impedir o transbordamento de dados entre páginas); Escrever em bits reservados das tabelas de páginas............................. A excepção Page Fault coloca um código de erro na pilha conhecido por mim como "PWURI" (não vou falar sobre ele aqui, sinceramente também nunca li muito sobre ele :P). Por fim o registador de controle CR2 vai apontar para o endereço virtual que causou a excepção.

-----------------------------------------------------------------------------------------------------------------------------------------------------

Traps/Armadilhas:

BreakPoint/Ponto de Paragem: Esta é uma excepção que ocorre devido à execução de uma instrução "INT3". Esta instrução é bastante utilizada para fins de debbug. Quando marcamos o famoso "breakpoint" num determinado local de um programa a partir de Debugger, apenas estamos a pedir ao Debugger que coloque uma instrução INT3 naquele determinado local, assim fazendo com que o fluxo de execução do processo para naquele ponto. O RIP que terá sido salvo na pilha irá apontar (conter o endereço) para/do primeiro endereço de memória logo após a instrução INT3 colocada pelo Debugger.

Overflow/Não encontro tradução que me satisfaça xD - Esta excepção é gerada quando o bit "overflow" (também conhecido como V Flag) de RFLAGS/EFLAGS  se encontra setado (valor 1) e é executada a instrução INTO. Essa flag de RFLAGS é utilizada para indicar o overflow gerado a partir de alguma instrução aritmética (Na nossa conhecida ALU). Para além deste caso, a excepção overflow também pode resultar do resultado do resultado de uma divisão entre dois operandos for maior em tamanho (normalmente bits/bytes)  do que estes mesmos, por exemplo, se numa divisão entre dois operandos de 32 bits for gerado um resultado final de 64 bits irá ser lançada uma excepção de overflow. O RIP salvo na pilha, caso a excepção seja gerada a partir da instrução INTO, irá apontar para o primeiro endereço de memória logo após a instrução que gerou o overflow (neste caso a INTO). Por outro lado, caso a excepção se gere por meio de um resultado final de uma divisão maior que os operandos da mesma, RIP apontará para a instrução causadora da excepção.

------------------------------------------------------------------------------------------------------------------------------------------------------------

Aborts/Abortos:

Double Fault/Falta Dupla - Esta excepção é gerada a partir de uma outra excepção que terá sido gerada e que não pode ser tratada. Outro caso que pode "lançar" esta excepção é uma excepção ser gerada quando a CPU está a tentar "chamar" o exception handler para uma outra excepção gerada... Pode ter ficado confuso, mas vou dar um exemplo bem intressante e intuitivo que li à uns tempos num livro. Imaginem que ocorre um Page Fault, cujo o exception handler se encontra numa página marcada como não presente (P = 0), neste caso quando a CPU fosse procurar pelo tratador de interrupção para chama-lo iria causa uma outra page fault pois tal página estaria marca como não presente, e por fim seria gerado um Double Fault. Double Fault gera sempre o código de erro 0, empilhando-o assim na pilha. Neste ponto RIP encontra-se inacessível, querendo assim dizer que o processo têm de ser terminado. (Daqui a gravidade dos abortos relativamente à perda de performace, por vezes até o sistema precisará de ser reiniciado, como no caso da Triple Fault por exemplo).

Triple Fault/Falta Tripla - Esta não tem nenhum exception handler associado, por isso sendo por muitos não considerada uma excepção, ela basicamente ocorre quando uma excepção é gerada durante o processo de tratamento da excepção Double Fault. Neste ponto, chegando a um estado critíco de execução, o processador precisa de ser reiniciado. A unica maneira de tentar prevenir as Triple Faults é fornecer uma estrutura TSS separada das outras especificamente à excepção Double Fault e usar Task Gates para este tipo de excepção. (Sinceramente não sei ao certo se a Triple Fault gera algum código de erro, se alguem souber agradeço que me informem :D).

--------------------------------------------------------------------------------------------------------------------------------------------------------

Bem, esta foi apenas uma "pequena" introdução às excepções, é um assunto bastante extenso que não pode ser cobrido aqui. 

Aqui encontram-se pelo menos as bases de uma matéria tão importante na ciência da computação.


Ricardo Santos

Archived

This topic is now archived and is closed to further replies.

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...