A IDT
A Interrupt Descriptor Table (IDT) é a estrutura que uma CPU x86 consulta toda vez que uma interrupção ou exceção ocorre, mapeando cada um dos 256 números de vetor possíveis para o endereço do código que deve tratá-lo. Enquanto o real mode usa uma Interrupt Vector Table mais simples em uma localização fixa de memória, tanto o protected mode quanto o long mode usam a IDT no lugar, ganhando aplicação de privilégio por vetor e, sob long mode, um mecanismo adicional para garantir uma stack limpa nas falhas mais críticas.
Carregando a tabela
Seção intitulada “Carregando a tabela”A instrução LIDT carrega o registrador interno IDTR da CPU a partir de uma estrutura de 10 bytes na memória: um limite de 16 bits (o tamanho da tabela em bytes, menos um) seguido de um endereço-base de 64 bits sob long mode. Até que LIDT tenha sido executada com uma tabela válida, qualquer interrupção ou exceção que a CPU receba não tem para onde despachar definido, razão pela qual configurar uma IDT mínima é um dos primeiros passos na inicialização da maioria dos kernels, geralmente realizado antes mesmo de interrupções serem desmascaradas.
struct idtr { uint16_t limit; uint64_t base;} __attribute__((packed));
void load_idt(void *table, uint16_t entry_count) { struct idtr idtr = { .limit = entry_count * sizeof(struct idt_entry) - 1, .base = (uint64_t)table, }; asm volatile("lidt %0" : : "m"(idtr));}Formato do gate descriptor
Seção intitulada “Formato do gate descriptor”Cada uma das 256 entradas da tabela é um gate descriptor de 16 bytes sob long mode (8 bytes sob protected mode, sem a extensão de endereço superior):
struct idt_entry { uint16_t offset_low; uint16_t selector; uint8_t ist; // bits 0-2: índice na Interrupt Stack Table; resto reservado uint8_t type_attr; // tipo do gate, DPL, present bit uint16_t offset_mid; uint32_t offset_high; uint32_t reserved;} __attribute__((packed));O endereço do handler é dividido em três campos separados (offset_low, offset_mid e offset_high) em vez de armazenado de forma contígua, um artefato da ancestralidade do formato do descritor em protected mode ter sido estendido, em vez de redesenhado, quando o long mode adicionou os 32 bits superiores de endereço. selector identifica sob qual segmento de código na GDT o handler é executado, e type_attr empacota junto o tipo do gate, o Descriptor Privilege Level (o privilégio mínimo exigido para invocar esse vetor diretamente via software, em contraste com hardware), e um present bit que precisa estar definido, ou qualquer interrupção caindo naquele vetor imediatamente gera uma falha em vez de ser tratada.
Tipos de gate
Seção intitulada “Tipos de gate”O campo de tipo distingue um interrupt gate de um trap gate, diferindo em exatamente um aspecto: um interrupt gate limpa automaticamente a interrupt flag na entrada, desabilitando interrupções mascaráveis adicionais até que o handler explicitamente as reabilite ou retorne, enquanto um trap gate deixa a interrupt flag inalterada. Handlers de interrupção de hardware são convencionalmente instalados como interrupt gates especificamente para evitar entrega reentrante de uma segunda interrupção no meio do tratamento da primeira; handlers de exceção são mais frequentemente instalados como trap gates, já que uma exceção como um page fault frequentemente precisa que interrupções permaneçam habilitadas enquanto é executada. Um terceiro tipo, o task gate, existe para troca de tarefas assistida por hardware, mas praticamente não tem uso sob long mode, que removeu completamente o suporte de troca de tarefas por hardware.
A Interrupt Stack Table
Seção intitulada “A Interrupt Stack Table”O long mode adiciona um mecanismo ausente no protected mode: o campo ist de cada gate descriptor pode selecionar uma de sete stacks alternativas, definidas no Task State Segment atual, para a qual a CPU troca incondicionalmente ao entrar naquele vetor, independentemente de qual stack estava em uso antes. Isso existe especificamente para falhas que podem ocorrer quando a própria stack atual não é confiável (um double fault causado por uma stack corrompida ou esgotada, ou uma non-maskable interrupt chegando durante uma janela sensível a stack), onde continuar usando qualquer stack que estivesse ativa poderia transformar uma falha recuperável em uma irrecuperável. Um kernel tipicamente reserva o mecanismo IST para um pequeno punhado de vetores (comumente double fault e NMI) e deixa o campo ist zerado, significando “sem troca de stack”, para o resto.
Vetores de exceção reservados
Seção intitulada “Vetores de exceção reservados”Os vetores 0 a 31 são reservados pela arquitetura para exceções geradas pela CPU, em vez de disponíveis para uso geral de software ou interrupção de hardware, motivo específico pelo qual uma configuração de PIC ou APIC precisa remapear IRQs de hardware para vetores 32 em diante. Deixar interrupções de hardware em seu padrão não remapeado de vetores 0 a 15 colidiria diretamente com essa faixa reservada. Um punhado dos vetores reservados aparece constantemente durante o desenvolvimento de kernel:
| Vetor | Nome | Notas |
|---|---|---|
| 0 | Divide error | Divisão inteira por zero, ou um quociente que estoura o destino |
| 1 | Debug | Trap de single-step ou correspondência de breakpoint/watchpoint de registrador de depuração |
| 6 | Invalid opcode | Executar uma sequência de bytes que a CPU não reconhece como uma instrução válida |
| 7 | Device not available | CR0.TS definido e uma instrução de ponto flutuante/SIMD rodou; ver FXSAVE/XSAVE |
| 8 | Double fault | Uma exceção ocorreu enquanto a CPU tentava invocar um handler para uma exceção anterior |
| 13 | General protection fault | A categoria mais ampla: violações de segmento, violações de privilégio e muitas outras condições relatam através deste único vetor |
| 14 | Page fault | Ver Paging & Virtual Memory |
Códigos de erro
Seção intitulada “Códigos de erro”Algumas, mas não todas, as exceções reservadas empilham um código de erro adicional de 32 bits na stack abaixo do frame padrão de interrupção, e um handler precisa saber de antemão quais vetores fazem isso: não há como detectar sua presença em tempo de execução, já que um handler para um vetor que nunca empilha um e um handler para um que sempre empilha são simplesmente escritos de forma diferente, retirando (ou não) da stack um número fixo de bytes correspondente ao que aquele vetor específico é documentado como empilhando. Page fault e general protection fault ambos empilham um código de erro; divide error e invalid opcode não.
Notas de implementação
Seção intitulada “Notas de implementação”Um handler entrado através de um interrupt gate precisa sair com IRET, não um RET comum: o frame de interrupção que a CPU empilhou na entrada (instruction pointer, code segment, flags, e, se uma mudança de privilégio ocorreu, stack pointer e stack segment) tem um formato diferente do endereço de retorno de uma chamada normal, e IRET é a única instrução que o desempilha corretamente, incluindo restaurar o registrador de flags. Valores de seletor na GDT carregados em selector também precisam estar exatamente corretos; um seletor incorreto ou desatualizado, particularmente um deixado de antes da GDT ser recarregada em estágios anteriores do boot, é uma causa comum de um general protection fault disparando no instante em que a primeiríssima interrupção chega, o que então falha em despachar corretamente se a IDT ainda não estiver em um estado de tratar um GPF também.
Referências
Seção intitulada “Referências”- ^ Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A, Capítulo 6: define o formato da IDT, os tipos de gate e a lista completa de vetores de exceção reservados.
- ^ AMD, AMD64 Architecture Programmer’s Manual, Volume 2, Capítulo 8: a referência correspondente para long mode, incluindo a Interrupt Stack Table.
Ver também
Seção intitulada “Ver também”- PIC & APIC: para onde interrupções de hardware são roteadas antes de chegar a um vetor que esta tabela despacha.
- System Calls: um caminho disparado por software para dentro do kernel que também pode, dependendo do mecanismo, passar pela IDT.
- A Task State Segment: a estrutura na qual os ponteiros de Interrupt Stack Table acima são definidos.
- FXSAVE/XSAVE e Estado de FPU/SIMD: o mecanismo de troca preguiçosa que depende do vetor #NM acima.
- Registradores de Depuração e Breakpoints de Hardware: o que de fato configura o vetor #DB acima e lê de volta qual condição disparou.
- Panics de Kernel e Stack Backtraces: o que um tratador para um desses vetores comumente faz quando a exceção não tem recuperação sã alguma.