2. 编写第一个虚拟设备

任务说明

本案例目标是在QEMU源码树中实现一个最小PCI虚拟设备,使其能够被QEMU正确注册,并在客户机(guest OS)中通过lspci识别到该设备。设备本身不实现具体功能,仅提供基本的PCI配置空间和BAR0内存区域,用于验证设备从注册到客户机可见的完整链路。

实现方案

在QEMU的hw/misc目录下新建设备源文件,使用QEMU标准设备接口(TYPE_PCI_DEVICE)实现一个PCI设备。设备ID使用QEMU官方预留的测试范围(0x0001),厂商ID使用0x1234(QEMU虚拟设备常用厂商ID)。设备提供4KB大小的BAR0内存区域,客户机可通过该区域进行读写访问。

代码实现

// hw/misc/qemu_demo_pci.c
#include "qemu/osdep.h"
#include "hw/pci/pci_device.h"
#include "hw/pci/msi.h"
#include "qemu/log.h"
#include "qemu/module.h"

#define TYPE_DEMO_PCI_DEVICE "demo-pci"
OBJECT_DECLARE_SIMPLE_TYPE(DemoPCIState, DEMO_PCI_DEVICE)

#define DEMO_PCI_VENDOR_ID 0x1234
#define DEMO_PCI_DEVICE_ID 0x0001
#define DEMO_PCI_BAR0_SIZE 0x1000  // 4KB

typedef struct DemoPCIState {
    PCIDevice parent_obj;
    MemoryRegion bar0;
} DemoPCIState;

static uint64_t demo_pci_bar0_read(void *opaque, hwaddr addr, unsigned size)
{
    // 简单返回固定值,用于验证读写通路
    return 0xDEADBEEF;
}

static void demo_pci_bar0_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)
{
    // 写操作不做处理,仅保证通路可用
}

static const MemoryRegionOps demo_pci_bar0_ops = {
    .read = demo_pci_bar0_read,
    .write = demo_pci_bar0_write,
    .endianness = DEVICE_LITTLE_ENDIAN,
    .impl = {
        .min_access_size = 1,
        .max_access_size = 4,
    },
};

static void demo_pci_realize(PCIDevice *pci_dev, Error **errp)
{
    DemoPCIState *s = DEMO_PCI_DEVICE(pci_dev);

    // 设置PCI配置空间中的设备ID和厂商ID
    pci_set_word(pci_dev->config + PCI_VENDOR_ID, DEMO_PCI_VENDOR_ID);
    pci_set_word(pci_dev->config + PCI_DEVICE_ID, DEMO_PCI_DEVICE_ID);

    // 初始化BAR0内存区域
    memory_region_init_io(&s->bar0, OBJECT(s), &demo_pci_bar0_ops,
                          s, "demo-pci-bar0", DEMO_PCI_BAR0_SIZE);
    pci_register_bar(pci_dev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &s->bar0);
}

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

    k->realize = demo_pci_realize;
    k->vendor_id = DEMO_PCI_VENDOR_ID;
    k->device_id = DEMO_PCI_DEVICE_ID;
    k->class_id = PCI_CLASS_OTHERS;
    dc->desc = "QEMU Demo PCI Device";
    set_bit(DEVICE_CATEGORY_MISC, dc->categories);
}

static const TypeInfo demo_pci_info = {
    .name = TYPE_DEMO_PCI_DEVICE,
    .parent = TYPE_PCI_DEVICE,
    .instance_size = sizeof(DemoPCIState),
    .class_init = demo_pci_class_init,
    .interfaces = (InterfaceInfo[]) {
        { INTERFACE_CONVENTIONAL_PCI_DEVICE },
        { }
    },
};

static void demo_pci_register_types(void)
{
    type_register_static(&demo_pci_info);
}

type_init(demo_pci_register_types)

编译集成

将上述文件加入QEMU构建系统。在hw/misc/meson.build中添加:

system_ss.add(when: 'CONFIG_DEMO_PCI',
              if_true: files('qemu_demo_pci.c'))

在hw/misc/Kconfig中添加:

config DEMO_PCI
    bool
    default y
    depends on PCI

运行与验证

重新编译QEMU后,使用以下命令行启动客户机:

./qemu-system-x86_64 \
    -machine q35,accel=kvm \
    -m 1G \
    -device demo-pci \
    -drive file=guest.img,format=raw \
    -nographic

在客户机中执行lspci命令,预期输出中应包含类似以下行:

00:04.0 Unclassified device [0000]: Device 1234:0001

该输出表明设备已被客户机PCI子系统正确枚举。进一步可通过setpci命令读取BAR0地址,然后使用devmem或编写简单驱动访问BAR0区域,验证读写通路正常。

判定标准

  • QEMU编译通过,无报错;
  • 启动命令中-device demo-pci被正确识别,无设备创建失败提示;
  • 客户机内lspci能列出厂商ID为1234、设备ID为0001的设备;
  • 通过lspci -v能看到BAR0已分配内存地址且大小为4KB。
PCI设备注册与客户机识别流程 QEMU 进程 demo_pci_realize() 设置VID/DID + 注册BAR0 PCI 总线枚举 配置空间读取 客户机 (Guest OS) lspci 命令 读取PCI配置空间 识别到设备 1234:0001 BAR0 读写访问 4KB 内存区域 配置空间 枚举结果 地址映射 验证点:lspci 输出包含 1234:0001