Marco 22 · Plataforma moderna concluída

O SOIA acorda de dois jeitos e sabe se desligar.

Pelo BIOS, a partir de um setor de 512 bytes que constrói tudo do zero, ou por firmware UEFI, que entrega o controle já em 64 bits. O mesmo kernel.bin roda nos dois. O PIC de 1976 saiu, e encerrar o sistema deixou de ser fechar a janela do emulador.

Ver o sistema sobreviver

2 MiB
disco virtual fixo
8 ELF64
apps C em ring 3
2 GiB
perfil de desenvolvimento
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

4.096 caixas de 512 bytes.

2.097.152 bytesdisco virtual de 2 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

Os 26 KiB já usados deixavam apenas 2,3 KiB livres. Os quarenta setores novos cabem o mouse e abrem espaço para eventos, janelas e composição. O último setor continua preenchido até fechar sua caixa de 512 bytes.

bootloader 96 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 0x16FFF para 0x1BFFF. O teto é 0x20000, onde começa a IDT: por isso o limite de 128 setores. Aqui cada endereço nomeia um byte; o kernel também gerencia a RAM em páginas de 4 KiB.

Em uma frase

O disco ganhou quatro caixas de kernel; na RAM, o mesmo terreno inicial recebeu 6.144 bytes a mais no final.

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 vinte e oito 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

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. Agora os três programas dividem essa tela e trabalham ao mesmo tempo. O dispositivo auxiliar do 8042 envia pacotes de três bytes pela IRQ12; o kernel estende o sinal de nove bits, aplica limites, compõe o cursor no front buffer e entrega estado, eventos e geometria de janela pelas syscalls 25, 26 e 27.

Experimento verificado

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 a caixa de dano faz cada publicação extra custar 384 bytes.

Fila de eventos. Trinta e duas vagas em anel. Cheia, o kernel descarta o novo e conta o descarte, em vez de sobrescrever um evento que ninguém leu ainda.

Escrever deixou de exigir foco. A syscall WRITE recusava quem não estava na frente. Agora ela aponta o desenho para a janela de quem chamou, e as três ficam vivas.

Fatia 4 · o gerenciador de janelas

O 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 dividem a tela sem se cruzar, e é justamente isso que impede uma tarefa de pintar pixel de outra. Janelas que se cobrem exigiriam uma superfície por janela e um compositor empilhando em ordem z: mais 2 MiB por janela e uma cópia extra por quadro. É a próxima dívida, não uma que esta fatia tenha pago.

Camada essencial

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.

lea rax, [double_fault_stack_top]
mov [tss64 + TSS_IST1_OFFSET], rax

mov byte [abs IDT_BASE + (8 * 16) + 4], 1

Duas metades distantes: a porta do vetor 8 guarda o índice, o TSS guarda o ponteiro. Ter uma sem a outra não serve de nada, e é por isso que a prova serial sai depois do TSS.

Double fault nunca devolve o controle. Ele contraria a regra do RPL: mesmo com CS de ring 3, o vetor 8 vai para parada declarada. Chegar nele significa que o quadro da falha original se perdeu, e encerrar só a tarefa daria a impressão de um sistema saudável que esqueceu o acidente.

Duas verdades no mesmo endereço. A fatia encontrou um bug que não era dela: PAGE_DIRECTORY_APIC, do Marco 22, e FILESYSTEM_SUPERBLOCK_BUFFER, do Marco 9, apontavam ambos para 0x00041000. Montar o page directory apagava o diretório lido do disco; gravar um arquivo apagava o mapeamento do LAPIC. O sintoma foi o bloco de notas recusando salvar em disco novo, sem nenhuma pista de memória.

Abaixo de 1 MiB nada é alocado. Todo endereço nessa faixa é reserva manual por constante, sem verificação nenhuma: o assembler aceita, o boot passa, e o dano aparece longe da causa.

Fatia 3 · Ctrl+C

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.

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. 25
    Superfície e compositorpixel por janela + dano + janelas sobrepostas
    planejado
  27. 26
    Imagensinflate + PNG + JPEG; FOTO.ELF
    meta · fotos
  28. 27
    Transporte virtioPCI capabilities + virtqueue + virtio-gpu
    planejado
  29. 28
    VídeoMJPEG + relógio de apresentação + virtio-snd
    meta · vídeo
  30. 29
    Rede de verdadevirtio-net com IRQ + TCP em fluxo + socket
    planejado
  31. 30
    HTTPcliente 1.1 + chunked + gzip; SOIAGET.ELF
    planejado
  32. 31
    TLS 1.3X25519 + AES-GCM + X.509 em ring 3
    planejado
  33. 32
    Motor de renderizaçãoHTML + CSS + layout + texto proporcional
    meta · site www
  34. 33
    Distribuição do SOIAwrite-combining por PAT + Launcher.exe + mídia bootável
    meta de entrega
  35. +
    Trilhas opcionaisJavaScript + fonte vetorial + SMP + USB
    aberto

Em andamento · Marco 22

A plataforma moderna começa apagando o boot antigo.

UEFI não acrescenta uma peça: ele remove a metade de baixo do sistema. O firmware entrega o controle já em long mode, e com isso somem o MBR de 512 bytes, o modo real, a A20, a INT 13h, o E820 e o VBE. Depois vêm ACPI, para o SOIA parar de adivinhar onde o hardware está, e o APIC, para aposentar um controlador de interrupções de 1976.

O teto de 128 setores some. Ele existia porque uma única INT 13h carregava o kernel em 0x10000 e a IDT começa em 0x20000. É consequência do BIOS, não uma lei.

Os dois caminhos convivem. O boot BIOS continua no repositório: ele é o material dos marcos 1 a 4, e a comparação entre os dois é o melhor conteúdo deste marco.

SMP fica para depois. Vários núcleos invalidam a premissa de que IF=0 dispensa trava, e isso muda o contrato do kernel inteiro. Uma coisa de cada vez.

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.