QEMU 设备模型入门:QOM、初始化流程与MemoryRegion

好,我们开始第4章。这一章是QEMU设备模拟的基石,说白了就是搞清楚QEMU怎么“认识”一个设备,怎么把它“造”出来,以及它怎么跟CPU和内存打交道。

我个人觉得,很多做驱动开发的朋友一开始看QEMU源码会觉得晕,就是因为没搞懂这三件事:对象模型(QOM)、设备初始化流程、内存区域(MemoryRegion)。今天咱们就把它们拆开揉碎了讲清楚。

4.1 QOM:QEMU的对象模型

QEMU本身是用C写的,但它需要模拟出各种复杂的硬件设备,这些设备之间有继承、有组合、有接口。纯C的struct搞不定这种面向对象的需求,所以QEMU自己搞了一套对象模型——QOM(QEMU Object Model)。

你可以把QOM想象成一个轻量级的C++对象系统。它用宏和函数指针实现了:

  • 类的定义与继承:一个设备类型可以继承另一个设备类型。
  • 实例化:从类创建具体的设备对象。
  • 属性系统:可以动态地给对象添加/读取属性(比如寄存器地址、中断号)。
  • 接口:类似C++的纯虚类,定义一组必须实现的方法。

核心概念:

  • TypeImpl:类型的内部表示,每个设备类型对应一个TypeImpl。
  • ObjectClass:类的结构体,存放所有实例共享的数据和方法(比如设备复位函数)。
  • Object:对象实例的结构体,存放每个设备独有的数据(比如寄存器状态)。

嗯,这里要注意,QOM的注册是通过宏 type_init 完成的。每个设备模块在加载时,都会调用这个宏来告诉QEMU:“嘿,我这里有个新设备类型,你记一下。”

// 一个典型的QOM设备类型注册
static const TypeInfo my_uart_type_info = {
    .name = "my-uart",
    .parent = TYPE_SYS_BUS_DEVICE,  // 继承自系统总线设备
    .instance_size = sizeof(MyUARTState), // 实例结构体大小
    .instance_init = my_uart_init,        // 实例初始化函数
    .class_init = my_uart_class_init,     // 类初始化函数
};

static void my_uart_register_types(void)
{
    type_register_static(&my_uart_type_info);
}
type_init(my_uart_register_types);

我在项目中遇到过一个问题:自己写了一个设备,注册了类型,但QEMU启动时就是找不到。后来发现是 type_init 的拼写错了,少了个‘i’。这种低级错误,查了一下午才找到,真是血泪教训。

4.2 设备初始化流程:从类型注册到设备就绪

一个QEMU设备从“概念”到“能用”,到底经历了什么?我习惯把它分成三个阶段:

  1. 类型注册阶段:QEMU启动时,遍历所有通过 type_init 注册的类型,建立类型继承树。
  2. 实例化阶段:当QEMU解析命令行(比如 -device my-uart)时,调用 object_new() 或 qdev_create() 创建设备实例。
  3. realize阶段:实例化完成后,调用 DeviceClass::realize 函数,完成设备的最终配置(比如分配内存区域、连接中断线)。

为什么要有 realize 这一步?你想想看,实例化只是分配了内存,但设备具体要映射到哪个地址、用哪个中断号,这些信息在实例化时可能还不知道。realize 就是做这些“后绑定”工作的。

我的习惯: 在写新设备时,我会把 instance_init 只做最基础的变量初始化(比如置零),把所有需要外部参数(如地址、中断号)的配置都放到 realize 里。这样代码更清晰,也更容易调试。

下面这张图,是我自己画的一个设备初始化流程,你可以对照着看:

QEMU 设备初始化流程 1. 类型注册 type_init → type_register 2. 实例化 object_new / qdev_create 3. Realize DeviceClass::realize Realize 阶段内部做了什么? • 解析设备属性(如地址、中断号) • 创建并初始化 MemoryRegion • 调用 sysbus_mmio_map 映射到系统总线 • 连接中断线到中断控制器

4.3 MemoryRegion:设备与CPU的桥梁

设备要工作,CPU得能读写它的寄存器。这个“读写”在QEMU里就是通过 MemoryRegion 来实现的。你可以把它理解成一块“虚拟地址空间”,CPU访问这块空间时,QEMU会回调你注册的函数。

说白了,MemoryRegion 就是QEMU模拟设备寄存器的核心机制。你告诉QEMU:“我的设备寄存器在地址0x10000000开始,大小0x1000,读的时候调用我的 read 函数,写的时候调用我的 write 函数。”

我曾经踩过的坑: 刚开始写设备时,我注册了一个MemoryRegion,但忘了设置 size 为2的幂次。结果CPU访问时,QEMU内部地址翻译出错,设备死活不工作。后来查文档才发现,MemoryRegion的size必须是2的幂次,而且起始地址要对齐到size边界。切记!

MemoryRegion 有几种常见类型:

类型 用途 示例
IO MemoryRegion 模拟设备寄存器,读写触发回调 UART、GPIO、定时器
RAM MemoryRegion 模拟普通内存,读写直接访问 板载RAM、显存
ROM MemoryRegion 模拟只读内存 Boot ROM、固件
Alias MemoryRegion 将一个区域映射到另一个地址 外设地址重映射

下面是一个典型的IO MemoryRegion创建和注册代码:

// 在设备结构体中定义 MemoryRegion
typedef struct {
    SysBusDevice parent_obj;
    MemoryRegion iomem;
    uint32_t regs[256];  // 模拟的寄存器数组
} MyUARTState;

// 实现读写回调
static uint64_t my_uart_read(void *opaque, hwaddr addr, unsigned size)
{
    MyUARTState *s = (MyUARTState *)opaque;
    // 地址偏移转成寄存器索引
    uint32_t idx = addr >> 2;
    return s->regs[idx];
}

static void my_uart_write(void *opaque, hwaddr addr, uint64_t data, unsigned size)
{
    MyUARTState *s = (MyUARTState *)opaque;
    uint32_t idx = addr >> 2;
    s->regs[idx] = data;
    // 根据寄存器值执行模拟逻辑
}

// 在 realize 函数中初始化并注册
static void my_uart_realize(DeviceState *dev, Error **errp)
{
    MyUARTState *s = MY_UART(dev);
    
    memory_region_init_io(&s->iomem, OBJECT(dev),
                          &my_uart_ops, s,
                          "my-uart", 0x1000);  // 4KB 地址空间
    
    sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->iomem);
    // 之后在板级代码中调用 sysbus_mmio_map 映射到具体地址
}

这里有个细节:memory_region_init_io 的第三个参数是一个 MemoryRegionOps 结构体,里面定义了读写函数指针和字节序、对齐要求等。我习惯在 ops 里设置 .min_access_size = 4 和 .max_access_size = 4,强制32位访问,这样能避免很多对齐问题。

4.4 三者如何协同工作?

好,现在我们把QOM、初始化流程和MemoryRegion串起来。一个完整的设备模拟,流程是这样的:

  1. QOM注册:通过 type_init 告诉QEMU有这个设备类型。
  2. 命令行解析:QEMU解析 -device my-uart,调用 qdev_create 创建实例。
  3. 属性设置:设置设备属性(如 chardev、addr)。
  4. realize调用:调用 qdev_realize,触发 DeviceClass::realize。
  5. MemoryRegion创建:在 realize 中创建IO MemoryRegion,并注册到系统总线。
  6. CPU访问:当Guest OS读写设备地址时,QEMU通过MemoryRegion回调,执行模拟逻辑。

一句话总结: QOM负责“认识”设备,初始化流程负责“造出”设备,MemoryRegion负责“连接”设备。三者缺一不可。

我个人建议,刚开始学的时候,不要急着看复杂的设备(比如网卡、GPU)。先从一个简单的GPIO或者定时器开始,把QOM注册、realize、MemoryRegion这三板斧练熟。等你看到 sysbus_create_simple 这种函数时,能条件反射地想到它背后发生了什么,那就算入门了。