Memory & I/O

Physical memory map, RAM, MMIO dispatch, dirty bits, and IRQ wiring

Data Flow

graph TD A[CPU Load/Store] --> B[TLB check] B -->|Hit| C[Direct RAM pointer] B -->|Miss| D[target_read_slow / target_write_slow] D --> E[MMU page table walk] E --> F[PhysMemoryMap lookup] F -->|RAM| G[phys_mem_get_ram_ptr] F -->|MMIO| H[Device read_func / write_func] H --> I[CLINT / PLIC / HTIF / VirtIO] C --> J[Dirty bit tracking] J --> K[simplefb_refresh / VGA update]

Physical Memory Map

iomem.c manages the PhysMemoryMap: an array of up to PHYS_MEM_RANGE_MAX ranges. Each range is either RAM (is_ram = TRUE) or a device (is_ram = FALSE). The map provides function pointers for RAM registration, freeing, dirty-bit retrieval, and address changes.

PhysMemoryMap *phys_mem_map_init(void)
{
    PhysMemoryMap *s;
    s = mallocz(sizeof(*s));
    s->register_ram = default_register_ram;
    s->free_ram = default_free_ram;
    s->get_dirty_bits = default_get_dirty_bits;
    s->set_ram_addr = default_set_addr;
    return s;
}

Memory Range Lookup

get_phys_mem_range() linearly searches the array for a range containing the physical address. This is called on every TLB miss and MMIO access.

PhysMemoryRange *get_phys_mem_range(PhysMemoryMap *s, uint64_t paddr)
{
    PhysMemoryRange *pr;
    int i;
    for(i = 0; i < s->n_phys_mem_range; i++) {
        pr = &s->phys_mem_range[i];
        if (paddr >= pr->addr && paddr < pr->addr + pr->size)
            return pr;
    }
    return NULL;
}

RAM Registration

cpu_register_ram() allocates host memory for a guest RAM region. The DEVRAM_FLAG_DIRTY_BITS flag enables tracking of modified pages via a bitmap, used by the display subsystem to detect framebuffer changes.

static PhysMemoryRange *default_register_ram(PhysMemoryMap *s, uint64_t addr,
                                             uint64_t size, int devram_flags)
{
    PhysMemoryRange *pr;
    pr = register_ram_entry(s, addr, size, devram_flags);
    pr->phys_mem = mallocz(size);
    if (!pr->phys_mem) {
        fprintf(stderr, "Could not allocate VM memory\n");
        exit(1);
    }
    if (devram_flags & DEVRAM_FLAG_DIRTY_BITS) {
        size_t nb_pages = size >> DEVRAM_PAGE_SIZE_LOG2;
        pr->dirty_bits_size = ((nb_pages + 31) / 32) * sizeof(uint32_t);
        pr->dirty_bits_index = 0;
        for(i = 0; i < 2; i++) {
            pr->dirty_bits_tab[i] = mallocz(pr->dirty_bits_size);
        }
        pr->dirty_bits = pr->dirty_bits_tab[pr->dirty_bits_index];
    }
    return pr;
}

MMIO Device Registration

cpu_register_device() maps a physical address range to callback functions. When the CPU accesses this range, the read/write callbacks are invoked with the offset and access size.

PhysMemoryRange *cpu_register_device(PhysMemoryMap *s, uint64_t addr,
                                     uint64_t size, void *opaque,
                                     DeviceReadFunc *read_func,
                                     DeviceWriteFunc *write_func,
                                     int devio_flags)
{
    PhysMemoryRange *pr;
    assert(s->n_phys_mem_range < PHYS_MEM_RANGE_MAX);
    assert(size <= 0xffffffff);
    pr = &s->phys_mem_range[s->n_phys_mem_range++];
    pr->map = s;
    pr->addr = addr;
    pr->org_size = size;
    pr->size = (devio_flags & DEVIO_DISABLED) ? 0 : pr->org_size;
    pr->is_ram = FALSE;
    pr->opaque = opaque;
    pr->read_func = read_func;
    pr->write_func = write_func;
    pr->devio_flags = devio_flags;
    return pr;
}

Dirty Bits

Dirty bits track which pages have been written by the CPU. The display code calls phys_mem_get_dirty_bits() to retrieve and reset the bitmap, then flushes only changed regions to the screen.

static const uint32_t *default_get_dirty_bits(PhysMemoryMap *map,
                                              PhysMemoryRange *pr)
{
    uint32_t *dirty_bits = pr->dirty_bits;
    BOOL has_dirty_bits = FALSE;
    size_t n = pr->dirty_bits_size / sizeof(uint32_t);
    for(i = 0; i < n; i++) {
        if (dirty_bits[i] != 0) { has_dirty_bits = TRUE; break; }
    }
    if (has_dirty_bits && pr->size != 0) {
        map->flush_tlb_write_range(map->opaque, pr->phys_mem, pr->org_size);
    }
    pr->dirty_bits_index ^= 1;
    pr->dirty_bits = pr->dirty_bits_tab[pr->dirty_bits_index];
    memset(pr->dirty_bits, 0, pr->dirty_bits_size);
    return dirty_bits;
}

IRQ Wiring

The IRQSignal abstraction connects devices to interrupt controllers. irq_init() binds a SetIRQFunc callback, and set_irq() raises or lowers the line. On RISC-V, PLIC IRQs are wired to the CPU's mip via plic_set_irq.

typedef void SetIRQFunc(void *opaque, int irq_num, int level);

typedef struct {
    SetIRQFunc *set_irq;
    void *opaque;
    int irq_num;
} IRQSignal;

void irq_init(IRQSignal *irq, SetIRQFunc *set_irq, void *opaque, int irq_num);

static inline void set_irq(IRQSignal *irq, int level)
{
    irq->set_irq(irq->opaque, irq->irq_num, level);
}