No post anterior nós finalmente finalizamos o P2, e começamos agora o P3, um Kernel Preemptivo, YAY! Mas o que isso significa?
Primeiro, até o momento, as tarefas precisam explicitamente ceder a CPU para as outras. Isso era feito via função do_exit
para as threads do kernel e syscall yield
para os processos. Agora, haverá um mecanismo externo que pode interromper a tarefa a qualquer momento.
Que brisa é essa?
A arquitetura x86 é uma arquitetura orientada a eventos. Significa que a CPU reage a estímulos externos e responde de acordo. Mas como isso funciona e qual seria a alternativa se esse mecanismo não existisse?
Suponha que você digitou uma tecla qualquer no seu teclado. Se o evento não foi informado a CPU, ela deve periodicamente consultar o hardware do teclado para saber se alguma tecla foi pressionada, e em caso positivo, verificar o seu código. Esse mecanismo é conhecido como Polling e é bem conhecido em várias aplicações.
Perceba que já usamos esse mecanismos de uma certa forma. As instruções in
e out
, que usamos para habilitar o A20 Gate nesse post aqui, são usadas para fazer polling no hardware.
Para isso funcionar, a tarefa deve voluntariamente ceder a CPU para que ela vá buscar os dados no hardware. Se a tarefa não colaborar, pouco pode ser feito. Além disso, pode ser que você vá fazer o polling e o hardware não tenha nada a informar (nenhuma tecla foi pressionada por exemplo). Nesse caso, estamos desperdiçando valiosos ciclos de CPU fazendo nada. Um desperdício de recursos!
Por isso, a arquitetura x86 (e qualquer arquitetura moderna, como ARM) trabalha com o conceito de interrupções. Sempre que necessário, o hardware envia um pedido de interrupção para a CPU. Esta para o que está fazendo, atende o hardware e volta para a tarefa como se nada tivesse acontecido. Na verdade, essa é toda a magia da coisa: a tarefa não faz ideia de que foi interrompida, e para ela esse mecanismo é totalmente transparente.
Ok, e como fazemos para isso funcionar na prática? A resposta curta é que basta instalar o IDT e as funções tratadoras das interrupções. A resposta longa vem a seguir.
Interrupt Descriptor Table
Se você acompanhou a saga da instalação do GDT nesse post aqui, adianto que o IDT (Interrupt Descriptor Table) é igual, mas é diferente. Igual porque o IDT também é um registrador especial, de 48 bits, que armazena o endereço base e o tamanho da tabela, tal qual o GDT. Cada entrada na tabela do IDT também tem 8 bytes, assim como as entradas do GDT.
Mas é diferente porque cada entrada nessa tabela é equivalente ao IVT (Interrupt Vector Table) da BIOS, que, sinto lhe dizer, não existe mais.
IVT?
Para entendermos o IDT, acredito ser pedagógico se dominarmos o IVT. Quando o computador é ligado, a CPU carrega o código da ROM BIOS, que faz várias checagens e executa os códigos contidos nesse chip, notadamente o POST (Power On Self Test). Uma das coisas que a BIOS faz é instalar o IVT na posição 0x0 da memória. Lembre-se que nesse momento a CPU está operando em modo real, com segmentação, em 16 bits.
Então, cada entrada no IVT contém 4 bytes: 2 bytes para o cs
e 2 bytes para o offset. cs:offset
indica o endereço da função que trata as interrupções. Quando você está nesse modo então, se você pressionar uma tecla, o hardware do teclado disparará um pedido de interrupção (também chamado de IRQ - Interrupt Request). As interrupções são exclusivas de cada hardware, assim quando um pedido desse chega, a CPU sabe de qual hardware ele está vindo. O teclado, por exemplo, envia pedidos pela IRQ 1. Um mouse PS2 envia pedidos de IRQ 12. Para ver outros exemplos, veja aqui.
Pois bem, esses números são usados pela CPU como índices para acessar as entradas do IVT e buscar a função que trata essa solicitação. Assim, no caso do teclado, a CPU carregará o cs:offset
da entrada 1 e fará um call
/jmp
para esse endereço. E o que essa função faz? Isso depende do hardware. No caso do teclado, a função usa instruções in
/out
para consultar os registradores do teclado e descobrir qual tecla foi pressionada.
Parece funcionar bem certo? Larga de ser burro e usa esse IVT para tratar as interrupções então, a tabela já foi construída pela BIOS, para que refazer o serviço?
Basta usar o IVT então!
Tenho péssimas notícias jovem. Uma vez que você mudou a CPU para o modo Protegido/32bits, o IVT foi perdido. Uma, porque não sabemos onde estão os endereços das funções que foram instaladas no IVT. Outra, porque aquelas funções usam manipulação de dados em modo real de 16 bits, que não equivale aos registradores de segmento usados em modo 32 bits. Enfim, não é possível.
Voltando ao IDT
Obviamente há de se haver um jeito de instalar tratadores de interrupções para o modo protegido/32bits. E há mesmo, e nesse caso, voltamos ao IDT. Nossa missão é então, em cada entrada do IDT, o tratador correto da interrupção. Vamos então ver como é uma típica entrada do IDT.
A entrada tem então 8 bytes, e é ligeiramente mais simples do que uma entrada do GDT. Os campos segment select e offset tem um significado similar ao IVT: no fim das contas é o endereço da função que vai tratar a interrupção. A diferença é que o seletor de segmento aponta para uma entrada válida para um seletor de código do GDT. Em especial, pois no futuro o código de usuário estará rodando em outro anel de proteção do kernel e por isso o registrador cs
selecionará um segmento diferente do seletor de código do kernel, que executa no anel de proteção 0.
Adianto aos aflitos que ainda não vamos separar o código de usuário para o Ring 3. Isso vai acontecer apenas no P5 - memória virtual. Adianto então que as tarefas estarão rodando no mesmo seletor de segmento do kernel, Ring 0, e todos eles apontam para a mesma entrada: cs = 0x8
(para você se situar, lembre-se que os seletores de segmento de dados ds
, es
, fs
, gs
e ss
possuem valor 0x10
). Portanto, todas as entradas do IDT possuem esse segment selector igual a 0x8
, e o offset
é o endereço da função (32 bits partidos ao meio, 16 para cada). Até aqui tudo bem?
Os bits 32 a 39 são reservados. Basta informá-los como zero, assim como o bit 44.
O bit 47 é o bit Present, indica que a entrada é válida e está ativa. Basta deixar com o valor 1.
Os bits 45 e 46 representam o DPL (Descriptor Privilege Level). Com dois bits, você sabe que os valores possíveis são 0, 1, 2 e 3, e você já deve ter associado aos anéis de proteção, corretamente. Mas o que esse valor significa neste contexto aqui?
Bom, esse valor só é usado quando essa entrada do IDT está relacionado a uma interrupção de software invocada com a instrução int. Você vai querer por exemplo que sua syscall seja disponível aos usuários, portanto, no Ring 3. Do restante, deixe sempre em Ring 0. As interrupções de hardware ignoram esse valor.
Por fim, os bits 40 a 43 representam o Gate Type. Vale a pena explicar um pouco mais isso.
Gate type
Temos quatro bits para o Gate Type, mas apenas 5 valores válidos.
Task Gate
O valor 0x5
indica que essa entrada é um Task Gate, usada quando há troca de contexto via hardware, usando portanto entradas TSS
no GDT. Nesse caso, o campo offset não é usado e deve estar zerado, pois não há função de tratamento da interrupção. Em vez disso, o estado do processador é salvo no TSS e o endereço da código interrompido é armazenado no campo Task Link no TSS.
Não estamos fazendo troca de contexto via hardware, então não usaremos esse valor.
Interrupt Gate
Os valores possíveis para esse tipo são 0x6
para um interrupt gate de 16 bits e 0xe
para um interrupt gate de 32 bits. Obviamente o que nos interessa é o valor 0xe
(1110b
). Interrupções de hardware usam esse tipo de gate, bem como as instruções de software int
. No caso de uma IRQ ou na instrução int
, o valor “informado” é o índice para essa entrada no IDT, e o processador carrega o seletor de segmento e o offset para a função que trata essa interrupção. É o tipo mais comum de interrupção. É importante destacar que esse tipo de gate desabilita as interrupções automaticamente e as reabilita quando o tratamento é encerrado, logo após a instrução iret
.
Trap Gate
Esse tipo de gate possui valores válidos de 0x7
para 16 bits e 0xf
para 32 bits. Novamente, nosso valor de interesse é o de 32 bits, então 0xf
. Esse tipo é pensado para ser usado no tratamento de exceções, por exemplo, uma divisão por zero.
Pois é, não sei se te ocorreu, mas há várias situações em que uma exceção pode ocorrer. Além da divisão por zero, por exemplo, há outros exemplos, como opcode inválido, ou um page fault, quando a memória virtual já está sendo usada, ou mesmo se você tentar usar as instruções in
, out
, lgdt
no Ring 3, por exemplo.. Em qualquer desses casos, a CPU interrompe a tarefa atual e procura um tratador adequado.
Note que, do ponto de vista de procedimento, Interrupt Gate e Trap Gate são virtualmente iguais. Inclusive, na sua entrada do IDT, você pode usar Trap Gate para tratar IRQs ou Interrupt Gate para tratar exceções. A diferença ainda não foi dita: na verdade uma Trap Gate não desabilita as interrupções e as reabilita ao final, com iret
.
As consequências são grandes, pense por um momento. Se você está tratando uma divisão por zero via Trap Gate, como as interrupções ainda estão habilitadas, pode chegar uma interrupção de teclado, por exemplo. Nesse caso, a função que está tratando a divisão por zero será interrompida para atender a interrupção de teclado. Se isso é desejável ou mesmo se isso pode acontecer depende de cada caso.
Você vai perceber que as primeiras 32 entradas do IDT são exceções, e por isso, são instaladas como Trap Gate. As demais serão Interrupt Gate.
Exceções
Exceções ocorrem quando o processador entra em um estado indefinido, seja por erros ou falhas, como por exemplo, a falta de página na memória. Exceções podem ou não informar um código de erro para o tratador saber o que aconteceu. As exceções podem ser divididas:
Falhas: se tratado adequadamente, essa exceção retorna para a mesma instrução que causou a falha (uma divisão por zero, talvez?). Nesse caso, a instrução que causou o problema pode ser reexecutada se o problema for corrigido. Exemplos são: Divisão por zero, TSS inválido, Falta de página, etc.
Traps: São causadas intencionalmente, e o retorno do tratamento dessa interrupção acontece na próxima instrução que originou o evento. Exemplos de traps são: Debug, Breakpoint e Overflow (mas um tipo bem específico que só acontece quando o Overflow Flag no registrador EFLAGS está setado, e a instrução
into
é executada). Obs: Não confundir o Trap Gate com uma exceção do tipo Trap. São coisas diferentes, apesar de relacionadas.Abortar: Esse tipo de exceção, em geral, é irrecuperável, e o processo que causou isso é morto sem grandes cerimônias. Um tipo de exceção que tem esse comportamento é a Dupla Falta. Imagine por exemplo que um determinado processo teve uma falha de página. Mas a função que trata falhas de página também não está na memória, causando uma nova falha de páginas novamente. Nesse caso, não há o que se fazer, já que nenhuma das falhas de páginas que ocorreram podem ser tratadas, e o processo é portanto encerrado.
A arquitetura x86 tem uma lista bem definida de exceções, e o número no vetor equivale a entrada no IDT conforme imagem abaixo:
Você ainda está falando de Gate Type no IDT? Show me the code!
Você é muito apressado, calma calabreso! Vamos começar definindo as definições (pleonasmo intencional).
No novo arquivo idt.h
, vamos criar a classe IDT
.
... #define PRESENT_BIT (1 << 7) #define DPL_RING_0 0x0 ... class IDT { uint16_t offset_bytes_0_1; uint16_t segment_selector_bytes_2_3{TYPE_CODE_SEG}; uint8_t reserved_byte_4{0}; uint8_t type_dpl_present_byte_5{PRESENT_BIT | ((DPL_RING_0 & 0x3) << 5)}; uint16_t offset_bytes_6_7; ... } __attribute__((__packed__)); ...
Essa classe representa aquela estrutura de 8 bytes já mostrada anteriormente. Perceba que já defino alguns valores de antemão: o seletor do segmento é igual a TYPE_CODE_SEG
(definido com o valor 0x8
no arquivo gdt.h
) e o byte de tipo de gate nós já setamos o bit Present, e também colocamos os bytes equivalentes ao Ring 0 na posição certa. O que mais falta então? O que vai mudar de entrada para entrada é o endereço da função tratadora (offset) e o tipo de gate. Isso é feito no médodo install_idt_entry
da classe IDT
.
... enum GATE_TYPE { INVALID_0, INVALID_1, INVALID_2, INVALID_3, INVALID_4, TASK_GATE, INT_GATE_16BIT, TRAP_GATE_16BIT, INVALID_8, INVALID_9, INVALID_A, INVALID_B, INVALID_C, INVALID_D, INT_GATE_32BIT, TRAP_GATE_32BIT }; class IDT { ... public: void install_idt_entry(void (*isr_entrypoint)(void), GATE_TYPE type) { this->offset_bytes_0_1 = (uint32_t)*isr_entrypoint & 0x0000ffff; this->offset_bytes_6_7 = (uint32_t)*isr_entrypoint >> 16; this->type_dpl_present_byte_5 |= type; ... }; } __attribute__((__packed__)); ...
Basicamente, quebramos o endereço da função isr_entrypoint
em duas partes, e as colocamos no lugar certo na entrada do IDT
. E fazemos o mesmo para o tipo de gate. Lembra que temos 4 bits para isso, mas nem todos os valores possíveis estão disponíveis? Por isso usamos essa enum
. E como o tipo de gate fica nos bits menos significativos do byte type_dpl_present_byte_5
, uma operação OR bitwise é suficiente (operador |=
).
E teremos também a Classe IDTPointer
, e tal qual o GDTPointer, armazena o endereço da tabela de entradas IDT e o tamanho.
... class IDTPointer { public: uint16_t size{0}; uint32_t entries_address{0}; IDTPointer(uint16_t size, int32_t address) : size(size), entries_address(address) {} } __attribute__((__packed__)); ... void install_idt(); ...
No código acima definimos ainda a função install_idt
que é chamada pelo kernel para instalação das entradas, nos mesmos moldes do GDT
. Vamos ver o que essa função faz, ela está no arquivo idt.cpp:
#include "include/idt.h" #include "include/exceptions.h" static IDT entries[256]; static IDTPointer idtp((uint16_t)(sizeof(entries) - 1), (uint32_t)&entries[0]); extern "C" void (*isr[])(void); extern "C" uint32_t isr_size; extern "C" void default_isr(void); void install_idt() { unsigned int i = 0; while (i < isr_size) { entries[i].install_idt_entry(isr[i], i < 32 ? TRAP_GATE_32BIT : INT_GATE_32BIT); i++; } while (i < TOTAL_ISR) { entries[i].install_idt_entry(&default_isr, INT_GATE_32BIT); i++; } asm("lidt %0" : : "m"(idtp)); }
Uma diferença notável do IDT
em relação ao GDT
é que a tabela do IDT
tem tamanho fixo, 256 entradas. Lembre-se que o GDT
tem tamanho variável a depender de quem o programa.
O que fazemos aqui é basicamente instalar as 256 entradas do IDT. Para algumas, temos tratadores definidos no vetor de ponteiros de função isr
(que não está aqui e será mostrado em um momento oportuno. A variável isr_size
também indica o tamanho desse vetor de ponteiros de função. Sabemos que as as primeiras entradas do IDT são exceções, conforme tabela, então, o tipo de gate é TRAP_GATE_32BIT
. O restante será interrupções de hardware, portanto INT_GATE_32BIT
.
Obs: A informação acima não é inteiramente verdade, há uma entrada no IDT
além das listadas do tipo Trap Gate: é justamente a entrada que trata syscalls. Quando ela for instalada, fazemos os ajustes necessários.
Depois das entradas conhecidas, preenchemos o restante das entradas com tratadores de interrupção que não faz nada, no caso, a função externa default_isr
.
Por fim, carregamos o registrador IDTR
e instalamos o IDT
usando a instrução especial, que só pode ser usada no Ring 0, lidt
.
E é isso, IDT instalado. Fácil né?
Ei, espera um minuto…

Muito suspeito aquelas declarações externas ali. Por que esse código não está ai junto? Qual é a pegadinha?
Então, de fato tem uma GRANDE pegadinha. Quando uma uma interrupção acontece, há alguns passos que o processador faz antes de realmente chamar a função que trata a interrupção (lembrando que isso vale para x86 vanilla. Isso é diferente para x86_64 ou quando há mudança de anel de proteção):
O processador empilha o registrador
eflags
O processador empilha o registrador
cs
O processador empilha o registrador
eip
(se é a instrução atual ou a próxima, depende da interrupção/exceção)Se houver um código de erro, o processador empilhará esse código.
O processador carrega o
cs:offset
da respectiva entrada noIDT
e pula para esse código.
Quando tem mudança do anel de proteção (por exemplo, do Ring 3 para Ring 0), antes de empilhar o registrador eflags
, o processador empilha os registradores ss
e esp
, nessa ordem. No nosso caso, o código está rodando no mesmo anel de proteção do kernel então os registradores ss
e esp
não são empilhados.
Assim que o processador começa a executar o código cs:offset
, devemos preservar o estado da tarefa anterior. Diferente da troca de contexto, o registrador eflags
já foi salvo, então é desnecessário usar a instrução pushf
. Mas salvar o restante dos registradores ainda é importante. Portanto as instruções pusha
/popa
ainda serão usadas. Por fim, a função retorna com iret
, e não ret
, como seria um código tradicional.
Perceba que com todos esses detalhes, é difícil usar C/C++ puro pois o código tentará criar os registros de ativação, retornar com ret
, etc. Até dá, mas acredite, o código assembly para tratar disso é mais legível que o código C/C++ equivalente.
Chega de conversa! Vamos então conferir o código no arquivo isr.asm
, mas por partes:
global isr, isr_size, default_isr extern isr_handlers ... %macro isr_no_error 1 isr%1: push 0 push %1 jmp prepare_isr %endmacro %macro isr_error 1 isr%1: push %1 jmp prepare_isr %endmacro ...
Primeiro, como há interrupções que empilham código de erro e outras não, criamos macros para uniformizar isso: quando o processador não empilha um código de erro, nós empilhamos um valor arbitrário (no caso, push 0
). Isso servirá para uniformizarmos o tratamento das interrupções, todas elas chegarão com a mesma estrutura de pilha ao tratador. Outra coisa que empilhamos é o número da interrupção: isso servirá para acessarmos um vetor de ponteiros de função com o tratador definitivo. Por exemplo, se a exceção for divisão por zero, ele empilhará zero. Se for Opcode Inválido, ele empilhará 6 e assim por diante. Depois disso ele pulará para o rótulo prepare_isr
.
Usamos essa macro assim: pegando como exemplo a divisão por zero. Ela é identificada pelo índice 0. Então fica:
isr_no_error 0x00 ; Division Error
Do ponto-e-vírgula para frente é comentário. Então isr_no_error 0x00
é traduzido diretamente para:
isr0x00: push 0 push 0x00 jmp prepare_isr
Sacou? A ideia é não fica repetindo código. Para definir as 32 exceções da tabela previamente apresentada, considerando se tem ou não código de erro, fica assim:
isr_no_error 0x00 ; Division Error isr_no_error 0x01 ; Debug isr_no_error 0x02 ; Non-maskable Interrupt isr_no_error 0x03 ; Breakpoint isr_no_error 0x04 ; Overflow isr_no_error 0x05 ; Bound Range Exceeded isr_no_error 0x06 ; Invalid Opcode isr_no_error 0x07 ; Device Not Available isr_error 0x08 ; Double Fault isr_no_error 0x09 ; Coprocessor Segment Overrun isr_error 0x10 ; Invalid TSS isr_error 0x11 ; Segment Not Present isr_error 0x12 ; Stack-Segment Fault isr_error 0x13 ; General Protection Fault isr_error 0x14 ; Page Fault isr_no_error 0x15 ; Reserved isr_no_error 0x16 ; x87 Floating-Point Exception isr_error 0x17 ; Alignment Check isr_no_error 0x18 ; Machine Check isr_no_error 0x19 ; SIMD Floating-Point Exception isr_no_error 0x20 ; Virtualization Exception isr_error 0x21 ; Control Protection Exception isr_no_error 0x22 ; Reserved isr_no_error 0x23 ; Reserved isr_no_error 0x24 ; Reserved isr_no_error 0x25 ; Reserved isr_no_error 0x26 ; Reserved isr_no_error 0x27 ; Reserved isr_no_error 0x28 ; Hypervisor Injection Exception isr_error 0x29 ; VMM Communication Exception isr_error 0x30 ; Security Exception isr_no_error 0x31 ; Reserved
De boa né? Entendendo a ideia da parada, fica mais fácil de entender o que está acontecendo. Agora vamos ver o rótulo prepare_isr
:
prepare_isr: pusha mov ebp, esp mov ebx, [ebp+32] ; isr_nr on the stack push dword [ebp+36] ; err_code to the handler call [isr_handlers + 4*ebx] add esp, 4 ; remove err_code off the stack popa add esp, 8 ; remove error code and isr_nr from stack iret
Começamos empilhando os registradores de propósito geral. Depois ajustamos o registrador ebp
para podermos acessar os parâmetros na pilha de uma forma inteligente.
Para prosseguirmos, precisamos entender como está o layout da pilha até esse momento. A imagem abaixo mostra isso.
Então, o que estamos fazendo afinal? Colocamos em ebx
o valor de isr_nr
que está na pilha, ele servirá de índice para acessar o tratador da interrupção correto (na chamada call [isr_handlers + 4*ebx]
). Antes, reempilhamos o valor err_code
(push dword [ebp+36]
) para o tratador, assim, todos os handlers podem ter a mesma assinatura: void isr_handler (int);
Você deve ter notado ainda que o isr_handlers
é definido como externo no arquivo isr.asm. Eu mostro esse depois, e esse não tem segredo, você vai ver. Mas vamos continuar a função prepare_isr
. Depois que a instrução call
retorna, nós removemos o err_code da pilha (add esp, 4)
, desempilhamos todos os registradores (popa
), e desempilhamos isr_nr
e err_code
(e o err_code
é desempilhado por nós, mesmo se foi o processador quem empilhou!). A instrução iret se encarrega de desempilhar os registradores eip
, cs
e eflags
.
Beleza, mas, se me lembro bem, o install_idt
usa uma variável isr
(e isr_size
)… você ainda não mostrou nada relacionado a ela! Ela é usada aqui no arquivo idt.cpp nesse trecho:
entries[i].install_idt_entry(isr[i], i < 32 ? TRAP_GATE_32BIT : INT_GATE_32BIT);
Com as macros definidas, é fácil definirmos a variável isr
no arquivo isr.asm
:
isr: dd isr0x00, isr0x01, isr0x02, isr0x03, isr0x04, isr0x05, isr0x06, isr0x07, isr0x08, isr0x09, dd isr0x10, isr0x11, isr0x12, isr0x13, isr0x14, isr0x15, isr0x16, isr0x17, isr0x18, isr0x19, dd isr0x20, isr0x21, isr0x22, isr0x23, isr0x24, isr0x25, isr0x26, isr0x27, isr0x28, isr0x29, dd isr0x30, isr0x31 isr_end: isr_size: dd (isr_end - isr)/DWORD_SIZE
Só isso. No fim das contas, como eu havia dito, é um vetor de ponteiros para função (ou algo similar a isso). São esses endereços que são efetivamente instalados no IDT.
Pode ter ficado confuso, é muito vai-e-volta, mas vamos exemplificar. Suponha que uma divisão por zero ocorreu. O processador vai fazer aqueles empilhamentos malucos que já explicitamos, e ele vai carregar o cs:offset
do IDT. E qual é esse valor? É 0x8:isr0x00.
A função empilha o err_code
(zero) e isr_nr
(também zero) e pula para prepare_isr
. Lá, os registradores são empilhados e ele chama a função definida no vetor isr_handlers
na posição zero.
Depois ele faz um clean-up e segue a vida.
Vamos ver o que tem em isr_handlers
então. Não poderia ser mais simples, está no arquivo isr_handlers.cpp
:
#include "include/exceptions.h" void (*isr_handlers[])(int) = { &division_error, &default_isr_handler, &default_isr_handler, &default_isr_handler, &default_isr_handler, &default_isr_handler, &invalid_opcode, &default_isr_handler, &default_isr_handler, &default_isr_handler, &default_isr_handler, &default_isr_handler, &default_isr_handler, &default_isr_handler, &default_isr_handler, &default_isr_handler, &default_isr_handler, &default_isr_handler, &default_isr_handler, &default_isr_handler, &default_isr_handler, &default_isr_handler, &default_isr_handler, &default_isr_handler, &default_isr_handler, &default_isr_handler, &default_isr_handler, &default_isr_handler, &default_isr_handler, &default_isr_handler, &default_isr_handler, &default_isr_handler};
Como eu disse, e eu espero que você tenha acreditado em mim, é literalmente um vetor de ponteiros de função, e agora sim, os tratadores de verdade das interrupções.
Para esse post, defini apenas 2 exceções: erro de divisão e opcode inválido. O restante usa um tratador genérico. E agora podemos usar C/C++! Primeiro, o arquivo exceptions.h
:
#ifndef __EXCEPTIONS_H #define __EXCEPTIONS_H void default_isr_handler(int); void division_error(int); void invalid_opcode(int); #endif
Funções com a mesma assinatura, hã? E o corpo dessas funções fica assim:
#include "include/exceptions.h" #include "include/util.h" void default_isr_handler(int err_code) { printk("default_isr_handler was called: err_code: %d\n", err_code); } void division_error(__attribute__((unused)) int err_code) { panic("Divisior error"); } void invalid_opcode(__attribute__((unused)) int err_code) { panic("Invalid Opcode"); }
O handler default só imprime o código de erro e retorna. As outras duas funções usam a função panic
, que é um printk
que entra em um loop infinito. Isso porque, você se recorda, essas exceções retornam para a mesma instrução que causou o problema, lembra? Não vou testar isso no contexto de tarefas, vou testar direto no kernel. Então, não há “processo” para derrubar. Se retornássemos como seria o fluxo normal, ele tentaria executar novamente as instruções problemáticas, e ficaria chamando para sempre os tratadores de interrupção. Não se preocupe, isso mudará depois.
Outro ponto de atenção é que para as funções division_error
e invalid_opcode
, não há razão para usarmos o parâmetro err_code. Por esse motivo, instruímos o compilador a não reclamar pelo fato de não o usarmos. Coisa besta, mas que deixa o código mais robusto e gera menos warnings na saída do compilador.
É isso?
Agora sim, isso é tudo. Pelo menos, da parte que queria apresentar nesse post. Você deve ter notado que não temos tratadores para as IRQ ainda. Fique calmo que logo elas vem.
Para testar isso, fiz dois testes. Logo após instalar o IDT
, causei uma divisão por zero:
... extern "C" void _start() { _init(); clear_screen(); install_gdt(); install_idt(); *(uint32_t *)(USER_ENTRY_POINT) = (uint32_t)&kernel_entry; volatile int a = 3; volatile int b = 0; printk("%d\n", a / b);
e o resultado é:
Para o opcode inválido:
... extern "C" void _start() { _init(); clear_screen(); install_gdt(); install_idt(); *(uint32_t *)(USER_ENTRY_POINT) = (uint32_t)&kernel_entry; ... asm("ud2 " : :);
o resultado é:
E é isso ai, tudo funcionando!
No próximo post pretendo tratar pelo menos da IRQ0, que trata das interrupções do timer do sistema. Essa é a principal ferramenta usada para fazer um kernel preemptivo, pois é um temporizador que de tempos em tempos interrompe o processador, e usamos isso para fazer a troca de contexto das tarefas.
Enquanto isso não ocorre, fique com o código desse post, que está disponível no meu github.
Queria agradecer às páginas OSDev e Skelix, em que baseei boa parte do conteúdo aqui apresentado. Em especial à OSDev, há muito conteúdo complementar relacionado que sugiro a leitura1 2 3 4 5 6:
https://wiki.osdev.org/Security#Rings
https://wiki.osdev.org/I_Can%27t_Get_Interrupts_Working#Problems_with_IDTs
https://wiki.osdev.org/Interrupt_Service_Routines
https://wiki.osdev.org/Interrupts_Tutorial
https://wiki.osdev.org/8259_PIC#Programming_the_PIC_chips
https://wiki.osdev.org/Interrupt_Descriptor_Table
Sensacional, desembargador!