Interrupções e exceções sendo entendidas na prática.
Uma interrupção é um sinal enviado para o processador solicitando a atenção dele para a execução de outro código. Ele para o que está executando agora, executa este determinado código da interrupção e depois volta a executar o código que estava executando antes. Esse sinal é geralmente enviado por um hardware externo para a CPU, cujo o mesmo é chamado de IRQ — Interrupt Request — que significa "pedido de interrupção".
Enquanto a interrupção de software é executada de maneira muito semelhante a uma chamada de procedimento por far call
. Ela é basicamente uma interrupção que é executada pelo software rodando na CPU, daí o nome.
No caso de interrupções de softwares sendo disparadas em um processo executando sob um sistema operacional, o código executado da interrupção é definido pelo próprio sistema operacional e está fora da memória do processo. Portanto há uma troca de contexto onde a tarefa momentaneamente fica suspensa enquanto a interrupção não finaliza.
O código que é executado quando uma interrupção é disparada se chama handler e o endereço do mesmo é definido na IDT — Interrupt Descriptor Table. Essa tabela nada mais é que uma sequência de valores indicando o offset e segmento do código à ser executado. É uma array onde cada elemento contém essas duas informações. Poderíamos representar em C da seguinte forma:
Ou seja o número que identifica a interrupção nada mais é que o índice a ser lido no vetor.
Provavelmente você já ouviu falar em exception. A exception nada mais é que uma interrupção e tem o seu handler definido na IDT. Por exemplo quando você comete o erro clássico de tentar acessar uma região de memória inválida ou sem permissões adequadas em C, você compila o código e recebe a clássica mensagem segmentation fault.
Nesse caso a exceção que foi disparada pelo processador se chama General Protection e pode ser referida pelo mnemônico #GP, seu índice na tabela é 13.
Essa exceção é disparada quando há um problema na referência de memória ou qualquer proteção à memória que foi violada. Como por exemplo ao tentar escrever em um segmento de memória que não tem permissão para escrita.
Um sistema operacional configura uma exceção da mesma forma que configura uma interrupção, modificando a IDT para apontar para o código que ele quer que execute. Nesse caso o índice 13 precisaria ser modificado.
No Linux basicamente o que o sistema faz é criar um handler que trata a exceção e manda um sinal para o processo. Esse sinal o processo pode configurar como ele quer tratar, mas por padrão o processo escreve uma mensagem no terminal e finaliza.
A instrução int imm8
é usada para disparar interrupções de software/exceções. Bastando simplesmente passar o índice da interrupção como operando.
Vamos ver na prática a configuração de uma interrupção em 16-bit. Para isso vamos usar o MS-DOS para que fique mais simples.
A IDT está localizada no endereço 0 em real mode, por isso podemos configurar para acessar o segmento zero e assim o offset seria o índice de cada elemento da IDT. O que precisamos fazer é acessar o índice que queremos modificar na IDT, depois é só jogar o offset e segmento do procedimento que queremos que seja executado. Em 16-bit isso acontece de uma maneira muito mais simples do que em protected mode, por isso é ideal para entender na prática.
Eis o código:
Para compilar e testar usando o Dosbox:
A interrupção simplesmente escreve os caracteres na parte superior esquerda da tela.
Note que a interrupção retorna usando a instrução iret
ao invés de ret
. Em 16-bit a única diferença nessa instrução é que ela também desempilha o registrador de flags, que é empilhado pelo processador ao disparar a interrupção/exceção.
Perceba que é unicamente um código de exemplo. Essa não é uma maneira segura de se configurar uma interrupção tendo em vista que seu handler está na memória do .com
que, após finalizar sua execução, poderá ser sobrescrita por outro programa executado posteriormente.
Mais um exemplo mas dessa vez configurando a exceção #BP de índice 3. Se você já usou um depurador, ou pelo menos tem uma noção à respeito, sabe que "breakpoint" é um ponto no código onde o depurador faz uma parada e te permite analisar o programa enquanto ele fica em pausa.
Os depuradores modificam a instrução original colocando a instrução que dispara a exceção de breakpoint. Depois tratam o sinal enviado para o processo, restauram a instrução original e continuam seu trabalho.
O breakpoint nada mais é que uma exceção que é disparada por uma instrução. Podemos usar int 0x03
(CD 03
em código de máquina) para fazer isso porém essa instrução tem 2 bytes de tamanho e não é muito apropriada para um depurador usar. Por isso existe a instrução int3
que dispara #BP explicitamente e tem somente 1 byte de tamanho (opcode 0xCC).
Repare que a cada disparo de int3
executou o código do nosso procedimento break. Esse por sua vez imprimiu o caractere 'X' na tela do Dosbox usando a interrupção 0x10
que será explicada no próximo tópico.
Só para deixar mais claro o que falei sobre os sinais que são enviados para o processo quando uma exception é disparada, aqui um código em C de exemplo:
Mais detalhes sobre os sinais serão descritos no tópico Entendendo os depuradores.