~/larhrissi
Retour aux projets

Projet personnel

Émulateur RISC-V (RV32I) en C

Émulateur RV32I from scratch en C99 strict — boucle fetch-decode-execute, 47 instructions, validé sur la suite officielle riscv-tests.

  • C99
  • CMake
  • Ninja
  • Unity
  • GCC RISC-V toolchain
  • WSL2

Problème

Comprendre intimement comment un CPU exécute du code — pas en lisant un manuel d'architecture, mais en construisant la chose. RV32I est l'ISA idéale pour ça : 47 instructions suffisent à exécuter du C compilé, et la spec est ouverte et claire.

Architecture

Le code est découpé en modules à responsabilité unique : cpu.c (état + boucle fetch-decode-execute), decoder.c (mot 32 bits → struct typée Instruction), executor.c (effet de chaque instruction sur le CPU), memory.c (RAM 1 MB little-endian). Pas de cycles dans les #include, pas de malloc dans le hot path. C99 strict avec -Wall -Wextra -Wpedantic -Wshadow -Wconversion. Build CMake + Ninja, dev sur WSL2 Ubuntu, cross-compilation des programmes de test avec riscv64-unknown-elf-gcc.

cpu_step() — boucle principaleFETCHmem_load32(pc)PC += 4DECODEopcode, funct3, rd, rs1switch (opcode)EXECUTEx[rd] = x[rs1] op immx[0] = 0loop

Points clés

  • 47 instructions RV32I — ALU, loads / stores, branches, jumps, system
  • Modules séparés : cpu, decoder, executor, memory, utils
  • Décodeur typé : un mot 32 bits → struct Instruction (6 formats)
  • Banc de 32 registres + PC, garde x0 = 0 enforcée en lecture ET en écriture
  • RAM 1 MB little-endian, accès byte / half / word
  • ~70 sous-tests unitaires Unity
  • Suite officielle riscv-tests (~40 rv32ui-p-*)
  • 6 programmes asm de démo (Fibonacci, factorielle, popcount, etc.)

Décisions techniques

01.Décodeur typé, pas un méga switch dans cpu_step

decode(raw) retourne une struct Instruction { type, rd, rs1, rs2, imm, raw }. Le type est un enum InstrType (INSTR_ADDI, INSTR_BEQ, …) et l'executor fait le switch là-dessus, pas sur l'opcode brut. Résultat : le décodeur est testable en isolation, et chaque cas d'exécution lit comme du pseudo-code.

02.L'executor renvoie un bool 'pc_modified'

Plutôt que de laisser l'executor toucher au PC pour les branches / jumps et un autre code l'incrémenter ailleurs, l'executor retourne pc_modified. cpu_step() incrémente PC de 4 seulement si l'instruction ne l'a pas modifié. Une seule source de vérité pour la logique du PC.

03.x0 hardwired aux deux endroits

cpu_read_reg(0) retourne 0 directement, et cpu_write_reg(0, _) est silencieusement ignoré. Defense-in-depth : un bug dans l'executor qui écrirait sur x0 ne pollue pas l'état. Conforme au comportement matériel.

04.C99 strict avec warnings élevés

Pas d'extensions GCC, pas de feature-flags qui dérivent ailleurs. -Wall -Wextra -Wpedantic -Wshadow -Wconversion. Le code passe sans warning — c'est une discipline qui force à expliciter chaque cast et chaque shadowing.

05.Triple validation

Niveau 1 : ~70 sous-tests Unity, un fichier par module. Niveau 2 : programmes asm faits maison (Fibonacci, factorielle, popcount) avec résultats attendus vérifiés en CI. Niveau 3 : suite officielle riscv-tests (~40 tests de conformité ISA rv32ui-p-*). Si les trois passent, l'émulateur exécute du vrai code.

Extrait de code

src/cpu.c — cpu_step()
c
// src/cpu.c — cycle fetch-decode-execute (extrait réel)void cpu_step(Cpu *cpu){    if (cpu->halted) return;     uint32_t raw = mem_read32(cpu->pc);    Instruction instr = decode(raw);    bool pc_modified = execute(cpu, &instr);     if (!pc_modified && !cpu->halted) {        cpu->pc += 4u;    }}
Tous les projets