Marco 27 · fatia 1: imagem do disco na janela

O SOIA liga numa área de trabalho, e a janela mostra foto.

Papel de parede lido do disco no boot, quatro atalhos no fundo, barra de tarefas com hora e data reais do CMOS, e janelas que arrastam, minimizam e fecham. Cada janela tem uma superfície de pixel própria em RAM, e desde o Marco 27 um programa de ring 3 abre um arquivo de imagem e desenha nela, uma linha por leitura.

Ver o desktop por dentro

32 MiB
disco virtual fixo
12 ELF64
apps C em ring 3
32
syscalls pela INT 0x80
COM1 · 0x3F8 verificado

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

Um computador dentro do computador.

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

Escolha a profundidade.

Você pode trocar de nível a qualquer momento. O assunto continua o mesmo.

Seção especial · C no SOIA

Não é “C+”. É C freestanding.

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.

Assembly

Controle fino.

Ideal para boot, registradores, interrupções e mudanças de privilégio.

mov cr3, rax
C++

Ainda não.

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

agoraTERMINAL.ELF

Comandos, histórico, métricas e arquivos.

agoraNOTEPAD.ELF

Ctrl+S persiste NOTE.TXT no disco virtual.

agoraMONITOR.ELF

CPU, RAM, SOIAFS1, ATA, scheduler e rede.

agoraNET.ELF

DHCP, UDP, DNS e TCP por syscalls validadas.

agoraPersistência

Capacidade fixa, escrita e blocos livres.

Estudar C freestanding em detalhes

Marco 11 · Interface gráfica

De letras prontas a pixels controlados.

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.

O salto conceitual

Um endereço agora representa uma cor.

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.

1920 × 1080
2.073.600 pixels
32 bpp
XRGB8888 direto
8.294.400 bytes
framebuffer inteiro
112 × 58
células do terminal

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.

Estudar o Marco 11 completo

Marco 13.1 · Monitor do sistema

Três programas pedem. O kernel mede e decide.

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

De cat até os bytes do disco.

SOIA · janela do terminal, 112 × 58
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> _
01Terminalpixel, cursor e rolagem
02Shelllinha, comparação e comando
03Kernelautoridade e validação

Contrato INT 0x80

Vinte e quatro pedidos pequenos e verificáveis.

RAX escolhe o serviço; registradores carregam argumentos e o retorno.

01writetexto → terminal
02read_keyfila → caractere
03clearredesenha a janela
04get_ticksconsulta IRQ0
05free_pagesconsulta memória
06read_direntrada da raiz
07read_filenome → bytes
08probeprova do aplicativo
09system_infométricas do SOIA
10active_taskconsulta o foco
11sleepespera sem polling
12set_cursoratualização parcial
13text_colorpaleta protegida
14write_filesalva NOTE.TXT
15spawncria CALC.ELF
16exitfinaliza o filho
17waitbloqueia o pai
18process_infoalimenta ps
19brkcresce o heap
20process_handlePID → handle
21ipc_sendcopia uma mensagem
22ipc_receiverecebe ou bloqueia
23dns_resolvenome → IPv4
24tcp_probehandshake virtual

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 shell

Mostra os comandos disponíveis.

clearJanela limpa

Redesenha a interface e reposiciona o cursor gráfico.

echo TEXTOTexto de volta

Preserva o trecho digitado depois do espaço.

ticksTempo do kernel

Consulta quantos pulsos de 20 Hz ocorreram.

memRAM disponível

Consulta as páginas físicas livres de 4 KiB.

statusResumo honesto

Mostra CPU pelo TSC virtual, uptime, RAM, SOIAFS1, ATA e tarefas.

lsDiretório raiz

Percorre as entradas oferecidas pela VFS.

cat ARQUIVOArquivo real

Lê README.TXT, NOTE.TXT ou outro arquivo da raiz pelo nome.

psTabela de processos

Mostra PID, estado e nome das vagas ocupadas.

run CALC.ELFNovo processo

Cria a calculadora, aguarda sua saída e recolhe a memória.

run HEAP.ELFMemória dinâmica

Testa malloc, free, calloc e páginas sob demanda.

run IPC.ELFMensagem isolada

Envia ping, recebe pong e prova que um handle antigo deixa de valer.

run NET.ELFRede útil

Mostra DHCP, UDP, DNS e um handshake TCP real no QEMU.

Marco 13.1 · telemetria protegida

O painel vê números; o kernel toca nos sensores.

valores · 500 ms
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
01SLEEP + HLTtempo ocioso real
02TSC virtualintervalos mais finos
03SET_CURSORsem apagar a janela
04CP437 + corblocos sem Unicode
Estudar o Marco 13.1 completo

Teste comportamental

O QEMU realmente digita.

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

Janela funcional, ainda fixa.

  • ASCII, sem acentos ou teclado USB
  • histórico volátil limitado a dez comandos
  • CPU mede a vCPU do SOIA, não o Ryzen inteiro
  • sem temperatura, energia ou frequência turbo
  • escrita limitada a NOTE.TXT, sem delete ou diretórios
  • terminal, notas e monitor são ELF carregados do disco
  • Ctrl+S salva até 2.047 bytes no SOIAFS1
  • sem mouse, compositor ou janelas móveis
Estudar o Marco 13 completo

Capítulo zero · A base

Do interruptor ao endereço.

Antes de estudar kernel, precisamos separar três perguntas: qual é o valor, onde ele está e o que ele significa.

1Veranalogia + desenho 2Manipularmude os valores 3Preverpense antes 4Conferirveja a conta 5Aplicaruse o SOIA real

01 + 02 · Bit e byte

Oito interruptores formam um 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⁷.

Valoré o número guardado Representaçãoé como o escrevemos Significadodepende de quem lê

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 Playground
Laboratório de 1 byte Toque nos bits
128643216 8421
Binário01010101
Hexadecimal0x55
Decimal85
Como textoU

Pesos 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

Quatro bits cabem em um símbolo.

Hexadecimal conta com dez números e seis letras. Cada casa vai de 0 a F; depois de 0F vem 10.

08090A0B 0C0D0E0F 10

00 até FF 256 valores possíveis · máximo decimal 255

Meio byte = 1 nibbleEscolha um símbolo
5
Binário0101 Decimal5

04 · A rua da memória

Base é o começo. Offset é a distância.

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.

Base
onde a região começa
Offset
distância desde o começo
Endereço
posição resultante no espaço
Calculadora hexadecimalTente prever antes de conferir
Endereço 0x7DFE

0x7C00 + 0x01FE = 0x7DFE · o offset vale 510 em decimal.

05 · Primeiro setor

Um terreno com exatamente 512 posições.

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.

primeiro endereço0x7C00 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.

Encontrar no stage1.bin ↓

06 · CPU e significado

Os bytes não mudam. A interpretação muda.

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.

A analogia que ajuda A CPU acorda com um manual antigo.

Ela começa com regras compatíveis com o 8086 e precisa ser preparada para usar os modos modernos.

Onde a analogia para 16 bits não significa ler só 2 bytes.

É principalmente o tamanho padrão de operandos e registradores. Instruções x86 têm tamanho variável.

Outro cuidado “Enxerga 1 MiB” é uma aproximação.

O modo real usa segmento × 16 + deslocamento; a linha A20 acrescenta uma nuance no limite.

Próxima volta do ciclo

Agora confronte a ideia com bytes reais.

O laboratório mostra offset, hexadecimal, texto e instrução nos arquivos que realmente inicializam o SOIA.

Abrir laboratório binário Ler o capítulo completo

Marcos 09 + 15 · Disco e persistência

Da porta do disco até salvar um nome.

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

65.536 caixas de 512 bytes.

33.554.432 bytesdisco virtual de 32 MiB

inicialização núcleo volume SOIAFS1

A distinção que evita a confusão

A caixa aumenta. O endereço inicial não.

Setor organiza bytes persistentes no disco. Endereço identifica onde cada byte será colocado na RAM durante a execução.

Armazenamento persistente

Disco · caixas de transporte

Cento e vinte é o teto, e não uma escolha: o kernel carrega em 0x10000 e a IDT começa em 0x20000. Passar disso exige mudar a IDT de lugar antes. O último setor continua preenchido até fechar sua caixa de 512 bytes.

bootloader 120 setores copia em sequência
Espaço de execução

RAM · rua de endereços

O começo continua em 0x10000. Apenas o fim passa de 0x1BFFF para 0x1EFFF. O teto é 0x20000, onde começa a IDT: sobraram 4 KiB, e o próximo recurso grande do kernel vai cobrar a mudança dela de lugar. Aqui cada endereço nomeia um byte; o kernel também gerencia a RAM em páginas de 4 KiB.

Em uma frase

O disco ganhou vinte e quatro caixas de kernel; na RAM, o mesmo terreno inicial recebeu 12.288 bytes a mais no final, e agora encosta no teto.

LBA 102 · setor relativo 0

Superbloco

A ficha que permite reconhecer e validar o volume antes de confiar nele.

0x00
assinatura
SOIAFS1\0
0x08
versão
2
0x0C
entradas
9
0x10
diretório relativo
1
0x14
setores do volume
3.994
0x30
setores livres
3.778
LBA 63–64 · relativos 1–2

Diretório + bitmap

Entradas associam nomes aos dados; um bit marca cada setor livre ou ocupado.

exemplo · 16 BTERMINAL.ELF setor relativo4 tamanho real22.704 B tipoarquivo
volume62 + relativo4 = inícioLBA 66

O transporte

ATA PIO lê e escreve uma palavra por vez.

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.

capacidadeIDENTIFY 256 palavras16 bits bloco512 bytes

A abstração

Quem pede não acessa portas.

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 conteúdo sobreviveu a outro boot.

O teste salva NOTE.TXT, fecha o primeiro QEMU e exige o texto ao iniciar outro com a mesma imagem.

  1. 1Salvardados · Ctrl+S
  2. 2Marcarbitmap + diretório
  3. 3Reiniciarmesma soia.img
  4. 4ReabrirNOTE.TXT intacto
“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.

Estudar o Marco 15 completo

Fundamentos do núcleo

Abra somente a camada que quer estudar.

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

Mesmo endereço. Memórias diferentes.

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

Ring 3 pede. Ring 0 decide.

A CPU impede que programas comuns executem instruções privilegiadas ou escrevam nas páginas do kernel.

Tradução por tarefa

CR3 escolhe a planta.

RIP 0x100000000
as duas enxergam0x100000000endereço virtual
Tarefa ACR3 APágina física A
Tarefa BCR3 BPágina física B

O mesmo número não significa o mesmo lugar depois da tradução.

Preempção a 20 Hz

O timer pode retirar a CPU de uma tarefa.

round-robin · A ↔ B
fotografia salva
15 registradoresRAX…R15
5 campos da CPURIP · CS · RFLAGS · RSP · SS
160 bytes
Do Marco 8 ao Marco 10

Isolamento, CR3 e scheduler ficam aqui. O contrato atual de INT 0x80, suas trinta e duas syscalls e a prova interativa pertencem ao Terminal.

Detalhes do Marco 8

Marco 07 · Gerenciamento

RAM não é um terreno vazio.

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

E820 separa utilizável de reservado.

boot_info v2

Esquema didático: o mapa real contém várias entradas e pode ter pequenos “buracos” reservados.

Dois tipos de endereço

Virtual passa por uma tradução.

código usavirtual0x02000000
tabelasCR3consulta
RAM recebefísico0x02000000

Hoje os números são iguais: isso é um mapeamento identidade. A tradução ainda existe.

Um bit por página

Um painel compacto de ocupação.

11 11 1000 0010 0000
reservada ocupada livre

1 não pode ser entregue; 0 está livre para alocação.

Autoteste real no boot

Não basta dizer que funciona.

  1. 1Alocarrecebe página P
  2. 2Escrevergrava SOIAMEM7
  3. 3Liberardevolve P
  4. 4Reutilizarmesma P, zerada
aprovado no QEMU

A máquina ganhou campainhas

Eventos chegam ao kernel.

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

A CPU recebeu o vetor 33. E agora?

registrador IDTR base 0x20000 · limite 4095
IRQ 1pedido do tecladolinha física
vetor 33índice para a CPU0x21 em hexadecimal
entrada 33porta na IDTguarda um endereço
ISRrotina do tecladocódigo executado
CPU recebe33vetor
cada porta16 Bdescritor
deslocamento0x21033 × 16
base da IDT0x20000IDTR
porta consultada0x20210entrada 33

Dentro de uma entrada

Uma porta de 16 bytes.

atributo 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

256 vetores não significam 256 aparelhos.

0–31exceções da CPUdivisão por zero, página inválida…
32–47IRQs do PICtimer 32 · teclado 33
48–255disponíveissoftware e futuras controladoras

O vetor é apenas um número de 8 bits usado como índice: 0 a 255.

Viagem completa da tecla A

O programa pausa e depois continua.

  1. 1

    Tecladolevanta IRQ 1

  2. 2

    PICentrega vetor 0x21

  3. 3

    CPUsalva RIP, CS e RFLAGS

  4. 4

    IDT[33]fornece o endereço da ISR

  5. 5

    ISRlê 0x60 e envia EOI

  6. 6

    IRETQrestaura e continua

Abrir a explicação técnica completa da IDT
Próxima consequência prática

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 6

A primeira entrega

Do carregador ao núcleo do sistema.

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.

  1. 01

    Disco

    Kernel é localizado

    O segundo estágio busca o novo programa nos setores seguintes.

    INT 13h/AH=42h lê os LBAs 6–101 em 1000:0000.

  2. 02

    Preparação

    Máquina chega a 64 bits

    O carregador monta o ambiente antes de chamar o núcleo.

    A20, GDT, paginação e long mode permanecem responsabilidades do bootloader.

  3. 03

    Contrato

    boot_info é preenchido

    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.

  4. 04

    Kernel

    Núcleo assume o controle

    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

Três lugares, responsabilidades diferentes.

soia.img contém carregador, kernel e SOIAFS1. Código é carregado na RAM; o volume continua no disco até o kernel pedir seus setores.

01
Persistente

Arquivo no disco

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.

02
Temporário

Estruturas na RAM

O carregador copia o kernel e cria estruturas temporárias em endereços distintos do computador virtual:

0x1000PML4
0x2000PDPT
0x3000Diretório
0x5000Mapa E820
0x6000Fonte BIOS 8 × 16
0x7000VBE ModeInfo
0x8000Estágio 2
0x10000Kernel
0x20000IDT
0x30000Bitmap · 64 KiB
0x100000000Tarefas
0x90000Pilha
VBEFramebuffer MMIO

Bootloader e kernel coexistem na RAM, mas têm funções separadas.

03
Resultado

Estado da CPU

A CPU continua em 64 bits; o salto para 0x10000 entrega a execução a outro binário.

16 bitsmodo real
32 bitsmodo protegido
64 bitskernel em execução
Em uma frase

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.

01Bits, bytes e endereçosFundamentos →

02Entrega ao kernelBoot →

03Bytes reais da imagemBinários →

Laboratório hexadecimal

Dois dígitos que escondem oito bits.

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

Como FA vira oito bits?

hexF1111
hexA1010
1 byteFA11111010

Um dígito hexadecimal representa quatro bits. Por isso dois dígitos, de 00 a FF, representam um byte.

Seu primeiro comando

Como ler 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

Como ler 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

Clique em qualquer byte para investigá-lo.

arquivo selecionado stage1.bin
funçãoSetor inicial
tamanho512 bytes
modo16 bits
bytes 0x000–0x0FF
16 bytes por linha
código ou dado texto ASCII preenchimento longo assinatura 55 AA

Marcos 14 + 19 · Rede básica e útil

Do dispositivo ao aplicativo, sem entregar o hardware.

O kernel encontra a placa, negocia DHCP, resolve um nome e completa TCP; NET.ELF vê resultados, nunca portas ou buffers DMA.

Analogia

A RTL8139 continua sendo a caixa de correio.

Ethernet é o envelope; DHCP entrega a configuração; DNS acha um endereço; TCP confirma que os dois lados podem conversar.

Negociação real

DISCOVER, OFFER, REQUEST e ACK.

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

SYN → SYN-ACK → ACK.

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

Protocolos no fio ou o teste falha.

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
Estudar PCI, RTL8139, ARP e IPv4 Estudar DHCP, UDP, DNS e TCP

Marco 16 · processos dinâmicos

Um ELF deixa de ser só arquivo e ganha vida.

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

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

Estudar o Marco 16 completo

A pergunta que veio da tela

Por que a calculadora aparece dormindo enquanto você digita nela?

Com o CALC.ELF recebendo conta e respondendo, a coluna ESTADO dizia dormindo em toda atualização do monitor. A contagem estava certa. A pergunta é que estava mal feita.

   PID  PROGRAMA      ESTADO     CPU%          CICLOS       BASE       HEAP       TOTAL
     1  TERMINAL.ELF  dormindo     0%     982.872.970     65.536          0      65.536
     2  NOTEPAD.ELF   dormindo     0%      80.062.113     49.152          0      49.152
     3  MONITOR.ELF   executando   6%   2.360.976.015     61.440          0      61.440

Como é. READ_KEY não bloqueia: sem tecla na fila ela devolve SOIA_KEY_NONE e o app chama soia_cpu_relax(), que é soia_sleep(1). O .sleep grava TASK_STATE_SLEEPING e cede a CPU no mesmo instante, então sempre que outra tarefa está rodando, a vaga do app já voltou a dormir. Não é sorte de amostragem: flagrar o app em READY exigiria a preempção cair na fresta de microssegundos entre acordar e dormir de novo.

Por que assim. A alternativa seria bloquear a tarefa no teclado e acordá-la pela IRQ1. Só que o foco decide quem recebe tecla, e a IRQ1 empilha seis registradores em vez do quadro completo: mexer no despacho a partir dela é exatamente o que o Ctrl+C do Marco 21 teve de evitar, adiando o encerramento para o tick. Enquanto o teclado tiver dono único, polling é o contrato mais simples que funciona.

O que muda. A tabela ganhou CPU%, que é o delta de ciclos da vaga contra o TSC decorrido na janela de 500 ms, e CICLOS, o acumulado desde que a vaga nasceu. O PROCESS_INFO foi de 48 para 56 bytes. Continua impossível ler tempo de CPU em milissegundos, porque a coluna é de ciclos crus.

Camada essencial

A foto do caixa e o contador de clientes.

Imagine conferir se um caixa de supermercado está trabalhando tirando uma foto a cada meio segundo. Ele atende um cliente em dois segundos e espera trinta. Quase toda foto vai pegá-lo parado, e nenhuma foto isolada responde quanto ele produziu no dia.

ESTADO é a foto, e ela nunca vai deixar de existir: saber que a vaga está dormindo é a informação certa para o escalonador. CICLOS é o contador, que só cresce quando o trabalho acontece e nunca anda para trás.

Camada técnica

Só quem pergunta pode aparecer executando.

cmp r14, [current_task]
jne .process_state_ready
cmp r15, TASK_STATE_READY
jne .process_state_ready
mov r15, TASK_STATE_RUNNING

O array task_state nunca guarda RUNNING: quem está na CPU fica marcado READY. O valor é sintetizado na leitura, e apenas para a current_task. Como quem chama a syscall é o monitor, ele é a única tarefa que pode aparecer como executando naquela tabela, em qualquer amostra. O teste do QEMU derruba a bateria se outra vaga aparecer assim.

O detalhe que faria o número mentir

Quem consulta subestimaria a si mesmo.

lea rdx, [task_run_cycles]
mov r15, [rdx + r14 * 8]
cmp r14, [current_task]
jne .process_info_cycles_ready
rdtsc                      ; o quantum em curso
shl rdx, 32
or rax, rdx
sub rax, [task_dispatch_tsc]
add r15, rax

scheduler_account_current_cycles_64 só fecha a conta quando a tarefa deixa a CPU. Sem somar a parcela decorrida, a única linha errada da tabela seria justamente a de quem está trabalhando naquele instante.

A diferença medida

Duzentas vezes, com os dois vivos.

MONITOR.ELF  redesenha a cada 500 ms   +891.718.054  pico 10%
NOTEPAD.ELF  acorda, olha a fila, dorme  +4.332.008  pico  0%

Os dois aparecem como vaga viva; o segundo aparece como dormindo em toda amostra e mostra 0% o tempo inteiro, mesmo acordando vinte vezes por segundo. É o mesmo laço da calculadora que motivou a pergunta, e o contador é o que separa um do outro.

Estudar a tabela do monitor

Marco 17 · heap + libc mínima

Memória que cresce quando o programa precisa.

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

Trê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

Free guarda para o próximo pedido.

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

Break lógico + páginas físicas.

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.

Estudar o Marco 17 completo

Marco 18 · IPC + serviços

Mensagens atravessam o isolamento com o kernel no meio.

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

O 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

Handle é uma senha com validade.

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

Contrato pequeno e limitado.

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.

Estudar o Marco 18 completo

Marco 20 · Desktop concluído

Três programas na mesma tela, e o clique escolhe quem escuta.

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. Neste marco os três programas passaram a dividir essa tela e trabalhar 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.

O mosaico descrito aqui foi substituído no Marco 26 por janelas flutuantes que se sobrepõem, arrastam e minimizam. O que continua valendo é a base: o pacote PS/2, a fila de eventos e o cursor composto no front buffer.

Experimento verificado

mouse

Seis 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

Duas telas em vez de uma.

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

Pacote de três bytes.

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 cada publicação extra custa 768 bytes dirigidos: 384 para devolver o fundo onde a seta estava e 384 para pintá-la no lugar novo. Custava a caixa de dano inteira até 31/07/2026

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 que barateou o mosaico.

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

A limitação que protege.

As três janelas dividiam a tela sem se cruzar, e era justamente isso que impedia uma tarefa de pintar pixel de outra. Janelas que se cobrem exigem 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 dívida foi paga nos Marcos 25 e 26. Cada janela ganhou superfície própria, o mosaico deu lugar a janelas flutuantes, e o compositor as monta na ordem de profundidade. Ver como

Camada essencial

Foco é quem escuta, não quem aparece.

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

A COM1 é um terminal só.

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

Botão que não fecha nada é pior que botão nenhum.

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

Por que o ponteiro escapa pela borda de cima.

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.

Estudar o Marco 20 completo

Marco 21 · Robustez

O programa quebra. O sistema não vai junto.

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

Um 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

Porta corta-fogo.

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

Quatro instruções decidem tudo.

mov rax, [rsp + EXCEPTION_CS_OFFSET]
and eax, 0x03
cmp eax, 0x03
jne exception_kernel_panic_64

Fatia 3 · Camada essencial

O extintor não pode ficar dentro do quarto em chamas.

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

IST1 troca o RSP antes de empilhar.

mov eax, 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.

A pilha saiu do binário, e o motivo é espaço. Ela continua reserva por constante e continua no mapa de identidade, que eram as duas razões da escolha original. O que mudou é o endereço: 0x56000, na área estática do kernel, em vez de dentro do kernel.bin. A auditoria do kernel mediu 13.283 bytes de times N db 0 embutidos no binário, 25,9 setores carregados do disco a cada boot para conter zeros, num kernel que tinha 6,42 setores livres até o teto de 120. Depois de mover os quatro maiores, a folga foi para 27,22. Zerar a faixa passou a ser trabalho do boot, em kernel_static_area_clear_64, e não do NASM em tempo de montagem.

Fatia 3 · Ctrl+C

Desistir não é o mesmo que quebrar.

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

A IRQ1 registra, o timer encerra.

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

Triple fault não deixa bilhete.

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

Despachar por inicial quebra no oitavo.

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.

Estudar o Marco 21 completo

Marco 22 · Plataforma moderna

O primeiro marco que apaga código em vez de somar.

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

O 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

Nove etapas, do zero.

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

Nenhuma delas.

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

Por que os dois caminhos ficam.

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

Três armadilhas silenciosas.

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

Retornar apaga o próprio trabalho.

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

O firmware tem um watchdog.

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

Quem troca o mapa some do mapa.

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

Emprestada também é utilizável.

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.

Estudar o Marco 22 completo

Marco 22 · ACPI

O computador que aprendeu a se desligar.

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

Puxar o cabo não é desligar.

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

Achar a assinatura não basta.

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

Ela não depende do UEFI.

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 log não podia provar isso.

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.

Estudar a cadeia ACPI completa

Marco 22 · APIC

Um chip de 1976 finalmente aposentado.

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

A falha disse exatamente onde doía.

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

Dois dispositivos disputando o mesmo GiB.

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

Roteamento fixo contra roteamento escolhido.

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

Registrador não é memória.

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.

Estudar a troca de controlador

Marco 22 · A entrega ao kernel

O mesmo kernel.bin, byte a byte, nos dois caminhos.

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

LoaderData também é memória livre.

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

Zero significa "procure você mesmo".

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.

Estudar o Marco 22 completo

Marco 23 · Precisão e tempo

O sistema aprendeu a contar com vírgula.

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

Quatro 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

Duas pessoas, uma mesa de oito gavetas.

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

O 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

Oito bytes dormindo desde o Marco 12.

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

Em C, o teste passaria com o kernel errado.

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

Um temporizador que não diz sua própria frequência.

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

Três coisas que batiam juntas agora batem separadas.

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

O tick de 20 Hz é contrato, não conservadorismo.

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

Um relógio conferido contra si mesmo mente sem se contradizer.

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

As quatro páginas de texto acabaram.

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.

Resolvido na fatia 1 do Marco 24. O relógio está no status, e o TERMINAL.ELF passou a ocupar 5 páginas de texto. Ver como

Marco 24 · Capacidade

Cada programa recebe o tamanho que pediu.

Todo programa do SOIA ganhava o mesmo espaço, do maior ao menor: quem era grande não cabia, quem era pequeno pagava pelo grande. Agora o espaço é medido a partir do próprio arquivo do programa, e a memória que ele pede depois só é entregue quando ele realmente usa. O mapa da região de usuário deixou de ser soma de constantes e passou a ser derivado dos PT_LOAD do ELF. USER_TEXT_PAGE_COUNT, USER_DATA_VIRTUAL e USER_TOTAL_PAGE_COUNT não existem mais; o que restou é o teto: 128 páginas de imagem em 8.192 de região, com o heap indo a 4.096 páginas entregues por falha.

Experimento verificado

Três programas, três mapas

A mensagem sai uma vez por tarefa criada, e não uma vez por boot. Um número único não mostraria o que interessa, que é a diferença entre eles.

SOIA imagem: tarefa 0 carregada com 5 paginas de texto, 1 de dados e heap em 0x0000000100006000.
SOIA imagem: tarefa 1 carregada com 1 paginas de texto, 1 de dados e heap em 0x0000000100002000.
SOIA imagem: tarefa 2 carregada com 3 paginas de texto, 1 de dados e heap em 0x0000000100004000.

Como é. elf64_validate_64 parou de comparar cada p_vaddr com uma constante e passou a devolver o mapa: páginas de texto em RAX, primeira página de dados em RDX, páginas de dados em R8 e início do heap em R9. O que ela ainda cobra é só o que precisa ser verdade: texto na base, dados em fronteira de página depois do texto, e a soma dentro de USER_IMAGE_PAGE_LIMIT.

Por que assim. A pilha foi para o fim da região em vez de ficar depois dos dados. Assim ela não se move quando o binário engorda, e USER_STACK_ENTRY_RSP continua sendo uma constante, o que preserva o alinhamento de ABI que o Marco 23 pagou caro para descobrir.

O que muda. O NOTEPAD.ELF ocupa 1 página de texto onde recebia 4, e as três tarefas do boot custam 41 páginas onde custavam 48. O que continua impossível é passar de 2 MiB de espaço virtual por processo: a região é um page table só, e mais que isso depende de vários.

Camada essencial

Prateleira serrada contra prateleira ajustável.

Imagine uma estante em que todas as divisões foram serradas do mesmo tamanho antes de saber o que ia ser guardado. Um livro grosso não entra. Um panfleto entra e sobra um espaço que ninguém mais pode usar, porque a divisão é fixa e está ali para sempre.

A estante continua do mesmo tamanho, e o limite total continua existindo. O que mudou é que agora quem precisa de mais recebe mais, e quem precisa de menos devolve a diferença.

Camada técnica

A página que existe para não existir.

USER_STACK_GUARD_PAGE_COUNT equ 1
USER_HEAP_PAGE_CEILING equ USER_STACK_FIRST_PAGE \
                         - USER_STACK_GUARD_PAGE_COUNT

Entre o teto do heap e a base da pilha fica uma página que nunca recebe PTE. Pilha que transborda cai nela e vira falha de página, ou seja, fim declarado da tarefa culpada pelo caminho do Marco 21. Sem a guarda, o transbordo escreveria no heap em silêncio e o dano apareceria longe da causa.

Conferir a ausência dela é a única prova possível de uma página que nunca deve estar lá, e é o que o boot faz.

A decisão que a fatia forçou

Duas verdades sobre o mesmo endereço.

Existia uma tabela, task_user_pages, com o endereço físico de cada página de cada tarefa. As PTEs já guardam exatamente esse endereço, e é a PTE que a MMU consulta: a tabela era uma cópia.

Com tamanho fixo, isso era só redundância. Com tamanho variável ela teria que ser dimensionada para o teto de páginas vezes oito tarefas, e as duas listas passariam a ter como discordar. Ela saiu, e no lugar entrou uma leitura da própria PTE.

mov rax, [rbx + rsi * 8]
test rax, 0x01          ; bit de presenca
jz .not_present
mov rdx, PAGE_FRAME_MASK
and rax, rdx

A liberação de uma vaga ganhou de graça: eram três laços, um por região, cada um sabendo de cor quantas páginas procurar. Virou uma varredura das 512 entradas devolvendo o que estiver presente, que não depende de tamanho nenhum.

O bug que a mudança quase deixou passar

A pilha saiu da faixa sem ninguém escrever isso.

As duas rotinas que validam ponteiro de userland aceitavam de USER_TEXT_VIRTUAL até o break do heap. No layout antigo isso cobria a pilha, porque o heap vinha depois dela. Uma comparação, e cobria tudo.

Com a pilha no fim da região, a mesma comparação passaria a recusar exatamente o caso mais comum que existe: um buffer declarado como variável local dentro de uma função C. Toda syscall com buffer teria começado a devolver erro.

    cmp rdi, rdx           ; parte baixa: imagem + heap
    jb .try_stack
    cmp rax, r9            ; ate o break
    jbe .success

.try_stack:                ; parte alta: a pilha
    mov rcx, USER_STACK_VIRTUAL
    cmp rdi, rcx
    jb .failure

A regra que fica: mudar layout de memória é mudar todas as afirmações implícitas sobre vizinhança. As que estão escritas o assembler aponta; as que só existiam como consequência de duas coisas serem vizinhas não aparecem em lugar nenhum.

O endereço que saiu da userland

Uma constante compilada sobre conta alheia.

#define SOIA_HEAP_BASE 0x0000000100008000UL
#define SOIA_HEAP_CAPACITY (16UL * 4096UL)

As duas saíram do soia.h. Com imagem de tamanho variável elas errariam em silêncio no primeiro binário que crescesse: o soia_malloc montaria a lista de blocos num lugar e o kernel mapearia as páginas em outro. No lugar entraram dois seletores de get_system_info, que respondem sobre a tarefa que perguntou.

soia> run heap.elf
heap base: 0x0000000100003000
teto do heap: 0x0000000100013000
primeiro bloco: 0x0000000100003020

O HEAP.ELF confere três coisas: que a base informada é igual ao break inicial, que ela fica acima do segmento de dados do próprio programa, e que crescer o heap consome exatamente duas páginas físicas.

O erro estava no teste

Quando a bateria recusa código que o sistema aceita.

O Test-BootImage.ps1 passou a conferir o mapa derivado e recusou o TERMINAL.ELF com "não cabe no mapa de imagem". O binário estava correto; a conta do teste, não.

# Errado: a divisao devolve double e [int] ARREDONDA
[int]((16288 + 4095) / 4096)              # -> 5

# Certo
[int][math]::Floor((16288 + 4095) / 4096) # -> 4

Um texto de 16.288 bytes ocupa quatro páginas. O teste calculava cinco, concluía que os dados começavam antes do fim do texto e acusava uma sobreposição que não existia. Fica o aviso: quando um teste novo recusa código que o sistema aceita, o suspeito é o teste.

Estudar o Marco 24 completo

Fatia 2 · Demand paging

Prometer 16 MiB sem gastar nada.

O heap era de 64 KiB porque o BRK alocava tudo no ato do pedido: teto alto significaria memória física reservada de verdade. Agora ele só promete, e a página aparece quando alguém a toca.

soia> run heap.elf
heap base: 0x0000000100003000
teto do heap: 0x0000000101003000

reservados 4 MiB sem custo: 0 pagina(s)
tocadas 3 paginas, entregues por falha: 3
break devolvido; paginas de volta ao alocador: OK

Como é. Três perguntas decidem, e todas precisam ser sim: o bit 0 do código de erro está em zero (página ausente, não violação de permissão), existe tarefa corrente, e o CR2 está entre a base do heap e o break vigente. Qualquer não manda a falha para o caminho do Marco 21, que encerra a tarefa culpada.

Por que assim. Adiar a entrega é o ganho inteiro; adiar a devolução seria vazamento. Por isso a assimetria: crescer o break não aloca nada, mas encolher desmapeia e devolve na hora, porque quem chama BRK para baixo espera a memória de volta no alocador.

O que muda. Um programa pode reservar megabytes e pagar só pelo que tocar, que é o que uma foto de 1920 × 1080 descomprimida (8,29 MB) vai exigir. O que continua impossível é troca para disco: página entregue só volta quando o break encolhe ou o processo morre.

Camada essencial

A reserva do restaurante.

Reservar dez mesas para um grupo que talvez venha não ocupa mesa nenhuma: ocupa uma linha no caderno. As mesas só saem de circulação quando as pessoas chegam e sentam.

Antes, o SOIA arrastava as dez mesas para o canto no instante da reserva, e elas ficavam lá vazias. Por isso a reserva máxima era pequena: mesa parada é mesa perdida.

Camada técnica

O privilégio não decide, e é de propósito.

O Marco 21 estabeleceu que o RPL do CS salvo decide o destino de uma exceção. Aqui a regra não se aplica, e repeti-la seria um bug grave:

char *buffer = soia_malloc(4096);
soia_read_file("README.TXT", 10, buffer, 4096);

A syscall escreve no buffer pelo endereço virtual, com o CR3 da tarefa carregado, e quem executa é o kernel. A primeira falha nessa página chega em ring 0, e é legítima. Filtrar por privilégio faria toda leitura de arquivo para um buffer recém-alocado derrubar o sistema.

O bug que a fatia encontrou

Ficar rápido acordou uma corrida que dormia.

Depois da fatia 2, o FAULT.ELF passou a ser encerrado com código -4 em vez de -6, e só no perfil de 64 MiB. Nenhuma linha do encerramento tinha sido tocada.

Quando um filho morre e o pai não está bloqueado em WAIT, o kernel liberava a vaga na hora, e o código de saída ia junto. O WAIT seguinte respondia "não encontrado" sobre um processo que tinha acabado de morrer.

A corrida existia desde o Marco 16 e nunca aparecia porque criar um processo era lento: oito páginas alocadas e zeradas. Com imagem derivada do ELF e heap sem alocação antecipada, são três, e o filho passou a alcançar a própria falha antes de o pai chegar no WAIT.

A regra que fica: otimizar não só acelera, também muda quais interleavings acontecem. Uma corrida latente não precisa de código novo para virar bug; basta o código antigo ficar rápido.

O segundo bug, na saída

Uma linha cortada ao meio por outra tarefa.

bytes reservados no break: Processo iniciado;
PID 4. O terminal aguardara sua saida.

O app imprimia número dígito a dígito, uma syscall por caractere, e o terminal escreve na mesma janela enquanto espera o filho. Entre duas syscalls cabe uma preempção, e a 100 Hz ela cabe com folga.

A saída é montar a linha inteira num buffer e emitir com uma chamada só: a syscall roda com interrupções desabilitadas, então a linha sai inteira. Vale para qualquer asserção de teste sobre linha com número interpolado.

Um page table cobre 2 MiB, e 2 MiB era o teto de tudo que um processo enxergava: um heap de 16 MiB não cabia nem em promessa. A região passou a ter 16 PTs, 32 MiB, e catorze deles não existem até o heap chegar neles, porque um PT vazio custaria 4 KiB para mapear nada. A lista de quais existem mora no próprio PD, pela mesma razão que os quadros moram nas PTEs desde a fatia 1.

Estudar a fatia 2 completa

Fatia 3 · A fronteira

2.048 bytes por chamada viraram 64 KiB.

O limite estava certo para o sistema de 2019 linhas atrás: o processo inteiro tinha 64 KiB, então nenhum buffer podia ser maior. Com heap em megabytes, ele virou o gargalo.

soia> run heap.elf
reservados 4 MiB sem custo: 0 pagina(s)
tocadas 3 paginas, entregues por falha: 3
arquivo lido em uma syscall: 22960 bytes
chamadas que isso custava antes: 12

Como é. USER_BUFFER_LIMIT foi para 65.536, o mesmo tamanho do buffer de arquivo da VFS, então um arquivo inteiro cabe numa chamada. E a validação de ponteiro deixou de comparar duas pontas: agora percorre cada página da faixa, no máximo 17 leituras de PTE.

Por que assim. Não foi zelo. Comparar as pontas deixa passar uma faixa que atravesse o vazio entre texto e dados, e aí o rep movsb do kernel falha em ring 0 num endereço que o demand paging não atende: panic, provocado por um ponteiro de userland. Com 2.048 bytes era difícil de alcançar; com 64 KiB, uma faixa cobre 17 páginas.

O que muda. Mover 1 MiB custava 512 syscalls e passa a custar 16. O que continua existindo é o intermediário: READ_FILE lê do disco para o buffer do kernel e depois copia para o usuário, dois movimentos onde poderia haver um.

Camada essencial

A portaria que só deixava passar duas caixas.

Não adianta ter um depósito grande se a porta só aceita duas caixas por viagem: mudar de sala vira o trabalho, e não carregar. A porta ficou trinta e duas vezes mais larga, e a conferência passou a ser caixa por caixa em vez de olhar só a primeira e a última da pilha.

Conferir mais dá mais trabalho, e é justamente por a porta ser larga que ele passou a valer: uma pilha grande tem mais lugares onde algo errado se esconde no meio.

Camada técnica

Duas razões para uma página passar.

cmp r9, r12          ; antes do heap?
jb .require_pte
cmp r9, r13          ; dentro do prometido?
jb .advance          ; passa sem PTE

.require_pte:
test rcx, 0x01       ; presente?
jz .failure
test r11, r11        ; e escrita?
jz .advance
test rcx, 0x02       ; gravavel?
jz .failure

Página no heap prometido passa sem PTE, porque a cópia do kernel vai gerar a falha e ela será atendida. É esse caminho que faz um buffer recém-alocado por malloc ser um destino válido mesmo sem nenhuma página física.

O erro que quase passou

Um jmp que roubava o RCX de quinze chamadas.

A direção da transferência virou parâmetro em RCX, e os wrappers montavam o argumento antes de saltar:

validate_user_read_range_64:
    xor ecx, ecx      ; destrói o RCX de quem chamou
    mov rdx, USER_TEXT_VIRTUAL
    jmp validate_user_range_64

São quinze chamadas espalhadas pelo despacho de syscalls, e nenhuma espera perder RCX. O assembler não avisa, e o dano apareceria em uma syscall qualquer, longe daqui.

Os wrappers passaram a salvar e restaurar tudo, com call e ret no lugar do jmp. pop não altera flags, então o CF da validação atravessa a restauração intacto.

A prova que encadeia tudo

As três fatias numa linha só.

O HEAP.ELF aloca 40 KiB no heap, o que só existe pela fatia 2, e pede o MONITOR.ELF inteiro numa chamada, o que só existe pela fatia 3.

As dez páginas do buffer estão prometidas e nenhuma foi tocada. Quem toca primeiro é o rep movsb do kernel, ou seja, a falha legítima acontece em ring 0.

É exatamente o cenário que decidiu não filtrar o demand paging por privilégio. Se a decisão olhasse o RPL, esta prova não imprimiria nada: o sistema teria parado ao ler um arquivo.

Estudar a fatia 3 completa

Fatia 4 · O armazenamento

Um arquivo de 1 MiB, criado e relido byte a byte.

O disco tinha 2 MiB e a leitura era tudo ou nada, para um buffer de 64 KiB no kernel. Um arquivo de 1 MiB não cabia em nenhuma das três medidas.

soia> run disco.elf
gravados 1048576 bytes
blocos de 64 KiB: 16 chamadas
conferidos 1048576 bytes, um a um
leitura apos o fim devolveu 0 sem erro: OK
GRANDE.BIN permanece no disco para o proximo boot.

Como é. Duas syscalls novas, 29 e 30, leem e gravam uma janela do arquivo em vez do arquivo inteiro. O quinto argumento, o deslocamento, vai em R8, que já estava no quadro salvo por PUSH_TASK_REGISTERS: do lado do kernel bastou nomear a posição dele.

Por que assim. A escrita exige deslocamento múltiplo de 512 e a leitura não. Escrever no meio de um setor obrigaria a lê-lo, alterar o pedaço e gravá-lo de volta, e quem escreve sequencialmente nunca precisa disso: só o último bloco tem tamanho quebrado, e ele termina o arquivo.

O que muda. Uma foto passa a caber no disco e a ter caminho até a memória do processo. O que continua impossível é apagar arquivo, e crescer um arquivo num disco fragmentado: a alocação é contígua, então estender depende de o vizinho estar livre.

Camada essencial

O limite que ninguém via porque os dois números eram iguais.

O mapa de blocos ocupados cabia em um setor. Um setor tem 512 bytes, ou seja 4.096 bits, ou seja descreve 4.096 blocos. E o disco tinha exatamente 4.096 blocos.

O limite existia desde sempre e nunca apareceu, porque a régua tinha o mesmo tamanho da coisa medida. Ele só se manifesta quando você tenta aumentar a coisa.

Camada técnica

Ler depois do fim devolve zero, e não erro.

; Depois do fim nao e erro: e o fim.
cmp r12, r10
jae .end_of_file

É o sinal de parada de qualquer leitor sequencial. Tratá-lo como falha faria todo programa confundir o fim normal de um arquivo com um problema, e o laço de leitura precisaria saber o tamanho de antemão para nunca chegar lá.

O DISCO.ELF confere justamente isso depois de ler o último bloco: pede mais 64 KiB no deslocamento 1 MiB e exige zero de volta.

O bug que só apareceu num dos caminhos

O kernel cresceu e o UEFI parou sozinho.

Subir o kernel de 96 para 112 setores foi anunciado pelo NASM, no lugar certo: TIMES value -65 is negative, o padding que empurra a mensagem final para o último setor ficando sem espaço.

Só que o carregador UEFI tem o tamanho do kernel escrito dentro dele:

#define KERNEL_IMAGE_SIZE (96ULL * 512ULL)

Ele carregou 49.152 dos 57.344 bytes, e o kernel respondeu contrato de boot invalido. O sintoma engana porque é assimétrico: o BIOS continuava bootando normalmente, já que lá o estágio 2 usa o valor montado no próprio binário.

A regra que fica: os dois caminhos de boot não compartilham arquivo nenhum, e é exatamente isso que faz um número duplicado passar despercebido. Ao mexer no tamanho do kernel, os dois lados precisam ser tocados.

Como a prova evita se enganar

Um padrão que depende da posição.

static soia_byte_t pattern_at(soia_size_t position) {
    return (soia_byte_t)((position * 31UL
        + (position >> 8)) & 0xFFUL);
}

Se todos os bytes fossem iguais, um bloco gravado no deslocamento errado passaria despercebido, e é justamente esse o erro que a escrita com deslocamento pode cometer.

E o teste de persistência confere o arquivo de fora, lendo a imagem no PowerShell com a fórmula reescrita ali. Comparar o arquivo com um valor lido do próprio arquivo seria compará-lo consigo mesmo, que é a mesma armadilha do relógio do Marco 23.

O Marco 24 fecha aqui, nas quatro fatias, e com ele o SOIA passa a ter as três medidas que uma foto exige: espaço no processo, memória para a imagem decodificada e um caminho do disco até ela. O que falta agora não é capacidade, é superfície: as syscalls de tela ainda movem caractere, e não pixel.

Estudar a fatia 4 completa

Marco 25 · Superfície e compositor

A janela deixou de ser só texto.

Antes, as três janelas escreviam no mesmo quadro-negro da sala, cada uma no seu canto combinado: ninguém podia mudar de lugar nem passar por cima da outra. Agora cada uma tem uma folha própria, escreve nela do canto dela, e quem monta as folhas no quadro é o compositor, cinquenta vezes por segundo. Cada janela recebeu uma superfície contígua na faixa de 16,6 MiB pedida ao alocador no boot. SURFACE_INFO mapeia a superfície no espaço da tarefa e devolve endereço, largura, altura e passo; SURFACE_PUBLISH alimenta a caixa de dano dela. O texto do kernel também passou a sair na superfície, em coordenada local.

Experimento verificado

785.840 pixels que não passaram por syscall

O PIXEL.ELF recebe um endereço e escreve row[x] = cor, como qualquer programa gráfico faz. A syscall só entra no fim, para dizer o que mudou.

superficie em 0x4320133120
largura: 836 pixels
altura: 940 pixels
passo de linha: 3344 bytes
superficie publicada e composta; pixels vieram de ring 3: OK

Como é. As rotinas de pixel do kernel escrevem num alvo: draw_target_base, draw_target_pitch e draw_target_window. window_select_64 aponta o alvo para a superfície da janela e zera a origem do console; moldura, atalhos e barra de tarefas empilham o alvo, desenham na tela e devolvem o que encontraram.

Por que assim. Guardar base e passo, e não um índice de janela, deixa graphics_fill_rect_64 e graphics_draw_glyph_64 ignorarem completamente a diferença entre os dois destinos. E guardar o alvo anterior, em vez de "voltar para a janela corrente", é o que permite uma rotina de moldura chamar outra sem que a de dentro estrague o trabalho da de fora.

O que muda. Uma janela pode ser movida, coberta e fechada sem que o conteúdo dela se perca: ele mora em RAM, não na tela. O que continua impossível é transparência, escala e duas superfícies por janela, então uma tarefa que desenha devagar mostra o quadro pela metade.

Camada essencial

Pixel não cabe numa conversa com o kernel.

Um quadro de 1920 × 1080 tem 8,29 MB. Mandar isso por syscall trinta vezes por segundo seriam 250 MB/s atravessando a fronteira, com conferência em cada travessia.

A saída é a mesma de todo sistema real: o kernel entrega o endereço uma vez, o programa escreve direto, e a syscall só carrega o retângulo que mudou.

Camada técnica

O dano do texto não pode ir para a caixa da tela.

cmp qword [draw_target_window], WINDOW_COUNT
jb .surface_damage

Quando o alvo é superfície, a coordenada é local e vai para a caixa daquela janela. Misturar as duas na mesma caixa marcaria o canto do desktop como sujo toda vez que alguém escrevesse uma letra.

É a mesma variável que o compositor consulta para saber que precisa forçar o back buffer antes de copiar: ele roda por interrupção e pode pegar uma tarefa no meio de uma linha.

O bug que o recurso encontrou

A rolagem nunca marcou dano, e acertava por acidente.

terminal_scroll_64 move o console inteiro com rep movsd, e mover pixel não passa por nenhuma rotina que marque dano. O único retângulo marcado era o da última linha.

Isso nunca apareceu porque as células escritas em seguida iam ampliando a caixa até cobrir quase tudo. Com o texto na superfície o acidente acabou, e a rolagem passou a marcar o console inteiro. O preço é real: ela ficou cara, e foi essa lentidão que expôs a corrida do item ao lado.

A corrida que a lentidão revelou

A fila de teclado tem dono no instante da leitura.

O roteiro do teste digitava run calc.elf, esperava 350 ms e apertava F2. Se o terminal ainda não tivesse lido o Enter quando o foco mudasse, quem recebia a tecla era o bloco de notas: o comando ficava ecoado na tela sem nunca executar.

A corrida sempre existiu. Ela ficou visível porque escrever caractere ficou mais caro. Nenhuma linha de kernel mudou para resolver: a espera do roteiro subiu, com o motivo escrito ao lado.

Estudar o marco completo

Marco 26 · Desktop visual

A área de trabalho nasce vazia, e o ícone abre.

O SOIA tinha janelas, mouse e programas desde antes, mas só havia um jeito de abrir alguma coisa: digitar o nome do arquivo. Agora a tela liga limpa, com atalhos na esquerda e uma barra embaixo. Clicar num atalho abre a janela; o terminal aparece com tudo o que já tinha escrito, e o monitor com o painel já preenchido, porque as tarefas nunca pararam de trabalhar. O mosaico do Marco 20 deu lugar a janelas flutuantes sobre o fundo, e window_open nasce zerado: as três tarefas do boot rodam com a janela escondida. A ordem da tabela é o z-order e o hit test varre de trás para frente. Todo clique registra pendência e o pulso do timer executa, pelo mesmo motivo do Ctrl+C do Marco 21.

Experimento verificado

Um programa aberto sem teclado nenhum

O teste move o cursor até o atalho, clica, fotografa a janela aberta, move até o x e clica de novo.

SOIA desktop: 4 atalhos no fundo, barra de tarefas e menu Iniciar; area de trabalho vazia no boot.
SOIA janela: TERMINAL.C / USERLAND mostrada pelo atalho; a tarefa nunca parou de escrever.
SOIA janela: CALC.ELF na janela flutuante 3; o mosaico continua vivo embaixo.
SOIA processo: CALC.ELF aberto pelo clique no dock.
SOIA janela: flutuante 3 fechada; desktop repintado e superficies recompostas.

Como é. As três janelas flutuantes já existem na tabela desde o boot, com posição, tamanho e superfície repartida; abrir é dar dono, copiar o título, limpar a superfície e desenhar. O botão de fechar grava pending_kill_task, exatamente o que o Ctrl+C faz, e a janela morre junto com o programa.

Por que assim. O botão não fecha a janela: ele encerra o programa. A ordem inversa deixaria na tela uma moldura com o conteúdo congelado de um processo que não existe mais. E alocar superfície no clique significaria pedir memória contígua com a tela dependendo do resultado.

O que muda. Dá para abrir, usar e fechar programa só com o mouse, e o que estava embaixo volta intacto. O que ainda não dá: redimensionar pela borda e abrir mais de três ao mesmo tempo. Arrastar e trazer para a frente entraram na quinta fatia.

Camada essencial

O garçom e o cardápio na parede.

O terminal é um garçom: você precisa saber o nome do prato e pedir com as palavras certas. Os atalhos e o menu Iniciar são o cardápio com fotos: você aponta.

Os dois levam ao mesmo prato. A lista de programas mora no kernel, e é a mesma que o SPAWN aceita: dois caminhos diferentes para executar arquivo seria um a mais para esquecer de proteger.

Camada técnica

Quem desenha por último é testado primeiro.

window_hit_test_64:
    mov rax, WINDOW_COUNT
.next:
    dec rax

As molduras são desenhadas na ordem da tabela, então as flutuantes cobrem as do sistema. A varredura do clique precisa ser a inversa, senão um clique numa janela de cima é atribuído à que ela está cobrindo.

Pelo mesmo motivo, os atalhos passaram de primeiro para último teste: agora qualquer janela passa por cima deles.

O bug que o dock encontrou

Processo sem pai vazava a vaga inteira.

O EXIT marcava a tarefa como zumbi e deixava a coleta para o WAIT do pai. Isso estava certo desde o Marco 16, porque todo processo tinha um pai que o pediu.

O programa aberto pelo clique não tem pai: ninguém vai chamar WAIT. Cada clique vazava uma vaga e uma janela flutuante, e em quatro cliques o desktop parava de abrir programa. Agora, sem pai, a vaga é coletada no próprio EXIT; com pai, ela continua zumbi de propósito.

O bug que a bateria verde escondeu

O cursor deixou um rastro de setas pela tela.

O present marca as duas posições do cursor, a velha e a nova, como danificadas. Com o texto na superfície, essa marcação passou a cair na caixa da superfície que estivesse sendo escrita no momento da interrupção, e o lugar antigo nunca era limpo.

A bateria estava verde: o cursor do teste parava dentro da janela do monitor, que se repinta duas vezes por segundo e apagava o rastro antes da captura. A asserção nova leva o cursor para um console ocioso e conta os pixels: uma seta tem exatamente 30.

E essa asserção nasceu certa pela metade, o que só apareceu no dia seguinte. Ver por que ela reprovou código correto

Arrastar sem repintar sessenta vezes por segundo

O contorno anda; a janela só muda de lugar no fim.

Mover a janela a cada pacote do mouse custaria um repintado do desktop a 100 Hz. Durante o arrasto o kernel desenha só um retângulo de contorno, direto no front buffer, como faz com o cursor. Ao soltar, a janela muda de lugar e o desktop é repintado uma vez.

É o que o Windows 95 fazia por padrão, e pelo mesmo motivo. O contorno some sozinho: o present marca as quatro tiras dele como danificadas e as recopia do back buffer no quadro seguinte.

Isso durou um dia. A janela inteira passou a seguir o mouse a 25 Hz, e o que tornou o repintado barato foi o papel de parede virar uma cópia. Ver o que mudou

Três estados, não dois

Minimizada sai da tela e fica na barra.

fechada     window_in_taskbar=0  window_open=0
minimizada  window_in_taskbar=1  window_open=0
na tela     window_in_taskbar=1  window_open=1

A primeira versão usava só um estado, e minimizar fazia o programa sumir da barra: sem botão e sem janela, a única forma de trazê-lo de volta era o atalho, o que transformava minimizar em fechar.

Para janela de programa há uma condição a mais: a tarefa dona precisa estar viva. Quando ela morre, o botão some sozinho, sem ninguém avisar a barra.

O limite honesto do compositor

Ele não recorta, então quem está por cima é reposto.

A superfície de cada janela é copiada inteira para a área dela, sem recortar contra as vizinhas. Uma janela de baixo que escreve o tempo todo, como o monitor, apagava a de cima duas vezes por segundo.

O conserto acontece na mesma passada: a moldura da janela de cima é redesenhada na hora, porque moldura não está em superfície nenhuma, e a superfície dela é marcada como suja, sendo composta em seguida na ordem de profundidade. Custa uma cópia a mais na área sobreposta, e nenhuma lista de retângulos.

Esconder não é encerrar

O mesmo botão, duas ações diferentes.

cmp rax, STATIC_WINDOW_COUNT
jae .close_program
mov [pending_hide_window], al

Fechar a janela do terminal, das notas ou do monitor esconde: as três tarefas são residentes e encerrá-las deixaria o sistema sem interface, que é a mesma regra do Ctrl+C desde o Marco 21. Fechar a janela de um programa aberto por clique encerra o programa, e a janela morre junto.

Por isso o hit test do botão devolve duas coisas: a janela e a tarefa dona. Quem decide é o chamador, e não um número sozinho.

O que a área de trabalho vazia prova

A janela fechada continua trabalhando.

As três tarefas do boot rodam com a janela escondida e escrevem na superfície delas o tempo todo. Clicar no atalho do monitor mostra o painel já preenchido, com os números que ele vinha calculando enquanto ninguém olhava.

É o teste mais direto de que a superfície é memória de verdade, e não um atalho para a tela: se fosse a tela, a janela escondida teria escrito no vazio.

Estudar o marco completo

31/07/2026 · depois do Marco 26

Um dia de acabamento, e o cursor que custava 8 MB.

A tela ganhou foto de fundo, o relógio passou a mostrar a hora de verdade e a janela passou a acompanhar o mouse enquanto você arrasta. E o ponteiro ficou leve, que é a parte que não se vê: ele estava obrigando o sistema a redesenhar meia tela a cada milímetro de movimento. Papel de parede em paleta de 256 cores lida do disco no boot e expandida para XRGB8888; hora e data do CMOS com espera de UIP e leitura dupla; dock de quatro entradas; window_drag_pending_64 a 25 Hz; e o cursor fora da caixa de dano, com reposição por cópia dirigida de 384 bytes.

A medida que decidiu tudo

O cursor saiu da caixa de dano

Mesma bateria, mesmo roteiro, medido pelo próprio monitor do SOIA.

Como é. A caixa de dano é um único min/max global: ela guarda o retângulo que contém tudo que mudou desde a última publicação. As duas posições da seta, a antiga para apagar e a nova, eram jogadas dentro dela. Agora mouse_cursor_restore_64 devolve ao front os 8 × 12 pixels da posição antiga copiando do back, e a caixa só carrega o que as janelas mudaram.

Por que assim. Não existe backing store para manter em sincronia: o back buffer nunca teve cursor, então ele já é o fundo verdadeiro daquela área. Guardar uma cópia do fundo seria uma segunda verdade sobre os mesmos pixels, e sincronia a mais para errar.

O que muda. Os 2.825 quadros que sumiram da conta não deixaram de acontecer: eles deixaram de copiar. São as publicações disparadas por pacote de mouse quando nenhuma janela mudou nada. O que continua impossível é o cursor custar zero: sem sprite de hardware, alguém tem que escrever esses pixels.

Camada essencial

A mesa e o post-it.

Para mover um post-it na mesa você limpa a marca onde ele estava e cola no lugar novo. O SOIA fazia isso pegando uma régua, medindo o retângulo que contém o post-it velho, o novo e qualquer papel que tivesse mudado na mesa, e redesenhando esse retângulo inteiro. Cem vezes por segundo.

Se um papel tinha mudado no canto oposto, o retângulo virava a mesa toda. Limpar o quadradinho do post-it não tem nada a ver com o resto da mesa, e é isso que a correção diz.

Camada técnica

A ordem das três operações é contrato.

surface_composite_pending_64   ; superficies -> back
mouse_cursor_restore_64        ; 384 B na posicao ANTIGA
cmp r8d, r10d
jae .compose                   ; caixa vazia pula a copia
                               ; ... copia a caixa ...
.compose:
mouse_cursor_compose_64        ; 384 B na posicao NOVA

Restaurar dentro do caminho da cópia deixa rastro no desktop parado, porque caixa vazia pula a etapa. Compor lá dentro faz o cursor sumir pelo mesmo motivo. E restaurar usando mouse_x apaga o lugar novo e deixa o antigo na tela.

O caminho errado, e ele ensinou mais que o certo

O teste reprovou código correto duas vezes.

A mesma correção foi implementada e revertida duas vezes, com a mensagem O cursor sumiu depois do trajeto pelo console: 0 pixels. Eu li isso como bug da implementação nova. Não era: o mesmo binário passava e falhava alternadamente.

antes de qualquer mudanca  original   falhou
backing store              alterado   falhou
copia dirigida             alterado   falhou
revertido                  original   passou
revertido                  original   falhou
revertido, isolado         original   passou

A causa era o teste tirar uma foto da tela. Entre a cópia da caixa de dano e a composição da seta, a tela realmente não tem cursor nenhum, e a foto às vezes cai exatamente aí.

A distinção que conserta a asserção

Ausência é instantânea; rastro é persistente.

Uma seta esquecida fica na tela até alguém repintar aquele retângulo, e ninguém repinta: o trajeto do teste termina no console ocioso do terminal. Já a ausência dura o tempo de uma cópia.

Então nenhuma amostra pode passar de 40 pixels, e basta uma mostrar a seta inteira. São três capturas espaçadas por 600 ms, e a prova de que a hipótese estava certa veio na primeira execução com o código novo:

Rastro: qemu-trail.ppm: 0 | qemu-trail.ppm.2: 30 | qemu-trail.ppm.3: 30

A primeira foto caiu no meio do present. O teste antigo teria reprovado ali, pela terceira vez, um código que está certo.

Papel de parede

256 cores, porque a imagem só tem 912.

cores distintas          23.217
cores a 5 bits/canal        912
erro medio com 256         0,8%
XRGB8888 puro          7,91 MiB
indice + paleta        1,98 MiB

O kernel não decodifica imagem: PNG e JPEG são o Marco 27, em ring 3. O conversor roda fora do build, em Python, para o build seguir sendo PowerShell puro. RLE foi descartado por medição: 95% dos pixels começam corrida nova.

Sem o arquivo, sem RAM contígua ou com dimensão diferente da tela, o fundo volta a ser cor sólida e a serial diz isso. Enfeite não derruba boot.

A decisão que mudou no mesmo dia

O cache de 8,1 MiB não se pagava, até o arraste existir.

A primeira versão desenhava sempre por lookup, e o argumento estava escrito no cabeçalho do arquivo: o desktop é repintado poucas vezes por sessão, então 8 MiB de cache não se pagariam num perfil de 64 MiB.

O arraste derrubou o argumento horas depois. Com a janela seguindo o mouse, o desktop voltou a ser repintado 25 vezes por segundo, e 2.073.600 iterações de lookup passam de 25 ms sob TCG: não cabe num quadro de 40 ms. Hoje a imagem é expandida uma vez no boot e desenhada com rep movsd.

O custo de uma operação depende de quantas vezes ela vai acontecer, e isso muda quando outra coisa muda. O comentário que justificava a decisão antiga sobreviveu à decisão por algumas horas.

Relógio

Esperar o UIP cair não basta.

0x70 = indice, 0x71 = dado
0x0A bit 7 = update in progress
0x0B bit 2 = BCD ou binario
0x0B bit 1 = 12 h ou 24 h

O chip atualiza os registradores uma vez por segundo, e ler no meio devolve minuto novo com segundo velho. Esperar o UIP cair não resolve, porque a atualização pode começar entre a checagem e a leitura: a defesa é ler duas vezes e comparar.

O contrato devolve EAX, EBX e ECX. A primeira versão usava AL, AH e BL, e o NASM recusou: em x86-64, instrução com prefixo REX perde o acesso a AH, BH, CH e DH.

Fuso não é assunto do kernel. O QEMU recebe -rtc base=localtime e entrega a hora local do host; sem isso o relógio mostra UTC.

O bug mais estranho do dia

O menu Iniciar desenhava a IVT do BIOS como nome.

O menu mostrava DOCK_ENTRY_PATH + 1, o caminho do executável sem a barra inicial. As três janelas do sistema têm PATH = 0, porque elas não lançam processo nenhum: para elas, o menu lia a partir do endereço físico 1, que é a tabela de vetores de interrupção do BIOS, e desenhava aquilo como texto.

O conserto é usar o LABEL, que toda entrada tem. A categoria é maior que o bug: ponteiro derivado de campo opcional precisa testar o campo antes de derivar. Sem o + 1, o sintoma seria nome vazio, e ninguém notaria diferença entre vazio e lixo.

O dock caiu de onze para quatro entradas: TERMINAL, NOTAS, MONITOR e CALCULADORA. Os outros sete programas continuam no disco e rodam por run nome.elf. E o modo janela do QEMU passou a usar SDL, porque o grab do GTK no Windows captura o teclado mas não confina o ponteiro: o cursor do host escapava pela borda e o do SOIA parava antes de alcançar os atalhos.

Estudar o dia completo

Marco 27 · fatia 1

Bytes de um arquivo viraram foto na janela.

A janela que era texto passou a mostrar uma imagem de verdade, guardada num arquivo do disco. Ainda não é PNG nem JPEG: é o formato mais simples que existe, aquele em que os pontinhos coloridos estão gravados um do lado do outro, sem nenhuma compressão. É o caminho inteiro funcionando, do disco até a tela. BMP de 24 e 32 bits, BI_RGB, aberto por FOTO.ELF em ring 3. O cabeçalho passa por cinco validações antes de virar conta, o ajuste à janela preserva a proporção com aritmética inteira, e o desenho lê uma linha por chamada com soia_read_file_range: o custo de memória é o passo de linha, e não a imagem.

Experimento verificado

Uma imagem de 642 × 482 numa janela de 968 × 568

O teste fotografa a tela e confere seis pontos por cor, em posições calculadas a partir do que o próprio programa relatou.

soia> run foto.elf
SOIA Foto 0.1 - a primeira imagem vinda do disco
BMP 642 x 482, 24 bits, linha de 1928 bytes
ordem das linhas: de baixo para cima; pixels em 54
superficie: 968 x 568; imagem ajustada para 756 x 568 em 106,0
desenhando linha a linha, direto do disco para a superficie.
imagem do disco composta na janela; ring 3 desenhou: OK

Como é. Para cada linha da janela o programa calcula qual linha do arquivo precisa e lê só ela, no deslocamento pixel_offset + linha × passo. O BMP guarda a última linha primeiro, então a conta inverte; o passo é arredondado para múltiplo de 4, porque o formato exige.

Por que assim. Carregar a imagem inteira caberia (929 KiB num heap de 16 MiB), e mesmo assim não é o que se faz: uma foto de 8 MiB custaria 8 MiB. Lendo por linha, o custo é o passo, 1.928 bytes, para qualquer tamanho de imagem. Isso só existe desde a fatia 4 do Marco 24, que deu leitura por deslocamento à VFS.

O que muda. O SOIA mostra imagem de arquivo. O que continua impossível é abrir PNG ou JPEG, que é o que uma foto costuma ser, e é a fatia 2 e a 3. Também não há canal alfa, nem filtragem na redução: a escala é vizinho mais próximo e serrilha.

Camada essencial

O rolo de filme e a janela.

Uma imagem no disco é uma tira longa de pontinhos em ordem, e a tela é um retângulo. Mostrar a foto é cortar a tira em linhas e pendurar cada uma na altura certa.

Duas coisas complicam, e as duas são do formato. O rolo está de trás para frente, porque o BMP guarda a última linha primeiro, herança de quem media a tela de baixo para cima em 1990. E cada linha tem um pedaço de sobra, porque o formato exige que ela ocupe um número de bytes múltiplo de quatro.

Camada técnica

Cabeçalho é dado hostil, e são cinco perguntas.

'BM'            senao nao e imagem
bfOffBits       depois dos dois cabecalhos
biWidth/Height  1 a 16.384; altura < 0 = topo primeiro
biBitCount      24 ou 32, e nada mais
biCompression   zero, ou os pixels nao sao pixels

A que mais importa é a segunda: deslocamento apontando para antes do fim dos cabeçalhos faz o programa desenhar o próprio cabeçalho como imagem, que é o começo de todo vazamento de memória adjacente.

E há uma sexta pergunta, que não está no cabeçalho: a última linha existe mesmo? O programa a lê antes de desenhar qualquer coisa. Sem isso, uma imagem truncada desenharia lixo da metade para baixo.

A prova que separa certo de quase certo

O marcador amarelo denuncia a imagem invertida.

A imagem de teste tem quatro quadrantes de cores diferentes e um quadrado amarelo no canto superior esquerdo. Os quadrantes provam que a imagem chegou; o marcador prova a ordem das linhas.

Se o programa lesse o BMP de cima para baixo, a imagem sairia espelhada na vertical, e os quatro quadrantes continuariam parecendo certos numa olhada rápida. No lugar do amarelo estaria o verde do quadrante de baixo, e é exatamente isso que o teste mede.

As dimensões são feias de propósito: 642 × 482. A largura em bytes não é múltiplo de quatro, então cada linha carrega dois bytes de enchimento, e nenhuma metade cai em número par. Uma imagem de 640 × 480 esconderia os dois erros.

O bug que a fatia encontrou, e não é de imagem

O diretório tinha 16 vagas; o sistema precisava de 18.

Com FOTO.ELF e FOTO.BMP, o volume passou a nascer com 16 arquivos. O diretório do SOIAFS1 era um setor de 512 bytes com entradas de 32, ou seja 16 entradas, e o sistema cria mais dois arquivos em uso: NOTE.TXT e GRANDE.BIN.

O sintoma foi o bloco de notas dizendo erro: NOTE.TXT nao foi salvo num disco com 29 MiB livres. Faltava linha no catálogo, e não espaço. O diretório passou a ter dois setores, e o build reserva duas vagas: ele recusa gastar as últimas, porque quem gasta só descobre em uso.

O dano silencioso da mesma mudança

Um literal perdeu o respaldo e ninguém viu.

; a montagem le o campo do superbloco
cmp dword [... BITMAP_LBA_OFFSET], FILESYSTEM_BITMAP_LBA

; a gravacao tinha o numero na mao
mov eax, FILESYSTEM_START_LBA + 2

Crescer o diretório empurrou o bitmap do LBA 2 para o 3. A montagem passou a exigir 3; a gravação continuou mandando para o 2. O comentário do arquivo até defendia o literal, dizendo que o 2 tinha respaldo porque a montagem recusava outro valor. O respaldo existia, e sumiu no instante em que a constante mudou.

O sintoma foi cruel. O arquivo era criado, o diretório ia ao disco, o conteúdo voltava intacto no boot seguinte, e só o bit daquele setor ficava zerado no bitmap gravado: o alocador entregaria o mesmo setor a outro arquivo depois. Nenhuma mensagem de erro. Quem pegou foi o teste de persistência, que confere o bitmap além do conteúdo.

O que a fatia deixou pronto para o PNG

O decodificador entra no meio, e nada mais muda.

O caminho está inteiro: arquivo no volume, leitura por deslocamento, validação de cabeçalho, ajuste à janela, escrita na superfície e composição. A fatia 2 troca uma coisa só, entre a leitura e o desenho: onde hoje há uma troca de ordem de bytes, entra o inflate.

E ele roda em ring 3 por decisão de projeto. Decodificador de mídia é vetor clássico de invasão, e no SOIA uma falha dele encerra a tarefa culpada sem derrubar o sistema, desde o Marco 21.

Estudar o marco completo

Roteiro

Uma camada de cada vez.

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.

  1. 01
    Setor de bootcontrole recebido
    concluído
  2. 02
    Segundo estágiomais de 512 bytes
    concluído
  3. 03
    Modo protegidoA20 + GDT + 32 bits
    concluído
  4. 04
    Long mode64 bits
    concluído
  5. 05
    Kernel mínimotela + serial
    concluído
  6. 06
    Interrupçõestimer + teclado
    concluído
  7. 07
    MemóriaE820 + páginas de 4 KiB
    concluído
  8. 08
    Processosring 3 + preempção + syscall
    concluído
  9. 09
    ArquivosATA + SOIAFS1 + VFS
    concluído
  10. 10
    Espaço de usuário terminal + shell em ring 3 ◆ marco importante
    concluído
  11. 11
    Interface gráfica desenho + janelas ◆ marco importante
    concluído
  12. 12
    Plataforma de desenvolvimento C freestanding + ELF + WHPX opcional ◆ marco importante
    concluído
  13. 13
    Terminal + bloco de notas primeiros aplicativos em C ◆ marco importante
    concluído
  14. 13.1
    Monitor do sistema CPU virtual + RAM + ATA
    concluído
  15. 14
    Rede básicaNIC + Ethernet + ARP + IPv4
    concluído
  16. 15
    Persistênciadisco fixo + escrita + blocos livres
    concluído
  17. 16
    Processos dinâmicosspawn + exit + wait + CALC.ELF
    concluído
  18. 17
    Heap + libc mínimabrk + malloc + free + HEAP.ELF
    concluído
  19. 18
    IPC + serviçosmensagens + handles + IPC.ELF
    concluído
  20. 19
    Rede útilDHCP + UDP + DNS + TCP + NET.ELF◆ marco importante
    concluído
  21. 20
    Desktopmouse + composição + eventos + janelas com foco◆ marco importante
    concluído
  22. 21
    Robustez32 vetores de exceção + FAULT.ELF + bateria hostil
    concluído
  23. 22
    Plataforma modernaUEFI + ACPI + APIC; o mesmo kernel nos dois boots
    concluído
  24. 21.3
    Pilha por ISTIST1 + double fault + Ctrl+C
    concluído
  25. 23
    Precisão e tempoFPU/SSE com fxsave + LAPIC timer calibrado
    concluído
  26. 24
    Capacidadebinário variável + heap em MiB + fronteira de 64 KiB + volume de 32 MiB
    concluído
  27. 25
    Superfície e compositorpixel por janela + caixa de dano + texto do kernel na superfície
    concluído
  28. 26
    Desktop visualatalhos + barra de tarefas + menu Iniciar + janelas que arrastam, minimizam e fecham◆ marco importante
    concluído
  29. 28
    Transporte virtioPCI capabilities + virtqueue + virtio-gpu
    planejado
  30. 29
    VídeoMJPEG + relógio de apresentação + virtio-snd
    meta · vídeo
  31. 30
    Rede de verdadevirtio-net com IRQ + TCP em fluxo + socket
    planejado
  32. 31
    HTTPcliente 1.1 + chunked + gzip; SOIAGET.ELF
    planejado
  33. 32
    TLS 1.3X25519 + AES-GCM + X.509 em ring 3
    planejado
  34. 33
    Motor de renderizaçãoHTML + CSS + layout + texto proporcional
    meta · site www
  35. 34
    Distribuição do SOIAwrite-combining por PAT + Launcher.exe + mídia bootável
    meta de entrega
  36. +
    Trilhas opcionaisJavaScript + fonte vetorial + SMP + USB
    aberto

Próximo · Marco 27, fatia 2

Falta o decodificador, e só ele.

O caminho inteiro já funciona com BMP: arquivo no volume, leitura por deslocamento, cabeçalho validado, ajuste à janela, superfície e compositor. A fatia 2 troca uma coisa só, entre a leitura e o desenho: onde hoje há uma troca de ordem de bytes, entra o inflate do PNG. E ele roda em ring 3 porque decodificador de mídia é o vetor clássico de invasão, onde uma falha encerra só a tarefa culpada.

Cabeçalho de arquivo é dado hostil. largura × altura × bytes precisa de checagem de estouro e de teto antes de qualquer alocação. Um PNG que declara 65.535 × 65.535 pede 17 GB.

Decodificador nunca entra no kernel. Ele roda em ring 3, e o Marco 21 já paga o isolamento: falha de programa encerra a tarefa e o desktop continua.

Duas dívidas do compositor ficaram registradas no Marco 26: recorte por lista de retângulos, que acaba com a cópia dupla na área sobreposta, e superfície redimensionável, que é o que falta para maximizar mostrar mais conteúdo.

Estudar o Marco 13 completo Estudar o Marco 13.1 completo Estudar o Marco 14 completo Estudar o Marco 15 completo Estudar o Marco 16 completo Estudar o Marco 17 completo Estudar o Marco 18 completo Estudar o Marco 19 completo Estudar o Marco 20 completo Estudar o Marco 21 completo

Ferramenta de desenvolvimento

A IA ajuda a construir; não executa no SOIA.

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

Termos sem mistério.

Abra um conceito quando precisar. O glossário completo permanece na documentação técnica.

Ver glossário em Markdown
01 Kernel

Simples: a autoridade que permanece administrando o computador.

Técnico: código em ring 0 que controla memória, hardware e serviços.

02 Ring 3

Simples: a área limitada em que programas comuns executam.

Técnico: CPL 3, sem acesso a páginas supervisoras ou instruções privilegiadas.

03 Syscall

Simples: um pedido controlado que um programa faz ao kernel.

Técnico: travessia validada por INT 0x80, com número e argumentos em registradores.

04 Terminal

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.

05 Shell

Simples: o programa que entende o comando digitado.

Técnico: tarefa em ring 3 que analisa comandos e usa syscalls para pedir serviços.

06 VFS

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.

07 Caixa de dano

Simples: o retângulo que contém tudo que mudou na tela desde a última vez que ela foi atualizada.

Técnico: um único mínimo e máximo global, e não uma lista: no pior caso copia demais, nunca de menos. Quem entra nela custa a união com todo o resto, e foi por isso que o cursor saiu dela.

08 Cabeçalho hostil

Simples: os números no começo de um arquivo, escritos por quem gerou o arquivo, e que podem estar errados de propósito.

Técnico: todo campo vira conta, tamanho ou deslocamento, então cada um é validado antes do uso: faixa, tipo e coerência com o tamanho real do arquivo. É a razão de o decodificador rodar em ring 3.