【VIRT.0x00】Qemu - I:Qemu 简易食用指南

本文最后更新于:2023年10月27日 中午

不如 VMWare👋

0x00.一切开始之前

Qemu 是一款开源的虚拟机软件,支持多种不同架构的模拟(Emulation)以及配合 kvm 完成当前架构的虚拟化(Virtualization)的特性,是当前最火热的开源虚拟机软件

image.png

Qemu 的基本运行架构如下图所示:

image.png

本篇文章笔者将简要叙述如何从源码编译特定架构的 Qemu 并进行一定程度的改造工作

PRE.安装依赖

大概需要安装这些依赖:

1
$ sudo apt -y install ninja-build build-essential zlib1g-dev pkg-config libglib2.0-dev binutils-dev libpixman-1-dev libfdt-dev

0x01.从源码编译 QEMU

一、获取 QEMU 源码

大概有两种途径:从官网下载或是直接从 Qemu 的GitHub 仓库拉下来。

I.官网下载源码

前往 qemu 的官网进行下载:

1
2
$ wget https://download.qemu.org/qemu-7.0.0.tar.xz
$ tar -xf qemu-7.0.0.tar.xz

II. GitHub 获取源码

直接从 GitHub 上面拉也行:

1
$ git clone git@github.com:qemu/qemu.git

二、配置编译选项

接下来创建 build 目录并配置对应的编译选项:

1
2
$ mkdir build && cd build
build$ ../qemu-7.0.0/configure --enable-kvm --target-list=x86_64-softmmu --enable-debug

这里我们手动指定了这几个编译选项:

  • --enable-kvm:开启 kvm 支持
  • --target-list=<架构名>:指定要编译的 CPU 架构,这里我们指定为 x86_64-softmmu 即表示我们要编译 x86 架构的 64位 CPU
  • --enable-debug:能够对 Qemu 进行调试

如果我们不指定的话会把所有架构都编译一遍,不过这里笔者只需要 x86 的;)

三、开始编译

直接 make 就完事了

1
build$ make

需要花的时间还是不短的,在笔者的小破服务器上编译大概需要十几分钟左右,大概编译了两千多个文件,完成之后在当前目录下就会有一个热乎乎的可执行文件 qemu-system_x86-64,这个就是 Qemu 的本体了

之后可以 make install 给他安到 bin 里边,这样就能直接从命令行启动了

1
build$ sudo make install

0x02.构建系统镜像并使用 vnc 连接

空有一个 qemu 的可执行文件还不行,我们最终还是要在 qemu 上面跑一个完整的操作系统的,那么这里有两种方法:

  • 使用 qemu-img 创建虚拟机镜像文件,通过 -cdrom 参数指定载入一个 ISO 镜像文件来安装一个现有的操作系统
  • 使用 debootstrap 创建 ext4 硬盘镜像,并直接运行一个现成的裸的内核镜像文件(bzImage)

一、创建虚拟机镜像文件并通过 CDROM 安装 Ubuntu

I.使用 qemu-img 创建虚拟机磁盘镜像文件

这一步比较简单,主要是用 build 目录下的 qemu-img 来完成构建:

1
2
$ ./build/qemu-img create -f qcow2 test.qcow2 20G
Formatting 'test.qcow2', fmt=qcow2 cluster_size=65536 extended_l2=off compression_type=zlib size=21474836480 lazy_refcounts=off refcount_bits=16

这里的 -f 参数指定的第一个参数为镜像格式,这里使用 QEMU 最通用的格式 qcow2;第二个参数为文件路径;第三个参数为镜像大小

参见这里

II.通过 vnc 连接完成安装

在 qemu 启动时通过 -cdrom 参数可以指定加载的ISO文件路径,这里笔者选择安装一个 Ubuntu 22.04:

1
2
$ sudo ./build/qemu-system-x86_64 -m 2G -drive format=qcow2,file=test.qcow2 -enable-kvm -cdrom ~/Download/ubuntu-22.04-desktop-amd64.iso
VNC server running on ::1:5900

参数说明如下:

  • -m:虚拟机的内存大小
  • -drive :qemu 启动时额外加载的设备,这里我们使用 format=qcow2,file=test.qcow2 指定了加载设备 test.qcow2、格式为 qcow2
  • -enable-kvm :启用 kvm 模式,需要注意的是该选项要求以 root 权限运行
  • -cdrom:指定 qemu 启动时装载的光碟文件路径

启动后 qemu 默认会在 5900 端口启动一个 VNC server,此时我们便能通过 VNC 连接到 qemu 上,需要注意的是这里只能在本地进行连接

如果是运行在远程服务器上的话,我们还需要额外指定 -vnc 参数:

1
$ sudo ./build/qemu-system-x86_64 -m 2G -drive format=qcow2,file=test.qcow2 -enable-kvm -cdrom ~/Download/ubuntu-22.04-desktop-amd64.iso -vnc yourip:0

需要注意的是 vnc 参数中 ip 后面跟着的不是端口号,而是 display numer,对于默认的 display 0 而言其监听的端口号为 5900,而 display 1 就是 5901 端口,以此类推

之后我们便能通过 vnc 连接上远程服务器上的 qemu 了,这里笔者选择使用 VNC Viewer 进行连接:

image.png

成功连接上远程服务器上的 qemu:

image.png

之后就是常规的安装流程了,不过可能是由于 qemu 模拟显卡的问题(或者是 VNC 配置的问题),在一开始的时候安装界面的颜色会有点失真:

image.png

不过在安装准备结束的时候又恢复正常的颜色了,笔者目前推测应该是和显卡驱动有关:

image.png

之后就和正常使用虚拟机没有什么区别了,下次再次启动就不需要指定 -cdrom 参数了

image.png

二、构建 ext4 磁盘镜像并运行 kernel bzImage

如果你不需要一个完整的发行版 Linux 系统环境,只是想跑一个裸的简易的内核,也可以通过下面的方式完成:

I.构建磁盘镜像

这里我们使用 debootstrap 来创建ext4硬盘镜像,直接使用由 Google 团队为 syzkaller 构建磁盘镜像的脚步即可

1
2
3
4
5
6
$ sudo apt-get install debootstrap
$ mkdir image
$ cd image
image$ wget https://raw.githubusercontent.com/google/syzkaller/master/tools/create-image.sh -O create-image.sh
image$ chmod +x create-image.sh
image$ ./create-image.sh

完成之后在当前目录下就会有一个热乎乎的 stretch.img,这便是 ext4 磁盘镜像文件了

wget 的这一步需要翻墙raw.githubusercontent.com 在国内似乎是被墙了,总之笔者记忆里从没成功在不翻墙的情况下成功上去过),若嫌麻烦可以直接 copy 笔者已经下好的

II.获取 kernel bzImage

这部分参见这里

III.运行 qemu 并通过 vnc 进行连接

创建如下 bash 脚本并运行:

1
2
3
4
5
6
7
8
9
#!/bin/bash
qemu-system-x86_64 \
-m 2G \
-smp 2 \
-kernel ./bzImage \
-append "root=/dev/sda" \
-drive file=./stretch.img,format=raw \
-enable-kvm \
-vnc yourip:0

之后还是直接用 vnc 进行连接即可(如果只需要在本地运行的话可以不用附加 -vnc 参数,而是加上 -nographic 参数):

image.png

0x03. QEMU 源码调试

QEMU 允许我们通过 -s 或是 -gdb tcp::1234 这样的附加参数来调试虚拟机(比如说调试 Linux kernel),但有的时候我们想要直接调试 QEMU 本体(比如说调试一些自己写的模拟设备),这个时候就需要我们将 Host 上的 QEMU 进程作为待调试对象

因为 QEMU 本身也是在 Host 上运行的一个进程,所以笔者这里给出一个比较直接的调试 QEMU 的办法:把 QEMU 本体启动起来后直接用 ps 找 QEMU 进程然后 gdb attach 即可像正常调试一个普通进程一样调试 QEMU:

image.png

如果是自己编译的 QEMU 这样就可以直接从源码进行调试了:

image.png

0x04.简易 QEMU 设备编写

虽然 Qemu 支持模拟多种设备,但是并不能涵盖现存所有的设备类型,同时有的时候出于一些特殊的目的我们也需要自定义一些设备,因此本节主要讲述如何在 Qemu 当中编写一个新的 PCI 类型的设备

注1:在开始之前你可能需要补充一些PCI 设备的基础知识

注2:qemu 官方在 hw/misc/edu.c 中也提供了一个教学用的设备样例,red hat 则在 hw/misc/pci-testdev.c 中提供了一个测试设备,我们可以参考这两个设备来构建我们的设备

一、Qemu Object Model

虽然 Qemu 是使用 C 编写的,但是其代码也充满了 OOP 的思想,在 Qemu 当中有着一套叫做 Qemu Object Model 的东西来实现面向对象,主要由这四个组件构成:

  • Type:用来定义一个「类」的基本属性,例如类的名字、大小、构造函数等
  • Class:用来定义一个「类」的静态内容,例如类中存储的静态数据、方法函数指针等
  • Object:动态分配的一个「类」的具体的实例(instance),储存类的动态数据
  • Property:动态对象数据的访问器(accessor),可以通过监视器接口进行检查

类似于 Golang,在 QOM 当中使用成员嵌套的方式来完成类的继承,父类作为类结构体的第一个成员 parent 而存在,因此也不支持多继承

参见这个ppt

I、TypeInfo - 类的基本属性

TypeInfo 这一结构体用来定义一个「类」的基本属性,该结构体定义于 include/qom/object.h 当中:

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
/**
* TypeInfo:
* @name: 类型名.
* @parent: 父类型名.
* @instance_size: 对象大小 (#Object 的衍生物).
* 若 @instance_size 为 0, 则对象的大小为其父类的大小
* @instance_init: 该函数被调用以初始化对象(译注:构造函数).
* (译注:调用前)父类已被初始化,因此子类只需要初始化他自己的成员。
* @instance_post_init: 该函数被调用以结束一个对象的初始化,
* 在所有的 @instance_init 函数被调用之后.
* @instance_finalize: 该函数在对象被析构时调用. 其在
* 父类的 @instance_finalize 被调用之前被调用.
* 在该函数中一个对象应当仅释放该对象特有的成员。
* @abstract: 若该域为真,则该类为一个虚类,不能被直接实例化。
* @class_size: 这个对象的类对象的大小 (#Object 的衍生物)
* 若 @class_size 为 0, 则类的大小为其父类的大小。
* 这允许一个类型在没有添加额外的虚函数时避免实现一个显式的类型。
* @class_init: 该函数在所有父类初始化结束后被调用,
* 以允许一个类设置他的默认虚方法指针.
* 这也允许该函数重写父类的虚方法。
* @class_base_init: 在所有的父类被初始化后、但
* 在类自身初始化前,为所有的基类调用该函数。
* 该函数用以撤销从父类 memcpy 到子类的影响.
* @class_data: 传递给 @class_init 与 @class_base_init 的数据,
* 这会在建立动态类型时有用。
* @interfaces: 与这个类型相关的接口.
* 其应当指向一个以 0 填充元素结尾的静态数组
*/
struct TypeInfo
{
const char *name;
const char *parent;

size_t instance_size;
void (*instance_init)(Object *obj);
void (*instance_post_init)(Object *obj);
void (*instance_finalize)(Object *obj);

bool abstract;
size_t class_size;

void (*class_init)(ObjectClass *klass, void *data);
void (*class_base_init)(ObjectClass *klass, void *data);
void *class_data;

InterfaceInfo *interfaces;
};

当我们在 Qemu 中要定义一个「类」的时候,我们实际上需要定义一个 TypeInfo 类型的变量,例如下面就是一个在 Qemu 定义一个自定义类的🌰:

1
2
3
4
5
6
7
8
9
10
11
12
13
static const TypeInfo a3_type_info = {
.name = "a3_type",
.parent = TYPE_OBJECT,
.interfaces = (InterfaceInfo[]) {
{ },
},
}

static void a3_register_types(void) {
type_register_static(&a3_type_info);
}

type_init(a3_register_types);

type_init() 其实就是 constructor 这一 gcc attribute 的封装,其作用就是将一个函数加入到一个 init_array 当中,在 Qemu 程序启动时在进入到 main 函数之前会先调用 init_array 中的函数,因此这里会调用我们自定义的函数,其作用便是调用 type_register_static() 将我们自定义的类型 a3_type_info 注册到全局的类型表中

II、Class - 类的静态内容

当我们通过一个 TypeInfo 结构体定义了一个类之后,我们还需要定义一个 Class 结构体来定义这个类的静态内容,包括函数表、静态成员等,其应当继承于对应的 Class 结构体类型,例如我们若是要定义一个新的机器类,则其 Class 应当继承于 MachineClass

所有 Class 结构体类型的最终的父类都是 ObjectClass 结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* ObjectClass:
*
* 所有类的基类. #ObjectClass 仅包含一个整型类型 handler
*/
struct ObjectClass
{
/*< private >*/
Type type;
GSList *interfaces;

const char *object_cast_cache[OBJECT_CLASS_CAST_CACHE];
const char *class_cast_cache[OBJECT_CLASS_CAST_CACHE];

ObjectUnparent *unparent;

GHashTable *properties;
};

下面是一个最简单的🌰:

1
2
3
4
5
struct A3Class
{
/*< private >*/
ObjectClass parent;
}

完成 Class 的定义之后我们还应当在前面定义的 a3_type_info 中添加上 Class size 与 Class 的构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void a3_class_init(ObjectClass *oc, void *data)
{
// 这里的 oc 参数便是新创建的 Class,全局只有一个该实例
// 我们应当 cast 为我们自己的 Class 类型,之后再进行相应操作
// do something
}

static const TypeInfo a3_type_info = {
.name = "a3_type",
.parent = TYPE_OBJECT,
.class_size = sizeof(A3Class),
.class_init = a3_class_init,
.interfaces = (InterfaceInfo[]) {
{ },
},
}

III、Object - 类的实例对象

我们还需要定义一个相应的 Object 类型来表示一个实例对象,其包含有这个类实际的具体数据,且应当继承于对应的 Object 结构体类型,例如我们若是要定义一个新的机器类型,其实例类型应当继承自 MachineState

所有 Object 结构体类型的最终的父类都是 Object 结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Object:
*
* 所有对象的基类。该对象的第一个成员为一个指向 #ObjectClass 的指针。
* 因为 C 中将一个结构体的第一个成员组织在该结构体的 0 字节起始处,
* 只要任何的子类将其父类作为第一个成员,我们都能直接转化为一个 #Object.
*
* 因此, #Object 包含一个对对象类的引用作为其第一个成员。
* 这允许在运行时识别对象的真实类型
*/
struct Object
{
/*< private >*/
ObjectClass *class;
ObjectFree *free;
GHashTable *properties;
uint32_t ref;
Object *parent;
};

下面是一个🌰:

1
2
3
4
5
struct A3Object
{
/*< private >*/
Object parent;
}

完成 Object 的定义之后我们还应当在前面定义的 a3_type_info 中添加上 Object size 与 Object 的构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void a3_object_init(Object *obj)
{
// 这里的 obj 参数便是动态创建的类型实例
// do something
}

static const TypeInfo a3_type_info = {
.name = "a3_type",
.parent = TYPE_OBJECT,
.instance_init = a3_object_init,
.instance_size = sizeof(A3Object),
.class_size = sizeof(A3Class),
.class_init = a3_class_init,
.interfaces = (InterfaceInfo[]) {
{ },
},
}

IV、类的创建与释放

类似于在 C++ 当中使用 newdelete 来创建与释放一个类实例,在 QOM 中我们应当使用 object_new()object_delete() 来创建与销毁一个 QOM 类实例,本质上就是 分配/释放类空间 + 显示调用构造/析构函数

QOM 判断创建类实例的类型是通过类的名字,即 TypeInfo->name,当创建类实例时 Qemu 会遍历所有的 TypeInfo 并寻找名字匹配的那个,从而调用到对应的构造函数,并将其基类 Object->class 指向对应的 class

下面是一个🌰:

1
2
3
4
// create a QOM object
A3Object *a3obj = object_new("a3_type");
// delete a QOM object
object_delete(a3obj);

二、MemoryRegion - Qemu 中的一块内存区域

在 Qemu 当中使用 MemoryRegion 结构体类型来表示一块具体的 Guest 物理内存区域,该结构体定义于 include/exec/memory.h 当中:

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
/** MemoryRegion:
*
* 表示一块内存区域的一个结构体.
*/
struct MemoryRegion {
Object parent_obj;

/* private: */

/* The following fields should fit in a cache line */
bool romd_mode;
bool ram;
bool subpage;
bool readonly; /* For RAM regions */
bool nonvolatile;
bool rom_device;
bool flush_coalesced_mmio;
bool global_locking;
uint8_t dirty_log_mask;
bool is_iommu;
RAMBlock *ram_block;
Object *owner;

const MemoryRegionOps *ops;
void *opaque;
MemoryRegion *container; // 指向父 MemoryRegion
Int128 size; // 内存区域大小
hwaddr addr; // 在父 MR 中的偏移量
void (*destructor)(MemoryRegion *mr);
uint64_t align;
bool terminates;
bool ram_device;
bool enabled;
bool warning_printed; /* For reservations */
uint8_t vga_logging_count;
MemoryRegion *alias; // 仅在 alias MR 中,指向实际的 MR
hwaddr alias_offset;
int32_t priority;
QTAILQ_HEAD(, MemoryRegion) subregions;
QTAILQ_ENTRY(MemoryRegion) subregions_link;
QTAILQ_HEAD(, CoalescedMemoryRange) coalesced;
const char *name;
unsigned ioeventfd_nb;
MemoryRegionIoeventfd *ioeventfds;
};

在 Qemu 当中有三种类型的 MemoryRegion:

  • MemoryRegion 根:通过 memory_region_init() 进行初始化,其用以表示与管理由多个 sub-MemoryRegion 组成的一个内存区域,并不实际指向一块内存区域,例如 system_memory
  • MemoryRegion 实体:通过 memory_region_init_ram() 初始化,表示具体的一块大小为 size 的内存空间,指向一块具体的内存
  • MemoryRegion 别名:通过 memory_region_init_alias() 初始化,作为另一个 MemoryRegion 实体的别名而存在,不指向一块实际内存

MR 容器与 MR 实体间构成树形结构,其中容器为根节点而实体为子节点:

下图来自于这里

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
                       struct MemoryRegion
+------------------------+
|name |
| (const char *) |
+------------------------+
|addr |
| (hwaddr) |
|size |
| (Int128) |
+------------------------+
|subregions |
| QTAILQ_HEAD() |
+------------------------+
|
|
----+-------------------+---------------------+----
| |
| |
| |

struct MemoryRegion struct MemoryRegion
+------------------------+ +------------------------+
|name | |name |
| (const char *) | | (const char *) |
+------------------------+ +------------------------+
|addr | |addr |
| (hwaddr) | | (hwaddr) |
|size | |size |
| (Int128) | | (Int128) |
+------------------------+ +------------------------+
|subregions | |subregions |
| QTAILQ_HEAD() | | QTAILQ_HEAD() |
+------------------------+ +------------------------+

相应地,基于 OOP 的思想,MemoryRegion 的成员函数被封装在函数表 MemoryRegionOps 当中:

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
57
58
59
60
/*
* Memory region callbacks
*/
struct MemoryRegionOps {
/* 从内存区域上读. @addr 与 @mr 有关; @size 单位为字节. */
uint64_t (*read)(void *opaque,
hwaddr addr,
unsigned size);
/* 往内存区域上写. @addr 与 @mr 有关; @size 单位为字节. */
void (*write)(void *opaque,
hwaddr addr,
uint64_t data,
unsigned size);

MemTxResult (*read_with_attrs)(void *opaque,
hwaddr addr,
uint64_t *data,
unsigned size,
MemTxAttrs attrs);
MemTxResult (*write_with_attrs)(void *opaque,
hwaddr addr,
uint64_t data,
unsigned size,
MemTxAttrs attrs);

enum device_endian endianness;
/* Guest可见约束: */
struct {
/* 若非 0,则指定了超出机器检查范围的访问大小界限
*/
unsigned min_access_size;
unsigned max_access_size;
/* If true, unaligned accesses are supported. Otherwise unaligned
* accesses throw machine checks.
*/
bool unaligned;
/*
* 若存在且 #false, 则该事务不会被设备所接受
* (并导致机器的相关行为,例如机器检查异常).
*/
bool (*accepts)(void *opaque, hwaddr addr,
unsigned size, bool is_write,
MemTxAttrs attrs);
} valid;
/* 内部应用约束: */
struct {
/* 若非 0,则决定了最小的实现的 size .
* 更小的 size 将被向上回绕,且将返回部分结果.
*/
unsigned min_access_size;
/* 若非 0,则决定了最大的实现的 size .
* 更大的 size 将被作为一系列的更小的 size 的访问而完成.
*/
unsigned max_access_size;
/* 若为 true, 支持非对齐的访问.
* 否则所有的访问都将被转换为(可能多种)对齐的访问.
*/
bool unaligned;
} impl;
};

当我们的 Guest 要读写虚拟机上的内存时,在 Qemu 内部实际上会调用 address_space_rw(),对于一般的 RAM 内存而言则直接对 MR 对应的内存进行操作,对于 MMIO 而言则最终调用到对应的 MR->ops->read()MR->ops->write()

关于 Qemu 内存管理更多的内容就暂且不在此展开了,不过现在我们知道的是在 Qemu 中使用 MemoryRegion 结构体来表示一段内存区域,那么我们同样可以通过在设备中添加 MemoryRegion 的方式来为设备添加内存,从而实现与设备间的 MMIO 通信

同样的,为了统一接口,在 Qemu 当中 PMIO 的实现同样是通过 MemoryRegion 来完成的

三、Qemu 中 PCI 设备的编写

在补充了这么多的 Qemu 相关的知识之后,现在我们可以开始在 Qemu 中编写 PCI 设备了,这里笔者将编写一个最简单的 Qemu 设备,并将源码放在 hw/misc/a3dev.c

Qemu 当中 PCI 设备实例的基类是 PCIDevice,因此我们应当创建一个继承自 PCIDevice 的类来表示我们的设备实例,这里笔者仅声明了两个 MemoryRegion 用作 MMIO 与 PMIO,以及一个用作数据存储的 buffer:

1
2
3
4
5
6
7
8
9
10
11
#define A3DEV_BUF_SIZE 0x100

typedef struct A3PCIDevState {
/*< private >*/
PCIDevice parent_obj;

/*< public >*/
MemoryRegion mmio;
MemoryRegion pmio;
uint8_t buf[A3DEV_BUF_SIZE];
} A3PCIDevState;

以及定义一个空的 Class 模板,继承自 PCI 设备的静态类型 PCIDeviceClass,不过这一步并不是必须的,事实上我们可以直接用 PCIDeviceClass 作为我们设备类的 Class:

1
2
3
4
typedef struct A3PCIDevClass {
/*< private >*/
PCIDeviceClass parent;
} A3PCIDevClass;

以及两个将父类转为子类的宏,因为 QOM 基本函数传递的大都是父类指针,所以我们需要一个宏来进行类型检查 + 转型,这也是 Qemu 中惯用的做法:

1
2
3
4
5
6
7
#define TYPE_A3DEV_PCI "a3dev-pci"
#define A3DEV_PCI(obj) \
OBJECT_CHECK(A3PCIDevState, (obj), TYPE_A3DEV_PCI)
#define A3DEV_PCI_GET_CLASS(obj) \
OBJECT_GET_CLASS(A3PCIDevClass, obj, TYPE_A3DEV_PCI)
#define A3DEV_PCI_CLASS(klass) \
OBJECT_CLASS_CHECK(A3PCIDevClass, klass, TYPE_A3DEV_PCI)

下面我们开始定义 MMIO 与 PMIO 的操作函数,这里笔者就简单地设置为读写设备内部的 buffer,并声明上两个 MemoryRegion 对应的函数表,需要注意的是这里传入的 hwaddr 类型参数其实为相对地址而非绝对地址:

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
57
58
59
60
61
62
63
64
65
static uint64_t
a3dev_read(void *opaque, hwaddr addr, unsigned size)
{
A3PCIDevState *ds = A3DEV_PCI(opaque);
uint64_t val = ~0LL;

if (size > 8)
return val;

if (addr + size > A3DEV_BUF_SIZE)
return val;

memcpy(&val, &ds->buf[addr], size);
return val;
}

static void
a3dev_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)
{
A3PCIDevState *ds = A3DEV_PCI(opaque);

if (size > 8)
return ;

if (addr + size > A3DEV_BUF_SIZE)
return ;

memcpy(&ds->buf[addr], &val, size);
}

static uint64_t
a3dev_mmio_read(void *opaque, hwaddr addr, unsigned size)
{
return a3dev_read(opaque, addr, size);
}

static uint64_t
a3dev_pmio_read(void *opaque, hwaddr addr, unsigned size)
{
return a3dev_read(opaque, addr, size);
}

static void
a3dev_mmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)
{
a3dev_write(opaque, addr, val, size);
}

static void
a3dev_pmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)
{
a3dev_write(opaque, addr, val, size);
}

static const MemoryRegionOps a3dev_mmio_ops = {
.read = a3dev_mmio_read,
.write = a3dev_mmio_write,
.endianness = DEVICE_LITTLE_ENDIAN,
};

static const MemoryRegionOps a3dev_pmio_ops = {
.read = a3dev_pmio_read,
.write = a3dev_pmio_write,
.endianness = DEVICE_LITTLE_ENDIAN,
};

然后是设备实例的初始化函数,在 PCIDeviceClass 当中定义了一个名为 realize 的函数指针,当 PCI 设备被载入时便会调用这个函数指针指向的函数来初始化,所以这里我们也定义一个自己的初始化函数,不过我们需要做的工作其实基本上就只有初始化两个 MemoryRegionmemory_region_init_io() 会为这两个 MemoryRegion 进行初始化的工作,并设置函数表为我们指定的函数表,pci_register_bar() 则用来注册 BAR:

1
2
3
4
5
6
7
8
9
10
11
static void a3dev_realize(PCIDevice *pci_dev, Error **errp)
{
A3PCIDevState *ds = A3DEV_PCI(pci_dev);

memory_region_init_io(&ds->mmio, OBJECT(ds), &a3dev_mmio_ops,
pci_dev, "a3dev-mmio", A3DEV_BUF_SIZE);
pci_register_bar(pci_dev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &ds->mmio);
memory_region_init_io(&ds->pmio, OBJECT(ds), &a3dev_pmio_ops,
pci_dev, "a3dev-pmio", A3DEV_BUF_SIZE);
pci_register_bar(pci_dev, 1, PCI_BASE_ADDRESS_SPACE_IO, &ds->pmio);
}

最后是 Class 与 Object(也就是 instance)的初始化函数,这里需要注意的是在 Class 的初始化函数中我们应当设置父类 PCIDeviceClass 的一系列基本属性(也就是 PCI 设备的基本属性):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void a3dev_instance_init(Object *obj)
{
// do something
}

static void a3dev_class_init(ObjectClass *oc, void *data)
{
DeviceClass *dc = DEVICE_CLASS(oc);
PCIDeviceClass *pci = PCI_DEVICE_CLASS(oc);

pci->realize = a3dev_realize;
pci->vendor_id = PCI_VENDOR_ID_QEMU;
pci->device_id = 0x1919;
pci->revision = 0x81;
pci->class_id = PCI_CLASS_OTHERS;

dc->desc = "arttnba3 test PCI device";
set_bit(DEVICE_CATEGORY_MISC, dc->categories);
}

最后就是为我们的 PCI 设备类型注册 TypeInfo 了,这里别忘了我们的接口中应当增加上 PCI 的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static const TypeInfo a3dev_type_info = {
.name = TYPE_A3DEV_PCI,
.parent = TYPE_PCI_DEVICE,
.instance_init = a3dev_instance_init,
.instance_size = sizeof(A3PCIDevState),
.class_size = sizeof(A3PCIDevClass),
.class_init = a3dev_class_init,
.interfaces = (InterfaceInfo[]) {
{ INTERFACE_CONVENTIONAL_PCI_DEVICE },
{ },
},
};

static void a3dev_register_types(void) {
type_register_static(&a3dev_type_info);
}

type_init(a3dev_register_types);

最后我们在 meson 构建系统中加入我们新增的这个设备,在 hw/misc/meson.build 中加入如下语句:

1
softmmu_ss.add(when: 'CONFIG_PCI_A3DEV', if_true: files('a3dev.c'))

并在 hw/misc/Kconfig 中添加如下内容,这表示我们的设备会在 CONFIG_PCI_DEVICES=y 时编译:

1
2
3
4
config PCI_A3DEV
bool
default y if PCI_DEVICES
depends on PCI

之后编译 Qemu 并附加上 -device a3dev-pci ,之后随便起一个 Linux 系统,此时使用 lspci 指令我们便能看到我们新添加的 pci 设备:

image.png

我们使用如下程序来测试我们的设备的输入输出,需要注意的是这需要 root 权限:

PMIO,使用 iopl 更改端口权限后便能通过 in/out 类指令读写端口

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
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/io.h>

int main(int argc, char **argv, char **envp)
{
unsigned short port_addr;

if (argc < 2) {
puts("[x] no port provided!");
exit(EXIT_FAILURE);
}

if (iopl(3) < 0) {
puts("[x] no privilege!");
exit(EXIT_FAILURE);
}

port_addr = atoi(argv[1]);

printf("[+] a3dev port addr start at: %d\n", port_addr);

puts("[*] now writing into a3dev-pci...");

for (int i = 0; i < 0x100 / 4; i++) {
outl(i, port_addr + i * 4);
}

puts("[+] writing done!");

printf("[*] now reading from a3dev-pci...");
for (int i = 0; i < 0x100 / 4; i++) {
if (i % 8 == 0) {
printf("\n[--%d--]", port_addr + i * 4);
}
printf(" %d ", inl(port_addr + i * 4));
}

puts("\n[+] reading done!");
}

PMIO 测试成功,设备读写功能正常:

image.png

MMIO,使用 mmap 映射 sys 目录下设备的 resource0 文件即可直接读写

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
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>
#include <sys/mman.h>

void mmio_write(uint32_t *addr, uint32_t val)
{
*addr = val;
}

uint32_t mmio_read(uint32_t *addr)
{
return *addr;
}

int main(int argc, char **argv, char **envp)
{
uint32_t *mmio_addr;
int dev_fd;

dev_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0",
O_RDWR | O_SYNC);
if (dev_fd < 0) {
puts("[x] failed to open mmio file! wrong path or no root!");
exit(EXIT_FAILURE);
}

mmio_addr = (uint32_t*)
mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, dev_fd, 0);
if (mmio_addr == MAP_FAILED) {
puts("failed to mmap!");
exit(EXIT_FAILURE);
}

puts("[*] start writing to a3dev-pci...");
for (int i = 0; i < 0x100 / 4; i++) {
mmio_write(mmio_addr + i, i);
}
puts("[+] write done!");

printf("[*] start reading from a3dev-pci...");
for (int i = 0; i < 0x100 / 4; i++) {
if (i % 8 == 0) {
printf("\n[--%p--]", mmio_addr);
}
printf(" %u ", mmio_read(mmio_addr + i));
}
puts("\n[+] read done!");
}

MMIO 测试成功,设备读写功能正常:

image.png

0x05.自定义 QEMU 机器类型(🕊)

众所周知在 Qemu 当中有很多种不同的机器类型,其表示着包含一些默认设备(包含PCIe显卡、以太网控制器、SATA控制器等)的虚拟芯片组,例如 pc 对应于 Intel 的 440FX 芯片组(这也是 Qemu 默认选择的机器类型)

image.png

Qemu 主要支持两种大的 x86 芯片组:i440FX 和 Q35,后者相比前者而言的一个大的亮点便是增加了对 PCIe 的支持:

image.png

我们可以使用 -machine 选项来指定我们要创建的虚拟机的机器类型,通过 -machine ? 选项可以查看当前支持的机器类型:

image.png

但自带的机器类型通常往往无法满足我们多样化的要求,因此有的时候我们需要自行编写一种机器类型来满足我们的需求

一、添加源码文件与编译选项

在 Qemu 源码目录中,与具体支持的硬件相关的代码都放在 hw/ 目录下,例如默认的 PC 架构便定义于 hw/i386/pc.c,因此若是我们想要定义一种新的机器类型则在该目录下进行定义是最好的

老版本的 Qemu 是纯粹基于 Makefile 进行构建的,而现在的新版本 Qemu 中则是使用 meson 进行项目构建,因此笔者接下来将会同时介绍两种配置方法

I、新版本 Qemu 配置方式(meson)

这里我们选择定义一种新的机器类型名为 a3-pc,并在 hw/i386/a3-pc 下创建如下目录结构:

1
2
3
4
5
6
7
$ tree hw/i386/a3-pc/
hw/i386/a3-pc/
├── accel.c
├── machine.c
└── meson.build

0 directories, 3 files

三个文件说明如下:

  • meson.build:meson 项目构建文件
  • machine.c:机器的主体代码
  • accel.c:自定义的 accelerator 代码

meson.build 中写入如下内容:

1
2
3
4
5
a3pc_ss = ss.source_set()
a3pc_ss.add(files('accel.c'))
a3pc_ss.add(files('machine.c'))

i386_ss.add_all(when: 'CONFIG_A3_PC', if_true: a3pc_ss)

之后在 hw/i386/meson.build 中添加该语句:

1
subdir('a3-pc')

这里笔者选择创建一个 i386 类型的机器,因此我们还需要修改 hw/i386/Kconfig,添加如下内容:

1
2
config A3_PC
bool

configs/devices/i386-softmmu/default.mak 末尾添加如下内容,使得我们的新的机器类型会被默认编译进去:

1
CONFIG_A3_PC=y

II、老版本 Qemu 配置方式(makefile)

如果是版本稍微老一点的 Qemu 则应当在 hw/a3-pc 下创建如下目录结构:

1
2
3
4
5
6
7
$ tree ./hw/a3-pc/
./hw/a3-pc/
├── accel.c
├── machine.c
└── Makefile.objs

0 directories, 3 files

三个文件说明如下:

  • Makefile.objs:机器的 Makefile 文件
  • machine.c:机器的主体代码
  • accel.c:自定义的 accelerator 代码,也可以直接用默认的 TCG accelerator

并在 Makefile.objs 中添加如下内容:

1
2
obj-$(CONFIG_A3_PC) += accel.o
obj-$(CONFIG_A3_PC) += machine.o

之后在 hw/Makefile.objs 中添加上该配置:

1
2
3
4
5
devices-dirs-y = core/
ifeq ($(CONFIG_SOFTMMU), y)
# ...
devices-dirs-$(CONFIG_A3_PC) += a3-pc/
endif

这里我们通过添加一个新的选项 CONFIG_A3_PC 来控制是否要进行该类型机器的编译

笔者选择创建一个 i386 类型的机器,因此我们还需要修改 hw/i386/Kconfig,添加如下内容,表示一个空白的机器,后面我们若是需要添加硬件则还需要在这部分进行改动:

1
2
config A3_PC
bool

最后我们在源码根目录的 configure 文件中添加如下内容即可:

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
supported_a3_pc_target() {
test "$a3_pc" = "yes" || return 1
glob "$1" "*-softmmu" || return 1
case "${1%-softmmu}" in
x86_64)
return 0
;;
esac
return 1
}

supported_target() {
case "$1" in
# ...
supported_a3_pc_target "$1" && return 0
print_error "TCG disabled, but hardware accelerator not available for '$target'"
return 1
}
# ...
for opt do
optarg=$(expr "x$opt" : 'x[^=]*=\(.*\)')
case "$opt" in
--help|-h) show_help=yes
;;
#...
;;
--enable-a3-pc) a3_pc="yes"
;;
# ...
# 这一块可以放在 supported_whpx_target 的那个语句块下面
if supported_a3_pc_target $target; then
echo "CONFIG_A3_PC=y" >> $config_target_mak
echo "CONFIG_A3_PC=y" >> $config_host_mak
fi

如果我们想要改变编译出来的可执行文件的名字,还可以在 Makefile.target 中修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
ifdef CONFIG_USER_ONLY
# user emulator name
QEMU_PROG=qemu-$(TARGET_NAME)
QEMU_PROG_BUILD = $(QEMU_PROG)
else
ifdef CONFIG_A3_PC
# arttnba3 type machine
QEMU_PROG=a3-pc
else
# system emulator name
QEMU_PROG=qemu-system-$(TARGET_NAME)$(EXESUF)
endif

二、定义新的 Machine Type

I、machine.c:machine 基本定义

虽然 Qemu 是使用 C 语言编写的,但是在 Qemu 当中同样使用了 OOP 的思想,通过结构体嵌套的形式实现继承

在 Qemu 当中使用 MachineState 结构体类型表示一个通用虚拟机的状态,使用 MachineClass 结构体类型表示一个通用的虚拟机类型,因此对于我们需要创建的新的机器类型,我们需要分别定义他的状态类与类型类,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "qemu/osdep.h"
#include "qemu-common.h"

#include "hw/boards.h"
#include "qom/object.h"
#include "sysemu/sysemu.h"

typedef struct A3PCMachineState {
/*< private >*/
MachineState parent;

Notifier machine_done;
} A3PCMachineState;

typedef struct A3PCMachineClass {
/*< private >*/
MachineClass parent;
} A3PCMachineClass;

这里对于继承自 MachineState 的子类我们添加了一个新的 Notifier 类型的成员,可以用来在后面构建事件通知链

我们还需要定义一些相应的父子类间转型的宏,以及一个表示新增的 a3-pc 类型的宏:

1
2
3
4
5
6
7
#define TYPE_A3PC_MACHINE MACHINE_TYPE_NAME("a3-pc")
#define A3PC_MACHINE(obj) \
OBJECT_CHECK(A3PCMachineState, (obj), TYPE_A3PC_MACHINE)
#define A3PC_MACHINE_GET_CLASS(obj) \
OBJECT_GET_CLASS(A3PCMachineClass, obj, TYPE_A3PC_MACHINE)
#define A3PC_MACHINE_CLASS(klass) \
OBJECT_CLASS_CHECK(A3PCMachineClass, klass, TYPE_A3PC_MACHINE)

接下来我们定义 MachineState 与 MachineClass 的初始化函数,这里只是一个最最简单的空模板,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void a3_pc_machine_init_done(Notifier *notifier, void *data)
{
}

static void a3_pc_machine_init(MachineState *machine)
{
A3PCMachineState *ms = A3PC_MACHINE(machine);

ms->machine_done.notify = a3_pc_machine_init_done;
qemu_add_machine_init_done_notifier(&ms->machine_done);
}

static void a3_pc_machine_class_init(ObjectClass *oc, void *data)
{
MachineClass *mc = MACHINE_CLASS(oc);

mc->init = a3_pc_machine_init;
// 这里设置了一个参数,指定了使用我们自己的 accelerator
mc->default_machine_opts = "accel=a3acl";
}

接下来我们需要声明一个 TypeInfo 类型的变量,用来表示我们新建的这一种机器类型:

1
2
3
4
5
6
7
8
9
10
static const TypeInfo a3_pc_machine_info = {
.name = TYPE_A3PC_MACHINE,
.parent = TYPE_MACHINE,
.instance_size = sizeof(A3PCMachineState),
.class_size = sizeof(A3PCMachineClass),
.class_init = a3_pc_machine_class_init,
.interfaces = (InterfaceInfo[]) {
{ },
},
};

最后就是注册我们的新机器类型了,这里使用 type_init() 来完成,原理是 gcc constructor attribute 使其会调用 a3_pc_machine_register() 来注册 a3_pc_machine_info

1
2
3
4
5
static void a3_pc_machine_register(void)
{
type_register_static(&a3_pc_machine_info);
}
type_init(a3_pc_machine_register);

II、accel.c:accelerator 定义

接下来就是定义我们自己的 accelerator,因为 Qemu 默认需要一个 accelerator,但如果再去和原有的 accelerator 做适配就太麻烦了(因为👴是懒🐕),所以这里我们自己定义一个空的 accelerator,不过这一部分我们只需要声明一个新的 TypeInfo 类型变量即可

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
#include "qemu/osdep.h"
#include "qemu/module.h"
#include "hw/boards.h"
#include "hw/qdev-core.h"
#include "sysemu/accel.h"

bool a3_pc_allowed;

static int a3_pc_init(MachineState *ms)
{
MachineClass *mc = MACHINE_GET_CLASS(ms);

/*
* opt out of system RAM being allocated by generic code
*/
mc->default_ram_id = NULL;

return 0;
}

static void a3_pc_accel_class_init(ObjectClass *oc, void *data)
{
AccelClass *ac = ACCEL_CLASS(oc);
static GlobalProperty compat[] = {
{ "migration", "store-global-state", "off" },
{ "migration", "send-configuration", "off" },
{ "migration", "send-section-footer", "off" },
};

ac->name = "A3ACL";
ac->init_machine = a3_pc_init;
ac->allowed = &a3_pc_allowed;
ac->compat_props = g_ptr_array_new();

compat_props_add(ac->compat_props, compat, G_N_ELEMENTS(compat));
}

#define TYPE_A3_ACCEL ACCEL_CLASS_NAME("a3acl")

static const TypeInfo a3_pc_accel_type = {
.name = TYPE_A3_ACCEL,
.parent = TYPE_ACCEL,
.class_init = a3_pc_accel_class_init,
};

static void a3_pc_type_init(void)
{
type_register_static(&a3_pc_accel_type);
}

type_init(a3_pc_type_init);

*新版本 accelerator 额外添加 ops

需要注意的是 qemu 的 7.0 和 5.0 的版本之间代码架构有一定的改动,所以对于 7.0 版本我们还需要额外定义一个 AccelClassOps:

当然,也可以直接用原有的 accelerator ,比如说 tcg,直接在 machine.c 代码中指定 accel=tcg 即可

添加文件:accel/a3acl/a3acl-accel-ops.c

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
57
/*
* QEMU A3ACL vCPU common functionality
*
*
* Copyright (c) 22 arttnba3
*
* Just modify it like what you want : )
*/

#include "qemu/osdep.h"
#include "qemu-common.h"
#include "sysemu/accel-ops.h"

static void a3acl_handle_interrupt(CPUState *cpu, int mask)
{
// do nothing
}

static void a3acl_kick_vcpu_thread(CPUState *unused)
{
// do nothing
}

static void a3acl_start_vcpu_thread(CPUState *cpu)
{
// do nothing
}

static void a3acl_accel_ops_init(AccelOpsClass *ops)
{
// 这几个函数可以按照个人需要来进行改造,笔者这里仅作占位符
ops->create_vcpu_thread = a3acl_start_vcpu_thread;
ops->kick_vcpu_thread = a3acl_kick_vcpu_thread;
ops->handle_interrupt = a3acl_handle_interrupt;
}

static void a3acl_accel_ops_class_init(ObjectClass *oc, void *data)
{
AccelOpsClass *ops = ACCEL_OPS_CLASS(oc);

ops->ops_init = a3acl_accel_ops_init;
}

static const TypeInfo a3acl_accel_ops_type = {
.name = ACCEL_OPS_NAME("a3acl"),
.parent = TYPE_ACCEL_OPS,
.class_init = a3acl_accel_ops_class_init,
.abstract = true,
};
module_obj(ACCEL_OPS_NAME("a3acl"));

static void a3acl_accel_ops_register_types(void)
{
type_register_static(&a3acl_accel_ops_type);
}
type_init(a3acl_accel_ops_register_types);

添加文件:accel/a3acl/meson.build

1
2
3
4
5
6
a3acl_ss = ss.source_set()
a3acl_ss.add(files(
'a3acl-accel-ops.c',
))

specific_ss.add_all(when: 'CONFIG_A3ACL', if_true: a3acl_ss)

修改文件:accel/meson.build

1
2
3
4
5
6
7
8
if have_system
subdir('hvf')
subdir('qtest')
subdir('kvm')
subdir('xen')
subdir('stubs')
subdir('a3acl') # 加上这句
endif

修改文件:accel/Kconfig

1
2
3
4
# 添加上这:
config A3ACL
bool
default y

三、添加设备结构🕊

现在我们已经有了一台可以运行的空白的机器——但包括 CPU 在内的所有设备目前暂且都是不存在的,因此我们需要手动地构造机器的设备结构

I、添加新的 PCIe Host Bridge

一个空的机器什么都没有,那自然是什么都干不了的,所以我们首先需要为这个机器添加上一个 PCIe Host Bridge,从而让我们的机器可以添加新的 PCIe 设备

惯例地就是定义一个新的 PCIe Host Bridge 类型的新 PCIe 设备:

添加文件:include/hw/pci-host/a3pc.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef HW_A3_PC_PCIE_HOST_H
#define HW_A3_PC_PCIE_HOST_H

#include "exec/memory.h"
#include "hw/pci/pcie_host.h"

typedef struct A3PCPCIEHost {
PCIExpressHost parent_obj;

MemoryRegion mem;
MemoryRegion io;
} A3PCPCIEHost;

#define TYPE_A3_PC_PCIE_HOST "a3-pc-pcie-host"
#define A3_PC_PCIE_HOST(obj) \
OBJECT_CHECK(A3PCPCIEHost, (obj), TYPE_A3_PC_PCIE_HOST)

#endif /* HW_A3_PC_PCIE_HOST_H */

添加文件:hw/pci-host/a3pc.c

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include "qemu/osdep.h"
#include "qemu-common.h"

#include "exec/memory.h"
#include "hw/qdev-properties.h"
#include "hw/pci/pci.h"
#include "hw/pci/pcie_host.h"
#include "hw/pci-host/a3pc.h"
#include "qemu/error-report.h"

static void a3_pc_host_init(Object *obj)
{
// nothing to do
}

static void a3_pc_pcie_set_irq(void *opaque, int irq_num, int level)
{
warn_report("A3-PC: not support INTx (irq %d, level %d)",
irq_num, level);
}

static int a3_pc_pcie_swizzle_map_irq_fn(PCIDevice *pci_dev, int pin)
{
warn_report("A3-PC: not support INTx (pin %d)", pin);
return 0;
}

static void a3_pc_pcie_host_realize(DeviceState *dev, Error **errp)
{
PCIHostState *pci = PCI_HOST_BRIDGE(dev);
A3PCPCIEHost *h = A3_PC_PCIE_HOST(dev);
SysBusDevice *sbd = SYS_BUS_DEVICE(dev);

memory_region_init(&h->mem, OBJECT(h), "a3-pc-mem", 16);
memory_region_init(&h->io, OBJECT(h), "a3-pc-io", 16);
sysbus_init_mmio(sbd, &h->mem);

/*
* A PCIe host in QEMU is required to provide
* a pair of callbacks: set_irq() and map_irq()
*/
pci->bus = pci_register_root_bus(dev, "a3-pcie0",
a3_pc_pcie_set_irq,
a3_pc_pcie_swizzle_map_irq_fn,
h, &h->mem, &h->io,
0, 1, TYPE_PCIE_BUS);
}

static Property a3_pc_pcie_host_props[] = {
DEFINE_PROP_END_OF_LIST(),
};

static void a3_pc_class_init(ObjectClass *oc, void *data)
{
DeviceClass *dc = DEVICE_CLASS(oc);

dc->realize = a3_pc_pcie_host_realize;
set_bit(DEVICE_CATEGORY_BRIDGE, dc->categories);
device_class_set_props(dc, a3_pc_pcie_host_props);
}

static const TypeInfo a3_pc_pcie_host = {
.name = TYPE_A3_PC_PCIE_HOST,
.parent = TYPE_PCI_HOST_BRIDGE,
.instance_size = sizeof(A3PCPCIEHost),
.instance_init = a3_pc_host_init,
.class_init = a3_pc_class_init,
};

static void a3_pc_pcie_host_register(void)
{
type_register(&a3_pc_pcie_host);
}

type_init(a3_pc_pcie_host_register);

修改文件:hw/pci-host/meson.build

老版本没测了,自己想该怎么改吧(笑)

1
2
# A3 devices
pci_ss.add(when: 'CONFIG_PCI', if_true: files('a3pc.c'))

之后我们在我们的机器类型中加上 PCI 相关的两个指针成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct A3PCMachineState {
/*< private >*/
MachineState parent;

/* <public> */

/* State for other subsystems/APIs: */
Notifier machine_done;

/* Pointers to devices and objects: */
PCIBus *bus;

/*
* QEMU requires the entire PCI(e) hierarchy be attached to
* a PCI(e) bus, so BES-VNC machine has to implement one.
*/
PCIHostState *pci;
} A3PCMachineState;

最后在机器初始化函数中初始化一个我们自定义的这个 PCIe 设备即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void a3_pc_machine_init(MachineState *machine)
{
A3PCMachineState *ms = A3PC_MACHINE(machine);
DeviceState *dev = qdev_new(TYPE_A3_PC_PCIE_HOST);

sysbus_realize_and_unref(SYS_BUS_DEVICE(dev), &error_fatal);
ms->pci = PCI_HOST_BRIDGE(dev);

memory_region_add_subregion(get_system_memory(), 0,
sysbus_mmio_get_region(SYS_BUS_DEVICE(dev), 0));

ms->machine_done.notify = a3_pc_machine_init_done;
qemu_add_machine_init_done_notifier(&ms->machine_done);
}

注意新版本和老版本的 API 不同,在老版本中应当使用如下 API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void a3_pc_machine_init(MachineState *machine)
{
A3PCMachineState *ms = A3PC_MACHINE(machine);

DeviceState *dev = qdev_create(NULL, TYPE_A3_PC_PCIE_HOST);

qdev_init_nofail(dev);
ms->pci = PCI_HOST_BRIDGE(dev);

memory_region_add_subregion(get_system_memory(), 0,
sysbus_mmio_get_region(SYS_BUS_DEVICE(dev), 0));

ms->machine_done.notify = a3_pc_machine_init_done;
qemu_add_machine_init_done_notifier(&ms->machine_done);
}

完成这些步骤之后我们的新机器就能随意插入各种 PCI 设备了;)

II、添加新的 CPU 插槽🕊

当然,我们的机器还缺少了 CPU,没有 CPU 的机器自然是跑不起来的,因此这里我们还需要在我们的机器类型当中添加上相应的 CPU 插槽,由于 Qemu 内部的基础 x86 机器架构已经实现好了基础框架,所以我们直接改为继承自对应的 x86 基础机器类即可

当然,如果是纯纯自定义的异架构,这里还是得自己手动写一套…

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
typedef struct A3PCMachineState {
/*< private >*/
X86MachineState parent;

/* <public> */

/* State for other subsystems/APIs: */
Notifier machine_done;

/* Pointers to devices and objects: */
PCIBus *bus;

/*
* QEMU requires the entire PCI(e) hierarchy be attached to
* a PCI(e) bus, so BES-VNC machine has to implement one.
*/
PCIHostState *pci;
} A3PCMachineState;

typedef struct A3PCMachineClass {
/*< private >*/
X86MachineClass parent;

/*< public >*/

/* Default CPU model version. See x86_cpu_set_default_version(). */
int default_cpu_version;
} A3PCMachineClass;

不过机器定义的文件当中需要改动的部分会比预想的要多,所以这里就先🕊🕊🕊了

Extra.自定义 CPU 🕊

🕊🕊🕊

四、编译运行🕊

由于我们新建立的机器类型为 x86 架构的机器,因此我们需要在执行 configure 脚本时指定 --target-list=x86_64-softmmu

这里需要注意的是前前面笔者提供了两种设置 CONFIG_A3_PC 的选项:如果我们是直接通过修改 default.mak 使得 CONFIG_A3_PC=y,则直接编译即可;若我们是通过修改了 configure 来指定 CONFIG_A3_PC 的值,则创建编译脚本的时候我们需要手动指定 --enable-a3-pc 来编译上我们新增的机器类型

编译完成后我们便能够看到我们新添加的机器类型 a3-pc

1
2
3
4
5
6
7
build$ ./qemu-system-x86_64 -machine ?
Supported machines are:
microvm microvm (i386)
pc Standard PC (i440FX + PIIX, 1996) (alias of pc-i440fx-7.0)
pc-i440fx-7.0 Standard PC (i440FX + PIIX, 1996) (default)
#...
a3-pc (null)

🕊🕊🕊


【VIRT.0x00】Qemu - I:Qemu 简易食用指南
https://arttnba3.github.io/2022/07/15/VIRTUALIZATION-0X00-QEMU-PART-I/
作者
arttnba3
发布于
2022年7月15日
许可协议