- QEMU permite emular RISC-V y otras arquitecturas en un PC x86_64, desde binarios sueltos hasta sistemas operativos completos.
- Es posible arrancar Linux RISC-V en QEMU usando OpenSBI, U-Boot, imágenes preparadas como Ubuntu o Debian y también montando tu propio kernel y rootFS.
- Las toolchains GNU y LLVM para RISC-V facilitan compilar software nativo, incluyendo ejemplos avanzados con extensiones como RVV.
- QEMU es una plataforma ideal para aprender ensamblador RISC-V y experimentar con la ISA sin necesidad de hardware físico.
Si alguna vez has pensado en probar software RISC-V nativo sin tener hardware real, QEMU es una de las herramientas más potentes que tienes a tu alcance. No solo permite lanzar sistemas completos de otras arquitecturas, sino también ejecutar binarios sueltos, hacer pruebas de bajo nivel e incluso jugar con extensiones avanzadas como las vectoriales de RISC-V.
En este artículo vamos a ver cómo ejecutar software RISC-V nativo con QEMU paso a paso, empezando por los conceptos básicos de emulación, repasando la instalación en Ubuntu, cómo crear entornos mínimos con Linux y BusyBox, cómo usar imágenes ya preparadas (como Ubuntu o Debian para RISC-V) y cómo compilar y probar tus propios programas, tanto sencillos en ensamblador como más complejos usando toolchains GNU o LLVM/Clang.
Qué es QEMU y por qué es ideal para RISC-V
A diferencia de soluciones como VirtualBox o VMWare, que normalmente se centran en virtualizar sistemas de la misma arquitectura que la máquina anfitriona, QEMU destaca por su capacidad de emulación completa: puedes arrancar sistemas hechos para RISC-V, MIPS, PowerPC o ARM en un PC x86_64 sin necesidad de disponer de ese hardware.
QEMU es, en esencia, un emulador de procesadores de código abierto y multiplataforma. Emplea un sistema de traducción dinámica de binarios (dynamic binary translation) con el que convierte sobre la marcha las instrucciones de la CPU huésped (por ejemplo, RISC-V) en instrucciones que entienda la CPU anfitriona (por ejemplo, AMD64), de forma transparente para el sistema operativo invitado.
Además de emular CPUs, QEMU integra capacidades de virtualización cuando se combina con KVM en Linux. Esto permite que, cuando huésped y anfitrión comparten arquitectura, el rendimiento se acerque mucho al nativo. Pero cuando hablamos de RISC-V sobre una máquina x86_64, lo que nos interesa es sobre todo la emulación pura, que aunque es más lenta, nos da una flexibilidad enorme.
Entre las muchas arquitecturas que puede manejar QEMU encontramos RISC-V, IA-32/AMD64, ARM/AArch64, PowerPC/POWER, MIPS, SPARC y unas cuantas más. Esto lo convierte en una especie de navaja suiza para sistemas operativos y desarrolladores de bajo nivel.
RISC-V y su soporte en QEMU
RISC-V es una ISA abierta y modular, diseñada mucho más recientemente que otras arquitecturas clásicas como x86 o MIPS. Uno de los puntos interesantes de RISC-V es que la especificación incluye desde el diseño soporte para la virtualización, de forma más limpia y coherente que en arquitecturas con décadas de parches encima.
En la práctica, esto significa que, conforme vayan madurando las implementaciones, la virtualización sobre RISC-V puede llegar a ser más eficiente. Por ahora, sin embargo, el uso más habitual con QEMU es la emulación de sistemas RISC-V sobre máquinas x86_64 o ARM64 para desarrollo, pruebas académicas, investigación de arquitecturas y docencia.
Con QEMU podemos lanzar tanto binarios sueltos (qemu-riscv64, qemu-riscv32) como sistemas completos (qemu-system-riscv64, qemu-system-riscv32) con kernel Linux, OpenSBI, U-Boot y un root filesystem. Esto incluye distribuciones completas como Debian o Ubuntu en su variante riscv64.
Instalar QEMU y preparar el entorno en Ubuntu
Para seguir un flujo de trabajo cómodo, es habitual usar Ubuntu o Debian como sistema anfitrión, ya que disponen de paquetes bastante actualizados para QEMU, toolchains RISC-V y utilidades relacionadas. Sobre otras distribuciones los pasos serán similares, cambiando los nombres de paquetes.
Si quieres usar QEMU como hipervisor con KVM para arquitecturas nativas, puedes instalar el conjunto habitual de paquetes en Ubuntu:
sudo apt install qemu-kvm virt-manager virtinst libvirt-clients bridge-utils libvirt-daemon-system
Después conviene comprobar que la CPU soporta virtualización por hardware. En una terminal puedes ejecutar algo como egrep -c ‘(vmx|svm)’ /proc/cpuinfo; si el número es mayor que cero, tu CPU tiene extensiones de virtualización (vmx para Intel y svm para AMD). También puedes usar la herramienta cpu-checker:
sudo apt install cpu-checker
kvm-ok
Con esto podrás tirar de virt-manager para montar máquinas virtuales clásicas con interfaz gráfica, pero para RISC-V normalmente trabajarás más con la línea de comandos y con los binarios qemu-system-riscv64 y qemu-riscv64. Si solo te interesa RISC-V y otras arquitecturas extra, puedes instalar el paquete:
sudo apt install qemu-system-misc qemu-utils
Emular sistemas RISC-V completos con QEMU
Para ejecutar un sistema operativo completo sobre RISC-V con QEMU, lo normal es usar qemu-system-riscv64 (para 64 bits) junto con un firmware tipo OpenSBI, un bootloader como U-Boot y una imagen de disco con Linux ya instalado.
Un ejemplo típico en Ubuntu consiste en aprovechar OpenSBI y U-Boot empaquetados por la propia distribución, junto con una imagen preinstalada de Ubuntu Server para la placa virtual «virt» de QEMU. El flujo podría ser algo como esto:
sudo apt install qemu-system-misc opensbi u-boot-qemu qemu-utils
cd
mkdir qemu-riscv
cd qemu-riscv
Después descargamos la imagen preinstalada de Ubuntu para RISC-V, por ejemplo para una placa SiFive Unmatched, que también es usable con la máquina virtual de QEMU:
wget https://cdimage.ubuntu.com/releases/20.04.4/release/ubuntu-20.04.5-preinstalled-server-riscv64+unmatched.img.xz
xz -dk ubuntu-20.04.5-preinstalled-server-riscv64+unmatched.img.xz
Conviene comprobar que los binarios de firmware están donde los espera el paquete:
ll /usr/lib/riscv64-linux-gnu/opensbi/generic/fw_jump.elf
ll /usr/lib/u-boot/qemu-riscv64_smode/uboot.elf
Una vez tenemos todo eso, podemos arrancar la máquina RISC-V con un comando similar a este:
qemu-system-riscv64 -machine virt -nographic -m 2048 -smp 4 \
-bios /usr/lib/riscv64-linux-gnu/opensbi/generic/fw_jump.elf \
-kernel /usr/lib/u-boot/qemu-riscv64_smode/uboot.elf \
-device virtio-net-device,netdev=eth0 -netdev user,id=eth0 \
-drive file=ubuntu-20.04.5-preinstalled-server-riscv64+unmatched.img,format=raw,if=virtio
La opción -nographic redirige la consola a la terminal, lo que resulta muy cómodo para servidores o pruebas de desarrollo. Una vez arranca el sistema, normalmente podrás entrar con credenciales por defecto como ubuntu/ubuntu o las que indique la imagen que estés usando.
Otras formas de emular arquitecturas con QEMU: ejemplo Debian DQIB
Además del enfoque «montar todo a mano» (firmware, kernel, rootFS, etc.), existen proyectos que proporcionan imágenes ya preparadas y scripts de arranque para diferentes arquitecturas, lo que simplifica mucho las cosas cuando solo quieres tener un sistema funcional para hacer pruebas.
Un caso interesante es DQIB, un conjunto de imágenes Debian que facilita el uso de QEMU con arquitecturas menos comunes como mips64el, sparc64, armhf, arm64, riscv64, hppa, powerpc, ppc64, ppc64el o s390x. La idea es descargar un paquete, descomprimirlo y seguir las instrucciones del fichero readme.txt incluido.
Por ejemplo, para MIPS64EL el comando de ejemplo para arrancar la máquina puede ser algo parecido a:
qemu-system-mips64el -machine «malta» -cpu «5KEc» -m 1G -drive file=image.qcow2 \
-device e1000,netdev=net -netdev user,id=net,hostfwd=tcp::2222-:22 \
-kernel kernel -initrd initrd -nographic -append «root=LABEL=rootfs console=ttyS0»
Una vez que el sistema ha arrancado, puedes iniciar sesión con los usuarios preconfigurados (por ejemplo, root/root o debian/debian) y, dado que se redirige el puerto 22 al 2222 en el host, es posible conectarse por SSH con algo como:
ssh debian@localhost -p 2222
Este enfoque no solo se aplica a MIPS; también hay imágenes para riscv64, de modo que puedes tener una Debian RISC-V funcional lista para usar con un solo comando de QEMU y sin tener que montar manualmente cada pieza.
Montar tu propio Linux RISC-V con kernel y BusyBox
Si lo que te interesa es aprender a bajo nivel cómo se arranca un sistema RISC-V bajo QEMU, puedes optar por un enfoque más «artesanal»: compilar un kernel Linux para RISC-V, generar un root filesystem mínimo con BusyBox y lanzarlo directamente con qemu-system-riscv64.
El procedimiento general, tomando como base una distro tipo Ubuntu o Debian en el host, suele incluir varios pasos: instalar dependencias de compilación, descargar las fuentes del kernel, hacer la compilación cruzada para RISC-V y crear un rootFS simple. Lo normal es trabajar con RISC-V de 64 bits (riscv64), ya que es la variante más extendida para sistemas generales.
Primero prepara un directorio de trabajo cualquiera (por ejemplo, riscv-qemu-lab) donde irás dejando kernel, rootFS y scripts. Después puedes descargar el código fuente de Linux y configurar la compilación para la arquitectura riscv:
export ARCH=riscv
export CROSS_COMPILE=riscv64-linux-gnu-
Con las variables configuradas, ejecutarías algo como make defconfig y luego make -j$(nproc) para compilar. Al finalizar, deberías tener un kernel booteable en una ruta similar a linux/arch/riscv/boot/Image, que es el binario que QEMU usará con la opción -kernel.
Para el espacio de usuario mínimo, es muy común tirar de BusyBox, una colección de utilidades tipo Unix empaquetadas en un único binario muy pequeño. Ojo con un detalle importante: al igual que con el kernel, también tendrás que compilar BusyBox para RISC-V, no para tu arquitectura anfitriona, así que nuevamente estarás haciendo compilación cruzada.
Una vez compiles e instales BusyBox en una carpeta destino, puedes crear una imagen de disco que haga de root filesystem, formatearla y copiar dentro los ficheros básicos: directorios /bin, /sbin, /etc, /proc, /sys, /dev, etc., junto con el binario de BusyBox enlazado a los nombres de los comandos que quieras usar.
Cuando ya tienes el kernel y el rootFS, puedes arrancar QEMU más o menos así (ejemplo genérico):
qemu-system-riscv64 -machine virt -m 1024 -nographic \
-kernel arch/riscv/boot/Image \
-append «root=/dev/vda console=ttyS0» \
-drive file=rootfs.img,format=raw,if=virtio
Al arrancar verás los mensajes de inicio de Linux y acabarás en un prompt de BusyBox, donde podrás ejecutar comandos básicos. Para que BusyBox funcione correctamente, suele ser necesario crear ciertos enlaces simbólicos o ejecutar algún comando de inicialización específico (por ejemplo, montar /proc y /sys) justo al inicio.
Cuando quieras salir de QEMU en modo nographic, puedes usar Ctrl+A seguido de X, que es la combinación típica para cerrar la máquina.
Usar toolchains RISC-V para compilar software nativo
Para poder generar binarios nativos RISC-V desde tu máquina x86_64 necesitas una cadena de herramientas de compilación cruzada. Hoy en día tienes dos grandes opciones: el toolchain GNU (GCC, binutils, glibc/newlib) y el ecosistema LLVM/Clang.
El proyecto riscv-gnu-toolchain proporciona GCC y utilidades asociadas para RISC-V, tanto para entornos bare-metal (sin sistema operativo, usando newlib) como para Linux (con glibc). Una vez instalado, dispondrás de compiladores como riscv64-unknown-elf-gcc (orientado a bare-metal) o riscv64-linux-gnu-gcc (para programas que se ejecuten sobre Linux RISC-V).
La compilación de un programa sencillo seguiría una línea muy similar a la de cualquier otro cross-compiler: por ejemplo, podrías usar riscv64-linux-gnu-gcc mi_programa.c -o mi_programa para generar un binario que luego ejecutarás dentro de tu máquina QEMU RISC-V con Linux.
Si prefieres el ecosistema LLVM, también puedes construir Clang y lld con soporte para RISC-V, tal y como recomiendan en la documentación oficial. Esto es especialmente interesante cuando quieres aprovechar características más modernas del compilador o jugar con extensiones experimentales de la ISA.
Ejecutar binarios RISC-V sueltos con qemu-riscv64
Además de máquinas completas, QEMU ofrece emulación de usuario, es decir, la capacidad de ejecutar directamente un binario RISC-V en tu sistema anfitrión usando qemu-riscv64 (o qemu-riscv32). En este modo, QEMU traduce las llamadas al sistema del binario huésped a llamadas del sistema anfitrión.
Esto es especialmente cómodo cuando estás desarrollando aplicaciones pequeñas o ejemplos en ensamblador y no quieres montar un kernel y un rootFS cada vez. Simplemente compilas tu programa para RISC-V (con un toolchain tipo riscv64-unknown-elf) y lo lanzas con qemu-riscv64.
Por ejemplo, para compilar y ejecutar un «Hola mundo» en ensamblador (bare-metal o con un entorno mínimo), seguirías una secuencia parecida a esta:
riscv64-unknown-elf-as -o hola.o hola.s -march=rv64gc
riscv64-unknown-elf-ld -o hola hola.o
qemu-riscv64 ./hola
En el caso de ejemplos más avanzados, como programas que utilizan la extensión vectorial de RISC-V (RVV), tienes que prestar atención adicional a dos cosas: las opciones de compilación (-march con las extensiones adecuadas) y la configuración de la CPU que le pasas a QEMU.
RVV, SVE, QEMU y el lío de las versiones de especificación
Cuando entras en el terreno de las extensiones vectoriales modernas, la situación se complica un poco, sobre todo porque las especificaciones han ido cambiando con el tiempo y no todas las versiones de QEMU soportan las mismas variantes o la misma sintaxis de opciones.
Un caso habitual es el de alguien que quiere ejecutar ejemplos de la extensión vectorial RISC-V (RVV), como los mostrados en el repositorio oficial de la especificación, y se encuentra con errores al intentar lanzarlos bajo QEMU. Por ejemplo, compilar un fichero de ensamblador tipo strcpy.s con:
riscv64-unknown-elf-as -o sample.o rv_strcpy.s -march=rv64gc_zve64x
riscv64-unknown-elf-ld -o sample sample.o
Y luego intentar ejecutarlo con un comando como:
qemu-riscv64 -cpu rv64,x-v=true,vlen=256,elen=64,vext_spec=0.7.1 ./sample
Puede desembocar en mensajes de error del tipo «unsupported spec» o «illegal instruction». Esto suele indicar que hay mismatch entre la versión de la extensión vectorial que asume QEMU y la que usa el toolchain o el código de ejemplo.
En versiones antiguas de QEMU, la forma de activar RVV era un poco diferente y la opción vext_spec=0.7.1 correspondía a una revisión concreta de la especificación. En QEMU más nuevos, esa opción puede desaparecer o cambiar de nombre, y la extensión se activa simplemente con el sufijo «v» o con otras banderas específicas.
Si al eliminar vext_spec el programa pasa a fallar con «instrucción ilegal», eso significa que el código está usando instrucciones vectoriales que QEMU no reconoce como válidas con la configuración de CPU actual. La solución pasa por:
- Asegurarte de que usas una versión reciente de QEMU que soporte la versión actual de RVV.
- Revisar la documentación de QEMU para esa versión en concreto y ver cómo se declara la CPU con vectores (por ejemplo, -cpu rv64gcv u otro modelo que tenga v habilitado).
- Compilar el código con un -march coherente con la extensión soportada por QEMU (por ejemplo, rv64gcv o rv64gc+v, según recomiende el toolchain/ensamblador).
En entornos de investigación de ISA y tesis, como los que requieren jugar con SVE en ARM o RVV en RISC-V, es muy frecuente tener que alinear con mucho cuidado versiones de QEMU, del toolchain GNU o LLVM, y de los ejemplos oficiales. Si algo falla con errores de especificación no soportada, casi siempre hay una desincronización entre esas piezas.
Aprender ensamblador RISC-V con QEMU: del “Hola mundo” al sistema completo
Si estás empezando con ensamblador RISC-V, QEMU puede dar la impresión de «hacer magia» arrancando tu programa sin que veas todo lo que hay por debajo: no te preocupas de BIOS, ni de gestor de arranque, ni de mapa de memoria de vídeo, y eso, aunque es cómodo, a veces dificulta comprender lo que pasa realmente.
Cuando ejecutas un pequeño «Hola mundo» con qemu-riscv64 en modo usuario, QEMU te da un entorno simplificado donde se asume un ABI concreto (por ejemplo, el ELF de RISC-V con llamadas al sistema estándar) y se encargan de traducir esas llamadas al kernel de tu sistema anfitrión. No necesitas escribir un bootloader, simplemente estás lanzando un binario ELF, como harías en Linux x86_64, pero para una ISA emulada.
Si quieres entenderlo todo desde cero, te puede venir bien separar dos escenarios de aprendizaje: por un lado, binarios bare-metal que arrancan sin sistema operativo, donde tú escribes el código que arranca desde la dirección de reset de la CPU emulada; por otro lado, programas que se apoyan en un sistema operativo Linux completo, con sus llamadas al sistema, librerías C, etc.
QEMU soporta ambos mundos: puedes configurar una máquina RISC-V con un firmware mínimo y decirle que ejecute tu binario como si fuera la ROM de arranque, o bien arrancar un kernel Linux y luego usar el sistema como harías en cualquier distro, compilando y lanzando tus programas desde ahí.
Si lo que buscas es aprender cómo interactuar con el hardware virtual (memoria, periféricos, consola serie, etc.), el enfoque de montar un Linux mínimo con BusyBox, o incluso un entorno bare-metal con una pequeña rutina de arranque escrita por ti, te ayudará a ver mucho más claramente cada capa de la pila en lugar de esconderlo todo tras un binario y un comando de QEMU.
QEMU y RISC-V forman un escenario muy potente tanto para quien quiere simplemente ejecutar software nativo RISC-V paso a paso en un PC convencional, como para quien está investigando extensiones avanzadas de la ISA o aprendiendo ensamblador y sistemas operativos desde sus cimientos.