蘇湘婷
This project involves improving the WebAssembly port of rv32emu, a RISC-V emulator capable of running the Linux kernel. The goal is to enable the emulated Linux/RISC-V system to access a designated browser directory via virtio, referencing concepts from previous implementations and standards
Virtio is a standardized interface for virtual devices, commonly used in virtualized and emulated environments. In the context of your project, using Virtio provides several key benefits:
Efficient Communication:
Virtio enables efficient communication between the emulated Linux/RISC-V system and the underlying host (in this case, the browser). It minimizes overhead, allowing faster data transfer and interaction with virtual devices.
Flexibility:
Virtio is designed to be flexible, supporting a variety of device types (e.g., block devices, network interfaces, etc.). For your project, it facilitates seamless access to browser-designated directories, enabling functionality similar to file sharing.
Standardization:
As a widely adopted standard, Virtio simplifies development by providing well-documented protocols. This reduces the complexity of integrating the emulated system with WebAssembly and the browser environment.
Compatibility:
By leveraging Virtio, your project can align with existing Linux drivers that natively support Virtio devices, reducing the need for custom driver development.
Cross-Platform Integration:
Virtio is not tied to a specific platform, making it ideal for environments like WebAssembly where portability and platform independence are critical.
sudo apt-get install llvm-18
$ make distclean
$ make ENABLE_SYSTEM=1 ENABLE_MOP_FUSION=0 ENABLE_JIT=1 ENABLE_T2C=0
Error messages:
src/riscv.c: In function ‘rv_delete’:
src/riscv.c:586:18: error: ‘attr’ undeclared (first use in this function)
586 | u8250_delete(attr->uart);
| ^~~~
src/riscv.c:586:18: note: each undeclared identifier is reported only once for each function it appears in
make: *** [Makefile:257: build/riscv.o] Error 1
Clone this branch as the issue has already been resolved.
Always write in English.
src/virtio-blk.c
/**
* virtio-blk.c - A minimal skeleton for virtio-blk device
*
* This file is for demonstration purpose only.
* It omits error handling, concurrency control,
* and other complexities involved in real projects.
*
* ...
*/
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <inttypes.h>
/* ========== VirtIO Block Related Definitions ========== */
#define VIRTIO_BLK_F_RO 5
#define VIRTIO_BLK_F_SCSI 7
#define VIRTIO_BLK_F_CONFIG_WCE 11
#define VIRTIO_BLK_F_MQ 12
#define VIRTIO_BLK_F_DISCARD 13
#define VIRTIO_BLK_F_WRITE_ZEROES 14
#define VIRTIO_CONFIG_S_ACKNOWLEDGE 1
#define VIRTIO_CONFIG_S_DRIVER 2
#define VIRTIO_CONFIG_S_DRIVER_OK 4
#define VIRTIO_CONFIG_S_FEATURES_OK 8
#define VIRTIO_CONFIG_S_FAILED 128
#define VIRTIO_BLK_T_IN 0
#define VIRTIO_BLK_T_OUT 1
#define VIRTIO_BLK_T_FLUSH 4
#define VIRTIO_BLK_T_DISCARD 11
#define VIRTIO_BLK_T_WRITE_ZEROES 13
#pragma pack(push, 1)
struct virtio_blk_req {
uint32_t type;
uint32_t reserved;
uint64_t sector;
};
#pragma pack(pop)
#pragma pack(push, 1)
struct virtio_blk_config {
uint64_t capacity;
uint32_t size_max;
uint32_t seg_max;
uint16_t cylinders;
uint8_t heads;
uint8_t sectors;
uint32_t blk_size;
};
#pragma pack(pop)
enum {
VIRTIO_BLK_S_OK = 0,
VIRTIO_BLK_S_IOERR,
VIRTIO_BLK_S_UNSUPP,
};
#define QUEUE_SIZE 128
struct virtqueue_desc {
uint64_t addr;
uint32_t len;
uint16_t flags;
uint16_t next;
};
struct virtqueue_avail {
uint16_t flags;
uint16_t idx;
uint16_t ring[QUEUE_SIZE];
};
struct virtqueue_used_elem {
uint32_t id;
uint32_t len;
};
struct virtqueue_used {
uint16_t flags;
uint16_t idx;
struct virtqueue_used_elem ring[QUEUE_SIZE];
};
struct virtio_queue {
struct virtqueue_desc desc[QUEUE_SIZE];
struct virtqueue_avail avail;
struct virtqueue_used used;
};
struct virtio_blk_dev {
struct virtio_blk_config config;
struct virtio_queue vq;
uint8_t status;
uint64_t capacity;
uint8_t *backend;
};
/* ========== Helper Functions ========== */
static inline void set_device_status(struct virtio_blk_dev *dev, uint8_t status)
{
dev->status |= status;
printf("[virtio-blk] Device status updated to 0x%x\n", dev->status);
}
void virtio_blk_init(struct virtio_blk_dev *dev, uint64_t capacity_in_sectors)
{
dev->status = 0;
set_device_status(dev, VIRTIO_CONFIG_S_ACKNOWLEDGE);
set_device_status(dev, VIRTIO_CONFIG_S_DRIVER);
set_device_status(dev, VIRTIO_CONFIG_S_FEATURES_OK);
dev->capacity = capacity_in_sectors;
dev->config.capacity = capacity_in_sectors;
dev->config.blk_size = 512;
dev->backend = (uint8_t*)malloc(dev->config.capacity * dev->config.blk_size);
memset(dev->backend, 0, dev->config.capacity * dev->config.blk_size);
memset(&dev->vq, 0, sizeof(dev->vq));
set_device_status(dev, VIRTIO_CONFIG_S_DRIVER_OK);
printf("[virtio-blk] Device initialized with capacity = %" PRIu64 " sectors\n",
capacity_in_sectors);
}
static void process_request(struct virtio_blk_dev *dev,
struct virtio_blk_req *req,
uint8_t *data_buf, size_t data_len,
uint8_t *status)
{
switch (req->type) {
case VIRTIO_BLK_T_IN:
if ((req->sector + (data_len / 512)) > dev->capacity) {
*status = VIRTIO_BLK_S_IOERR;
break;
}
memcpy(data_buf,
dev->backend + (req->sector * dev->config.blk_size),
data_len);
*status = VIRTIO_BLK_S_OK;
break;
case VIRTIO_BLK_T_OUT:
if ((req->sector + (data_len / 512)) > dev->capacity) {
*status = VIRTIO_BLK_S_IOERR;
break;
}
memcpy(dev->backend + (req->sector * dev->config.blk_size),
data_buf, data_len);
*status = VIRTIO_BLK_S_OK;
break;
case VIRTIO_BLK_T_FLUSH:
*status = VIRTIO_BLK_S_OK;
break;
default:
*status = VIRTIO_BLK_S_UNSUPP;
break;
}
}
void virtio_blk_handle_queue(struct virtio_blk_dev *dev)
{
struct virtio_queue *vq = &dev->vq;
/* Assume there is a request placed in desc[0] */
struct virtqueue_desc *desc = &vq->desc[0];
/* These pointers usually require address translation (MMU / host-guest mapping) */
struct virtio_blk_req *req_hdr = (struct virtio_blk_req*)(uintptr_t)desc->addr;
size_t data_len = 512;
uint8_t data_buf[512];
uint8_t status_byte = 0;
printf("[virtio-blk] Handling request: type=%d, sector=%" PRIu64 "\n",
req_hdr->type, req_hdr->sector);
process_request(dev, req_hdr, data_buf, data_len, &status_byte);
printf("[virtio-blk] Request done, status = %u\n", status_byte);
vq->used.ring[vq->used.idx].id = 0;
vq->used.ring[vq->used.idx].len = (uint32_t)data_len;
vq->used.idx++;
}
/*
* For use in main.c.
*/
void virtio_blk_demo(void)
{
struct virtio_blk_dev blk_dev;
/* Initialize and test a simple request */
virtio_blk_init(&blk_dev, 1024);
static struct virtio_blk_req req;
req.type = VIRTIO_BLK_T_IN; /* read */
req.reserved = 0;
req.sector = 10;
blk_dev.vq.desc[0].addr = (uintptr_t)&req;
blk_dev.vq.desc[0].len = sizeof(req);
virtio_blk_handle_queue(&blk_dev);
free(blk_dev.backend);
}
Refine the comments. Always write in English!
src/main.c
...
extern void virtio_blk_demo(void);
...
virtio_mmio_init();
virtio_blk_demo();
rv_run(rv);
...
src/virtio_mmio.h
#ifndef VIRTIO_MMIO_H
#define VIRTIO_MMIO_H
#define VIRTIO_MMIO_DRIVER_FEATURES 0x20
#define VIRTIO_MMIO_BASE 0xf5000000
#define VIRTIO_MMIO_SIZE 0x1000
extern struct virtio_mmio_dev g_virtio_mmio0;
#ifdef __cplusplus
extern "C" {
#endif
struct virtio_mmio_dev {
uint32_t magic_value;
uint32_t version;
uint32_t device_id;
uint32_t vendor_id;
uint32_t device_features;
uint32_t driver_features;
uint32_t queue_sel;
uint32_t queue_num_max;
uint32_t queue_ready;
uint32_t status;
};
/**
* Initialize the virtio-mmio region, mapping the address range
* [0xf5000000, 0xf5000fff] to the emulator's memory/IO map.
* This allows Linux to detect virtio-mmio devices (e.g., block devices)
* through these addresses.
*/
void virtio_mmio_init(void);
#ifdef __cplusplus
}
#endif
#endif /* VIRTIO_MMIO_H */
src/virtio_mmio.c
#include "virtio_mmio.h"
#include "io_map.h"
#include <stdio.h>
#include <string.h>
struct virtio_mmio_dev g_virtio_mmio0;
static uint8_t virtio_mmio_read8(uint32_t addr, void *opaque)
{
struct virtio_mmio_dev *dev = opaque;
uint32_t offset = addr - VIRTIO_MMIO_BASE;
switch (offset) {
case 0x00: // MAGIC: lower 8 bits
case 0x01:
case 0x02:
case 0x03: {
uint32_t val = dev->magic_value;
int shift = (offset - 0x00) * 8; // 0,8,16,24
return (val >> shift) & 0xFF;
}
case 0x04: // VERSION (lower 8 bits)
return (uint8_t)dev->version;
default:
return 0;
}
}
static uint16_t virtio_mmio_read16(uint32_t addr, void *opaque)
{
struct virtio_mmio_dev *dev = opaque;
uint32_t offset = addr - VIRTIO_MMIO_BASE;
switch (offset) {
case 0x00: // MAGIC: lower 16 bits
case 0x02: {
uint32_t val = dev->magic_value;
int shift = (offset & 0x2) * 8; // offset=0 => shift=0; offset=2 => shift=16
return (val >> shift) & 0xFFFF;
}
case 0x04: // VERSION (lower 16 bits)
return (uint16_t)dev->version;
default:
return 0;
}
}
static uint32_t virtio_mmio_read32(uint32_t addr, void *opaque)
{
struct virtio_mmio_dev *dev = opaque;
uint32_t offset = addr - VIRTIO_MMIO_BASE;
switch (offset) {
case 0x00: // MAGIC
return dev->magic_value;
case 0x04: // VERSION
return dev->version;
case 0x08: // DEVICE_ID
return dev->device_id;
default:
return 0;
}
}
/*
* Write callbacks must return `int`,
* matching `io_write8_fn_t`, `io_write16_fn_t`, `io_write32_fn_t`.
* Return 1 if handled, 0 if not handled.
*/
static int virtio_mmio_write8(uint32_t addr, uint8_t val, void *opaque)
{
// Example: Not actually handling offset except for demonstration
// Return 1 if handled, 0 if not
(void)addr;
(void)val;
(void)opaque;
return 0;
}
static int virtio_mmio_write16(uint32_t addr, uint16_t val, void *opaque)
{
// Example: Not actually handling offset except for demonstration
// Return 1 if handled, 0 if not
(void)addr;
(void)val;
(void)opaque;
return 0;
}
static int virtio_mmio_write32(uint32_t addr, uint32_t val, void *opaque)
{
struct virtio_mmio_dev *dev = opaque;
uint32_t offset = addr - VIRTIO_MMIO_BASE;
switch (offset) {
case 0x60: // STATUS
dev->status = val;
printf("virtio_mmio: STATUS <- 0x%x\n", val);
return 1;
default:
break;
}
return 0;
}
void virtio_mmio_init(void)
{
memset(&g_virtio_mmio0, 0, sizeof(g_virtio_mmio0));
g_virtio_mmio0.magic_value = 0x74726976; // 'triv'
g_virtio_mmio0.version = 2;
g_virtio_mmio0.device_id = 2; // block device
g_virtio_mmio0.vendor_id = 0x554D4552; // 'UMER'
g_virtio_mmio0.queue_num_max = 128;
map_io_register_ex(
VIRTIO_MMIO_BASE,
VIRTIO_MMIO_SIZE,
virtio_mmio_read8,
virtio_mmio_read16,
virtio_mmio_read32,
virtio_mmio_write8,
virtio_mmio_write16,
virtio_mmio_write32,
&g_virtio_mmio0
);
printf("virtio_mmio_init: successfully registered mmio dev\n");
}
src/io_map.h
#ifndef IO_MAP_H
#define IO_MAP_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Functions used by the CPU/bus to perform read/write */
uint8_t io_map_read8(uint32_t addr);
uint16_t io_map_read16(uint32_t addr);
uint32_t io_map_read32(uint32_t addr);
int io_map_write8(uint32_t addr, uint8_t val);
int io_map_write16(uint32_t addr, uint16_t val);
int io_map_write32(uint32_t addr, uint32_t val);
/* Callback types for read and write operations of various sizes */
typedef uint8_t (*io_read8_fn_t) (uint32_t addr, void *opaque);
typedef uint16_t (*io_read16_fn_t)(uint32_t addr, void *opaque);
typedef uint32_t (*io_read32_fn_t)(uint32_t addr, void *opaque);
/* Write callbacks must return int: 1 = handled, 0 = not handled */
typedef int (*io_write8_fn_t) (uint32_t addr, uint8_t val, void *opaque);
typedef int (*io_write16_fn_t)(uint32_t addr, uint16_t val, void *opaque);
typedef int (*io_write32_fn_t)(uint32_t addr, uint32_t val, void *opaque);
/**
* Registers the address range [base, base + size) into the emulator's I/O map,
* allowing 8/16/32-bit read and write operations within this range.
*/
void map_io_register_ex(uint32_t base,
uint32_t size,
io_read8_fn_t read8_cb,
io_read16_fn_t read16_cb,
io_read32_fn_t read32_cb,
io_write8_fn_t write8_cb,
io_write16_fn_t write16_cb,
io_write32_fn_t write32_cb,
void *opaque);
#ifdef __cplusplus
}
#endif
#endif /* IO_MAP_H */
src/io_map.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "io_map.h"
#include "map.h" // Red-black tree or your chosen data structure
#include "io.h" // If fallback to memory_xxx is needed within these calls (optional)
typedef struct io_region_s {
uint32_t base;
uint32_t size;
io_read8_fn_t read8;
io_read16_fn_t read16;
io_read32_fn_t read32;
io_write8_fn_t write8;
io_write16_fn_t write16;
io_write32_fn_t write32;
void *opaque; // Device-specific pointer
} io_region_t;
/* Compare two base addresses as keys */
static map_cmp_t region_comparator(const void *key1, const void *key2)
{
const uint32_t *a = key1;
const uint32_t *b = key2;
if (*a < *b) return _CMP_LESS;
if (*a > *b) return _CMP_GREATER;
return _CMP_EQUAL;
}
static map_t g_io_map = NULL;
static void ensure_map_initialized(void)
{
if (!g_io_map) {
g_io_map = map_new(sizeof(uint32_t), sizeof(io_region_t),
region_comparator);
}
}
void map_io_register_ex(uint32_t base,
uint32_t size,
io_read8_fn_t read8_cb,
io_read16_fn_t read16_cb,
io_read32_fn_t read32_cb,
io_write8_fn_t write8_cb,
io_write16_fn_t write16_cb,
io_write32_fn_t write32_cb,
void *opaque)
{
ensure_map_initialized();
io_region_t region = {
.base = base,
.size = size,
.read8 = read8_cb,
.read16 = read16_cb,
.read32 = read32_cb,
.write8 = write8_cb,
.write16 = write16_cb,
.write32 = write32_cb,
.opaque = opaque
};
map_insert(g_io_map, &base, ®ion);
printf("map_io_register_ex: [0x%x .. 0x%x] registered\n",
base, base + size - 1);
}
uint8_t io_map_read8(uint32_t addr)
{
ensure_map_initialized();
map_iter_t it;
map_find(g_io_map, &it, &addr);
if (map_at_end(g_io_map, &it))
return 0xFF; // Not in any region
io_region_t *r = (io_region_t *)it.node->data;
/* Check range */
if (addr < r->base || addr >= r->base + r->size)
return 0xFF;
if (r->read8)
return r->read8(addr, r->opaque);
return 0xFF;
}
uint16_t io_map_read16(uint32_t addr)
{
ensure_map_initialized();
map_iter_t it;
map_find(g_io_map, &it, &addr);
if (map_at_end(g_io_map, &it))
return 0xFFFF;
io_region_t *r = (io_region_t *)it.node->data;
if (addr < r->base || addr >= r->base + r->size)
return 0xFFFF;
if (r->read16)
return r->read16(addr, r->opaque);
return 0xFFFF;
}
uint32_t io_map_read32(uint32_t addr)
{
ensure_map_initialized();
map_iter_t it;
map_find(g_io_map, &it, &addr);
if (map_at_end(g_io_map, &it))
return 0xFFFFFFFF;
io_region_t *r = (io_region_t *)it.node->data;
if (addr < r->base || addr >= r->base + r->size)
return 0xFFFFFFFF;
if (r->read32)
return r->read32(addr, r->opaque);
return 0xFFFFFFFF;
}
/* Write functions return int: 1 if handled, 0 if not. */
int io_map_write8(uint32_t addr, uint8_t val)
{
ensure_map_initialized();
map_iter_t it;
map_find(g_io_map, &it, &addr);
if (map_at_end(g_io_map, &it))
return 0;
io_region_t *r = (io_region_t *)it.node->data;
if (addr < r->base || addr >= r->base + r->size)
return 0;
if (r->write8)
return r->write8(addr, val, r->opaque);
return 0;
}
int io_map_write16(uint32_t addr, uint16_t val)
{
ensure_map_initialized();
map_iter_t it;
map_find(g_io_map, &it, &addr);
if (map_at_end(g_io_map, &it))
return 0;
io_region_t *r = (io_region_t *)it.node->data;
if (addr < r->base || addr >= r->base + r->size)
return 0;
if (r->write16)
return r->write16(addr, val, r->opaque);
return 0;
}
int io_map_write32(uint32_t addr, uint32_t val)
{
ensure_map_initialized();
map_iter_t it;
map_find(g_io_map, &it, &addr);
if (map_at_end(g_io_map, &it))
return 0;
io_region_t *r = (io_region_t *)it.node->data;
if (addr < r->base || addr >= r->base + r->size)
return 0;
if (r->write32)
return r->write32(addr, val, r->opaque);
return 0;
}
src/devices.c
#include "io.h"
#include "io_map.h"
static inline uint8_t bus_read8(uint32_t addr)
{
uint8_t val = io_map_read8(addr);
if (val == 0xFF) { // Sentinel value indicates IO map did not intercept
return memory_read_b(addr); // Fallback to normal RAM read
}
return val;
}
static inline uint16_t bus_read16(uint32_t addr)
{
uint16_t val = io_map_read16(addr);
if (val == 0xFFFF) { // Sentinel value for 16-bit
return memory_read_s(addr); // Fallback to normal RAM read
}
return val;
}
static inline uint32_t bus_read32(uint32_t addr)
{
uint32_t val = io_map_read32(addr);
if (val == 0xFFFFFFFF) { // Sentinel value for 32-bit
return memory_read_w(addr); // Fallback to normal RAM read
}
return val;
}
static inline void bus_write8(uint32_t addr, uint8_t val)
{
if (!io_map_write8(addr, val)) { // If IO map didn't handle, write to RAM
memory_write_b(addr, &val);
}
}
static inline void bus_write16(uint32_t addr, uint16_t val)
{
if (!io_map_write16(addr, val)) {
memory_write_s(addr, (const uint8_t *)&val);
}
}
static inline void bus_write32(uint32_t addr, uint32_t val)
{
if (!io_map_write32(addr, val)) {
memory_write_w(addr, (const uint8_t *)&val);
}
}
/*
Make sure you call bus_read8/bus_read16/bus_read32 and bus_write8/bus_write16/bus_write32
in your CPU instruction decode (lb/lh/lw/sb/sh/sw) so that MMIO devices can be intercepted.
*/
Makefile
$(OUT)/virtio-blk.o: src/virtio-blk.c
$(VECHO) " CC\t$@\n"
$(Q)$(CC) -o $@ $(CFLAGS) $(CFLAGS_emcc) -c -MMD -MF $@.d $<
$(OUT)/virtio-mmio.o: src/virtio-mmio.c
$(VECHO) " CC\t$@\n"
$(Q)$(CC) -o $@ $(CFLAGS) $(CFLAGS_emcc) -c -MMD -MF $@.d $<
$(OUT)/io_map.o: src/io_map.c
$(VECHO) " CC\t$@\n"
$(Q)$(CC) -o $@ $(CFLAGS) $(CFLAGS_emcc) -c -MMD -MF $@.d $<
$(OUT)/devices.o: src/devices.c
$(VECHO) " CC\t$@\n"
$(Q)$(CC) -o $@ $(CFLAGS) $(CFLAGS_emcc) -c -MMD -MF $@.d $<
...
OBJS := \
...
virtio_mmio.o \
io_map.o \
devices.o \
virtio-blk.o \
assets/system/configs/linux.config
CONFIG_CMDLINE="root=/dev/vda rw console=ttyS0"
src/devices/minimal.dts
soc: soc@F0000000 {
...
virtio_mmio@5000000 {
compatible = "virtio,mmio";
reg = <0x5000000 0x1000>;
interrupt-parent = <&plic0>;
interrupts = <2>;
status = "okay";
};
};
Console on web
...
[ 0.000000] Kernel command line: root=/dev/vda rw console=ttyS0
...
[ 0.196825] virtio-mmio f5000000.virtio_mmio: Wrong magic value 0x00000000!
...
Welcome to Buildroot
buildroot login:
Avoid using screenshots when you can simply include the text directly.
I have implemented a VirtIO block and virtio-mmio device simulation in my custom RISC-V emulator. I created map_io_register_ex(…) to handle 8/16/32-bit read/write callbacks, allowing the kernel to access the device’s registers. However, the kernel still reads 0x00000000 instead of the correct magic value. The problem is that the CPU core’s load/store instructions are directly accessing memory (memory_read_), bypassing my bus_read/bus_write* system. This prevents my virtio-mmio callbacks from being invoked. I need to modify the CPU’s instruction decode/execution to ensure all memory accesses, especially in IO regions, go through my bus_read*/bus_write* handlers. Once fixed, Linux should recognize the correct magic value and properly detect /dev/vda.