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 ↗