Controle fino.
Ideal para boot, registradores, interrupções e mudanças de privilégio.
mov cr3, rax
Marco 22 · Plataforma moderna concluída
Pelo BIOS, a partir de um setor de 512 bytes que constrói tudo do
zero, ou por firmware UEFI, que entrega o controle já em 64 bits. O
mesmo kernel.bin roda nos dois. O PIC de 1976 saiu, e
encerrar o sistema deixou de ser fechar a janela do emulador.
saída do SOIA
soia> mouse
mouse: x=1080 y=612 botoes=--- pacotes=8
soia> janelas
foco na janela 0 | trocas por clique: 1
soia> run fault.elf
Recusadas 14 de 14 | fronteira intacta
SOIA excecao: falha de pagina | vetor 14 | tarefa 4
soia> echo vivo apos a falha
vivo apos a falha
soia> run net.elf
DHCP 10.0.2.15/24: OK
gateway.soia → 10.0.2.2: OK
TCP SYN → SYN-ACK → ACK: OK
Ambiente seguro
O QEMU cria um laboratório isolado. O SOIA enxerga recursos virtuais; seu Windows e seu disco físico ficam fora do experimento.
O que entregamos
1 CPU virtual, perfil de 64 MiB ou 2 GiB, tela, teclado, COM1 e a imagem do projeto.
O que não entregamos
Seu HD/SSD físico, arquivos pessoais ou acesso direto ao Windows.
Seu ritmo
Você pode trocar de nível a qualquer momento. O assunto continua o mesmo.
Seção especial · C no SOIA
C++ é outra linguagem e ainda não foi habilitada. Aqui o Clang transforma C em instruções x86_64 que o SOIA consegue carregar.
C é uma forma mais curta e organizada de escrever regras. A CPU continua executando código de máquina; o compilador faz a tradução.
O alvo é x86_64-unknown-none-elf.
Não há Windows, libc, runtime hospedado ou símbolos externos.
Ideal para boot, registradores, interrupções e mudanças de privilégio.
mov cr3, rax
Ideal para strings, buffers, comandos, editores e protocolos.
soia_write("ola", 3);
Exigiria definir runtime, construtores, new/delete, exceções e RTTI.
etapa futura
libsoia é a ponte: organiza registradores e chama
INT 0x80. O aplicativo não toca no hardware.
.datavariável com valor gravado no ELF
.bssvariável que começa zerada
W^Xgravável ou executável; nunca ambos
Comandos, histórico, métricas e arquivos.
Ctrl+S persiste NOTE.TXT no disco virtual.
CPU, RAM, SOIAFS1, ATA, scheduler e rede.
DHCP, UDP, DNS e TCP por syscalls validadas.
Capacidade fixa, escrita e blocos livres.
Marco 11 · Interface gráfica
Antes, a VGA entregava uma grade pronta de caracteres. Agora o SOIA recebe uma tela vazia e decide a cor de cada ponto.
O estágio 2 seleciona VBE, copia a fonte BIOS 8 × 16 para
0x6000 e entrega ao kernel o endereço do framebuffer
XRGB8888 informado pelo ModeInfo.
SOIA Terminal 0.7 - rede e processos em ring 3 F1 Terminal | F2 Notas | F3 Monitor | F4 ultimo app soia> status Status do SOIA: CPU: 1 vCPU x86_64 RAM gerenciada: 2 GiB soia> _
O salto conceitual
Pense no framebuffer como papel quadriculado: para colorir um quadrado, o kernel encontra sua posição e grava o número da cor.
O pixel (x, y) fica em
framebuffer + y × 4.096 + x × 4. O valor
0x00RRGGBB contém diretamente os três canais.
Já funciona
1920 × 1080 em janela GTK 1:1,
terminal e notas C, foco por F1/F2/F3, texto 8 × 16, cursor,
Backspace e rolagem.
Custo medido
2,56× mais pixels e cerca de
1,83 MiB adicionais de memória de vídeo.
Marco 13.1 · Monitor do sistema
Terminal, notas e monitor são aplicativos C separados; o kernel conserva as chaves e fornece métricas validadas.
Três ELF executam em CPL 3. O scheduler usa round-robin, SLEEP e HLT; o TSC virtual separa trabalho de tempo ocioso.
A viagem de uma linha
cat até os bytes do disco.SOIA Terminal 0.7 - rede e processos em ring 3
F1 Terminal | F2 Notas | F3 Monitor | F4 ultimo app
soia> echo ola
ola
soia> ticks
ticks: 46
soia> _
Contrato INT 0x80
RAX escolhe o serviço; registradores carregam argumentos e o retorno.
O endereço também é conferido. O shell não pode apontar para qualquer lugar e mandar o kernel escrever.
Leitura: texto, dados, pilha ou heap concedido. Escrita: dados/BSS, pilha e heap RW+NX, nunca acima do break atual.
helpMapa do shellMostra os comandos disponíveis.
clearJanela limpaRedesenha a interface e reposiciona o cursor gráfico.
echo TEXTOTexto de voltaPreserva o trecho digitado depois do espaço.
ticksTempo do kernelConsulta quantos pulsos de 20 Hz ocorreram.
memRAM disponívelConsulta as páginas físicas livres de 4 KiB.
statusResumo honestoMostra CPU pelo TSC virtual, uptime, RAM, SOIAFS1, ATA e tarefas.
lsDiretório raizPercorre as entradas oferecidas pela VFS.
cat ARQUIVOArquivo realLê README.TXT, NOTE.TXT ou outro arquivo da raiz pelo nome.
psTabela de processosMostra PID, estado e nome das vagas ocupadas.
run CALC.ELFNovo processoCria a calculadora, aguarda sua saída e recolhe a memória.
run HEAP.ELFMemória dinâmicaTesta malloc, free, calloc e páginas sob demanda.
run IPC.ELFMensagem isoladaEnvia ping, recebe pong e prova que um handle antigo deixa de valer.
run NET.ELFRede útilMostra DHCP, UDP, DNS e um handshake TCP real no QEMU.
Marco 13.1 · telemetria protegida
SOIA Monitor 0.1
CPU virtual 4%
terminal 0%
notas 0%
monitor 3%
RAM usada 192 KiB
SOIAFS1 105.070 / 2.044.928 B
ATA leituras + escritas · 0 erros
rede RTL8139 DHCP 1 · UDP 2 · DNS 2 · TCP 2
Teste comportamental
O teste injeta comandos e percorre dez posições com as setas. O resultado só aparece se IRQ1, fila, tarefa, syscall, terminal, VFS e ATA cooperarem.
soia> ls
README.TXT
soia> cat readme.txt
Ola do SOIAFS: bytes persistentes no disco virtual.
Limite honesto
Capítulo zero · A base
Antes de estudar kernel, precisamos separar três perguntas: qual é o valor, onde ele está e o que ele significa.
01 + 02 · Bit e byte
Cada bit é uma escolha entre 0 e 1.
Ligue alguns interruptores; os pesos ligados são somados.
Um byte possui 2⁸ = 256 combinações. O bit mais à
direita vale 2⁰; o mais à esquerda vale
2⁷.
Um byte nem sempre viaja bit por bit. A COM1 usa start e stop; o ATA PIO entrega 16 bits por leitura.
A COM1 atual usa 38.400 baud em 8N1: 26,04 µs por bit e 260,42 µs por quadro. Offset localiza bytes já armazenados; não delimita o sinal.
Comparar tempo e enquadramento no PlaygroundPesos ligados: 64 + 16 + 4 + 1 = 85
O U só aparece porque ASCII interpreta
0x55 como texto. Em uma instrução, o mesmo byte teria
outro significado.
03 · Hexadecimal
Hexadecimal conta com dez números e seis letras. Cada casa vai de
0 a F; depois de 0F vem
10.
08090A0B
0C0D0E0F
vai um →10
00 até FF
256 valores possíveis · máximo decimal 255
01015
+
01015
= 0x55
04 · A rua da memória
A base é o número da porteira do terreno; o offset diz quantas casas andar dentro dele. Somando os dois, chegamos ao endereço.
Neste exemplo, segmentos estão zerados e ORG 0x7C00
corresponde à carga da BIOS. Em modo real, a regra geral é
segmento × 16 + deslocamento.
05 · Primeiro setor
A BIOS copia o primeiro setor para 0x7C00. Os
offsets começam em zero, então o último byte fica em
0x1FF, não em 0x200.
0x7C00
assinatura0x7DFE e 0x7DFF
55 AA não é o texto “55 AA”.
São dois valores binários usados pela BIOS como uma marca de setor
inicializável.
06 · CPU e significado
A CPU moderna inicia em um estado compatível com PCs antigos. Nosso carregador prepara gradualmente os ambientes de 32 e 64 bits antes de entregar o controle ao kernel.
Ela começa com regras compatíveis com o 8086 e precisa ser preparada para usar os modos modernos.
É principalmente o tamanho padrão de operandos e registradores. Instruções x86 têm tamanho variável.
O modo real usa segmento × 16 + deslocamento; a linha A20 acrescenta uma nuance no limite.
01010101
→
como número85
como texto ASCIIU
como opcode x86push bp/ebp/rbp
Próxima volta do ciclo
O laboratório mostra offset, hexadecimal, texto e instrução nos arquivos que realmente inicializam o SOIA.
Marcos 09 + 15 · Disco e persistência
O disco é o depósito, o driver busca caixas, o sistema de arquivos organiza e a VFS oferece um catálogo comum.
O kernel usa ATA PIO com LBA28, valida o SOIAFS1 v2,
consulta o bitmap e grava dados e metadados com flush.
Capacidade fixa
2.097.152 bytesdisco virtual de 2 MiB
A distinção que evita a confusão
Setor organiza bytes persistentes no disco. Endereço identifica onde cada byte será colocado na RAM durante a execução.
56 × 512 = 28.672 B
mouse + fatias do desktop
kernel.bin · 49.152 B
Os 26 KiB já usados deixavam apenas 2,3 KiB livres. Os quarenta setores novos cabem o mouse e abrem espaço para eventos, janelas e composição. O último setor continua preenchido até fechar sua caixa de 512 bytes.
0x100000x1BFFFinício fixo49.152 bytes consecutivosnovo fim
O começo continua em 0x10000. Apenas o fim passa
de 0x16FFF para 0x1BFFF. O teto é
0x20000, onde começa a IDT: por isso o limite de
128 setores. Aqui cada endereço nomeia um byte; o kernel também
gerencia a RAM em páginas de 4 KiB.
O disco ganhou quatro caixas de kernel; na RAM, o mesmo terreno inicial recebeu 6.144 bytes a mais no final.
A ficha que permite reconhecer e validar o volume antes de confiar nele.
0x000x080x0C0x100x140x30Entradas associam nomes aos dados; um bit marca cada setor livre ou ocupado.
O transporte
O kernel informa a caixa, espera o disco e transporta um setor entre a imagem e a RAM.
0x20 + REP INSW leem; 0x30
+ REP OUTSW escrevem; 0xE7 solicita flush.
A abstração
A VFS recebe um nome. O driver do formato localiza a entrada; o driver de bloco busca os bytes.
vfs_read_file("README.TXT")
└─ soiafs_lookup()
└─ ata_read_sector(40)
Evidência no QEMU
O teste salva NOTE.TXT, fecha o primeiro QEMU e
exige o texto ao iniciar outro com a mesma imagem.
“NOTE.TXT carregado do disco virtual.”
Gravável, com uma fronteira pequena e explícita.
Somente NOTE.TXT, com até 2.047 bytes, pode ser salvo.
A imagem limpa começa com 216 usados · 3.778 livres no
volume. Ainda não existem exclusão, diretórios, journal ou recuperação.
Fundamentos do núcleo
Estes marcos sustentam o terminal atual. O seletor evita repetir toda a história na página; a documentação Markdown conserva o desenvolvimento completo.
A syscall atual pertence ao Marco 10; aqui, Processos explica apenas o isolamento e a troca de tarefas que a tornam possível.
Marco 08 · Isolamento
Cada programa vive em seu próprio apartamento. O número da sala pode ser igual; a planta escolhida pela CPU leva a lugares físicos diferentes.
Cada tarefa possui CR3, tabelas, código e pilhas próprios. PTEs de usuário usam U/S=1; as folhas do kernel permanecem U/S=0.
Autoridade em camadas
A CPU impede que programas comuns executem instruções privilegiadas ou escrevam nas páginas do kernel.
Tradução por tarefa
RIP 0x100000000
O mesmo número não significa o mesmo lugar depois da tradução.
Preempção a 20 Hz
Isolamento, CR3 e scheduler ficam aqui.
O contrato atual de INT 0x80, suas vinte e oito syscalls e a
prova interativa pertencem ao Terminal.
Marco 07 · Gerenciamento
A BIOS entrega uma planta. O kernel abre caminhos somente até a memória existente e distribui quartos de 4 KiB sem colisões.
E820 descreve intervalos físicos; 1.024 PDEs de 2 MiB ampliam o mapa e um bitmap controla 524.288 quadros de 4 KiB.
Mapa recebido da BIOS
Esquema didático: o mapa real contém várias entradas e pode ter pequenos “buracos” reservados.
quais regiões existem?
onde está o mapa?
até 2 GiB identidade
livre ou ocupada?
Dois tipos de endereço
0x020000000x02000000Hoje os números são iguais: isso é um mapeamento identidade. A tradução ainda existe.
Um bit por página
1 não pode ser entregue; 0 está livre para alocação.
Autoteste real no boot
A máquina ganhou campainhas
Em vez de perguntar sem parar se algo aconteceu, o kernel dorme. Timer e teclado tocam a campainha quando precisam de atenção.
IRQ0 e IRQ1 passam pelo PIC remapeado, chegam aos vetores
0x20 e 0x21 da IDT e retornam com
IRETQ.
IDT na prática
base 0x20000 · limite 4095
Dentro de uma entrada
0x8E
O endereço da rotina é repartido em três pedaços porque o formato da porta evoluiu junto com a família x86.
O espaço de 8 bits
O vetor é apenas um número de 8 bits usado como índice:
0 a 255.
Viagem completa da tecla A
Tecladolevanta IRQ 1
PICentrega vetor 0x21
CPUsalva RIP, CS e RFLAGS
IDT[33]fornece o endereço da ISR
ISRlê 0x60 e envia EOI
IRETQrestaura e continua
IDT, vetores e a viagem da tecla ficam aqui. A fila de teclado, edição de linha e interpretação do comando estão reunidas no Terminal.
Detalhes do Marco 6A primeira entrega
O bootloader encontra o kernel, prepara a máquina, entrega uma ficha com o ambiente disponível e sai de cena.
O estágio 2 lê 96 setores por INT 13h/AH=42h em
0x10000, obtém o mapa E820, prepara VBE, entra em
long mode e salta com RAX=SOIABOOT e
RDI=&boot_info.
O segundo estágio busca o novo programa nos setores seguintes.
INT 13h/AH=42h lê os LBAs 6–101 em 1000:0000.
O carregador monta o ambiente antes de chamar o núcleo.
A20, GDT, paginação e long mode permanecem responsabilidades do bootloader.
Uma ficha informa memória, tela, serial e endereço do kernel.
A versão 4 descreve VBE, framebuffer, pitch, profundidade, fonte e conserva o mapa E820.
Depois de conferir a ficha, ele limpa a tela e usa seus próprios recursos.
A entrada em 0x10000 valida RAX/RDI e não retorna ao bootloader.
Disco, RAM e CPU
soia.img contém carregador, kernel e SOIAFS1. Código é
carregado na RAM; o volume continua no disco até o kernel pedir
seus setores.
O arquivo soia.img reúne quatro blocos persistentes
que precisam existir antes de a máquina ligar.
O kernel abre o conteúdo que ficou depois dele no disco.
O carregador copia o kernel e cria estruturas temporárias em endereços distintos do computador virtual:
0x1000PML40x2000PDPT0x3000Diretório0x5000Mapa E8200x6000Fonte BIOS 8 × 160x7000VBE ModeInfo0x8000Estágio 20x10000Kernel0x20000IDT0x30000Bitmap · 64 KiB0x100000000Tarefas0x90000PilhaVBEFramebuffer MMIOBootloader e kernel coexistem na RAM, mas têm funções separadas.
A CPU continua em 64 bits; o salto para 0x10000
entrega a execução a outro binário.
O disco guarda código e arquivos. A RAM recebe
o código no boot e os setores do volume quando o kernel os lê.
A CPU troca o dono da execução no salto para 0x10000.
Laboratório hexadecimal
Arquivos .bin não são texto: cada posição contém um
número de 0 a 255. Aqui você pode abrir os bytes reais do SOIA sem
precisar saber binário previamente.
A unidade
FA vira oito bits?
Um dígito hexadecimal representa quatro bits. Por isso dois
dígitos, de 00 a FF, representam um byte.
Seu primeiro comando
Format-Hex.00000000
FA 31 C0 8E D8 8E C0 8E
.1......
1Offsetposição dentro do arquivo
2Bytesvalores em hexadecimal
3Textotentativa de ler como caracteres
Um ponto significa “este byte não forma um caractere imprimível”. Isso não é erro: provavelmente é instrução ou dado.
Seu segundo comando
ndisasm.00007C00
FA
cli
1Endereçoonde a CPU vê a instrução
2Byteo número armazenado
3Assemblytradução para humanos
Nesse contexto, FA significa cli:
desligar interrupções enquanto a pilha é preparada.
Bytes reais do build
Compile o projeto com .\scripts\Build.ps1 para gerar
os dados deste visualizador.
Marcos 14 + 19 · Rede básica e útil
O kernel encontra a placa, negocia DHCP, resolve um nome e completa
TCP; NET.ELF vê resultados, nunca portas ou buffers DMA.
Analogia
Ethernet é o envelope; DHCP entrega a configuração; DNS acha um endereço; TCP confirma que os dois lados podem conversar.
Negociação real
O endereço não é mais constante: DHCP entrega
10.0.2.15/24, gateway .2 e DNS .3.
UDP 68 → 67. O cliente transmite DISCOVER e REQUEST em broadcast.
Opções 1, 3 e 6. Máscara, gateway e DNS vêm do ACK aceito.
Validação. XID, magic cookie, portas, tipo DHCP e checksum IPv4 precisam coincidir.
Nome seguro e reproduzível
gateway.soia vira 10.0.2.2.A syscall 23 copia até 63 bytes para o kernel. O registro local usa o gateway recebido; o cliente por UDP continua disponível para consultas A externas.
Determinístico. O teste não depende de internet nem do resolver do host.
Formato real. O caminho por fio monta QNAME, valida a resposta e percorre nomes comprimidos.
Limite honesto. A prova automática não afirma resolução externa quando o sandbox bloqueia a resposta.
Handshake mínimo
guestfwd liga 10.0.2.100:8080 a um listener temporário em localhost. A syscall 24 repete a prova para NET.ELF.
Ainda não existem sockets genéricos, payload TCP, retransmissão, FIN, HTTP, TLS ou IPv6.
No fio virtual
qemu-network.pcap
ARP 6 quadros
IPv4 14 quadros
DHCP 4 mensagens
TCP 2 handshakes
Na userland
run NET.ELF.DHCP + UDP OK
gateway.soia OK
TCP :8080 OK
memória devolvida OK
Marco 16 · processos dinâmicos
O kernel prepara um espaço isolado para o programa e limpa tudo quando ele sai. As syscalls 15–18 implementam spawn, exit, wait e leitura validada da tabela de processos.
Experimento útil
run CALC.ELFA calculadora aceita inteiros, + - * /, trata divisão por zero e encerra com sair.
Ciclo do pai. spawn + wait cria o filho e bloqueia o terminal sem polling.
Tabela de oito. Três vagas residentes e cinco vagas dinâmicas.
Estados reais. Livre, pronto, dormindo, aguardando e finalizado.
Sem vazamento. O teste exige a mesma contagem de páginas antes e depois.
soia> ps
PID ESTADO PROGRAMA
1 executando TERMINAL.ELF
2 dormindo NOTEPAD.ELF
3 dormindo MONITOR.ELF
soia> run calc.elf
Processo iniciado; PID 4.
calc> -7 + 2
resultado: -5
exit primeiro marca o filho como zombie, pois sua pilha
de kernel ainda está em uso. Depois da troca de contexto, o pai
recolhe CR3, sete páginas de tabelas, oito de userland e uma pilha
de kernel. O mesmo loader valida CALC.ELF e continua
rejeitando deliberadamente BROKEN.ELF.
Marco 17 · heap + libc mínima
O aplicativo ganha uma bancada privada, reaproveita espaços livres e devolve tudo ao fechar. A syscall 19 mapeia até 16 páginas RW+NX no PT de usuário; first-fit administra blocos alinhados em 16 bytes.
Experimento verificado
run HEAP.ELFTrês alocações atravessam duas páginas, um bloco liberado volta no mesmo endereço e a coleta encerra sem vazamento.
Base fixa. O heap começa vazio em 0x100008000.
Limite honesto. São no máximo 16 páginas, ou 64 KiB por processo.
W^X preservado. Dados dinâmicos são graváveis e nunca executáveis.
Camada essencial
Pequenos blocos vizinhos são unidos. As páginas continuam no processo para evitar trabalho repetido e voltam ao kernel no exit.
Camada técnica
base 0x100008000
break base + 6.288 B
mapa 2 × 4 KiB
limite 0x100018000
soia> run heap.elf
primeiro bloco: 0x0000000100008020
buffer dinamico via WRITE: buffer do heap aceito pela syscall WRITE
bloco liberado reutilizado no mesmo endereco: OK
calloc zerou 128 bytes: OK
paginas fisicas mapeadas sob demanda: 2
Memoria devolvida: OK.
Se uma alocação física falhar, BRK desfaz somente as PTEs
criadas naquela chamada. INVLPG invalida o TLB e a
validação das outras syscalls usa o break atual como limite.
Marco 18 · IPC + serviços
Cada processo tem uma caixa postal privada. O kernel confere o destinatário, copia a mensagem e acorda quem estava esperando. Cada tarefa possui uma fila circular de duas mensagens de até 48 bytes; handles codificam geração e PID para rejeitar identidades obsoletas.
Experimento verificado
run IPC.ELFO terminal envia ping, bloqueia sem consumir CPU e volta a executar quando o filho responde pong.
Identidade temporal. O handle combina geração e PID; reutilizar a vaga não ressuscita uma referência antiga.
Cópia controlada. O remetente não recebe acesso ao CR3 nem aos ponteiros do destinatário.
Espera eficiente. RECEIVE vazio muda a tarefa para RECEIVING; o timer executa outra tarefa.
Camada essencial
O PID diz qual vaga procurar; a geração confirma que ainda é a mesma vida do processo. Ao recolher o filho, o kernel troca a geração.
Camada técnica
handle = geração << 8 | PID
fila = 2 mensagens
dados = até 48 bytes
struct = 64 bytes
soia> run ipc.elf
Processo iniciado; PID 4; handle 772.
ping enviado ao filho: OK
RECEIVE bloqueou sem consumir CPU: OK
pong recebido do filho: OK
conteudo da resposta: OK
handle do remetente: OK
handle antigo rejeitado: OK
Memoria devolvida: OK.
As syscalls 20, 21 e 22 abrem um handle, enviam e recebem. Ao entregar
diretamente para um receptor bloqueado, o kernel desabilita interrupções,
troca temporariamente para o CR3 do receptor, valida e copia o resultado,
grava o retorno salvo em RAX e restaura o CR3 do remetente.
Ainda não existem nomes de serviço, permissões por handle nem mensagens maiores.
Marco 20 · Desktop concluído
O mouse nunca diz onde está: diz apenas quanto andou. Quem guarda a posição é o kernel, e é ele que desenha a seta em uma cópia da tela. Agora os três programas dividem essa tela e trabalham ao mesmo tempo. O dispositivo auxiliar do 8042 envia pacotes de três bytes pela IRQ12; o kernel estende o sinal de nove bits, aplica limites, compõe o cursor no front buffer e entrega estado, eventos e geometria de janela pelas syscalls 25, 26 e 27.
Experimento verificado
mouseSeis deslocamentos de 20 × 12 no monitor do QEMU tiram o cursor do centro e o comando confirma a posição exata.
Sincronização. O bit 3 do primeiro byte é sempre 1. Sem essa checagem, um byte perdido desalinha o fluxo para sempre.
Sinal de nove bits. 0xFF com o bit de sinal ligado vale −1, não 255; sem estender, a seta salta ao andar para a esquerda.
Eixo invertido. No PS/2 o Y cresce para cima; no framebuffer, para baixo. O kernel subtrai onde o mouse soma.
Camada essencial
Os aplicativos desenham em uma cópia da tela guardada na RAM. O cursor nem existe lá: ele é pintado só na hora de copiar a cópia para a tela de verdade. Assim a seta nunca suja o que está embaixo dela.
Camada técnica
byte 0 = YO XO YS XS 1 BM BR BL
byte 1 = deslocamento X
byte 2 = deslocamento Y
cursor = 8 × 12 px, 2 bits/px
soia> mouse
mouse: x=1080 y=612 botoes=--- pacotes=8
soia> eventos
mover x=1080 y=612 botoes=0 tick=64
apertar x=1080 y=612 botoes=1 tick=65
soltar x=1080 y=612 botoes=0 tick=67
eventos drenados: 8 | descartados: 0
soia> janelas
N POSICAO TAMANHO GRADE DONO FOCO ESCRITOS
0 24,44 928x1012 112x58 1 sim 1459
1 968,720 928x336 112x16 2 - 196
2 968,44 928x660 112x36 3 - 8896
foco na janela 0 | trocas por clique: 1
Caixa de dano. Copiar os 8,29 MiB da tela inteira a cada quadro seria 166 MB/s por software. O kernel amplia um retângulo com o que mudou e copia só ele: 732 KiB por quadro, onze vezes menos.
Publicar na hora do movimento. O mouse relata a 100 Hz e o relógio bate a 20 Hz. Publicar só no relógio engolia quatro em cada cinco pacotes e a seta pulava. Agora a IRQ12 também publica, e a caixa de dano faz cada publicação extra custar 384 bytes.
Fila de eventos. Trinta e duas vagas em anel. Cheia, o kernel descarta o novo e conta o descarte, em vez de sobrescrever um evento que ninguém leu ainda.
Escrever deixou de exigir foco. A syscall WRITE recusava quem não estava na frente. Agora ela aponta o desenho para a janela de quem chamou, e as três ficam vivas.
Fatia 4 · o gerenciador de janelas
O terminal do SOIA nunca guardou texto: ele escreve pixels e rola por cópia de pixels. Isso parecia uma limitação e virou vantagem. O conteúdo de cada janela já mora no back buffer, na região dela, então não foi preciso inventar um buffer de caracteres por janela. Bastou reapontar quatro variáveis de origem e grade a cada escrita.
Mosaico, não sobreposição
As três janelas dividem a tela sem se cruzar, e é justamente isso que impede uma tarefa de pintar pixel de outra. Janelas que se cobrem exigiriam uma superfície por janela e um compositor empilhando em ordem z: mais 2 MiB por janela e uma cópia extra por quadro. É a próxima dívida, não uma que esta fatia tenha pago.
Camada essencial
Antes, F1, F2 e F3 escolhiam qual programa ocupava a tela inteira; os outros dois congelavam. Agora os três aparecem sempre, e o clique escolhe apenas para onde vai a tecla. A distinção entre "estar visível" e "ter o teclado" só existe quando há mais de uma janela viva.
Camada técnica
Com três tarefas escrevendo, a saída serial virou uma trança de textos. A serial passou a espelhar só a janela em foco, como faria um console físico. Isso quebrou a verificação: o texto das janelas de fundo não passa mais por lá. A resposta foi um contador de caracteres por janela, exposto pela syscall 27.
A prova da fatia está nesse contador. Na primeira leitura do teste,
feita antes de qualquer F3, a janela do monitor já
acumulou 8.896 caracteres sem nunca ter recebido o foco. Nenhuma
outra explicação cabe: uma tarefa de fundo desenhou na própria
janela. O teste ainda confere, na captura do framebuffer, que as três
áreas de console têm pixels de texto no mesmo instante.
A IRQ12 nasce no PIC escravo: a linha 2 do mestre, que é a cascata,
também sai da máscara, e o fim de interrupção vai para as portas
0xA0 e 0x20. Pacote com bit de estouro é
descartado inteiro, porque a magnitude real já se perdeu. O kernel
passou de 56 para 96 setores neste marco, empurrando o SOIAFS1 para o
LBA 102. As portas do vetor 0x2C e do IRQ0 têm atributo
0x8E, que zera o IF na entrada: por isso timer e mouse
nunca entram um no meio da publicação do outro, e nem a fila de
eventos nem o hit test do clique precisam de trava.
O que o teste não pega
O SOIA desenhava um X vermelho na barra de título, em uma coordenada literal da época de 1024 × 768. A 1920 ele caía no meio da barra, e nunca houve clique ligado a ele. Somado ao X real da janela do QEMU, eram dois botões de fechar, um deles falso. Ele volta na fatia 4, quando existir janela para fechar.
Relativo contra absoluto
O PS/2 informa deslocamento; um tablet USB informaria coordenada. Com dispositivo relativo existem dois ponteiros independentes, o do Windows e o do SOIA, que nunca se reconciliam. Sem pilha USB, a saída é pedir ao QEMU que prenda o ponteiro na janela com grab-on-hover=on. Ctrl+Alt+G solta.
Marco 21 · Robustez
Até aqui, um erro em qualquer programa congelava a máquina inteira. Agora o programa que erra morre sozinho, a janela dele mostra o motivo, e o terminal aceita o próximo comando no segundo seguinte. Os 32 vetores de exceção ganharam stubs próprios que uniformizam o quadro; o RPL do CS salvo decide entre encerrar a tarefa e parar o kernel de forma declarada.
Experimento verificado
run FAULT.ELFUm programa escrito para quebrar de propósito, logo depois de provar que a fronteira das syscalls recusa catorze argumentos hostis sem se abalar.
soia> run fault.elf
Recusadas 14 de 14 | fronteira intacta
SOIA excecao: falha de pagina | vetor 14 | codigo 0x0000000000000007 | RIP 0x0000000100001072 | CR2 0x0000000000000000 | tarefa 4.
SOIA robustez: tarefa encerrada pela excecao; as demais seguem executando.
Processo encerrado por falha; nao chegou a chamar exit. Memoria devolvida: OK.
soia> echo vivo apos a falha
vivo apos a falha
O que provava nada. Antes, os 256 vetores apontavam para o mesmo manipulador: cli, uma mensagem genérica e hlt eterno. Toda exceção terminava igual, e não dava para saber qual tinha acontecido.
Um zero que uniformiza. Dez exceções empurram código de erro e vinte e duas não. O stub das que não têm empurra um zero no lugar, e o tratador passa a ter um caminho de leitura só.
A última linha é a prova. Não é a ausência de travamento que fecha o marco: é o shell respondendo um comando novo depois da falha.
Camada essencial
Um prédio sem porta corta-fogo queima inteiro quando um apartamento pega fogo. O privilégio guardado no seletor de código é a porta: valor 3 significa que quem errou foi o programa, e só o apartamento dele precisa ser isolado.
Camada técnica
mov rax, [rsp + EXCEPTION_CS_OFFSET]
and eax, 0x03
cmp eax, 0x03
jne exception_kernel_panic_64
Fatia 3 · Camada essencial
Para contar que houve um acidente, a CPU precisa de um papel onde anotar. Ela anota na pilha. Quando o problema é a pilha, não há onde anotar, e a máquina reinicia sem dizer nada. A IST é um papel guardado em outro lugar, reservado só para essa hora.
Fatia 3 · Camada técnica
RSP antes de empilhar.lea rax, [double_fault_stack_top]
mov [tss64 + TSS_IST1_OFFSET], rax
mov byte [abs IDT_BASE + (8 * 16) + 4], 1
Duas metades distantes: a porta do vetor 8 guarda o índice, o TSS guarda o ponteiro. Ter uma sem a outra não serve de nada, e é por isso que a prova serial sai depois do TSS.
Double fault nunca devolve o controle. Ele contraria a regra do RPL: mesmo com CS de ring 3, o vetor 8 vai para parada declarada. Chegar nele significa que o quadro da falha original se perdeu, e encerrar só a tarefa daria a impressão de um sistema saudável que esqueceu o acidente.
Duas verdades no mesmo endereço. A fatia encontrou um bug que não era dela: PAGE_DIRECTORY_APIC, do Marco 22, e FILESYSTEM_SUPERBLOCK_BUFFER, do Marco 9, apontavam ambos para 0x00041000. Montar o page directory apagava o diretório lido do disco; gravar um arquivo apagava o mapeamento do LAPIC. O sintoma foi o bloco de notas recusando salvar em disco novo, sem nenhuma pista de memória.
Abaixo de 1 MiB nada é alocado. Todo endereço nessa faixa é reserva manual por constante, sem verificação nenhuma: o assembler aceita, o boot passa, e o dano aparece longe da causa.
Fatia 3 · Ctrl+C
Até aqui todo encerramento vinha de um erro. Ctrl+C é o primeiro caminho em que ninguém errou: o programa está funcionando, esperando entrada, e você simplesmente não quer mais. Por isso ele tem código de saída próprio, -7, e o terminal diz uma frase diferente da frase de falha.
Fatia 3 · Quem executa o pedido
O teclado empilha seis registradores, não o quadro completo que o escalonador precisa para recarregar RSP. Encerrar a tarefa ali quebraria o despacho, então a tecla só deixa o alvo anotado e o tick seguinte faz o trabalho. A latência máxima é um pulso de 50 ms.
calc>
[SOIA] processo encerrado: Ctrl+C do teclado.
Processo encerrado por Ctrl+C; o programa nao errou.
soia> ps
Só processo dinâmico. As três vagas estáticas fazem parte do contrato do boot; sem terminal, notas e monitor o sistema ficaria sem interface. Na janela de uma delas a tecla não faz nada, de propósito.
Uma frase plausível e falsa, evitada. A rotina que escreve na janela montava o texto com o nome da última exceção. Chamada pelo Ctrl+C, ela usaria a falha anterior, possivelmente de outra tarefa, e diria "encerrado: falha de pagina" sobre um processo que ninguém quebrou. Agora ela aceita um motivo explícito.
Encerrar a tarefa exige acordar o pai que estava bloqueado em
WAIT: um filho morto por exceção nunca chega a chamar
EXIT, e sem isso o terminal esperaria para sempre por um
processo que já não existe. O quadro de exceção na pilha do kernel é
simplesmente abandonado, porque o despacho recarrega
RSP a partir da tarefa escolhida. Vaga estática não
volta para o pool: ela não tem pai para coletá-la, e a contagem de
tarefas residentes faz parte do contrato do boot.
Sem tratador, o que acontecia
Uma exceção que a CPU não consegue entregar vira double fault; sem tratador para esse, vira triple fault, e o processador reinicia. De fora, os três casos pareciam idênticos: a janela sumia. Nomear a falha é a diferença entre depurar e adivinhar.
Um bug que o marco encontrou
O anúncio de cada programa escolhia a frase pela primeira letra do nome. FAULT.ELF começa com F, não batia com nenhum caso e era anunciado como CALC.ELF. Agora o nome vem da tabela de tarefas.
Marco 22 · Plataforma moderna
Todos os marcos anteriores acrescentaram peças. Este remove nove: com firmware UEFI, o computador já acorda em 64 bits, e metade do trabalho de boot simplesmente deixa de existir. O firmware carrega um PE32+ de uma partição FAT e entrega o controle em long mode com paginação ativa; GOP substitui o VBE, GetMemoryMap substitui o E820 e o RSDP chega pela tabela de configuração.
Experimento verificado
Run-QemuUefi.ps1O mesmo SOIA, acordado por um firmware de 3,6 MB em vez de um setor de 512 bytes. Os dois caminhos convivem e nenhum arquivo é compartilhado.
O que o BIOS precisa fazer
Setor de 512 bytes terminando em 0x55AA, segundo estágio porque 512 bytes não bastam, modo real de 16 bits, linha A20, GDT, CR0.PE, INT 13h para ler o disco, INT 15h/E820 para a memória e INT 10h para o vídeo.
O que o UEFI já entrega
A CPU já está em long mode de 64 bits, com paginação ligada e pilha válida, antes da primeira instrução do SOIA. O firmware leu o arquivo de uma partição FAT e chamou uma função C.
SOIA UEFI: firmware entregou o controle em long mode.
SOIA UEFI: sem MBR, sem modo real, sem A20, sem INT 13h.
SOIA UEFI: firmware oferece 30 modos de video.
SOIA UEFI: modo 22 selecionado para 1920x1080.
SOIA UEFI: GOP em 1920x1080, pitch 1920 pixels, base 0x0000000080000000.
SOIA UEFI: formato de pixel BGRX8888 compativel com o desenho atual, framebuffer de 8294400 bytes.
SOIA UEFI: mapa de memoria com 102 entradas de 48 bytes; 1998 MiB utilizaveis.
SOIA UEFI: RSDP da ACPI 2.0 em 0x000000007F77E014.
SOIA UEFI: kernel.bin lido da particao para 0x0000000000010000; 49152 bytes.
SOIA UEFI: fonte 8x16 copiada para 0x6000.
SOIA UEFI: 4 GiB mapeados por identidade em paginas de 2 MiB.
SOIA UEFI: contrato versao 4 pronto com 5 regioes de memoria; entregando o controle ao kernel.
SOIA kernel 64-bit: controle recebido!
SOIA kernel: contrato de boot validado.
8.294.400. É o mesmo tamanho de framebuffer do caminho BIOS, no mesmo formato BGRX8888. O kernel vai desenhar ali sem uma linha de conversão.
O modo não tem número. No BIOS a resolução vinha de 0x0192, tirado da tabela do vgabios. No GOP os modos são enumerados e escolhidos pela dimensão: no QEMU o 1920 × 1080 é o modo 22, e esse número não está escrito em lugar nenhum do código.
Outro formato de binário. O firmware não executa binário cru: só PE32+, o mesmo de um .exe. Clang e lld-link já sabem produzir isso, então nenhuma ferramenta nova entrou no projeto.
Rodar e testar são a mesma máquina. Quando o kernel passou a ser carregado pelo UEFI, a placa de rede e o disco entraram no script de teste e ficaram de fora do script de execução. A bateria passava verde e o comando do dia a dia morria com a tela preta, porque o kernel para sem rede e nada chega a ser publicado no framebuffer. Duas listas descrevendo o mesmo computador sempre divergem: hoje o hardware virtual mora em QemuUefiHardware.ps1 e cada script só acrescenta a saída.
Camada essencial
O boot BIOS é o material dos marcos 1 a 4. Apagá-lo apagaria a explicação de como um computador acorda do nada. Manter os dois transforma o marco em comparação: a mesma máquina, dois jeitos de ligar.
Camada técnica
A ABI do UEFI passa argumentos em RCX/RDX/R8/R9, e o alvo errado no compilador só falha na primeira chamada ao firmware. As tabelas são vetores de ponteiros, onde um campo faltando desloca todos os seguintes. E o mapa de memória avança por descriptor_size, nunca por sizeof: o OVMF publica 48 bytes contra os 40 da estrutura.
O que a primeira execução ensinou
O carregador terminava com sucesso e o firmware seguia para a próxima entrada da lista de boot: o menu de configuração do OVMF, que limpava a tela. Do lado de fora, parecia que o SOIA não tinha feito nada. Um carregador de verdade nunca retorna: ele chama ExitBootServices, toma posse da máquina e salta para o kernel.
Um efeito colateral de não retornar
Ele arma um temporizador de cinco minutos ao iniciar um aplicativo. Um carregador que nunca devolve o controle levaria um reset com o sistema já rodando, e pareceria um travamento aleatório. set_watchdog_timer(0, ...) desarma.
A troca de CR3 quase matou o carregador
A primeira tabela de páginas cobria 2 GiB, o mesmo teto do caminho BIOS. Só que o firmware havia carregado o próprio BOOTX64.EFI perto dos 2 GiB: no instante do mov cr3, a instrução seguinte deixou de existir. A correção é mapear 4 GiB, o que também traz o framebuffer do GOP, o LAPIC e o IO-APIC para dentro do mapa.
Contar memória livre não basta
Contar só EfiConventionalMemory abria um buraco justo onde o back buffer precisa de 8 MiB contíguos, e o kernel recusava o contrato. Memória de LoaderCode, LoaderData e BootServices* volta a ser livre no instante do ExitBootServices: ela entra no mapa entregue ao kernel.
O carregador não observa mais: ele assume. Lê kernel.bin da
partição para 0x10000, copia uma fonte 8 × 16 embutida no
binário para 0x6000 (o firmware UEFI não tem a fonte que a
BIOS deixava na memória, então ela foi extraída de um boot BIOS real e
virou um vetor de 4.096 bytes em font-8x16.h), monta 4 GiB de
identidade em páginas de 2 MiB, escreve o boot_info versão 4 em
0x7000, chama ExitBootServices e salta. A partir
daí quem fala na serial é o mesmo kernel.bin do caminho BIOS,
sem recompilar, e é essa igualdade linha a linha que o teste cobra. O
desenho não é enfeite: a captura confere cinco pontos com as cores exatas
do mosaico, entre eles a marca âmbar em 7,5 e o console do
terminal em 400,900.
Marco 22 · ACPI
Até aqui o SOIA adivinhava o hardware: PIC em 0x20, PIT em 0x40, teclado em 0x60, tudo herdado do IBM PC de 1981. A ACPI é a tabela que o firmware publica para acabar com a adivinhação, e a primeira coisa que ela destrava é a mais simples de conferir.
SOIA ACPI: revisao 0 lida pelo RSDT; PM1a em 0x0000000000000604; SLP_TYP do _S5 = 0.
soia> desligar
Desligando pela ACPI...
SOIA energia: desligando pela ACPI a pedido do ring 3.
Camada essencial
Até o Marco 21, encerrar o SOIA era fechar a janela do QEMU: o equivalente a tirar da tomada. kernel_halt era três instruções, cli e um laço de hlt eterno. Agora o sistema pede a própria energia de volta, e a janela fecha sozinha.
Camada técnica
Oito bytes RSD PTR podem aparecer por acaso em qualquer lugar da memória. A defesa é o checksum: a soma de todos os bytes da tabela tem que fechar em zero. Toda tabela ACPI carrega esse campo, e o SOIA confere em todas.
O valor que desliga mora em bytecode. O _S5 está dentro do DSDT, escrito em AML. O SOIA não interpreta AML: ele procura os quatro bytes _S5_, confirma que vem um PackageOp, pula o comprimento e lê o primeiro valor. É um atalho, e está registrado como tal: um interpretador de verdade seria maior que todo o resto do kernel.
0x604 não foi escrito no código. Muitos tutoriais mandam fazer outw(0x604, 0x2000) direto, porque é o que funciona no QEMU. Aqui esse número foi descoberto pela FACP, e o mesmo código funcionaria numa máquina que usasse outra porta.
Um bug de dois colchetes. A primeira execução disse "tabelas ausentes". A causa era mov rbx, rotulo em vez de mov rbx, [rotulo]: o registrador recebia o endereço da assinatura em vez dos oito bytes dela, e nenhuma posição da memória batia.
Por que ACPI veio antes do resto
O plano era UEFI carregar o kernel primeiro. Mas o RSDP também é encontrável pelo caminho BIOS, varrendo dois lugares: o UEFI só entrega o ponteiro de graça. Fazendo ACPI antes, ela roda no kernel que já funciona, é coberta pela bateria de testes existente e serve os dois caminhos de boot desde o primeiro dia.
Uma prova de tipo novo
O kernel escreve a intenção antes de escrever no PM1a. Uma máquina que ignorasse a ordem produziria exatamente o mesmo log. Então a prova do desligar é o processo do QEMU terminar, verificado de fora. Quando o efeito acontece fora do sistema, a prova precisa vir de fora também.
Marco 22 · APIC
O 8259 que o SOIA usa desde o Marco 6 é o controlador do IBM PC. Ele tem oito linhas, e o segundo chip existe só porque oito não bastavam: a IRQ2 do mestre carrega as oito do escravo em cascata. O APIC troca isso por 24 linhas, cada uma escolhendo o vetor e o núcleo de destino.
SOIA APIC: 1 nucleo(s) na MADT; LAPIC em 0x00000000FEE00000;
IO-APIC em 0x00000000FEC00000 com 24 linhas e 5 remapeamentos; 8259 mascarado.
Cinco remapeamentos. O firmware não entrega as IRQs legadas nas linhas de mesmo número. Ler as Interrupt Source Overrides da MADT é o que evita programar o timer no lugar errado.
A IDT não mudou uma linha. O IO-APIC entrega nos mesmos vetores em que o PIC foi remapeado no Marco 6. O que muda é o fim de interrupção: duas escritas no 8259 viram uma no LAPIC.
Uma CPU, de propósito. A MADT já diz quantos núcleos existem e o LAPIC já traz o mecanismo para acordá-los. Usá-los exige trava em todo estado compartilhado, e isso é marco próprio.
O Marco 21 pagou por si mesmo
O LAPIC fica em 0xFEE00000, perto de 4 GiB, e o mapa identidade do kernel para em 2 GiB. A primeira escrita virou falha de página, e o tratador de exceções entregou o diagnóstico inteiro: vetor 14, CR2 = 0xFEE000F0. Sem ele, teria sido um triple fault silencioso e a janela do emulador sumindo.
A segunda falha, mais sutil
O PDPT tem quatro entradas, uma por GiB. O LAPIC, o IO-APIC e o framebuffer VBE caem todos no quarto. Como cada tarefa tem um page directory próprio para o vídeo, apontar o slot para o do APIC apagaria a tela, e o contrário apagaria o APIC. A saída foi comparar os slots antes de escolher: coincidindo, as duas regiões de 2 MiB cabem lado a lado no mesmo diretório.
Camada essencial
No 8259, qual dispositivo usa qual linha foi decidido pelo fabricante da placa em 1981, e o sistema operacional apenas obedece. No IO-APIC, cada linha é programada: o SOIA escolhe o vetor, e numa máquina com vários núcleos escolheria também qual deles atende.
Camada técnica
As páginas do APIC são mapeadas com PWT e PCD, que desligam o cache. Uma escrita retida no cache nunca chegaria ao controlador, e uma leitura poderia devolver o valor antigo. É a mesma correção que o framebuffer ainda não recebeu, e que lá continua registrada como dívida.
Marco 22 · A entrega ao kernel
O carregador UEFI monta o contrato que o estágio 2 do BIOS já montava e some. O kernel não mudou uma linha para aceitar o boot novo: ele encontra a fonte em 0x6000, o mapa de memória em 0x5000 e as tabelas de página onde sempre estiveram.
SOIA UEFI: kernel.bin lido da particao para 0x0000000000010000; 49152 bytes.
SOIA UEFI: fonte 8x16 copiada para 0x6000.
SOIA UEFI: 4 GiB mapeados por identidade em paginas de 2 MiB.
SOIA UEFI: contrato versao 4 pronto com 5 regioes de memoria.
SOIA kernel 64-bit: controle recebido!
SOIA kernel: contrato de boot validado.
A fonte foi extraída, não inventada. O firmware UEFI não tem fonte VGA. Um script sobe o próprio caminho BIOS no QEMU, espera o contrato ser validado e lê 0x6000 pelo monitor. As letras das duas telas são idênticas, não parecidas.
Quatro GiB de mapa por causa de uma instrução. O firmware pôs o carregador perto de 2 GiB. No instante em que CR3 troca, a próxima instrução está lá. Um mapa de 12 MiB desmapearia o próprio código no meio do salto.
A mesma máquina, dois firmwares, dois relatos. Pelo BIOS a ACPI é revisão 0 pelo RSDT, com energia em 0x604. Pelo UEFI é revisão 2 pelo XSDT, em 0xB004.
O erro que parecia conservador
Contar só EfiConventionalMemory parece a leitura segura e é a errada: a especificação diz que o código e os dados do carregador e dos boot services deixam de ter dono quando ExitBootServices é chamado. Descartá-los abria um buraco em 0x800000, justo onde o back buffer de 8 MiB precisa caber contíguo.
Um campo novo no contrato
No UEFI a assinatura RSD PTR não está na memória baixa, e a varredura não acha nada. O campo em +136 carrega o ponteiro que o firmware entregou; o estágio 2 do BIOS deixa zero. E o ponteiro vindo de fora não é aceito de graça: assinatura e checksum são conferidos igual aos da varredura.
Marco 23 · Precisão e tempo
Até aqui o SOIA só trabalhava com números inteiros. O processador sempre soube fazer conta com vírgula, em gavetas próprias chamadas XMM, mas exigia que o sistema avisasse antes que sabe cuidar delas. Agora avisa.
CR0.EM e CR0.TS zerados, CR4.OSFXSR e CR4.OSXMMEXCPT ligados, e 512 bytes de FXSAVE por tarefa no despacho. A userland passou de -msoft-float para -msse2.
Experimento verificado
run FPU.ELFQuatro contas provam que a unidade responde. A quinta prova o que ninguém vê: oito registradores anotados, uma soneca de propósito para outras tarefas rodarem, e a conferência de que os oito voltaram intactos.
soia> run fpu.elf
0,1 + 0,2 = 0.300 (nao e 0,3 exato, e isso e correto)
raiz de 2 = 1.414
0,1 somado 10x = 1.000 (o erro de arredondamento acumula)
A prova que importa: XMM atravessando troca de contexto.
dormi 3 ticks; o escalonador rodou outras tarefas nesse meio
8 registradores XMM intactos apos dormir: OK
Como é. O processador não libera XMM sem dois consentimentos: CR4.OSFXSR promete que o sistema sabe salvar os registradores com FXSAVE, e CR4.OSXMMEXCPT promete que a exceção 19 tem tratador. A segunda promessa só pôde ser feita porque o Marco 21 instalou os 32 vetores; antes dele, seria mentira.
Por que assim. A área de estado mora no binário do kernel, e não no alocador de páginas, pelo mesmo motivo da pilha de IST: ela precisa estar no mapa de identidade que todo CR3 enxerga, seja qual for a tarefa que entra ou sai.
O que muda. Escala de imagem, decodificação de vídeo e qualquer motor de script futuro dependiam disso: número em JavaScript é double. O que continua impossível é AVX, cujo estado passa de 512 bytes e exigiria XSAVE com área negociada.
Camada essencial
Dois funcionários se alternam na mesma mesa a cada 50 milissegundos e nenhum sabe que o outro existe. Se ninguém guardar as gavetas na troca de turno, o segundo mexe nas contas do primeiro, e o primeiro volta e continua sem perceber que os números mudaram. O resultado não é um erro visível: é uma conta errada com cara de conta certa.
Camada técnica
je que evita um bug intermitente.cmp rbx, [current_task]
je .load ; mesma tarefa: nada a fazer
call fpu_save_current_64
mov [current_task], rbx
call fpu_restore_current_64
Quando o escalonador escolhe a mesma tarefa de novo, restaurar carregaria a cópia do despacho anterior e apagaria toda conta feita desde então. O erro seria proporcional à carga do sistema.
O bug que o marco encontrou
Ligar SSE quebrou o boot antes do shell aparecer, com protecao geral | vetor 13 | codigo 0x0 em duas tarefas. Código de erro zero em #GP não é violação de segmento: é instrução SSE alinhada operando em endereço que não é múltiplo de 16.
A ABI diz que, na entrada de uma função, RSP % 16 vale 8, porque o CALL empurrou o endereço de retorno. O kernel entregava USER_STACK_TOP, múltiplo de 4096, e o frame inteiro ficava deslocado em oito bytes. Estava errado desde o primeiro programa em C, e ficou invisível dez marcos porque nenhuma instrução exigia alinhamento.
USER_STACK_ENTRY_RSP equ USER_STACK_TOP - 8
A lição é geral: habilitar um recurso não só adiciona o que ele faz, também passa a cobrar contratos que ninguém verificava.
Por que a prova é assembly
A ABI trata XMM como registradores voláteis. Escrito em C, o compilador salvaria os valores na pilha antes da syscall e os recarregaria depois: o programa compararia a pilha com ela mesma e imprimiria OK sobre um kernel que não salva nada.
movsd 0x00(%[src]), %%xmm0
...
int $0x80 ; a tarefa dorme aqui
movsd %%xmm0, 0x00(%[dst])
Mantendo os valores nos registradores através do int 0x80, o único responsável por preservá-los é o FXSAVE do escalonador, e mais ninguém.
Um detalhe que morde quem tenta economizar a inicialização: um
FXRSTOR de área zerada falha, porque o
MXCSR tem bits reservados cujo valor zero é inválido. Cada
vaga nasce com uma cópia de um estado real, produzido por
fninit, ldmxcsr 0x1F80 e um
FXSAVE de referência. E a raiz quadrada do
FPU.ELF vem da instrução sqrtsd, não de
biblioteca: não existe libm aqui, e não vai existir.
Fatia 2 · O relógio
O LAPIC tem temporizador próprio, melhor que o PIT em tudo menos numa coisa: ninguém informa em que velocidade ele conta. Descobrir isso é medir contra o único relógio de frequência conhecida da máquina.
SOIA APIC timer: 626293 contagens em 10 ms, ou seja 62629300 Hz
apos o divisor 16; pulso a 100 Hz, video a 50 Hz e tick publico a 20 Hz.
Como é. O canal 2 do PIT, que existe desde 1981 para ligar o alto-falante, serve de cronômetro: o canal 0 está ocupado pela batida do escalonador desde o Marco 6. A janela de medição é exatamente o período que será programado, então a contagem medida vira o recarregamento sem nenhuma divisão no meio, e sem resto para acumular erro.
Por que assim. O LVT fica mascarado durante a medição, porque um pulso no meio dela entraria no handler antes de existir contagem válida. E depois de calibrar, a IRQ0 sai do IO-APIC mas o PIT continua contando: ele é a referência, e desligá-lo seria jogar fora o instrumento.
O que muda. Existe uma unidade de tempo que não existia. A menor coisa que o SOIA sabia medir era um tick de 50 ms; agora uma soneca de três ticks pode ser descrita como 150 ms. O que continua impossível é microssegundo, que exigiria ler o TSC com a frequência dele calibrada pelo mesmo método.
Camada essencial
Uma única interrupção fazia três trabalhos ao mesmo tempo: trocar de programa, desenhar a tela e contar o tempo. Como era a mesma batida, mudar o ritmo de um mudava o dos outros três. Agora cada um tem seu próprio compasso, e o do meio, o que conta o tempo, continua igual de propósito: tudo que já existia foi escrito contra ele.
Camada técnica
dec byte [timer_tick_divider]
jnz .acknowledge
mov byte [timer_tick_divider], 5
inc qword [timer_ticks]
SLEEP recebe ticks. Um soia_sleep(10) que passasse a valer 100 ms em vez de 500 ms mudaria o comportamento de todos os aplicativos de uma vez, sem nenhum deles ter sido tocado.
O erro que custou quatro rodadas
Depois de ligar o timer, o uptime relatado parecia ser metade do de uma execução anterior. Conclusão imediata: pulsos estavam se perdendo. E os números internos pareciam confirmar, porque 3 ticks e 150 ms fechavam perfeitamente.
Só que uptime_milliseconds e timer_ticks derivam os dois do pulso. Se pulsos se perdessem, ambos errariam junto, na mesma proporção, e continuariam coerentes: eles nunca poderiam detectar o próprio defeito.
$sw = [System.Diagnostics.Stopwatch]::StartNew()
.\scripts\Test-QemuBoot.ps1 -MemoryMiB 64
$sw.Stop()
parede: 41 s | uptime do SOIA: 37 s | diferenca: o boot
O relógio sempre estivera certo, e os "74 s de referência" vinham de uma execução que demorou mais por disputar a máquina com outros testes. No caminho, o pulso chegou a ser reduzido para 40 Hz e o código chegou a afirmar que 100 Hz não era sustentável. Era falso, e foi revertido.
A regra que fica: contador derivado de um sinal não valida esse sinal. A referência precisa ser independente, e a mais independente disponível costuma ser o mundo de fora.
O próximo marco se anunciou aqui
A primeira tentativa foi mostrar o relógio novo no status do terminal. O linker recusou:
ld.lld: error: section .text virtual address range
overlaps with .data
Cada processo recebe 16 KiB de texto, e o TERMINAL.ELF não aceita mais nenhuma função nem string sem perder outra. A prova foi para o FPU.ELF, que tinha folga, e o limite ficou registrado onde será resolvido: o Marco 24, com binário de tamanho variável, demand paging e heap em MiB.
Roteiro
A IA ajuda a construir este projeto, mas não executa dentro dele. O roteiro agora segue os subsistemas clássicos de um SO completo.
Em andamento · Marco 22
UEFI não acrescenta uma peça: ele remove a metade de baixo do
sistema. O firmware entrega o controle já em long mode, e com isso
somem o MBR de 512 bytes, o modo real, a A20, a
INT 13h, o E820 e o VBE. Depois vêm ACPI,
para o SOIA parar de adivinhar onde o hardware está, e o APIC, para
aposentar um controlador de interrupções de 1976.
O teto de 128 setores some. Ele existia porque uma única INT 13h carregava o kernel em 0x10000 e a IDT começa em 0x20000. É consequência do BIOS, não uma lei.
Os dois caminhos convivem. O boot BIOS continua no repositório: ele é o material dos marcos 1 a 4, e a comparação entre os dois é o melhor conteúdo deste marco.
SMP fica para depois. Vários núcleos invalidam a premissa de que IF=0 dispensa trava, e isso muda o contrato do kernel inteiro. Uma coisa de cada vez.
Ferramenta de desenvolvimento
Ela apoia arquitetura, código, revisão, testes e documentação. A imagem final funciona sem modelo, conta, API ou conexão de IA.
Vocabulário
Abra um conceito quando precisar. O glossário completo permanece na documentação técnica.
Ver glossário em MarkdownSimples: a autoridade que permanece administrando o computador.
Técnico: código em ring 0 que controla memória, hardware e serviços.
Simples: a área limitada em que programas comuns executam.
Técnico: CPL 3, sem acesso a páginas supervisoras ou instruções privilegiadas.
Simples: um pedido controlado que um programa faz ao kernel.
Técnico: travessia validada por INT 0x80, com número e argumentos em registradores.
Simples: a área que mostra texto e permite editar uma linha.
Técnico: driver de console em janela gráfica, com cursor, rolagem e entrada enfileirada.
Simples: o programa que entende o comando digitado.
Técnico: tarefa em ring 3 que analisa comandos e usa syscalls para pedir serviços.
Simples: uma porta comum para abrir arquivos sem conhecer o disco por baixo.
Técnico: interface que traduz operações genéricas para o formato SOIAFS1.
Fontes deste caderno
O Markdown guarda o detalhe completo. Este caderno organiza o mesmo conhecimento para revisão e estudo.