本文最后更新于:2024年4月29日 早上
不如 VMWare👋
0x00.一切开始之前 Qemu 是一款开源的虚拟机软件,支持多种不同架构的模拟(Emulation)以及配合 kvm 完成当前架构的虚拟化(Virtualization)的特性,是当前最火热的开源虚拟机软件
Qemu 的基本运行架构如下图所示:
本篇文章笔者将简要叙述如何从源码编译特定架构的 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 buildbuild$ ../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 就完事了
需要花的时间还是不短的,在笔者的小破服务器上编译大概需要十几分钟左右,大概编译了两千多个文件,完成之后在当前目录下就会有一个热乎乎的可执行文件 qemu-system_x86-64
,这个就是 Qemu 的本体了
如果需要的话可以通过 make install
安装到系统中,这样就能直接从命令行启动了
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 上,需要注意的是这里只能在本地进行连接
注:我们默认从包管理器安装的 QEMU 使用的是 GTK 图形界面,如果你更喜欢使用这个,则可以在编译选项中额外加上 --enable-gtk
,这样默认就是用 gtk 在本地绘制图形界面,不过这通常额外需要类似 gtk-devel
的库
如果是运行在远程服务器上的话,我们还需要额外指定 -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
进行连接:
成功连接上远程服务器上的 qemu:
之后就是常规的安装流程了,不过可能是由于 qemu 模拟显卡的问题(或者是 VNC 配置的问题),在一开始的时候安装界面的颜色会有点失真:
不过在安装准备结束的时候又恢复正常的颜色了,笔者目前推测应该是和显卡驱动有关:
之后就和正常使用虚拟机没有什么区别了,下次再次启动就不需要指定 -cdrom
参数了
二、构建 ext4 磁盘镜像并运行 kernel bzImage 如果你不需要一个完整的发行版 Linux 系统环境,只是想跑一个裸的简易的内核,也可以通过下面的方式完成:
I.构建磁盘镜像 这里我们使用 debootstrap
来创建ext4硬盘镜像,直接使用由 Google 团队为 syzkaller 构建磁盘镜像的脚步即可
1 2 3 4 5 6 $ sudo apt-get install debootstrap $ mkdir image$ cd imageimage$ wget https://raw.githubusercontent.com/google/syzkaller/master/tools/create-image.sh -O create-image.sh image$ chmod +x create-image.shimage$ ./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
参数):
0x03. QEMU 源码调试 QEMU 允许我们通过 -s
或是 -gdb tcp::1234
这样的附加参数来调试虚拟机(比如说调试 Linux kernel),但有的时候我们想要直接调试 QEMU 本体 (比如说调试一些自己写的模拟设备),这个时候就需要我们将 Host 上的 QEMU 进程作为待调试对象
因为 QEMU 本身也是在 Host 上运行的一个进程,所以笔者这里给出一个比较直接的调试 QEMU 的办法:把 QEMU 本体启动起来后直接用 ps
找 QEMU 进程然后 gdb attach 即可像正常调试一个普通进程一样调试 QEMU:
如果是自己编译的 QEMU 这样就可以直接从源码进行调试了:
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 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 struct ObjectClass { 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 { 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) { }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 struct Object { ObjectClass *class ; ObjectFree *free ; GHashTable *properties; uint32_t ref; Object *parent; };
下面是一个🌰:
1 2 3 4 5 struct A3Object { 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) { }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++ 当中使用 new
与 delete
来创建与释放一个类实例,在 QOM 中我们应当使用 object_new()
与 object_delete()
来创建与销毁一个 QOM 类实例,本质上就是 分配/释放类空间 + 显示调用构造/析构函数
QOM 判断创建类实例的类型是通过类的名字,即 TypeInfo->name
,当创建类实例时 Qemu 会遍历所有的 TypeInfo 并寻找名字匹配的那个,从而调用到对应的构造函数,并将其基类 Object->class
指向对应的 class
下面是一个🌰:
1 2 3 4 A3Object *a3obj = object_new("a3_type" ); 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 struct MemoryRegion { Object parent_obj; bool romd_mode; bool ram; bool subpage; bool readonly; 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; Int128 size; hwaddr addr; void (*destructor)(MemoryRegion *mr); uint64_t align; bool terminates; bool ram_device; bool enabled; bool warning_printed; uint8_t vga_logging_count; MemoryRegion *alias; 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 struct MemoryRegionOps { uint64_t (*read)(void *opaque, hwaddr addr, unsigned 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 ; struct { unsigned min_access_size; unsigned max_access_size; bool unaligned; bool (*accepts)(void *opaque, hwaddr addr, unsigned size, bool is_write, MemTxAttrs attrs); } valid; struct { unsigned min_access_size; unsigned max_access_size; 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 { PCIDevice parent_obj; MemoryRegion mmio; MemoryRegion pmio; uint8_t buf[A3DEV_BUF_SIZE]; } A3PCIDevState;
以及定义一个空的 Class 模板,继承自 PCI 设备的静态类型 PCIDeviceClass
,不过这一步并不是必须的,事实上我们可以直接用 PCIDeviceClass
作为我们设备类的 Class:
1 2 3 4 typedef struct A3PCIDevClass { 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 设备被载入时便会调用这个函数指针指向的函数来初始化,所以这里我们也定义一个自己的初始化函数,不过我们需要做的工作其实基本上就只有初始化两个 MemoryRegion
,memory_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) { }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 设备:
我们使用如下程序来测试我们的设备的输入输出,需要注意的是这需要 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 测试成功,设备读写功能正常:
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 测试成功,设备读写功能正常:
0x05.自定义 QEMU 机器类型(🕊) 众所周知在 Qemu 当中有很多种不同的机器类型,其表示着包含一些默认设备(包含PCIe显卡、以太网控制器、SATA控制器等)的虚拟芯片组,例如 pc
对应于 Intel 的 440FX
芯片组(这也是 Qemu 默认选择的机器类型)
Qemu 主要支持两种大的 x86 芯片组:i440FX 和 Q35,后者相比前者而言的一个大的亮点便是增加了对 PCIe 的支持:
我们可以使用 -machine
选项来指定我们要创建的虚拟机的机器类型,通过 -machine ?
选项可以查看当前支持的机器类型:
但自带的机器类型通常往往无法满足我们多样化的要求,因此有的时候我们需要自行编写一种机器类型来满足我们的需求
一、添加源码文件与编译选项 在 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
中添加该语句:
这里笔者选择创建一个 i386 类型的机器,因此我们还需要修改 hw/i386/Kconfig
,添加如下内容:
在 configs/devices/i386-softmmu/default.mak
末尾添加如下内容,使得我们的新的机器类型会被默认编译进去:
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
,添加如下内容,表示一个空白的机器,后面我们若是需要添加硬件则还需要在这部分进行改动:
最后我们在源码根目录的 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" ;;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 QEMU_PROG=qemu-$(TARGET_NAME) QEMU_PROG_BUILD = $(QEMU_PROG) else ifdef CONFIG_A3_PC QEMU_PROG=a3-pcelse 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 { MachineState parent; Notifier machine_done; } A3PCMachineState;typedef struct A3PCMachineClass { 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; 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); 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 #include "qemu/osdep.h" #include "qemu-common.h" #include "sysemu/accel-ops.h" static void a3acl_handle_interrupt (CPUState *cpu, int mask) { }static void a3acl_kick_vcpu_thread (CPUState *unused) { }static void a3acl_start_vcpu_thread (CPUState *cpu) { }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/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) { }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); 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 { MachineState parent; Notifier machine_done; PCIBus *bus; 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 { X86MachineState parent; Notifier machine_done; PCIBus *bus; PCIHostState *pci; } A3PCMachineState;typedef struct A3PCMachineClass { X86MachineClass parent; int default_cpu_version; } A3PCMachineClass;
不过机器定义的文件当中需要改动的部分会比预想的要多,所以这里就先🕊🕊🕊了
🕊🕊🕊
四、编译运行🕊 由于我们新建立的机器类型为 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)
🕊🕊🕊