3. 设备寄存器读写测试

设备寄存器读写测试 学习卡片

任务说明

本案例为QEMU虚拟设备添加一组MMIO寄存器,并编写测试用例验证寄存器读写行为正确。设备模拟一个简单的状态寄存器组:包含版本号寄存器(只读)、控制寄存器(可读写)、状态寄存器(只读,由设备内部逻辑驱动)。测试需覆盖:初始值正确性、只读寄存器写操作被忽略、可读写寄存器写后读回一致。

设备实现

// regdev.c - 简单MMIO寄存器设备
#include "qemu/osdep.h"
#include "hw/pci/pci_device.h"
#include "qemu/log.h"
#include "qapi/error.h"

#define TYPE_REGDEV "regdev"
OBJECT_DECLARE_SIMPLE_TYPE(RegDevState, REGDEV)

#define REGDEV_MMIO_BAR 0
#define REGDEV_MMIO_SIZE 0x1000
#define REG_VERSION 0x00   // 只读,固定0x0001
#define REG_CONTROL 0x04   // 可读写
#define REG_STATUS  0x08   // 只读,bit0反映CONTROL bit0取反

typedef struct {
    PCIDevice parent_obj;
    MemoryRegion mmio;
    uint32_t control;
} RegDevState;

static uint64_t regdev_mmio_read(void *opaque, hwaddr addr, unsigned size)
{
    RegDevState *s = opaque;
    uint32_t val = 0;

    switch (addr) {
    case REG_VERSION:
        val = 0x0001;
        break;
    case REG_CONTROL:
        val = s->control;
        break;
    case REG_STATUS:
        val = (s->control & 1) ? 0 : 1;  // 逻辑反相
        break;
    default:
        qemu_log_mask(LOG_GUEST_ERROR, "regdev: bad read addr 0x%"HWADDR_PRIx"\n", addr);
        break;
    }
    return val;
}

static void regdev_mmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)
{
    RegDevState *s = opaque;

    switch (addr) {
    case REG_CONTROL:
        s->control = (uint32_t)val;
        break;
    case REG_VERSION:
    case REG_STATUS:
        // 只读寄存器,写操作忽略
        qemu_log_mask(LOG_GUEST_ERROR, "regdev: write to RO reg 0x%"HWADDR_PRIx"\n", addr);
        break;
    default:
        qemu_log_mask(LOG_GUEST_ERROR, "regdev: bad write addr 0x%"HWADDR_PRIx"\n", addr);
        break;
    }
}

static const MemoryRegionOps regdev_mmio_ops = {
    .read = regdev_mmio_read,
    .write = regdev_mmio_write,
    .endianness = DEVICE_LITTLE_ENDIAN,
    .impl = { .min_access_size = 4, .max_access_size = 4 },
};

static void regdev_realize(PCIDevice *pci_dev, Error **errp)
{
    RegDevState *s = REGDEV(pci_dev);

    memory_region_init_io(&s->mmio, OBJECT(s), ®dev_mmio_ops, s,
                          "regdev-mmio", REGDEV_MMIO_SIZE);
    pci_register_bar(pci_dev, REGDEV_MMIO_BAR, PCI_BASE_ADDRESS_SPACE_MEMORY,
                     &s->mmio);
}

static void regdev_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    PCIDeviceClass *k = PCI_DEVICE_CLASS(klass);

    k->realize = regdev_realize;
    k->vendor_id = 0x1234;
    k->device_id = 0x5678;
    k->class_id = PCI_CLASS_OTHERS;
    dc->desc = "Simple MMIO register device";
}

static const TypeInfo regdev_info = {
    .name = TYPE_REGDEV,
    .parent = TYPE_PCI_DEVICE,
    .instance_size = sizeof(RegDevState),
    .class_init = regdev_class_init,
};

static void regdev_register_types(void)
{
    type_register_static(®dev_info);
}
type_init(regdev_register_types)

测试程序

// test_regdev.c - 在guest中验证寄存器读写
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <assert.h>

#define PCI_DEV "/sys/bus/pci/devices/0000:00:03.0/resource0"
#define REG_VERSION  0x00
#define REG_CONTROL  0x04
#define REG_STATUS   0x08

int main(void)
{
    int fd = open(PCI_DEV, O_RDWR | O_SYNC);
    if (fd < 0) { perror("open"); return 1; }

    volatile uint32_t *regs = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
                                   MAP_SHARED, fd, 0);
    if (regs == MAP_FAILED) { perror("mmap"); return 1; }

    // 1. 初始值检查
    assert(regs[REG_VERSION/4] == 0x0001);
    assert(regs[REG_CONTROL/4] == 0x0000);
    assert(regs[REG_STATUS/4] == 0x0001);  // control bit0=0 → status=1

    // 2. 只读寄存器写忽略
    regs[REG_VERSION/4] = 0xFFFF;
    regs[REG_STATUS/4] = 0xAAAA;
    assert(regs[REG_VERSION/4] == 0x0001);
    assert(regs[REG_STATUS/4] == 0x0001);

    // 3. 可读写寄存器写后读回
    regs[REG_CONTROL/4] = 0x5A5A;
    assert(regs[REG_CONTROL/4] == 0x5A5A);
    assert(regs[REG_STATUS/4] == 0x0000);  // bit0=0 → status=1? 0x5A5A bit0=0 → status=1

    regs[REG_CONTROL/4] = 0x0001;
    assert(regs[REG_CONTROL/4] == 0x0001);
    assert(regs[REG_STATUS/4] == 0x0000);  // bit0=1 → status=0

    printf("ALL REGDEV TESTS PASSED\n");
    munmap((void*)regs, 0x1000);
    close(fd);
    return 0;
}

编译与运行

# 编译设备(在QEMU源码树内)
make qemu-x86_64

# 启动带设备的虚拟机(假设已有磁盘镜像disk.img)
./qemu-system-x86_64 -machine q35 -m 512 \
    -drive file=disk.img,format=raw \
    -device regdev \
    -net none -nographic

# 在guest内编译并运行测试
gcc -o test_regdev test_regdev.c
./test_regdev

验证方法

测试程序通过PCI sysfs接口将设备BAR0映射到用户空间,直接以32位小端访问寄存器。判定标准:程序输出ALL REGDEV TESTS PASSED且退出码为0,表示三项断言全部通过——初始值正确、只读寄存器写入被忽略、控制寄存器读写一致且状态寄存器逻辑正确。若断言失败,程序会打印失败行号并异常退出。

BAR0 内存布局 (0x1000) 0x00 VERSION 只读 = 0x0001 0x04 CONTROL 可读写 0x08 STATUS 只读 = ~CTRL.bit0 测试访问路径 Guest 测试程序 PCI sysfs mmap BAR0 地址空间 regdev MMIO回调 验证判定 断言1: VERSION==0x0001 且 STATUS==~CTRL.bit0(初始) 断言2: 写VERSION/STATUS后值不变 → 只读保护生效 断言3: 写CONTROL后读回一致,STATUS随bit0反相 → 读写正确

关键点说明

  • 设备通过pci_register_bar将MMIO区域暴露为PCI BAR0,guest可直接映射访问。
  • 只读寄存器在write回调中仅记录guest错误日志,不修改内部状态,保证写忽略语义。
  • 测试程序使用assert硬校验,任何一项失败都会导致非零退出码,便于自动化集成。
  • 设备ID需与QEMU命令行-device regdev匹配,PCI地址可能因总线拓扑变化,测试中硬编码为00:03.0,若实际地址不同需调整。