QEMU设备的对象模型QOM
本篇博客记录在QEMU源码中添加新的设备文件,主要介绍QEMU对象模型QOM
,以hw/misc/pci-testdev.c
和 hw/misc/edu.c
设备为例子。
qemu中需要模拟出设备与总线的关系。因为 在主板上,一个device会通过bus与其他的device相连接,一个device上可以通过不同的bus端口连接到其他的device,其他的device也可以进一步通过bus与其他的设备连接。 同时一个bus上也可以连接多个device,这种device连bus、bus连device的关系,qemu是需要模拟出来的。为了方便模拟设备的这种特性,面向对象的编程模型也是必不可少的。
QOM
QEMU Object Model (QOM)
模型提供了注册用户创建的类型的框架,这些类型包括
总线bus、接口interface、设备device等。
以 QEMU v2.12.0
版本中的
hw/misc/pci-testdev.c
文件为例子,分析QOM创建新类型的过程。
>pci-testdev >> A PCI "device" that when read and written tests
PCI accesses.
edu > A PCI device that supports testing both INTx and MSI interrupts and DMA transfers.
QOM
创建新类型 Type
时需要指定其
类对象OjectClass
实例和 类Object
实例,这两者通过 类型TypeInfo
结构体指定,基本结构定义在
include\qom\object.h
,
文件中的注释非常详细,对数据结构的字段说明和QOM模型的用法。
TypeImpl
定义在 qom/object.c
中,没有注释。
- ObjectClass: 是所有类对象的基类,仅仅保存了一个整数
type
。 - Object: 是所有对象的 基类
Base Object
, 第一个成员变量为指向ObjectClass
的指针。 - TypeInfo:是用户用来定义一个
Type
的工具型的数据结构。 - TypeImpl:对数据类型的抽象数据结构,TypeInfo的属性与TypeImpl的属性对应。
对象的初始化分为4步: 1. 将 TypeInfo 注册 TypeImpl 2. 实例化 Class(ObjectClass) 3. 实例化 Object 4. 添加 Property
TypeInfo
结构体里面的字段:
/**
* TypeInfo:
* @name: The name of the type.
* @parent: The name of the parent type.
* @instance_size: The size of the object (derivative of #Object). If
* @instance_size is 0, then the size of the object will be the size of the
* parent object.
* @instance_init: This function is called to initialize an object. The parent
* class will have already been initialized so the type is only responsible
* for initializing its own members.
* @instance_post_init: This function is called to finish initialization of
* an object, after all @instance_init functions were called.
* @instance_finalize: This function is called during object destruction. This
* is called before the parent @instance_finalize function has been called.
* An object should only free the members that are unique to its type in this
* function.
* @abstract: If this field is true, then the class is considered abstract and
* cannot be directly instantiated.
* @class_size: The size of the class object (derivative of #ObjectClass)
* for this object. If @class_size is 0, then the size of the class will be
* assumed to be the size of the parent class. This allows a type to avoid
* implementing an explicit class type if they are not adding additional
* virtual functions.
* @class_init: This function is called after all parent class initialization
* has occurred to allow a class to set its default virtual method pointers.
* This is also the function to use to override virtual methods from a parent
* class.
* @class_base_init: This function is called for all base classes after all
* parent class initialization has occurred, but before the class itself
* is initialized. This is the function to use to undo the effects of
* memcpy from the parent class to the descendants.
* @class_finalize: This function is called during class destruction and is
* meant to release and dynamic parameters allocated by @class_init.
* @class_data: Data to pass to the @class_init, @class_base_init and
* @class_finalize functions. This can be useful when building dynamic
* classes.
* @interfaces: The list of interfaces associated with this type. This
* should point to a static array that's terminated with a zero filled
* element.
*/
归纳起来: TypeInfo中定义了如下几类信息:
1. Name
包括自己的Name,Parent的Name。
2. Class
ObjectClass的信息包括,class_size,class_data,class相关函数:class_base_init,class_init,class_finalize。
这些函数都是为了初始化,释放结构体ObjectClass。
3. Instance
对象Object信息包括:instance_size,instance相关函数:instance_post_init,instance_init,instance_finalize。
这些函数都是为了初始化,释放结构体Object。
4. 其他信息
abstract是否为抽象。interface数组。
用户定义了一个TypeInfo,然后调用 type_register(TypeInfo)
或者 type_register_static(TypeInfo)
函数,就会生成相应的TypeImpl实例,将这个TypeInfo注册到全局的TypeImpl的hash表中。
TypeInfo的属性与TypeImpl的属性对应,实际上qemu就是通过用户提供的TypeInfo创建的TypeImpl的对象。
TYPE_DEVICE has a pure virtual method 'init' which is a bit of a misnomer. The 'init' method is called after construction but before the guest is started for the first time. In QOM nomenclature, we call this realize.
模块注册
向QOM模块注册Type,类似于Linux驱动的注册,通过 type_init
宏注册,它在 include/qemu/module.h
中。 这个宏调用发生在
qemu main
函数之前。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17static const TypeInfo pci_testdev_info = {
.name = TYPE_PCI_TEST_DEV, /*类型的名字*/
.parent = TYPE_PCI_DEVICE, /*父类的名字*/
.instance_size = sizeof(PCITestDevState), /*必须向系统说明Object的大小,以便系统为Object的实例分配内存*/
.class_init = pci_testdev_class_init, /*在类初始化时就会调用这个函数,将虚拟函数赋值*/
.interfaces = (InterfaceInfo[]) {
{ INTERFACE_CONVENTIONAL_PCI_DEVICE },
{ },
},
};
static void pci_testdev_register_types(void)
{
type_register_static(&pci_testdev_info);
}
type_init(pci_testdev_register_types)
这里会调用 pci_testdev_register_types
,这个函数以
pci_testdev_info
为参数调用了
type_register_static
类型注册函数。
这一过程的目的就是分配并创建TypeImpl
结构,使用
TypeInfo
数据赋值,之后插入到一个hash表之中,这个hash表以
ti->name
(TypeImpl ti),也就是
info->name
为key,value就是根据 TypeInfo
生成的 TypeImpl
。
在QEMU启动阶段vl.c:main()
,pci_testdev_register_types()
函数将会在module_call_init(MODULE_INIT_QOM)
中执行,通过遍历QOM队列中的module entry。
在 pci_testdev_info
中得字段 name
定义了我们将来启动此设备时候传参 -device
后面跟的值。
Class的初始化
现在已经有了一个TypeImpl的哈希表。下一步就是初始化每个type了,这一步可以看成是class的初始化,可以理解成每一个type对应了一个class,接下来会初始化class。
由于在初始化每个type时候,调用到的是 type_initialize
函数。ObjectClass
的分配和初始化就在此函数中实现,此外ObjectClass
和Type
的关联操作也在此函数中实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56/* include/qom/object.h */
struct TypeImpl;
typedef struct TypeImpl *Type;
/* qom/object.c */
struct TypeImpl
{
...
ObjectClass *class;
...
};
static void type_initialize(TypeImpl *ti)
{
TypeImpl *parent;
if (ti->class) {
return;
}
ti->class_size = type_class_get_size(ti);
ti->instance_size = type_object_get_size(ti);
/* Any type with zero instance_size is implicitly abstract.
* This means interface types are all abstract.
*/
if (ti->instance_size == 0) {
ti->abstract = true;
}
if (type_is_ancestor(ti, type_interface)) {
...
}
ti->class = g_malloc0(ti->class_size);
parent = type_get_parent(ti);
if (parent) {
type_initialize(parent);
...
memcpy(ti->class, parent->class, parent->class_size);
...
} else {
ti->class->properties = g_hash_table_new_full(
g_str_hash, g_str_equal, g_free, object_property_free);
}
ti->class->type = ti;
while (parent) {
if (parent->class_base_init) {
parent->class_base_init(ti->class, ti->class_data);
}
parent = type_get_parent(parent);
}
if (ti->class_init) {
ti->class_init(ti->class, ti->class_data);
}
}ti->class
已经存在说明已经初始化了,直接返回。如果有
parent
,会递归调用
type_initialize()
,即调用父对象的初始化函数。
ti->class->type = ti;
将当前Type
和刚派生的Class
。最后,ti->class
字段被当作第一个参数传给Type
的class构造函数,通过ti->class_init(ti->class, ti->class-data).
。
这里Type
也有一个层次关系,即QOM 对象的层次结构。在
pci_testdev_info
结构的定义中,我们可以看到有一个
.parent
域,值为 TYPE_PCI_DEVICE
。 这说明
TYPE_PCI_TEST_DEV
的 父类型 是 TYPE_PCI_DEVICE
,在 hw/pci/pci.c
中可以看到
pci_device_type_info
的父type是
TYPE_DEVICE
static const TypeInfo pci_device_type_info = {
.name = TYPE_PCI_DEVICE,
.parent = TYPE_DEVICE,
.instance_size = sizeof(PCIDevice),
.abstract = true,
.class_size = sizeof(PCIDeviceClass),
.class_init = pci_device_class_init,
.class_base_init = pci_device_class_base_init,
};
依次往上溯我们可以得到这样一条type的链, > TYPE_PCI_TEST_DEV->TYPE_PCI_DEVICE->TYPE_DEVICE->TYPE_OBJECT
之后,最重要的就是调用 parent->class_base_init
以及
ti->class_init
了,这相当于C++里面的构造基类的数据。
在定义新类型中,实现了父类的虚拟方法,那么需要定义新的class的初始化函数,并且在TypeInfo数据结构中,给TypeInfo的
class_init
字段赋予该初始化函数的函数指针。 我们以一个
class_init
为例,
1 | /* hw/misc/pci-testdev.c */ |
class_init
构造函数钩子。 这个函数将负责初始化Type的
ObjectClass
实例。 这里从 ObjectClass
转换成了
DeviceClass
。为什么这么转换,还需要从
ObjectClass
的层次结构说起。
1 | /* include/qom/object.h */ |
根据结构体定义,struct PCIDeviceClass
的首个字段是
struct DeviceClass
,因为C标准确保结构的第一个字段始终位于字节0,因此可以直接将
PCIDeviceClass *
指针转换成 DeviceClass *
类型。以此类推,这里可以看成C++中的继承关系,即Type的ObjectClass实例的基类就是
ObjectClass
,子类是DeviceClass
和
PCIDeviceClass
,越往下包含的数据越具体。
PCIDeviceClass->DeviceClass->ObjectClass
因此,ObjectClass基类和子类间可以相互转换,如下。
1
2
3
4
5/* hw/misc/pci-testdev.c */
static void pci_testdev_class_init(ObjectClass *klass, void *data)
...
((PCIDeviceClass *)klass)->realize = pci_testdev_realize;
...
将一个父类的指针直接转换为子类的指针是不安全的,为了安全校验从Type实例的父类型转换成子类,各类需要提供强制类型转换的宏,以下面这句为例:
1 | DeviceClass *dc = DEVICE_CLASS(klass); |
而这些宏,都由 OBJECT_CLASS_CHECK()
封装。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/* include/hw/qdev-core.h */
/* include/hw/pci/pci.h */
/* include/qom/object.h */
/**
* OBJECT_CLASS_CHECK:
* @class_type: The C type to use for the return value.
* @class: A derivative class of @class_type to cast.
* @name: the QOM typename of @class_type.
*
* A type safe version of @object_class_dynamic_cast_assert. This macro is
* typically wrapped by each type to perform type safe casts of a class to a
* specific class type.
*/
DEVICE_CLASS()
宏断言用户Type的DeviceClass
子类是从TYPE_DEVICE
父类的ObjectClass
实例派生的。同理,
PCI_DEVICE_CLASS()
宏断言用户的Type的PCIDeviceClass
子类是从TYPE_PCI_DEVICE
父类的OjbectClass
实例派生而来。
object_class_dynamic_cast_assert()
根据class对应的type以及typename对应的type,判断是否能够转换,判断的主要依据就是type_is_ancestor,
这个判断target_type是否是type的一个祖先,如果是当然可以进行转换,否则就不行。
该函数会在 type_table_lookup()
中通过
g_hash_table_lookup
比较字符串来断言
DeviceClass
和 PCIDeviceClass
的父类。
总之,就是从最开始的 TypeImpl
初始化了每一个type对应的
ObjectClass *class
,并且构建好了各个Class的继承关系。
Object的构造
用户定义的PCI
Type的ObjectClass
实例的构造函数调用在type_register_static()
调用时即可完成,而Type的Object
实例只有在QEMU命令行中添加-device
选项时才会创建。
我们上面已经看到了Type哈希表的构造以及 Class
的初始化,接下来讨论具体设备的创建。
Object基类和子类
与OjbectClass类似,Object也有继承关系;与ObjectClass不同的是,用户可以定义Type的自己的Object结构体,继承自PCIDevice
。
1 | typedef struct PCITestDevState { |
Object 的继承关系:
> PCITestDevState->PCIDevice->DeviceState->Object
而Object
继承类之间的转换同样是靠宏,DEVICE
和PCI_DEVICE
由OBJECT_CHECK
封装,OBJECT_CHECK
用于转换Object
基类和子类的转换。
1 | /* include/hw/qdev-core.h */ |
此外,OBJECT_GET_CLASS()
用于从Object
获取
ObjectClass
。OBJECT_GET_CLASS()
的用处就是PCI_DEVICE_GET_CLASS()
或
DEVICE_GET_CLASS
,对于给定的Object
实例,可以获得相应的PCIDeviceClass
或DeviceClass
的ObjectClass
实例。
1 | /* include/hw/qdev-core.h */ |
Object的创建由 k->realize = pci_testdev_realize;
函数实现,不同于type和class的构造,Object
当然是根据需要创建的,只有在命令行指定了设备或者是热插一个设备之后才会有
Object
的创建。 ObjectClass
和
Object
之间是通过 Object
的
class
字段 联系在一起的。
属性
属性分为类对象(ObjectClass)属性和类实例对象(Object)属性,存储于 properties 成员中。properties 是一个 GHashTable ,存储了 属性名 到 ObjectProperty 的映射。
属性对象
属性对象包含属性名称、类型、描述,类型对应的属性结构,以及相应访问函数。
1 | typedef struct ObjectProperty |
静态属性
凡是在代码中就已经定义好名称和类型的属性,都是静态属性。包括在初始化过程中添加 和 props 。
初始化过程中添加
类实例 初始化函数instance_init
中调用的
object_property_add
函数。
该属性会直接加到类实例对象的properties中。
props
一些类对象会在 class_init
中设置
ObjectClass
对象的 props
成员。 例如:
1
2
3
4
5
6
7
8
9
10
11
12static Property test_properties[] = {
DEFINE_PROP_STRING("test_string", struct_type, , struct_type_property),
DEFINE_PROP_END_OF_LIST()
};
...
static void test_class_init(ObjectClass *klass, void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);
dc->props = test_properties;
}TypeInfo
的 name
:
1 | x86_64-softmmu/qemu-system-x86_64 -device pci-testdev,? |
动态属性
指在运行时动态进行添加的属性。比如用户通过参数传入了一个设备,需要作为属性和其它设备关联起来。
典型的动态属性就是 child<>
和
link<>
(因为其类型就是这样构造的,后文简称child和link) 。
child
child
实现了
组成composition关系,表示一个设备(parent)创建了另外一个设备(child),parent掌控child的生命周期,负责向其发送事件。一个device只能有一个parent,但能有多个child。这样就构成一棵组合树。
通过 object_property_add_child
添加 child:
1
2
3=> object_property_add
将 child 作为 obj 的属性,属性名name,类型为 "child<child的类名>",同时getter为object_get_child_property,没有setter
=> child->parent = obj
link
link实现了backlink关系,表示一个设备引用了另外一个设备,是一种松散的联系。两个设备之间能有多个link关系,可以进行修改。它完善了组合树,使其构成构成了一幅有向图。
通过 object_property_add_link
添加link: 1
2=> 创建 LinkProperty ,填充目标(child)的信息
=> object_property_add 将 LinkProperty 作为 obj 的属性,属性名name,类型为 "link<child的类名>",同时getter为 object_get_link_property 。如果传入了check函数,则需要回调,设置setter为 object_set_link_property
在完成了此文件后,需要添加到 Makefile.objs
中,QEMU好知道编译并将其连接到二进制文件中。格式为: 1
common-obj-$(CONFIG_PCI_TESTDEV) += pci-testdev.o
1
obj-$(CONFIG_PCI) += pci-testdev.o
CONFIG_PCI_TESTDEV
启用后,pci-testdev.c
才会被编译并链接到qemu模拟器中。
CONFIG_PCI_TESTDEV
在文件
config-all-devices.mak
中配置。
总结
QOM的对象构造分成三部分,第一部分是type的构造,这是通过TypeInfo构造一个TypeImpl的哈希表,这是在main之前完成的。 第二部分是class的构造,这是在main中进行的,这两部分都是全局的,也就是只要编译进去了的QOM对象都会调用。 第三部分是object的构造,这是构造具体的对象实例,在命令行指定了对应的设备时,才会创建object。
可以参考QEMU PCI device emulator and guest ldd ,里面有详细的解释和QEMU device源码与Guest Driver源码。
参考
- How to add a new device in QEMU source code?
- QEMU中的对象模型——QOM(介绍篇)
- QOM介绍
- [System] Emulate a PCI device with Qemu
- How to create a custom PCI device in QEMU
- qemu-object-model
- Features/QOM
- Qemu之QOM(Qemu Object Module)
- QEMU学习笔记——QOM(Qemu Object Model)
- How to debug qemu devices
- QEMU漏洞挖掘
- Documentation/QOMConventions QOM编程规范
- QEMU's instance_init() vs. realize()
- QEMU/KVM VM Execution Basics
- QOM说明文档
- QEMU学习笔记——QOM(Qemu Object Model)
- Essential QEMU PCI API