From 8fcc4dc06780530645c220691b4df569a645eba9 Mon Sep 17 00:00:00 2001 From: sergio Date: Sat, 23 May 2026 01:28:32 +0000 Subject: [PATCH] =?UTF-8?q?fix(renaser):=20mapeador=20MMIO=20en=20el=20ker?= =?UTF-8?q?nel=20=E2=80=94=20la=20causa=20real=20del=20colapso?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- renaser/boot/src/main.rs | 7 -- renaser/kernel/src/drivers/disco.rs | 17 +++- renaser/kernel/src/main.rs | 9 ++ renaser/kernel/src/memory/mmio.rs | 134 ++++++++++++++++++++++++++++ renaser/kernel/src/memory/mod.rs | 1 + 5 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 renaser/kernel/src/memory/mmio.rs diff --git a/renaser/boot/src/main.rs b/renaser/boot/src/main.rs index 28b9a16..a185fa8 100644 --- a/renaser/boot/src/main.rs +++ b/renaser/boot/src/main.rs @@ -323,13 +323,6 @@ fn lanzar_qemu(imagen: &Path, ovmf: &str) -> Result<(), String> { .arg("-vga").arg("std") .arg("-serial").arg("stdio") .arg("--no-reboot") - // El cargador `bootloader` 0.11 mapea la memoria fisica que ve, pero NO - // las regiones MMIO en el agujero PCI de 64 bits (por encima de 4 GiB), - // donde OVMF coloca los BAR de virtio-blk en QEMU q35 modernos con - // KVM. Sin mapeo, leer un registro MMIO del disco era un #PF - // inevitable. Apagar `pci-hole64-size` fuerza a OVMF a alojar todos - // los BAR en los primeros 4 GiB, que el cargador si mapea. - .arg("-global").arg("q35-pcihost.pci-hole64-size=0") // 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"); diff --git a/renaser/kernel/src/drivers/disco.rs b/renaser/kernel/src/drivers/disco.rs index 112f0f3..0592b68 100644 --- a/renaser/kernel/src/drivers/disco.rs +++ b/renaser/kernel/src/drivers/disco.rs @@ -181,6 +181,14 @@ fn liberar_marcos(fisica: u64, paginas: usize) { } } +/// Asigna UN marco para servir de tabla de paginas. Sin pánico: si la arena +/// esta exhausta, devuelve `None` y deja al mapeador decidir como reaccionar +/// — el kernel no puede caerse por no poder añadir una tabla intermedia, ya +/// se delatara en cuanto el dispositivo lea su propio MMIO no mapeado. +pub fn asignar_marco_para_tabla() -> Option { + ASIGNADOR.get()?.lock().asignar(1) +} + /// Traduce una direccion fisica a la virtual que el kernel puede desreferenciar. fn a_virtual(fisica: u64) -> *mut u8 { (fisica + OFFSET_FISICO.load(Ordering::Relaxed)) as *mut u8 @@ -218,9 +226,12 @@ unsafe impl Hal for KernelHal { 0 } - unsafe fn mmio_phys_to_virt(fisica: PhysAddr, _tam: usize) -> NonNull { - // El cargador mapeo, como minimo, los primeros 4 GiB de memoria fisica; - // todo BAR MMIO de PCI cae dentro y es accesible en `fisica + offset`. + unsafe fn mmio_phys_to_virt(fisica: PhysAddr, tam: usize) -> NonNull { + // OVMF aloja los BAR prefetchables 64-bit de virtio en la «ventana PCI + // de 64 bits» —decenas o cientos de GiB de phys—, que el cargador NO + // mapea. Antes de devolver el puntero virtual, abrimos en la tabla L4 + // las paginas que cubren la region pedida; si ya estaban, no pasa nada. + crate::memory::mmio::mapear(fisica as u64, tam); NonNull::new(a_virtual(fisica)).expect("MMIO :: direccion fisica nula") } diff --git a/renaser/kernel/src/main.rs b/renaser/kernel/src/main.rs index bda07c4..5d53b35 100644 --- a/renaser/kernel/src/main.rs +++ b/renaser/kernel/src/main.rs @@ -439,6 +439,15 @@ fn kernel_main(boot_info: &'static mut BootInfo) -> ! { memory::init(); traza("heap fundado"); + // --- 4.5. Mapeador de MMIO: envuelve la tabla L4 activa para abrir paginas + // nuevas hacia los BAR MMIO de virtio (que pueden caer fuera de + // lo que el cargador mapeo). Necesita `physical_memory_offset` + // para alcanzar la L4 via el mapeo de memoria fisica. --- + if let Some(offset) = offset_fisico { + memory::mmio::init(offset); + traza("mmio :: mapeador fundado"); + } + // --- 5. Con el heap activo, fundar lo que depende de el: el canal de // scancodes, el reloj de fotogramas y la tipografia vectorial. --- async_system::teclado::init(); diff --git a/renaser/kernel/src/memory/mmio.rs b/renaser/kernel/src/memory/mmio.rs new file mode 100644 index 0000000..31be720 --- /dev/null +++ b/renaser/kernel/src/memory/mmio.rs @@ -0,0 +1,134 @@ +// ============================================================================= +// renaser :: kernel/src/memory/mmio.rs — el mapeador de regiones MMIO +// ----------------------------------------------------------------------------- +// El cargador `bootloader_api` mapea la memoria fisica de la maquina, pero la +// «ventana PCI de 64 bits» —donde OVMF aloja los BAR prefetchables de virtio +// en QEMU q35 modernos— suele caer FUERA de ese mapeo: phys 32 GiB, 768 GiB o +// mas, segun la fase de la luna. Sin mapeo, leer el primer registro del disco +// era un #PF inmediato; con el, el kernel puede hablar con el dispositivo. +// +// Este modulo abre paginas en la tabla L4 que el cargador nos cedio. Reutiliza +// como asignador de marcos el de DMA del disco (`drivers::disco`): los marcos +// para las tablas intermedias salen del mismo banco que los buferes virtio. +// No mapea de mas: solo lo que se le pide, por paginas. +// ============================================================================= + +use core::sync::atomic::{AtomicU64, Ordering}; + +use spin::{Mutex, Once}; + +use x86_64::registers::control::Cr3; +use x86_64::structures::paging::mapper::{MapToError, Mapper}; +use x86_64::structures::paging::{ + FrameAllocator, OffsetPageTable, Page, PageTable, PageTableFlags, PhysFrame, Size4KiB, +}; +use x86_64::{PhysAddr, VirtAddr}; + +/// Desplazamiento al que el cargador mapeo la memoria fisica. Lo necesitamos +/// tanto para alcanzar la tabla L4 actual como para traducir la direccion +/// fisica del BAR a su virtual de destino. +static OFFSET_FISICO: AtomicU64 = AtomicU64::new(0); + +/// El mapeador del kernel: una vista mutable de la tabla L4 actual envuelta en +/// el truco de `OffsetPageTable`, que lo asume todo accesible via el mapeo de +/// memoria fisica que el cargador ya dejo activo. Se funda en el arranque, una +/// sola vez, antes de cualquier llamada a `mapear`. +static MAPEADOR: Once>> = Once::new(); + +/// Funda el mapeador: localiza la tabla L4 activa (via CR3) y la envuelve en un +/// `OffsetPageTable` que aprovecha el mapeo de memoria fisica del cargador. A +/// partir de aqui `mapear` puede abrir paginas nuevas en la tabla. +pub fn init(offset_fisico: u64) { + OFFSET_FISICO.store(offset_fisico, Ordering::Relaxed); + let (l4_frame, _) = Cr3::read(); + let l4_virt = l4_frame.start_address().as_u64() + offset_fisico; + // SEGURIDAD: el cargador mapeo toda la RAM fisica (incluida la tabla L4 + // activa) en `l4_phys + offset_fisico`. Esa direccion es valida, esta + // alineada a pagina y la tabla vive lo que vive el kernel. La tomamos como + // referencia mutable porque renaser es de un solo nucleo y todo acceso al + // mapeador queda serializado tras el `Mutex` de `MAPEADOR`. + let l4: &'static mut PageTable = unsafe { &mut *(l4_virt as *mut PageTable) }; + let mapeador = unsafe { OffsetPageTable::new(l4, VirtAddr::new(offset_fisico)) }; + MAPEADOR.call_once(|| Mutex::new(mapeador)); +} + +/// Asigna marcos para las tablas de paginas intermedias. Toma del banco DMA +/// del disco —no hay heap aqui— y deja cada marco a CEROS, como una tabla +/// vacia exige. Si el banco esta exhausto, devuelve `None` y el mapeo falla +/// en lugar de incendiar el kernel. +struct Marcos; + +unsafe impl FrameAllocator for Marcos { + fn allocate_frame(&mut self) -> Option> { + let fisica = crate::drivers::disco::asignar_marco_para_tabla()?; + let offset = OFFSET_FISICO.load(Ordering::Relaxed); + // SEGURIDAD: `asignar_marco_para_tabla` entrego un marco exclusivo y + // mapeado a `fisica + offset` por el cargador. Las tablas de paginas + // exigen empezar a cero — si no, la CPU las leeria como llenas de + // basura— asi que lo limpiamos antes de cederlo. + unsafe { + core::ptr::write_bytes((fisica + offset) as *mut u8, 0, 4096); + } + Some(PhysFrame::containing_address(PhysAddr::new(fisica))) + } +} + +/// Abre en la tabla L4 las paginas que cubren la region MMIO [fisica, fisica + +/// tam), de modo que cada pagina sea accesible en `fisica + offset_fisico` con +/// las banderas habituales del MMIO (escribible, sin cache). Si la pagina ya +/// estaba mapeada, se respeta la entrada existente sin gritar — un BAR puede +/// solaparse con regiones que el cargador ya cubrio—. Si el mapeo falla en +/// medio, registramos el problema en COM1 y seguimos: que la app que llame al +/// MMIO se entere por su propio fallo. +pub fn mapear(fisica: u64, tam: usize) { + use core::fmt::Write; + + let Some(mapeador) = MAPEADOR.get() else { + return; + }; + if tam == 0 { + return; + } + let offset = OFFSET_FISICO.load(Ordering::Relaxed); + let virt_inicio = fisica + offset; + let virt_fin = virt_inicio + tam as u64 - 1; + + let pagina_inicio = Page::::containing_address(VirtAddr::new(virt_inicio)); + let pagina_fin = Page::::containing_address(VirtAddr::new(virt_fin)); + let frame_inicio = PhysFrame::::containing_address(PhysAddr::new(fisica)); + + let banderas = PageTableFlags::PRESENT + | PageTableFlags::WRITABLE + | PageTableFlags::NO_CACHE + | PageTableFlags::WRITE_THROUGH; + + let mut mapeador = mapeador.lock(); + let mut frame = frame_inicio; + let mut pagina = pagina_inicio; + while pagina <= pagina_fin { + // SEGURIDAD: cada par (pagina, frame) describe un mapeo MMIO honesto: + // la pagina virtual `fisica + offset` apunta a la pagina fisica + // `fisica` que el dispositivo posee. El `Marcos` cede marcos limpios + // para las tablas intermedias. + let resultado = unsafe { mapeador.map_to(pagina, frame, banderas, &mut Marcos) }; + match resultado { + Ok(flush) => flush.flush(), + // El cargador ya habia mapeado la region como una pagina 4 KiB + // (PageAlreadyMapped) o como una pagina huge — 2 MiB / 1 GiB — + // (ParentEntryHugePage). En ambos casos el acceso ya funciona; no + // hay nada que añadir y SEGUIMOS sin detenernos. Solo abortamos si + // se nos agotan los marcos para una tabla intermedia nueva. + Err(MapToError::PageAlreadyMapped(_)) | Err(MapToError::ParentEntryHugePage) => {} + Err(MapToError::FrameAllocationFailed) => { + let _ = writeln!( + crate::baliza::Serie, + "mmio :: sin marcos para la tabla — mapeo incompleto en {:#x}", + pagina.start_address().as_u64(), + ); + return; + } + } + pagina += 1; + frame += 1; + } +} diff --git a/renaser/kernel/src/memory/mod.rs b/renaser/kernel/src/memory/mod.rs index c858b3f..eafcc63 100644 --- a/renaser/kernel/src/memory/mod.rs +++ b/renaser/kernel/src/memory/mod.rs @@ -7,5 +7,6 @@ // ============================================================================= pub mod allocator; +pub mod mmio; pub use allocator::init;