Files
brahman/renaser/boot/src/main.rs
T
sergio 8fcc4dc067 fix(renaser): mapeador MMIO en el kernel — la causa real del colapso
El `-global pci-hole64-size=0` del commit anterior NO movía los BAR:
verifiqué con `info pci` que OVMF seguía alojando el BAR4 prefetchable
64-bit del virtio-blk en `0xc000000000` (mi Proxmox) o `0x800000000`
(la laptop del usuario). El cargador `bootloader_api` 0.11 mapea la
memoria física pero no extiende su mapeo hasta la ventana PCI de 64
bits, y `KernelHal::mmio_phys_to_virt` devolvía `phys + offset` a
ciegas — un puntero a memoria sin tabla de páginas, al primer registro
MMIO leído → #PF.

La solución: un mapeador MMIO propio del kernel.

- `memory::mmio`: envuelve la tabla L4 activa (vía CR3 + el mapeo de
  memoria física del cargador) en un `OffsetPageTable`. Su función
  `mapear(fisica, tam)` abre, para cada página de la región, una
  entrada en la L4 con `PRESENT | WRITABLE | NO_CACHE | WRITE_THROUGH`
  — las banderas habituales del MMIO.
- Los marcos para tablas intermedias salen del banco DMA del disco
  (`drivers::disco::asignar_marco_para_tabla`, sin pánico). Se ponen
  a cero antes de cederlos: las tablas empiezan vacías.
- Tratamos `PageAlreadyMapped` y `ParentEntryHugePage` como éxito: la
  región ya estaba cubierta por el cargador (con páginas 4 KiB o
  hugepages 2 MiB / 1 GiB) y el acceso ya funciona. Solo abortamos el
  mapeo si se nos agota la arena DMA.
- `KernelHal::mmio_phys_to_virt` llama a `memory::mmio::mapear` antes
  de devolver el puntero virtual. virtio-drivers lo invoca con la
  base y el tamaño exactos de cada BAR; el kernel asegura que cada
  uno sea accesible antes de devolverlo.
- `kernel_main` funda el mapeador justo después del heap (paso 4.5),
  antes del disco. Necesita `physical_memory_offset` para alcanzar
  la L4 activa.

Quito el `-global q35-pcihost.pci-hole64-size=0` que añadí antes: no
movía los BAR (verificado con `info pci`) y solo confundía la
descripción del fix. Esta solución es la robusta: el kernel sabe
mapear sus propios MMIOs y deja de depender del firmware.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 01:28:32 +00:00

346 lines
16 KiB
Rust

// =============================================================================
// renaser :: boot/src/main.rs — Fase 1.5 :: el puente hacia el silicio
// -----------------------------------------------------------------------------
// Un kernel bare-metal no nace solo: alguien debe fusionarlo con un cargador,
// sellarlo en una imagen de disco arrancable y entregarlo al hardware. Esa es
// la mision de este orquestador de ANFITRION.
//
// Desde la Fase 7b hace algo mas: SIEMBRA el grafo. El kernel ya no empotra
// el userspace —ni un solo `include_bytes!` de un `.wasm`—; en su lugar, este
// constructor pre-puebla el disco de objetos con el bytecode de las apps de
// genesis y el Manifiesto de Genesis que dicta cuales arrancan, en que region
// y con que cuota. Para ello habla el MISMO formato del grafo que el kernel,
// a traves de la crate compartida `formato`.
//
// El flujo es deliberadamente lineal y sin ambiguedad:
//
// 1. Localizar el ELF nativo del kernel (lo inyecta la dep. de artefacto).
// 2. Fusionarlo con el cargador UEFI en una imagen de disco GPT.
// 3. Sembrar el disco de objetos: el grafo poblado con el bytecode del
// userspace y el Manifiesto de Genesis (Fase 7b).
// 4. Lanzar QEMU con la imagen, el disco de objetos y el firmware OVMF.
//
// Cada paso que pueda fallar lo hace en voz alta, con un mensaje accionable:
// preferimos un error claro a un arranque silencioso hacia la nada.
// =============================================================================
use std::collections::BTreeMap;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
// El formato del grafo de objetos en disco — el MISMO nucleo `no_std` que
// enlaza el kernel. Gracias a el, lo que `boot` siembra y lo que el kernel lee
// es, byte a byte, el mismo idioma.
use formato::{
EntradaApp, Hash, Manifiesto, Objeto, SuperBloque, MAGIA, MAX_OBJETO, TAM_SECTOR,
VERSION_MANIFIESTO, VERSION_SUPERBLOQUE,
};
/// Ruta del ELF del kernel, ya compilado para `x86_64-unknown-none`.
///
/// La dependencia de artefacto define esta variable de entorno en tiempo de
/// compilacion: cuando este binario de anfitrion existe, el kernel ya existe.
const KERNEL_ELF: &str = env!("CARGO_BIN_FILE_KERNEL_kernel");
/// Firmware UEFI OVMF tal como lo empaqueta Artix Linux (paquete `edk2-ovmf`).
/// Es la imagen combinada codigo+variables, apta para `-bios`.
const OVMF_POR_DEFECTO: &str = "/usr/share/edk2/x64/OVMF.4m.fd";
/// Nombre de la imagen de disco UEFI que renaser genera.
const NOMBRE_IMAGEN: &str = "renaser-uefi.img";
/// Ruta del disco de objetos del grafo persistente (Fase 6.1c). Relativa al
/// directorio de trabajo —la raiz del repo—, comun a `boot` y a QEMU.
const NOMBRE_DISCO: &str = "target/disk.img";
/// Tamaño del disco de objetos: 32 MiB. La imagen sembrada ocupa solo unos
/// pocos KiB; el resto queda a cero —espacio libre para que el grafo crezca—.
const TAM_DISCO: u64 = 32 * 1024 * 1024;
fn main() {
if let Err(fallo) = orquestar() {
// Un error de orquestacion se anuncia en rojo y aborta con codigo 1:
// ninguna falla del anfitrion debe disfrazarse de exito.
eprintln!("\x1b[1;31m[renaser/boot] fallo:\x1b[0m {fallo}");
std::process::exit(1);
}
}
/// Ejecuta, en orden, las operaciones de la Fase 1.5.
fn orquestar() -> Result<(), String> {
// --- 1. Localizar el artefacto del kernel. ---
let kernel = Path::new(KERNEL_ELF);
if !kernel.is_file() {
return Err(format!(
"no se encontro el ELF del kernel en {}\n \
(¿se interrumpio la compilacion de la dependencia de artefacto?)",
kernel.display()
));
}
println!("[renaser/boot] kernel localizado :: {}", kernel.display());
// --- 2. Fusionar kernel + cargador UEFI en una imagen de disco. ---
let imagen = ruta_imagen(kernel);
println!("[renaser/boot] forjando imagen UEFI :: {}", imagen.display());
bootloader::UefiBoot::new(kernel)
.create_disk_image(&imagen)
.map_err(|e| format!("la crate `bootloader` no pudo crear la imagen UEFI: {e:?}"))?;
// --- 3. Garantizar —y, si es virgen, SEMBRAR— el disco de objetos. ---
preparar_disco_objetos()?;
// --- 4. Lanzar QEMU sobre esa imagen. ---
let ovmf = localizar_ovmf()?;
lanzar_qemu(&imagen, &ovmf)
}
// =============================================================================
// Fase 7b — la siembra del grafo: el userspace nace de la imagen de disco
// =============================================================================
/// Una app de genesis: su nombre legible, el `.wasm` que la encarna y la
/// ventana del framebuffer que habitara — `(x, y, ancho, alto)` en pixeles.
struct AppGenesis {
nombre: &'static str,
archivo: &'static str,
region: (u32, u32, u32, u32),
}
/// El userspace de genesis — las siete aplicaciones que pueblan un disco recien
/// forjado. La melodia visual `tonada` (Fase 12), el compas visual `pulso`
/// (Fase 11), un saludo (`hola`), la `memoriosa` interactiva que recuerda entre
/// sesiones (Fase 7c), y tres demos de los guardarrailes del kernel: `discola`
/// (combustible), `glotona` (memoria) y `cronista` (la cronica de los arranques).
const GENESIS: [AppGenesis; 7] = [
AppGenesis { nombre: "tonada", archivo: "tonada.wasm", region: (100, 120, 360, 120) },
AppGenesis { nombre: "pulso", archivo: "pulso.wasm", region: (100, 120, 360, 120) },
AppGenesis { nombre: "hola", archivo: "app.wasm", region: (100, 120, 480, 560) },
AppGenesis { nombre: "memoriosa", archivo: "memoriosa.wasm", region: (700, 120, 360, 80) },
AppGenesis { nombre: "discola", archivo: "discola.wasm", region: (60, 700, 360, 80) },
AppGenesis { nombre: "glotona", archivo: "glotona.wasm", region: (460, 700, 360, 80) },
AppGenesis { nombre: "cronista", archivo: "cronista.wasm", region: (860, 700, 360, 80) },
];
/// Techo de memoria lineal de cada app de genesis: 4 MiB. Un modulo que intente
/// crecer su memoria mas alla es desalojado por el kernel.
const TECHO_GENESIS: u32 = 4 * 1024 * 1024;
/// Garantiza la existencia del disco de objetos del grafo persistente. Si ya
/// existe, lo RESPETA — el grafo perdura entre arranques (la cuenta de la
/// cronista, el estado del userspace). Si no existe, lo forja Y LO SIEMBRA:
/// graba el grafo ya poblado con el bytecode de las apps y su Manifiesto de
/// Genesis. El kernel jamas vuelve a empotrar una sola app.
fn preparar_disco_objetos() -> Result<(), String> {
let disco = Path::new(NOMBRE_DISCO);
if disco.is_file() {
println!(
"[renaser/boot] disco de objetos presente :: {} — el grafo perdura",
disco.display()
);
return Ok(());
}
if let Some(directorio) = disco.parent() {
std::fs::create_dir_all(directorio)
.map_err(|e| format!("no se pudo crear el directorio del disco de objetos: {e}"))?;
}
// Sembrar el grafo: el bytecode del userspace y el Manifiesto de Genesis.
let (imagen, objetos) = sembrar_grafo()?;
if imagen.len() as u64 > TAM_DISCO {
return Err(format!(
"el grafo sembrado ({} bytes) no cabe en el disco de objetos ({TAM_DISCO} bytes)",
imagen.len()
));
}
// Escribir la imagen sembrada y extender el fichero a 32 MiB: el log queda
// al principio; el resto, a cero —`set_len` lo deja disperso—.
let mut fichero = std::fs::File::create(disco)
.map_err(|e| format!("no se pudo crear el disco de objetos «{}»: {e}", disco.display()))?;
fichero
.write_all(&imagen)
.map_err(|e| format!("no se pudo escribir el grafo sembrado: {e}"))?;
fichero
.set_len(TAM_DISCO)
.map_err(|e| format!("no se pudo dimensionar el disco de objetos: {e}"))?;
println!(
"[renaser/boot] disco de objetos sembrado :: {} ({objetos} objetos, manifiesto anclado)",
disco.display()
);
Ok(())
}
/// Lee el bytecode `.wasm` de una app de genesis desde `kernel/assets/`. La
/// ruta se ancla al directorio de ESTE crate —no al de trabajo—: el
/// constructor funciona se invoque desde donde se invoque.
fn leer_wasm(archivo: &str) -> Result<Vec<u8>, String> {
let ruta = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../kernel/assets")
.join(archivo);
std::fs::read(&ruta)
.map_err(|e| format!("no se pudo leer el bytecode «{}»: {e}", ruta.display()))
}
/// Anexa un objeto al log: compone su registro `[longitud][payload][relleno]`,
/// lo añade a la imagen y avanza el cursor. Devuelve el hash del objeto — su
/// identidad en el grafo direccionado por contenido.
fn anexar_objeto(log: &mut Vec<u8>, cursor: &mut u64, payload: &[u8]) -> Result<Hash, String> {
if payload.is_empty() || payload.len() > MAX_OBJETO {
return Err(format!(
"un objeto del grafo tiene un tamaño invalido: {} bytes",
payload.len()
));
}
let hash = formato::hash(payload);
log.extend_from_slice(&formato::componer_registro(payload));
*cursor += formato::sectores_registro(payload.len());
Ok(hash)
}
/// Siembra el grafo de objetos de un disco virgen: graba el bytecode de cada
/// app de genesis como un objeto del grafo, compone el Manifiesto de Genesis
/// —con sus regiones y cuotas—, lo graba con las aristas hacia los objetos de
/// bytecode, y forja el superbloque que lo ancla. Devuelve la imagen del disco
/// (superbloque en el sector 0 + el log de registros) y el numero de objetos
/// sembrados. Habla, byte a byte, el formato que el kernel leera al montar.
fn sembrar_grafo() -> Result<(Vec<u8>, usize), String> {
// El log de registros: del sector 1 en adelante. El sector 0 es el
// superbloque, que aun no podemos escribir —no conocemos el cursor final—.
let mut log: Vec<u8> = Vec::new();
let mut cursor: u64 = 1;
// --- 1. Los objetos de bytecode, DEDUPLICADOS por archivo. Dos apps que
// comparten el mismo `.wasm` comparten un unico objeto del grafo. ---
let mut hash_de: BTreeMap<&str, Hash> = BTreeMap::new();
let mut hijos_manifiesto: Vec<Hash> = Vec::new();
let mut apps: Vec<EntradaApp> = Vec::new();
for app in &GENESIS {
let bytecode = match hash_de.get(app.archivo) {
// Ya grabado: el grafo no guarda dos veces el mismo contenido.
Some(&hash) => hash,
None => {
let datos = leer_wasm(app.archivo)?;
let objeto = Objeto { datos, hijos: Vec::new() };
let payload = objeto.serializar().map_err(|e| e.to_string())?;
let hash = anexar_objeto(&mut log, &mut cursor, &payload)?;
hash_de.insert(app.archivo, hash);
hijos_manifiesto.push(hash);
hash
}
};
let (x, y, ancho, alto) = app.region;
apps.push(EntradaApp {
nombre: app.nombre.to_string(),
bytecode,
region_x: x,
region_y: y,
region_ancho: ancho,
region_alto: alto,
techo_memoria: TECHO_GENESIS,
estado: None,
});
}
// --- 2. El objeto del Manifiesto de Genesis. Sus `hijos` son los objetos
// de bytecode: el grafo lo lee como el nodo padre del userspace. ---
let manifiesto = Manifiesto { version: VERSION_MANIFIESTO, apps };
let man_datos = manifiesto.serializar().map_err(|e| e.to_string())?;
let man_objeto = Objeto { datos: man_datos, hijos: hijos_manifiesto };
let man_payload = man_objeto.serializar().map_err(|e| e.to_string())?;
let hash_manifiesto = anexar_objeto(&mut log, &mut cursor, &man_payload)?;
// El grafo sembrado: un objeto por cada `.wasm` unico, mas el manifiesto.
let objetos = hash_de.len() + 1;
// --- 3. El superbloque: el ancla del grafo, en el sector 0. `raiz` queda
// vacia —el userspace la fija; `manifiesto` apunta a la genesis. ---
let superbloque = SuperBloque {
magia: MAGIA,
version: VERSION_SUPERBLOQUE,
cursor,
raiz: None,
manifiesto: Some(hash_manifiesto),
};
let sb_bytes = superbloque.serializar().map_err(|e| e.to_string())?;
if sb_bytes.len() > TAM_SECTOR {
return Err("el superbloque sembrado no cabe en un sector".to_string());
}
// La imagen: el superbloque en el sector 0 (relleno a cero) y, tras el, el
// log de registros que acabamos de componer.
let mut imagen = vec![0u8; TAM_SECTOR];
imagen[..sb_bytes.len()].copy_from_slice(&sb_bytes);
imagen.extend_from_slice(&log);
Ok((imagen, objetos))
}
/// Calcula la ruta de la imagen: junto al propio ELF del kernel, es decir,
/// dentro de `target/`. Una ubicacion predecible y siempre escribible.
fn ruta_imagen(kernel: &Path) -> PathBuf {
kernel
.parent()
.unwrap_or_else(|| Path::new("."))
.join(NOMBRE_IMAGEN)
}
/// Resuelve la ruta del firmware OVMF. Permite sobreescribirla con la variable
/// de entorno `RENASER_OVMF` para entornos cuyo `edk2-ovmf` viva en otra ruta.
fn localizar_ovmf() -> Result<String, String> {
let ruta = std::env::var("RENASER_OVMF").unwrap_or_else(|_| OVMF_POR_DEFECTO.to_string());
if Path::new(&ruta).is_file() {
Ok(ruta)
} else {
Err(format!(
"firmware UEFI OVMF no encontrado en «{ruta}»\n \
instala el paquete `edk2-ovmf`, o exporta RENASER_OVMF=<ruta a OVMF.fd>"
))
}
}
/// Invoca QEMU como subproceso. Los argumentos se ciñen a las primitivas
/// minimas necesarias para que el Framebuffer GOP cobre vida:
///
/// * `-bios` firmware UEFI OVMF.
/// * `-drive raw` la imagen de disco UEFI, sin capa de traduccion.
/// * `-vga std` VGA estandar => framebuffer lineal que el GOP expone.
/// * `-serial stdio` telemetria serial del procesador hacia esta consola.
/// * `--no-reboot` un fallo triple detiene la maquina en vez de reiniciar
/// en bucle: asi la baliza de panico permanece visible.
/// * `virtio-blk-pci` el disco de objetos, sobre el bus PCI (q35 es x86_64;
/// `virtio-blk-device`, su gemelo MMIO, es cosa de ARM).
fn lanzar_qemu(imagen: &Path, ovmf: &str) -> Result<(), String> {
println!("[renaser/boot] arrancando QEMU :: la superficie indigo nace ahora\n");
let mut qemu = Command::new("qemu-system-x86_64");
// `accel=kvm:tcg` intenta KVM y, si no esta disponible, recae en TCG puro.
qemu.arg("-machine").arg("q35,accel=kvm:tcg")
.arg("-m").arg("256M")
.arg("-bios").arg(ovmf)
.arg("-drive").arg(format!("format=raw,file={}", imagen.display()))
.arg("-vga").arg("std")
.arg("-serial").arg("stdio")
.arg("--no-reboot")
// El disco de objetos, como dispositivo virtio-blk sobre el bus PCI.
.arg("-drive").arg(format!("format=raw,file={NOMBRE_DISCO},if=none,id=drv0"))
.arg("-device").arg("virtio-blk-pci,drive=drv0");
// Cualquier argumento extra tras `--` se reenvia a QEMU intacto.
// Ejemplo: `cargo run -p boot -- -display none -d int`.
qemu.args(std::env::args().skip(1));
match qemu.status() {
Ok(estado) if estado.success() => {
println!("\n[renaser/boot] QEMU finalizo limpiamente.");
Ok(())
}
Ok(estado) => Err(format!("QEMU termino con estado anomalo: {estado}")),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(
"`qemu-system-x86_64` no esta en el PATH; instala el paquete `qemu-full`".to_string(),
),
Err(e) => Err(format!("no se pudo ejecutar QEMU: {e}")),
}
}