Bloco 01 · nível 0
Tudo começa com energia passando ou não passando.
Antes de existir programa, existe um fio com corrente e um fio sem corrente. O resto é consequência disso.
O bit é uma faixa de tensão amostrada na borda de um pulso de clock. Toda decisão de barramento, enquadramento e largura nasce dessa restrição elétrica.
Os três materiais e por que ouro só nos contatos
Ordem de condutividade: prata > cobre > ouro > alumínio. A
velocidade do sinal é praticamente a mesma em todos, uma fração da
velocidade da luz. O que muda é a resistência. Ouro é
armadura, não performance.
O bit é voltagem, e um fio só tem uma por vez
bit 1~1,0 a 1,2 V
bit 00 V
É código Morse elétrico rodando bilhões de vezes por segundo.
A amostragem acontece na borda do clock; fora dela o nível do fio é irrelevante.
Um fio não pode ter duas voltagens ao mesmo tempo.
Essa limitação é a origem de tudo o que vem depois: para mandar
8 bits por um fio só você precisa de 8 pulsos em sequência
(serial); para mandar os 8 de uma vez, precisa de 8 fios
(paralelo).
O clock: uma lasca de quartzo e um multiplicador
Na placa-mãe existe um oscilador de cristal. O quartzo recebe
energia, vibra fisicamente numa frequência absurdamente constante
e gera uma onda elétrica regular. A CPU não roda nessa frequência:
ela multiplica o ritmo com circuitos PLL
(Phase-Locked Loop). Um cristal de 100 MHz vira 4,6 GHz com
multiplicador 46×.
A cada batida, bilhões de transistores abrem ou fecham ao mesmo tempo.
A escala real: quase não existe "fio"
Num computador moderno o fio tradicional só aparece na fonte. O
resto é trilha impressa e interconexão nanométrica. Uma placa-mãe
de desktop é um sanduíche de 4 a 8 camadas de cobre (servidor
passa de 24) com 10.000 a 50.000 trilhas
independentes, ligadas entre camadas por microfuros
chamados vias.
Dentro do processador existem de 10 a 15 andares microscópicos de
fiação de cobre. Esticadas em linha reta, as interconexões de um
único chip moderno passariam de 20 a 30 km.
Dos 288 pinos de um DIMM, só 64 são o barramento de dados. O resto
é endereço, controle, clock e, na maioria, energia e aterramento:
em frequência altíssima o pente precisa de muitos pontos de
alimentação para o sinal não degradar.
Serial e paralelo: por que a RAM não tem start bit
Serial assíncrona · COM1
Um fio só, sem clock compartilhado. Por isso precisa de enquadramento.
1 start + 8 dados + 1 stop
= 10 bits no fio por byte
Paralela síncrona · RAM
Uma trilha dedicada só ao clock liga o controlador ao pente. O ritmo é combinado, ninguém avisa começo nem fim.
64 trilhas lado a lado
= 8 bytes por pulso
O pedido de leitura é um balcão: a CPU forma o endereço nos fios de
endereço, sinaliza "quero LER" num fio de controle, a RAM procura
nos capacitores e joga os sinais nos 64 fios de dados, e no pulso
seguinte a CPU puxa. Em DDR5 o mesmo DIMM é quebrado em dois
subcanais independentes de 32 bits; com ECC são 72 bits, 64 de dado
e 8 de correção.
No SOIA
O enquadramento 8N1 a 38.400 baud não é teoria aqui: é como toda
mensagem de prova do kernel sai para o teste automatizado. O
laboratório de sinais do Playground desenha esse quadro bit a
bit.
Abrir o laboratório de sinais ↗
Registradores são os seus bolsos
Se a RAM é um armazém do outro lado da cidade, os registradores são
os bolsos. A CPU não consegue fazer conta direto na
RAM: para somar dois números da memória ela busca o
primeiro para um registrador, o segundo para outro, e só então
manda a ALU somar.
O mesmo bolso, fatiado por 40 anos de compatibilidade
RAX · 64 bits8 bytes
EAX · 32 bits4 bytes
AX · 16 bits2 bytes
AL · 8 bits1 byte
Detalhe do x86_64 que economiza instruções: escrever em
EAX zera automaticamente a metade alta do
RAX.
O que 0xC3 dispara em frações de nanossegundo
0x90 (144) → NOP (não faça nada)
0xC3 (195) → RET (retorne da função)
0xB8 (184) → MOV EAX, <imediato de 32 bits>
- Fetch. O RIP aponta um endereço. A CPU busca o byte que está lá.
- Decode. A Unidade de Controle reconhece o opcode e dispara o microcódigo gravado em hardware.
- Execute. Os sinais elétricos percorrem os circuitos.
A cadeia exata do RET: a Unidade de Controle lê o
RSP, busca o endereço guardado no topo da pilha, copia
para o RIP e manda a ALU somar 8 ao RSP,
apagando aquele endereço. No pulso seguinte a CPU busca no novo
endereço, e o programa "voltou".
Instrução de tamanho variável e o colchete
MOV EAX, 5 ; vira: B8 05 00 00 00
0xB8 é o opcode e já diz "preciso ler os próximos 4
bytes". 05 00 00 00 é o valor 5 em little-endian.
RET cabe em 1 byte porque não precisa de parâmetro.
Uma instrução x86_64 vai de 1 a 15 bytes.
MOV RAX, 5 ; coloque o NÚMERO 5 dentro de RAX
MOV RAX, [5] ; vá ao ENDEREÇO 5, pegue o que está lá
O Assembly que quase todo programa usa
Movimentação
MOV copia. PUSH empurra para o topo da pilha. POP tira do topo.
Matemática
ADD, SUB, e INC/DEC, que são o i++ dos loops.
Lógica
AND, OR, NOT, XOR.
Fluxo
CMP altera as flags; JE, JNE, JG, JL pulam com base nelas. CALL e RET fazem função.
Kernel
INT n dispara interrupção de software. SYSCALL é o substituto moderno.
Por que xor eax, eax e não mov eax, 0.
Não é só velocidade: são 2 bytes (31 C0) contra 5
(B8 00 00 00 00), o que poupa cache de instrução. E os
processadores reconhecem o padrão como zeroing idiom: o
renomeador de registradores resolve na hora, quebrando a cadeia de
dependência com o valor antigo sem gastar porta de execução.
Por que SYSCALL é mais rápido que INT 0x80.
O INT obriga a CPU a ler a IDT na memória, validar
privilégio de gate e trocar a pilha via TSS. O SYSCALL
pega o handler direto de registradores internos (MSRs
LSTAR e STAR) e mascara flags com
SFMASK, sem tocar na memória. O retorno é
SYSRET em vez de IRETQ.
No SOIA
O SOIA usa INT 0x80 de propósito, mesmo sendo o
caminho mais lento: a porta com DPL 3 e o IRETQ
tornam a troca de privilégio visível no código, que é o
objetivo de um sistema educacional. Migrar para
SYSCALL é otimização, não correção.
Tarefas, ring 3 e syscalls ↗
Bloco 03 · nível 1
A CPU passa a maior parte da vida esperando.
O processador é um dragão que come mil quilos por segundo, e a RAM é um garçom que traz oitenta na bandeja.
A diferença entre a vazão interna e a da memória é de mais de uma ordem de grandeza. O cache existe para amortecer exatamente esse desequilíbrio.
4,6 GHz não são bits, são batidas
1 ciclo a 4,6 GHz = 1 / 4.600.000.000 = 0,217 ns
A CPU não faz nada "entre" duas batidas: o universo dela avança
tique por tique. Uma divisão inteira que custa 15 ciclos leva
15 × 0,217 = 3,26 ns. Nesse tempo a luz percorre pouco
menos de um metro. No intervalo em que a luz sai da lâmpada do teto
e chega ao teclado, a divisão já acabou.
Ciclos por instrução (CPI): MOV e ADD
custam 1 ciclo; DIV inteira custa de 10 a 15.
Vazão teórica e o gargalo real
Por núcleo: 4.600.000.000 × 64 bits ≈ 36,8 GB/s
8 núcleos: 36,8 × 8 ≈ 294,4 GB/s
E ainda subestima: cada núcleo é superescalar e despacha de 4 a 6
instruções por ciclo, e SIMD (AVX2 em 256 bits, AVX-512 em 512)
opera em blocos maiores numa pancada só. Com código otimizado, a
capacidade interna passa de 1 TB/s.
L1, L2 e L3: supermercado, geladeira e a mão
A RAM é o supermercado, longe e gigante. O L3 é a geladeira da
cozinha. O L1 é o que você já está segurando na mão. Se o núcleo 1
puxou uma textura da RAM, o núcleo 5 pega a cópia do L3 sem chamar
o garçom lento.
Por que não colocam 16 GB de cache. DRAM usa
1 transistor + 1 capacitor por bit: denso e barato, mas precisa de
refresh constante, por isso é lento. SRAM usa 6
transistores: responde imediatamente, mas ocupa 6× mais espaço.
16 GB de SRAM daria um processador do tamanho de um prato,
esquentando como forno. Para referência do outro extremo: o AMD
EPYC 9684X tem 1,1 GB de L3, o bastante para rodar
um sistema operacional antigo inteiro sem plugar um pente de RAM.
O que o programador controla: localidade de referência
Quem administra o cache é o hardware, 100%. O
sistema operacional não move dados entre níveis. O único poder do
código é escrever de um jeito de que o controlador goste.
Acesso sequencial
Percorrer um array em ordem. O controlador percebe o padrão e busca os próximos dados antes de você pedir (prefetch). O código voa.
Acesso espalhado
Posição 10, depois 5.000, depois 2. Impossível de prever. Cada acesso é um cache miss e o sistema fica esperando a RAM.
Quando você pede 1 byte, o controlador traz uma cache line
inteira de 64 bytes. Ignorar isso é o que torna
uma matriz percorrida na ordem errada dezenas de vezes mais lenta
que a mesma matriz na ordem certa.
No SOIA
O monitor mede atividade por TSC virtual, não por cache: o QEMU em
TCG não emula hierarquia de cache, então nenhum número desta
página é observável de dentro do SOIA. É teoria de hardware real,
útil para quando o projeto sair do emulador.
Bloco 04 · nível 1 e 2
Prédios idênticos numa rua, e um guarda na fronteira.
Cada programa acha que é dono do prédio. O hardware é que redireciona cada um para o endereço real correto.
A MMU traduz endereço virtual para físico consultando a árvore de tabelas apontada por CR3. Trocar de processo é trocar CR3, e é só isso que garante o isolamento.
Página, offset e por que o offset sozinho não diz nada
- Página (bloco): o prédio. Na x86_64, exatos 4 KiB = 4.096 bytes.
- Offset: o número do apartamento, de 0 a 4.095.
- Cada apartamento guarda 1 byte, não 2.
Bloco A começa no endereço físico 0x1000 (4096)
Ler o offset 5 do bloco A → 4096 + 5 = endereço físico 4101
"Vá ao offset 10" não significa nada sozinho: o offset 10 existe em
milhares de blocos, e cada um é um lugar físico diferente.
Por que 4.096, e por que você não pode mudar
Definido em 1985 com o Intel 80386, por equilíbrio. Página menor
(512 B) faria a própria tabela de mapeamento consumir memória
absurda. Página maior (64 KiB) faria um programa de 100 bytes
desperdiçar quase 64 mil (fragmentação interna).
A MMU é fabricada no silício para 4 KiB: não existe página de 3 KiB.
O que a arquitetura permite são huge pages de
2 MiB ou 1 GiB, configurando as tabelas de um jeito específico para
reduzir pressão de TLB.
Page fault: o alarme, passo a passo
- A tentativa. Código em Ring 3 executa algo como
mov rax, [0x90000000].
- A checagem. A MMU traduz e percebe que a página não está mapeada, ou que o privilégio está errado.
- O bloqueio. A CPU aborta a instrução antes de qualquer dado ser lido ou modificado.
- A evidência. O hardware salva o endereço culpado no registrador
CR2.
- A interrupção. Eleva para Ring 0, empurra um código de erro na pilha e pula para o handler da interrupção 14.
Nem todo page fault é fatal. Dois usos legítimos e
muito comuns: demand paging, em que o kernel promete
memória sem mapear nada e só mapeia de verdade no primeiro toque,
reexecutando a instrução sem o programa perceber; e W^X,
em que a área de dados não tem permissão de execução, então um
exploit que injete código na pilha dispara o fault com o bit I/D
ligado. No Linux, o fault fatal chega ao usuário como
Segmentation Fault.
No SOIA
O W^X já é real: o loader ELF64 exige texto RX e dados/BSS RW+NX,
as pilhas também são NX, e BROKEN.ELF existe no disco
só para o teste provar a recusa a cada boot. O que ainda não
existe é o demand paging: o SOIA mapeia tudo na criação
do processo, e o BRK mapeia na hora do pedido.
Gerenciamento de memória ↗
Os 64 bits que não são 64
O x86_64 não implementa os 64 bits de endereço. A paginação de
4 níveis usa 48 bits canônicos (256 TiB de espaço
virtual); com 5 níveis (LA57) sobe para 57 bits. Os bits de cima
precisam ser cópia do bit 47, senão o endereço é "não canônico" e
gera falha. E o limite de 4 GiB dos 32 bits era contornável no
endereço físico via PAE, chegando a 64 GiB. O que
continuava travado em 4 GiB era o espaço virtual de cada
processo.
Bloco 05 · nível 1
O mesmo byte é uma letra, uma cor ou um comando.
Na memória, código, texto, senha e imagem estão todos misturados. O byte não vem com etiqueta dizendo o que é.
Arquitetura de Von Neumann: código e dado dividem o mesmo espaço endereçável. A distinção é feita por contexto de acesso e por permissão de página.
Um byte, três significados
o byte110000110xC3 · 195
como textoÃLatin-1
como corcinzaíndice de paleta
como códigoRETvolte da função
A CPU resolve isso puramente por contexto e posição,
guiada pelo RIP. Se o RIP aponta para ali, é comando. Se uma
instrução manda copiar aquele endereço para a tela, é número puro.
A consequência: permissão explícita por página
É exatamente porque a CPU pode confundir texto com comando que se
marca cada página. Sem isso, o clássico estouro de buffer
escreveria código na pilha e desviaria o RIP para lá.
No SOIA
Esta é uma das poucas partes onde o SOIA já está no nível de um
sistema real. Os nove aplicativos C são linkados com dois
segmentos obrigatórios, e o kernel recusa qualquer ELF que não
respeite a política.
Plataforma de desenvolvimento ↗
Bloco 06 · nível 2
A tela é uma tinta burra. A inteligência está na RAM.
O pixel não faz ideia se faz parte de um botão. Tudo que parece clicável é matemática rodando na CPU.
O framebuffer é memória linear de cor. Hit test, foco e ordem de janelas são estruturas na RAM principal, sem qualquer suporte do hardware de vídeo.
Peso na memória e tráfego por segundo
peso = largura × altura × bytes_por_pixel
tráfego = peso × taxa_de_atualização
Com 1 byte por pixel não existe cor, existe índice:
o byte aponta uma entrada numa paleta (LUT) que o hardware consulta
ao varrer. Economizava muita RAM quando memória era cara.
Capacidade não é largura de banda
Este é o erro conceitual mais fácil de cometer:
uma tela FHD a 60 Hz não come 474 MB da sua RAM.
Você guarda um quadro por vez, 7,9 MiB fixos. Os 474 MiB/s
são fluxo passando pelos fios, não espaço reservado.
A RAM é uma caixa d'água de 4.000 litros. O framebuffer é um balde
de 8 litros dentro dela. A placa de vídeo é uma bomba que puxa o
balde inteiro 60 vezes por segundo. No fim de um segundo passaram
480 litros pelo cano, mas o balde continua ocupando 8 litros.
É pelo segundo número que existem GPUs com memória dedicada: uma
DDR5 comum engasgaria carregando CPU, sistema e mais 7,4 GB/s de
vídeo ao mesmo tempo.
Hz contra FPS: tearing, V-Sync e double buffering
Hz · o hardware
Velocidade fixa e burra. A 60 Hz o monitor lê o framebuffer inteiro a cada 16,6 ms, tenha mudado algo ou não.
FPS · o seu código
A velocidade em que o software consegue calcular e gravar novas cores no framebuffer.
V-Sync resolve o tearing travando o software até o
monitor terminar de varrer. Double buffering
resolve o piscar: você mantém dois framebuffers, desenha o próximo
quadro no de trás e avisa o hardware para trocar. Custo: o dobro de
memória, o que é irrisório.
No SOIA
Nada disso existe ainda. O kernel desenha direto no framebuffer
único, e é por isso que o cursor do mouse precisa guardar e
devolver os pixels que cobre. Double buffering é a fatia final do
Marco 20: com 1920×1080×4, o buffer de trás custa 7,91 MiB, ou
2.025 páginas de uma vez.
Desktop: mouse, cursor e eventos ↗
A tela é cega: como um clique vira ação
- Desenho. O SO pinta uma caixa nas coordenadas X de 50 a 150.
- Movimento. O mouse dispara uma interrupção de hardware, a IRQ12 no PS/2: "me moveram para a direita".
- Cursor. O kernel apaga a setinha antiga do framebuffer e redesenha na posição nova.
- Hit test. Ao apertar o botão, outra interrupção. O kernel pergunta: em que X,Y está o cursor?
- Lógica. Percorre a lista de janelas na RAM: "cursor em X=100, botão desenhado de 50 a 150, logo o clique foi no botão".
Tudo que é visual é pintura temporária. A colisão e as regras são
números que a CPU rastreia na memória principal.
No SOIA
Os passos 1 a 3 já funcionam desde o Marco 20. Os passos 4 e 5
ainda não: a syscall 25 entrega posição e botões, mas não existe
lista de janelas para percorrer nem fila de eventos ordenada.
O bug da tela preta, e a dívida que o SOIA tem aqui
Quem administra o cache é o hardware, mas o SO tem uma autoridade:
dizer quais áreas podem usar cache, por bits na
tabela de páginas. Com cache ligado no framebuffer, ao mandar
"pinte o pixel de vermelho" a CPU escreve no L1 e deixa lá. O
código roda perfeito e a tela continua preta,
porque a placa de vídeo não tem acesso ao L1 do processador.
Desligar cache com PCD resolve, mas é o caminho mais
lento possível: cada escrita vira uma transação isolada no
barramento. Kernels reais marcam o framebuffer como
write-combining via PAT ou MTRR, acumulando
escritas num buffer interno e despejando em rajadas. A diferença ao
preencher a tela chega a uma ordem de grandeza.
No SOIA · dívida conhecida
O SOIA mapeia as três páginas de 2 MiB do framebuffer com flags
0x83: presente, gravável e página grande. Os bits
PWT e PCD ficam em zero, ou seja,
memória de vídeo tratada como RAM comum, em
write-back.
bit 0 P = 1 presente
bit 1 RW = 1 gravável
bit 7 PS = 1 página grande de 2 MiB
bit 3 PWT = 0 sem write-through
bit 4 PCD = 0 cache LIGADO
Funciona porque o QEMU em TCG não emula hierarquia de cache: o
"hardware de vídeo" lê a mesma memória do host que a CPU emulada
escreveu. É um teste que passa por causa do ambiente, não por
causa do código. Vai doer quando o SOIA sair para hardware real,
no Marco 22.
Bloco 07 · níveis 0 a 4
TypeScript não vira C. São estradas diferentes.
A base ficou tão escondida que a máquina parece mágica. Construir um SO do zero é descer até o porão e ver as engrenagens.
Cada camada existe para poupar esforço humano, e cada uma cobra em previsibilidade. Um kernel se escreve embaixo justamente para não pagar esse pedágio.
A pirâmide
O hexadecimal não é um degrau. É lente de aumento
sobre o nível 1. 11000011 e 0xC3 são o
mesmo byte, só que um é confortável de ler. E
HTML não é linguagem de programação: é texto de
marcação dizendo "desenhe uma caixa aqui".
As três estradas até o silício
1 · Compilada
C, C++, Rust
Um compilador traduz para Assembly e depois binário, empacotando num executável definitivo.
Escrever um livro em inglês, dar a um tradutor e receber o livro impresso em português. Você entrega pronto para a CPU ler.
2 · Interpretada
Python, JavaScript
O texto nunca vira executável. Um motor já compilado (V8, CPython), escrito em C++, lê o seu texto e dispara os comandos binários.
Um intérprete simultâneo na reunião. Você fala JS, ele sussurra binário no ouvido do processador.
3 · Transpilada
TypeScript
Não existe para o computador. Nenhuma CPU nem motor roda TS. Os tipos servem para checagem em tempo de escrita, depois são apagados e sobra JavaScript puro.
O TypeScript morre no build.
TypeScript
↓ (transpilador apaga os tipos)
JavaScript
↓ (lido pelo motor V8/Node, um binário escrito em C++)
JIT Compiler (compila o trecho quente para binário nativo e injeta na RAM)
↓
CPU (pulsos elétricos)
O JIT é o que embaralha a fronteira entre
compilado e interpretado: o motor começa interpretando e, quando
percebe que um trecho roda muito, compila aquele trecho para código
de máquina na hora.
No SOIA
Um SO se escreve em C ou Assembly justamente porque ali você
elimina o intérprete da jogada. Não existe motor para carregar
antes do kernel: no instante em que a BIOS entrega o controle,
não há nada além dos 512 bytes que você escreveu.
C freestanding no SOIA ↗
Bloco 08 · aplicação de tudo acima
Por que cheats existem, e o que isso ensina sobre isolamento.
O isolamento de memória protege programas do mesmo nível uns dos outros. Ele nunca protegeu ninguém de quem está num nível acima.
Paginação, rings, DMA e a cegueira do framebuffer aparecem juntos aqui. Cada tática de burla ataca exatamente uma dessas camadas.
A hierarquia é o ponto
Se um programa tentasse escrever direto na memória de outro, o
hardware barraria. O contorno clássico é pedir ao
kernel: rodando como administrador, o programa chama uma
syscall do sistema; o kernel, em Ring 0, pausa o alvo, escreve
através da tabela de páginas dele e despausa. O alvo, em Ring 3,
não tem como recusar.
Isolamento protege processos de mesmo nível. Um
Ring 3 não invade outro Ring 3. Mas o kernel é dono de tudo, e a
proteção termina onde o privilégio começa.
Nunca confiar no cliente
A regra de ouro de multiplayer é que o servidor é a
autoridade: o cliente manda intenção, o servidor decide.
Nesse modelo alterar a própria memória é inútil, porque na hora de
gastar o servidor responde "no meu banco você tem 100, negado".
O que quebra isso é a balança segurança contra latência. Em jogos de
física rápida, esperar o servidor calcular cada empurrão travaria a
partida para quem tem internet lenta, então alguns
desenvolvedores escolheram confiar no cliente: a física rodava
local e o PC apenas avisava a posição final. Aí nem era preciso
enganar o servidor, bastava alterar a gravidade na RAM local.
É a mesma regra que vale para qualquer backend: cálculo que decide
algo, seja preço, dano ou saldo, roda no servidor. O cliente manda
intenção.
A escalada, e por que ela é cara
Quando os anti-cheats passaram a rodar em Ring 0, no mesmo nível do
kernel, sobraram três caminhos, todos caros:
- BYOVD. Instalar um driver legítimo e assinado que tem falha conhecida, e explorar a falha por dentro para sequestrar o privilégio Ring 0.
- DMA por hardware. Uma placa PCIe lê a RAM direto pelas trilhas de cobre e envia para um segundo computador. O software da máquina alvo não vê nada, porque quem leu não foi software. É exatamente contra isso que o IOMMU existe: ele faz para os dispositivos o que a MMU faz para os processos, limitando quais endereços físicos cada periférico pode tocar.
- Visão computacional. Nem tocam mais na memória: capturam a tela, passam por uma rede neural de detecção e agem por um microcontrolador que o PC enxerga como mouse físico. Do lado do sistema, é indistinguível de uma pessoa.
O terceiro caminho é o bloco 06 virando arma: como
a tela é cega, o pixel não sabe que é um inimigo,
mas uma rede treinada sabe.
No SOIA
O SOIA tem paginação por processo, rings e DMA, então tem as três
primeiras peças. Não tem IOMMU: a RTL8139 virtual
recebe quatro páginas de 4 KiB para DMA e, do ponto de vista do
hardware, poderia escrever em qualquer lugar da RAM. O QEMU não
exige a proteção, e por isso ela ainda não existe. Fica registrado
junto com as outras dívidas de plataforma do Marco 22.
Rede básica: PCI, DMA e RTL8139 ↗
Bloco 09 · níveis 1 a 3
Assembly não é a língua da máquina. É a máquina escrita à mão.
A CPU só entende binário. Assembly é o mesmo binário escrito de um jeito que uma pessoa consegue ler, quase palavra por palavra.
O assembler faz transcrição praticamente 1 para 1, com uma exceção que muda tudo: ele resolve rótulos e calcula deslocamento de salto. É essa exceção que torna binário à mão inviável em escala.
A mesma instrução, quatro escritas
As quatro linhas produzem os mesmos sete bytes. A diferença entre a
linha 2 e a linha 3 é quem escolhe o registrador: em
Assembly você escreveu RAX, em C o compilador escolhe, e
pode nem usar registrador nenhum se achar melhor.
48 REX.W prefixo: a operação é de 64 bits
C7 opcode mover valor imediato para registrador
C0 ModRM o destino é RAX
05 00 00 00 imediato o número 5, em little-endian
É a mesma ideia do bloco 05: o byte não tem etiqueta. Aqui ele é
instrução porque o RIP apontou para ele.
Por que ninguém escreve binário direto
Dá para fazer, e já foi feito: o Altair 8800, de 1975, era programado em chaves no painel frontal. O que mata não é a dificuldade de decorar opcode, é o que acontece quando você edita.
Não é a memorização de opcode que inviabiliza. É que a codificação x86-64 tem tamanho variável de 1 a 15 bytes, e o salto guarda distância, não endereço.
Um jmp não guarda o endereço do destino, guarda
quantos bytes pular. Insira uma instrução no meio de
uma rotina e todo salto que passa por cima dela muda de valor.
Assembly resolve isso com rótulo: você escreve jmp .done
e o NASM conta os bytes.
E ele faz mais que contar. Escolhe entre a codificação curta do salto
(2 bytes) e a longa (5 bytes) conforme a distância, o que muda o
tamanho do binário, o que muda outras distâncias, até estabilizar.
O tamanho disso, medido aqui
A otimização de salto do NASM vale 2.704 bytes no
kernel do SOIA, ou 4,4% do binário. Foi por isso que
a refatoração dos 29 arquivos não pôde provar equivalência comparando
bytes: mover uma rotina de lugar muda quais saltos cabem na forma
curta. A prova teve que ser o multiset de linhas que emitem bytes.
A refatoração e as três provas ↗
Por que o kernel não é todo em C
A pergunta é boa porque o Linux é 98% C. Kernel não
precisa ser Assembly. O que precisa ser Assembly é um conjunto pequeno
e teimoso de coisas:
- Sintaxe que C não tem. Não existe forma de escrever
lgdt, lidt, ltr, wrmsr, iretq ou trocar CR0/CR3/CR4 em C. Nem o salto que entra em long mode, que troca o modo da CPU no meio do voo. Em C isso vira __asm__ embutido, ou seja, Assembly dentro do C.
- O byte zero é contrato. Os dois carregadores do SOIA fazem
jmp KERNEL_LOAD_ADDRESS sem trampolim, então kernel_entry tem que ser o primeiro byte do arquivo. Com C você depende de um linker script para garantir isso, e troca uma garantia direta por uma camada que falha calada.
- A ABI esconde justamente o que se quer medir. A prova de que o kernel salva registradores XMM entre tarefas não pode ser escrita em C: a ABI trata XMM como volátil, então o compilador salvaria na pilha antes da syscall e recarregaria depois. O teste compararia a pilha com ela mesma e diria OK sobre um kernel que não salva nada.
- Handler de interrupção não tem prólogo de função. Quando a CPU entra no vetor 14, ela já empilhou um quadro específico. Função C começa mexendo em
RSP e assumindo a convenção de chamada. Ou você usa um atributo de compilador, ou escreve o stub em Assembly que arruma a casa antes de chamar o C.
Onde cada linguagem parou, neste repositório
No SOIA
A linha divisória não é "baixo nível contra alto nível", é
se o código precisa controlar o estado exato da CPU.
Onde precisa, Assembly. Onde não precisa, C. O carregador UEFI ser em
C é a prova de que a régua é essa: ele faz o mesmo trabalho que o
estágio 2 em NASM, só que começa depois que o firmware já ligou o
long mode.
O carregador UEFI em C ↗
Bloco 10 · a categoria, não o tamanho
O SOIA é um SO de verdade. A distância até o Windows é de escala.
Sistema operacional não é definido por ter muitos programas ou parecer bonito. É definido por rodar sem nada por baixo e mandar no hardware direto.
A categoria se define por mecanismos de hardware, e não por superfície de API: privilégio, tradução de endereço, preempção por interrupção e uma fronteira validada entre os dois lados.
Cinco perguntas que separam SO de programa
- Roda sem nada por baixo? O SOIA não roda dentro de outro sistema. Ele é o software que a máquina executa depois do firmware.
- Usa o privilégio do hardware? Kernel em ring 0, programas em ring 3. Um app que tenta um
IN ou OUT leva #GP da CPU, não uma checagem em software.
- Cada programa tem seu mapa de memória? Um
CR3 por tarefa. O app enxerga o espaço dele em 0x100000000 sem saber onde está na RAM física.
- Tira a CPU de quem está rodando, sem pedir? O timer do LAPIC interrompe a 100 Hz, salva 512 bytes de estado FPU e troca de tarefa. O programa não coopera, ele é interrompido.
- Fala com o hardware por driver próprio? Disco, teclado, mouse, rede, relógio, vídeo e desligamento, todos escritos do zero.
Um kernel de 60 KiB respondendo sim às cinco é a
definição funcional de sistema operacional. Nenhuma das cinco tem a ver
com quantos programas existem por cima.
O mecanismo por trás de cada resposta
O que o Windows tem e o SOIA não
A premissa "IF=0 dispensa trava" está
escrita em quatro arquivos deste repositório, e é ela que faz SMP virar
um marco inteiro em vez de um ajuste. Vários núcleos invalidam a
afirmação em todo lugar de uma vez.
A analogia
Uma casa construída do zero com fundação, hidráulica, elétrica e telhado,
contra um prédio de 40 andares. As duas são construção de verdade e
obedecem à mesma física. Uma abriga uma família, a outra abriga mil
pessoas com elevador, gerador e brigada de incêndio.
No SOIA
O que existe aqui é a coisa real em escala pequena. E é justamente
por ser real que as dívidas doem: as três registradas neste apêndice
(framebuffer cacheável, sem IOMMU, sem demand paging fora do heap) só
fazem sentido como dívida porque o resto do mecanismo está de pé.
Ring 3, syscalls e a fronteira ↗
Bloco 11 · a distância medida
O que falta para gravar num pendrive e bootar num notebook.
Boa parte do caminho já foi andada, e o que falta não é "ajustar detalhes". São quatro drivers que hoje não existem, e um deles é enorme.
O bloqueio não está no boot, que o Marco 22 resolveu. Está em entrada, armazenamento e nas duas premissas de vídeo que o QEMU perdoa e o silício não.
Onde o SOIA já chegou
O Marco 22 resolveu a parte que costuma ser o obstáculo de projeto
educacional. O SOIA não precisa de GRUB nem de Limine:
- Carregador UEFI próprio.
BOOTX64.EFI em PE32+, compilado com Clang na ABI da Microsoft, rodando sobre OVMF.
- Mapa de memória do firmware. E820 no caminho BIOS,
GetMemoryMap no UEFI. O SOIA nunca assume que a RAM é terreno plano.
- ACPI. RSDP, FADT e MADT lidos e validados por assinatura e checksum, nos dois caminhos de boot.
- APIC no lugar do PIC 8259. IO-APIC roteando linhas, LAPIC com temporizador próprio calibrado.
- Vídeo por GOP. O carregador UEFI procura o modo de vídeo entre os que o firmware oferece, em vez de pedir um número fixo de tabela.
Correção do que este apêndice dizia antes. A seção
10.1 foi escrita antes do Marco 22 e recomendava usar um bootloader
pronto para lidar com UEFI e mapa de memória. Isso ficou obsoleto: o
SOIA faz as duas coisas sozinho desde então.
Os quatro bloqueios duros
Estes não são ajuste de constante. Cada um é código que não existe, e
sem qualquer um deles a máquina liga e não serve para nada.
-
Entrada: PS/2 não existe em notebook. O SOIA lê
teclado e mouse nas portas
0x60/0x64, um
protocolo dos anos 90. Em notebook o teclado está em USB (controlador
xHCI) ou I2C HID. Sem uma pilha USB, o sistema liga, a tela acende e
você não digita nada. É o maior dos quatro, com folga.
-
Armazenamento: ATA PIO não existe. O único caminho
do SOIA até o disco é
ata_read_sector_64 na porta
0x1F0. Notebook moderno usa NVMe sobre PCIe, ou no
mínimo AHCI. Sem driver novo não há SOIAFS1, e sem SOIAFS1 não há
aplicativo, papel de parede nem persistência.
-
Vídeo: o framebuffer está no cache. As páginas de
vídeo são mapeadas com
0x83, ou seja, write-back. O QEMU
em TCG não emula hierarquia de cache e por isso perdoa. Em hardware
real, os pixels ficam presos no cache da CPU e a tela fica preta. A
correção certa é write-combining por PAT, e não ligar PCD.
-
Vídeo: a resolução está compilada.
FRAMEBUFFER_WIDTH e FRAMEBUFFER_HEIGHT valem
1920×1080 dentro do kernel, e toda a geometria de janela, terminal e
cursor deriva delas. O carregador UEFI procura esse modo e, se não
achar, avisa na serial e segue mesmo assim. Num painel de
1366×768 o kernel escreveria fora do framebuffer que existe.
Os obstáculos menores, e um que ninguém lembra
O que ninguém lembra: a prova some
Todo recurso do SOIA se prova por uma mensagem em
COM1, e é isso que Test-QemuBoot.ps1 lê.
Notebook não tem porta serial. No hardware real, uma falha grave vira
triple fault, a CPU reinicia na hora e não sobra log nenhum:
tela preta e reboot, sem pista de onde quebrou.
E as duas saídas são circulares. Serial por adaptador USB precisa da
pilha USB, que é o bloqueio 1. Gravar log em disco precisa do driver
NVMe, que é o bloqueio 2. A instrumentação depende
exatamente do que ainda não existe, e essa é a diferença
real entre depurar no QEMU e depurar em bare metal.
Por que isso ficou para o fim da fila
A distribuição em pendrive era o Marco 23 e foi movida para o
Marco 34, o último. O motivo é que os três objetivos
de uso do SOIA (ver foto, tocar vídeo e abrir um site em navegador
próprio) não dependem de mídia bootável. Trocar PS/2
por USB não deixa o SOIA fazer nada novo, só o deixa fazer o mesmo em
outro lugar.
No SOIA
As duas dívidas de vídeo deste bloco são as mais baratas de pagar e
as que mais mudam o resultado: PAT no lugar de write-back, e
resolução vinda do firmware em vez de constante. Nenhuma das duas
exige driver novo, e as duas juntas são o que separa "tela preta" de
"imagem na tela" no primeiro boot em hardware real.
A fila dos marcos 23 a 34 ↗