Interrupções de software e exceções

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.

Interrupt Descriptor Table

O código que é executado quando uma interrupção é disparada se chama handler e o endereço do mesmo é definido na IDTInterrupt 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:

// Em 16-bit

struct elem {
  uint16_t offset;
  uint16_t segment;
}

struct elem idt[256];

Ou seja o número que identifica a interrupção nada mais é que o índice a ser lido no vetor.

Exception

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.

IDT em Real Mode

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:

int.asm
bits 16
org  0x100

VADDR equ 0xb800

; ID, segmento, offset
%macro setint 3
  mov bx, (%1) * 4
  mov word [es:bx], %3
  mov word [es:bx + 2], %2
%endmacro


; -- Main -- ;
mov ax, 0
mov es, ax

setint 0x66, cs, int_putchar

mov al, 'A'
mov ah, 0x0B
int 0x66

mov ah, 0x0C
int 0x66

ret

; -- Interrupção -- ;
int_cursor: dw 0

; Argumentos:
;   AL    Caractere
;   AH    Atributo
int_putchar:
  push es
  mov bx, VADDR
  mov es, bx

  mov di, [int_cursor]
  mov word [es:di], ax

  add word [int_cursor], 2
  pop es
  iret

Para compilar e testar usando o Dosbox:

$ nasm int.asm -o int.com
$ dosbox int.com

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

int.asm
bits 16
org  0x100

; ID, segmento, offset
%macro setint 3
  mov bx, (%1) * 4
  mov word [es:bx], %3
  mov word [es:bx + 2], %2
%endmacro


; -- Main -- ;

xor ax, ax
mov es, ax

setint 0x03, cs, break

int3
int3

ret

; -- Breakpoint -- ;

break:
  mov ah, 0x0E
  mov al, 'X'
  int 0x10
  iret

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.

Sinais

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:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>

void segfault(int signum)
{
  fputs("Tá pegando fogo bixo!\n", stderr);
  exit(signum);
}

// Esse código também funciona no Windows.
int main(void)
{
  char *desastre = NULL;
  struct sigaction action = {
      .sa_handler = segfault,
  };

  sigaction(SIGSEGV, &action, NULL);

  strcpy(desastre, "Eita!");

  puts("Tchau mundo!");
  return 0;
}

Mais detalhes sobre os sinais serão descritos no tópico Entendendo os depuradores.

Last updated