ADRIANOLAUREANO← Artigos
Arquitetura de Computadores CPU Registradores Rust

Como uma CPU Funciona por Dentro:
Da Nossa VM ao Silício

A máquina virtual que construímos no artigo anterior, sobre a Cadência e a mini-vm, não é um acidente de design. Ela imita, em escala reduzida, a mesma anatomia que existe dentro de todo processador físico. Neste artigo vamos descer um andar e entender o que muda — e o que permanece idêntico — quando a máquina deixa de ser um programa Rust e vira silício.

ARTIGO 11 · DA MÁQUINA VIRTUAL AO HARDWARE

8registradores

6instruções

1CPU em Rust

PLACA LÓGICA · UMA CPU REDUZIDA ÀS PEÇAS ESSENCIAIS
UNIDADE DE CONTROLEMICRO-CPUfetch · decode · execute
R00000 0000R10000 0000R20000 0000R30000 0000PC0x0042FLAGSZ C
ALU+aritmética e lógica
RAM10 LOAD R021 ADD R1FF HALT
BARRAMENTO · ENDEREÇO / DADOS / CONTROLE

VOCABULÁRIO VISUAL

Antes de avançar, conheça as peças deste capítulo.

01Silíciomaterial semicondutor onde os circuitos são fabricados
02ALUunidade que realiza cálculos e comparações
03Registradormemória pequena dentro do núcleo
04PCendereço da próxima instrução
05Opcodenúmero que identifica uma operação
06Pipelinesobreposição de etapas de várias instruções
Você não precisa memorizar agora. Este mapa existe para consultar sempre que um termo reaparecer.
01 /
Não Começaremos do Zero
A VM já existe. Vamos entender o que ela estava imitando.

No artigo “Construindo um compilador e uma máquina virtual do zero em Rust”, em que construímos a Cadência e a mini-vm, criamos uma máquina virtual de software. Ela possui:

  • um Program Counter (ip) que aponta para a próxima instrução
  • uma stack onde operandos vivem temporariamente
  • uma Constant Pool com valores imutáveis
  • globals e locals para variáveis
  • frames para chamadas de função
  • um loop que busca, decodifica e executa opcodes

Tudo isso vive dentro de um programa Rust. Mas esses mesmos conceitos também existem dentro de um processador físico. A diferença não é de ideia; é de material.

A ideia que precisa ficar

Nossa VM é uma CPU desenhada em software. Quando entendemos o que ela copiou do hardware, ganhamos o vocabulário necessário para o próximo passo: pedir a uma ferramenta como o LLVM que traduza nossa linguagem para código que um processador real execute diretamente.

MAPA GERALDo que escrevemos ao que o processador executaComece pelas caixas; os nomes técnicos aparecem abaixo delas.
01Código Pulsotexto escrito por uma pessoamostre(2 + 3)
02Bytecodeinstruções da nossa VMCONST · ADD · PRINT
03VM em Rustinterpreta cada opcodefetch · decode · execute
04CPU realexecuta a própria VMregistradores · ALU · RAM

Leitura técnica: o bytecode não chega diretamente ao silício. A CPU executa as instruções nativas da mini-VM, e a mini-VM dá significado aos opcodes da Pulso.

02 /
O Que uma CPU Real Tem em Comum com a Nossa VM
Mesmos papéis, atores diferentes.
Nossa VM (software)CPU Real (hardware)O que faz
ipPC (Program Counter)guarda o endereço da próxima instrução
OpCodeOpcode (número bruto)identifica qual operação executar
stackStack (região da RAM)pilha de operandos temporários
globalsMemória RAMespaço de dados do programa
framesStack de chamadasendereços de retorno e variáveis locais
read_u8()Busca (Fetch)lê o próximo byte da memória
match opcodeDecodificação (Decode)descobre qual operação o número representa
stack.push()Execução (Execute)altera registradores ou memória
program.pbcExecutável (ELF, PE, Mach-O)arquivo com instruções e dados
A ideia que precisa ficar

A tabela não é analogia. É correspondência. Cada conceito que inventamos para a mini-vm já existia em alguma CPU real décadas antes.

03 /
Buscar, Decodificar, Executar
O ciclo que mantém qualquer processador vivo.

No artigo anterior, nossa VM repetia este loop:

loop {
    let byte  = self.read_u8()?;           // FETCH
    let opcode = OpCode::try_from(byte)?;  // DECODE
    match opcode { /* ... */ }           // EXECUTE
}

Uma CPU real faz exatamente a mesma coisa, só que em hardware:

O ciclo fetch-decode-execute

Fetch — o PC aponta para a memória e a unidade de controle lê o próximo opcode.

Decode — circuitos decodificam o número e escolhem qual operação elétrica ativar.

Execute — a ALU ou unidade de carga escreve o resultado em registradores ou RAM.

Atualizar PC — o contador avança (ou recebe um salto) e o ciclo recomeça.

A ideia que precisa ficar

O ciclo fetch-decode-execute é o coração de qualquer processador. Nossa VM o escreveu em Rust; uma CPU o implementa em portas lógicas e fios.

UMA INSTRUÇÃO EM MOVIMENTOO ciclo que se repete milhões ou bilhões de vezesO contador de programa fecha o circuito apontando para a próxima instrução.
1Buscaro PC fornece o endereço
2Decodificara unidade de controle reconhece o opcode
3ExecutarALU, registradores ou memória trabalham
4Atualizar PCavança ou realiza um salto

Exemplo: em ADD R2, R0, R1, a busca lê a instrução; a decodificação identifica soma e operandos; a ALU soma R0 e R1; o resultado vai para R2.

04 /
Registradores: A Memória Dentro do Processador
Nossa VM usava uma stack. CPUs reais preferem gavetas numeradas.

Na mini-vm, quando queríamos somar dois números, empilhávamos ambos na stack e depois executávamos Add. Esse modelo é chamado stack machine.

CPUs reais, como x86 e ARM, usam o modelo register machine: valores ficam em registradores, pequenas gavetas de memória dentro do próprio processador.

Por que registradores?

A stack ocupa uma região da memória do processo e seus dados normalmente passam pelas caches antes de chegar à RAM. Registradores ficam dentro do núcleo e oferecem o acesso mais direto aos operandos. A latência exata varia conforme a microarquitetura, o nível de cache e as dependências entre instruções, mas a regra permanece: o compilador tenta manter no processador os valores usados com maior frequência.

Comparando as duas formas de somar

// Nossa VM (stack machine)
Constant 0   // empilha 2
Constant 1   // empilha 3
Add          // retira dois, empilha 5

// CPU x86 (register machine)
mov eax, 2   // coloca 2 no registrador eax
add eax, 3   // soma 3 ao que está em eax
; resultado: eax agora vale 5

Nossa VM não precisava dizer onde guardar o resultado. A stack implicitamente colocava o resultado no topo. A CPU precisa nomear o destino: eax.

Implementação 01 — Criando uma CPU minimalista em Rust

micro-cpu/src/lib.rs

pub struct Cpu {
    pub regs: [u64; 8],      // 8 registradores de 64 bits
    pub pc: usize,           // Program Counter
    pub ram: Vec<u8>,        // memória principal
    pub running: bool,       // a CPU está ativa?
}

pub enum Instruction {
    Load { dst: u8, addr: u16 },
    Store { src: u8, addr: u16 },
    Add { dst: u8, a: u8, b: u8 },
    Sub { dst: u8, a: u8, b: u8 },
    Jump { addr: u16 },
    Halt,
}

01[u64; 8]: Oito registradores. Números reais de 64 bits, como os registradores de propósito geral do x86-64.

02pc: usize: Mesmo papel do ip da nossa VM: aponta para a próxima instrução na memória.

03ram: Vec<u8>: Nosso modelo didático da memória principal. Na VM, tínhamos code, constants, globals e stack separados. Aqui tudo vive em um único vetor de bytes; numa máquina real, caches, memória virtual e proteção de páginas acrescentam outras camadas entre a CPU e a RAM física.

04Instruction enum: Cada variante carrega os operandos necessários. Add precisa de três registradores: destino, operando A e operando B.

05 /
Memória: Um Único Espaço vs. Regiões Separadas
Na VM, separamos dados por função. Na CPU, tudo é endereço.

Nossa mini-vm organizava a memória em regiões com propósitos definidos: Constant Pool, Globals, Locais do frame, Stack de valores, Stack de frames.

Na CPU real, essas regiões são apenas convenções de software. O hardware vê apenas uma sequência de bytes numerados de 0 até o limite da RAM.

Endereço  Conteúdo (na nossa micro-cpu)
0x0000    Load 0, 0x0100     // código do programa
0x0004    Load 1, 0x0108
0x0008    Add 2, 0, 1
0x000C    Store 2, 0x0110
0x0010    Halt
...
0x0100    42                 // dados (constantes/variáveis)
0x0108    8
0x0110    0                  // espaço para resultado

O mesmo vetor ram guarda código e dados. A CPU não sabe a diferença. Quem decide o que é código é o pc; quem decide o que é dado são as instruções que leem e escrevem endereços.

Do PBC1 para a RAM

No formato PBC1 que criamos, separamos constantes de opcodes de funções. Era um contrato binário legível. Na memória de uma CPU real, essa separação é uma decisão do compilador, não do hardware. O programa compilado decide: "a partir do endereço 0x1000 começam as constantes; a partir de 0x2000 começa o código."

06 /
O Ciclo de Execução na Prática
Vamos executar um programa inteiro no nosso emulador de CPU.

Programa: calcular 42 + 8 e guardar o resultado.

let programa = vec![
    // Load 0, 0x0100  → regs[0] = ram[0x0100] (42)
    0x01, 0x00, 0x00, 0x01,
    // Load 1, 0x0108  → regs[1] = ram[0x0108] (8)
    0x01, 0x01, 0x08, 0x01,
    // Add 2, 0, 1     → regs[2] = regs[0] + regs[1]
    0x03, 0x02, 0x00, 0x01,
    // Store 2, 0x0110 → ram[0x0110] = regs[2]
    0x02, 0x02, 0x10, 0x01,
    // Halt
    0x06,
];

Fotografias do estado da CPU a cada instrução:

PassoInstruçãoPCregs[0]regs[1]regs[2]ram[0x0110]
inicial0x00000000
1Load 0, 0x01000x000442000
2Load 1, 0x01080x000842800
3Add 2, 0, 10x000C428500
4Store 2, 0x01100x00104285050
5Halt4285050

A tabela é o trace da CPU. Cada linha mostra o estado completo após a execução. Note que pc avança em passos fixos de 4 bytes — exceto quando um salto ocorre.

Implementação 02 — O loop da micro-CPU

micro-cpu/src/lib.rs

impl Cpu {
    pub fn step(&mut self) -> Result<(), String> {
        // FETCH: ler o opcode no endereço apontado por pc
        let opcode = self.ram.get(self.pc).copied()
            .ok_or("pc fora da memória")?;
        self.pc += 1;

        // DECODE + EXECUTE
        match opcode {
            0x01 => { // Load
                let dst = self.read_u8()? as usize;
                let addr = self.read_u16()? as usize;
                let value = self.read_ram_u64(addr)?;
                self.regs[dst] = value;
            }
            0x02 => { // Store
                let src = self.read_u8()? as usize;
                let addr = self.read_u16()? as usize;
                self.write_ram_u64(addr, self.regs[src]);
            }
            0x03 => { // Add
                let dst = self.read_u8()? as usize;
                let a = self.read_u8()? as usize;
                let b = self.read_u8()? as usize;
                self.regs[dst] = self.regs[a].wrapping_add(self.regs[b]);
            }
            0x04 => { // Sub
                let dst = self.read_u8()? as usize;
                let a = self.read_u8()? as usize;
                let b = self.read_u8()? as usize;
                self.regs[dst] = self.regs[a].wrapping_sub(self.regs[b]);
            }
            0x05 => { // Jump
                let addr = self.read_u16()? as usize;
                self.pc = addr;
            }
            0x06 => { // Halt
                self.running = false;
            }
            other => return Err(format!("opcode desconhecido: {other}")),
        }
        Ok(())
    }

    pub fn run(&mut self) -> Result<(), String> {
        self.running = true;
        while self.running {
            self.step()?;
        }
        Ok(())
    }
}

01self.pc avança byte a byte. Cada instrução consome opcode + operandos.

02wrapping_add. Em CPUs reais, a adição não falha por overflow — descarta os bits que não cabem.

03running: bool. A CPU para quando encontra Halt. Até lá, o loop é infinito.

O que já funciona
  • Registradores de 64 bits
  • Load e Store entre registradores e RAM
  • Add e Sub com três operandos
  • Jump incondicional
  • Halt
07 /
Da Stack Machine para a Register Machine
Por que nossa VM usava pilha e a CPU usa registradores?

Nossa mini-vm era uma stack machine por uma razão didática: cada instrução fica pequena. Add não precisa dizer onde estão os operandos; eles estão implicitamente no topo da pilha.

CPUs reais são register machines porque registradores são extremamente rápidos. Um programa compilado para registradores executa de 10 a 100 vezes mais rápido que o mesmo programa em uma stack machine pura.

Mas e se quiséssemos emular uma stack na CPU real?

Podemos. Basta escolher um registrador para ser o ponteiro de stack (rsp no x86-64) e usar instruções que empilham e desempilham:

push rax      // empilha o valor de rax
pop rbx       // desempilha para rbx
A ideia que precisa ficar

A stack não desapareceu. Ela foi promovida a uma convenção implementada com registradores e memória. Nossa VM usava Vec<Value>; a CPU real usa ram[sp] onde sp é um registrador especial.

08 /
Instruções Reais: x86, ARM e RISC-V
Nossos opcodes eram inventados. Estes existem de verdade.
Nosso VMx86-64ARM64O que faz
Constantmov rax, 42mov x0, #42carrega valor imediato
Addadd rax, rbxadd x0, x1, x2soma
LoadGlobalmov rax, [mem]ldr x0, [x1]lê da memória
StoreGlobalmov [mem], raxstr x0, [x1]escreve na memória
Jumpjmp enderecob enderecosalto incondicional
JumpIfFalsejz enderecocbz x0, enderecosalto condicional
Callcall funcaobl funcaochama função
Returnretretretorna

A semântica é a mesma. A sintaxe muda. Cada arquitetura escolheu nomes e formatos diferentes, mas todas precisam das mesmas operações fundamentais.

Tamanho de instruções

Nosso bytecode PBC1 tinha instruções de tamanho variável. CPUs reais também variam:

  • x86-64: tamanho variável (1 a 15 bytes). Herança histórica.
  • ARM64: tamanho fixo (4 bytes). Mais fácil de decodificar em hardware.
  • RISC-V: tamanho fixo (4 bytes base, 2 bytes compressão). Design aberto.

Nossa micro-cpu usava tamanho fixo implícito. É mais parecido com ARM/RISC-V do que com x86.

09 /
Pipeline: A CPU Não Espera
Enquanto uma instrução executa, a próxima já está sendo buscada.

Nossa VM executava passo a passo: buscava um opcode, decodificava, executava, e só então buscava o próximo. CPUs reais modernas não esperam.

Pipeline de 4 estágios

Ciclo 1: I1 — Fetch

Ciclo 2: I1 — Decode | I2 — Fetch

Ciclo 3: I1 — Execute | I2 — Decode | I3 — Fetch

Ciclo 4: I1 — Write | I2 — Execute | I3 — Decode | I4 — Fetch

Cada linha horizontal é uma instrução. Cada coluna é um ciclo de clock. Quatro instruções em execução simultânea — como uma linha de montagem.

A ideia que precisa ficar

Isso é um pipeline. A CPU divide o trabalho em estágios independentes e processa várias instruções simultaneamente. Quando gerarmos código nativo, precisaremos saber que a CPU não lê nosso programa na ordem exata — ela reordena internamente para manter o pipeline cheio.

10 /
Da Nossa VM à Geração de Código Nativo
Agora sabemos o que LLVM vai fazer por nós.

No artigo anterior, compilamos Pulso para PBC1. Para transformar Pulso em um executável nativo, precisaríamos:

  1. Traduzir nossos opcodes para instruções x86/ARM reais
  2. Mapear nossa stack para registradores e a stack da CPU
  3. Gerar um arquivo executável no formato ELF, PE ou Mach-O
  4. Resolver endereços de funções e variáveis

Fazer isso manualmente para cada arquitetura seria um projeto de anos. É aí que entra o LLVM.

LLVM — uma fábrica de compiladores

Recebe código intermediário (LLVM IR)

Otimiza — remove redundâncias, aloca registradores

Gera código de máquina — x86, ARM, RISC-V

Produz executável — ELF, PE, Mach-O

LLVM é uma fábrica de compiladores. Ele não sabe sobre Pulso, mas entende LLVM IR — uma linguagem intermediária parecida com assembly, porém independente de arquitetura. Nosso trabalho será traduzir a AST da Pulso para LLVM IR. O LLVM faz o resto.

Por que precisávamos deste artigo antes de LLVM?

Porque LLVM IR fala em termos de CPU real: registradores, endereços de memória, tipos de máquina. Se não soubéssemos o que é um Program Counter, um registrador ou uma instrução de máquina, LLVM IR pareceria magia. Agora sabemos que cada instrução LLVM IR eventualmente vira um fetch-decode-execute em silício.

11 /
Implementação Completa da Micro-CPU
Todo o código usado no artigo.
micro-cpu/
├── Cargo.toml
└── src/
    ├── lib.rs
    └── main.rs

Cargo.toml

[package]
name = "micro-cpu"
version = "0.1.0"
edition = "2021"

src/lib.rs (completo)

pub struct Cpu {
    pub regs: [u64; 8],
    pub pc: usize,
    pub ram: Vec<u8>,
    pub running: bool,
}

impl Cpu {
    pub fn new(ram: Vec<u8>) -> Self {
        Self { regs: [0; 8], pc: 0, ram, running: false }
    }

    fn read_u8(&mut self) -> Result<u8, String> {
        let v = self.ram.get(self.pc).copied()
            .ok_or("fim inesperado da memória")?;
        self.pc += 1;
        Ok(v)
    }

    fn read_u16(&mut self) -> Result<u16, String> {
        let lo = self.read_u8()? as u16;
        let hi = self.read_u8()? as u16;
        Ok((hi << 8) | lo)
    }

    fn read_ram_u64(&self, addr: usize) -> Result<u64, String> {
        let bytes = self.ram.get(addr..addr + 8)
            .ok_or("endereço de leitura inválido")?;
        let mut buf = [0u8; 8];
        buf.copy_from_slice(bytes);
        Ok(u64::from_le_bytes(buf))
    }

    fn write_ram_u64(&mut self, addr: usize, value: u64) {
        let bytes = value.to_le_bytes();
        self.ram[addr..addr + 8].copy_from_slice(&bytes);
    }

    pub fn step(&mut self) -> Result<(), String> {
        let opcode = self.read_u8()?;
        match opcode {
            0x01 => {
                let dst = self.read_u8()? as usize;
                let addr = self.read_u16()? as usize;
                self.regs[dst] = self.read_ram_u64(addr)?;
            }
            0x02 => {
                let src = self.read_u8()? as usize;
                let addr = self.read_u16()? as usize;
                self.write_ram_u64(addr, self.regs[src]);
            }
            0x03 => {
                let dst = self.read_u8()? as usize;
                let a = self.read_u8()? as usize;
                let b = self.read_u8()? as usize;
                self.regs[dst] = self.regs[a].wrapping_add(self.regs[b]);
            }
            0x04 => {
                let dst = self.read_u8()? as usize;
                let a = self.read_u8()? as usize;
                let b = self.read_u8()? as usize;
                self.regs[dst] = self.regs[a].wrapping_sub(self.regs[b]);
            }
            0x05 => {
                let addr = self.read_u16()? as usize;
                self.pc = addr;
            }
            0x06 => { self.running = false; }
            other => return Err(format!("opcode {other} desconhecido")),
        }
        Ok(())
    }

    pub fn run(&mut self) -> Result<(), String> {
        self.running = true;
        while self.running { self.step()?; }
        Ok(())
    }
}

src/main.rs

use micro_cpu::Cpu;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut ram = vec![0u8; 0x0200];
    ram[0x0100..0x0108].copy_from_slice(&42u64.to_le_bytes());
    ram[0x0108..0x0110].copy_from_slice(&8u64.to_le_bytes());

    let programa: Vec<u8> = vec![
        0x01, 0x00, 0x00, 0x01,
        0x01, 0x01, 0x08, 0x01,
        0x03, 0x02, 0x00, 0x01,
        0x02, 0x02, 0x10, 0x01,
        0x06,
    ];
    ram[..programa.len()].copy_from_slice(&programa);

    let mut cpu = Cpu::new(ram);
    cpu.run()?;

    let resultado = u64::from_le_bytes(
        cpu.ram[0x0110..0x0118].try_into()?
    );

    println!("Resultado na RAM[0x0110]: {}", resultado);
    println!("Registradores: {:?}", cpu.regs);
    Ok(())
}
12 /
Laboratório: Faça a CPU Revelar Como Ela Funciona
Um projeto só ensina de verdade quando conseguimos alterar uma hipótese e observar a consequência.

O download desta sequência contém a micro-cpu como um projeto Rust independente. Ela não é uma maquete que apenas imprime um resultado pronto: cada byte do programa é buscado na RAM, decodificado e executado pelo mesmo loop estudado nas seções anteriores.

Experimentos em ordem de dificuldade
  • Troque os valores gravados em 0x0100 e 0x0108 e acompanhe quais registradores mudam.
  • Substitua Add por Sub e explique por que wrapping_sub evita pânico em underflow.
  • Insira um opcode inexistente e observe o erro antes de adicionar sua decodificação.
  • Crie JumpIfZero: leia um registrador, altere o PC e monte o primeiro if da CPU.
  • Adicione uma stack e então implemente Call e Return; esse experimento prepara diretamente o artigo “Como uma chamada de função funciona por dentro”.

O teste mais importante não verifica somente o número 50

Um bom teste fotografa o estado da máquina: o PC deve terminar depois de Halt, R0 deve conter 42, R1 deve conter 8, R2 deve conter 50 e os oito bytes em 0x0110 precisam representar 50 em little-endian. Assim testamos controle, registradores e memória separadamente.

A ponte para o próximo artigo

Agora sabemos o destino final: registradores e instruções reais. O próximo mini livro volta à AST da Pulso e constrói a ponte que faltava entre essa árvore e a CPU: o LLVM.

12 /
A Próxima Ponte
Agora que vimos o silício, podemos pedir ao LLVM que fale com ele.
09Pulsolinguagem com lexer, parser e AST
10Cadência + mini-vmcompilador e máquina virtual de bytecode
11micro-cpucomo uma CPU real executa instruções ← você está aqui
12LLVMgerando código nativo a partir da AST da Pulso
A série completa

“Construindo um compilador e uma máquina virtual do zero em Rust”: Pulso → AST → Cadência → PBC1 → mini-vm (a VM executa em software)

Este artigo: CPU real → registradores → RAM → fetch-decode-execute (o hardware executa diretamente)

Próximo artigo: Pulso → AST → LLVM IR → código nativo (LLVM traduz para a linguagem do silício)

13 /
Perguntas Frequentes
O que costuma ficar depois da primeira CPU emulada.
A nossa VM era lenta porque usava stack?

Não necessariamente "lenta" para fins didáticos. Stack machines são mais simples de implementar, mas em hardware puro são menos eficientes que register machines. A JVM, por exemplo, é uma stack machine sofisticada que compensa com JIT.

Por que não usamos registradores na VM?

Porque a stack torna cada instrução auto-contida. Add não precisa dizer onde estão os operandos. Isso simplifica o compilador e deixa o bytecode compacto. É uma troca deliberada: simplicidade didática vs. performance máxima.

A micro-cpu que construímos é realista?

É uma versão didática. CPUs reais têm dezenas de registradores, caches, execução fora de ordem, unidades especializadas e proteção de memória. O ciclo fetch-decode-execute e o uso de registradores continuam reconhecíveis, mas a implementação física é muito mais paralela e sofisticada que nosso loop em Rust.

Por que precisamos saber disso para usar LLVM?

Porque LLVM IR expõe conceitos de máquina: tipos de tamanho fixo, blocos básicos, instruções que operam em valores virtuais (que viram registradores). Entender registradores e memória torna LLVM IR legível em vez de opaco.

Little-endian ou big-endian?

Nossa micro-cpu usou little-endian para dados (igual x86 e ARM). Os endereços de 16 bits no código usamos big-endian arbitrariamente para mostrar que a ordem é uma convenção — o importante é que quem grava e quem lê concordem.

O que vem depois?

No próximo artigo, vamos conectar a AST da Pulso ao LLVM. Em vez de gerar bytecode para nossa VM, geraremos LLVM IR. O LLVM transformará esse IR em código de máquina real — primeiro para x86-64, depois para ARM64 — produzindo um arquivo executável que roda diretamente no sistema operacional, sem precisar da mini-vm.

PROJETO COMPLETO · CÓDIGO DO CAPÍTULO

Continue exatamente de onde paramos.

O pacote reúne a linguagem Pulso, o compilador Cadência, a máquina virtual, a micro-CPU e os exemplos de LLVM IR usados nesta sequência. O README indica a ordem dos estágios e os comandos para executar cada experiência.

Baixar código final deste artigo (.zip)Rust · Pulso · Cadência · micro-CPU · LLVM IR

O que este artigo fez você pensar?

Dúvidas, experiências e contrapontos ajudam a próxima pessoa a enxergar o assunto por outro ângulo.

Todos passam por moderação. Ao enviar, você concorda com a política de privacidade.

Receba os próximos artigos.

Uma mensagem quando uma nova investigação estiver pronta. Só isso.