不如 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 给他安到 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
进行连接:
成功连接上远程服务器上的 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 支持模拟多种设备,但是并不能涵盖现存所有的设备类型,同时有的时候出于一些特殊的目的我们也需要自定义一些设备,因此本节主要讲述如何在 Qemu 当中编写一个新的设备
注:qemu 官方在 hw/misc/edu.c
中也提供了一个教学用的设备样例,red hat 则在 hw/misc/pci-testdev.c
中提供了一个测试设备,我们可以参考这两个设备来构建我们的设备
一、PCI basic knowledge 在开始之前,我们首先来补充一些基础知识
I、总线 && PCI 设备 现代计算机通常采用总线(bus)结构,即存在一根主要的公共通信干线,CPU 及各种设备都通过这跟总线进行通信;总线是可以扩展的,即可以存在多个不同类型的总线相连,不同的设备接入到不同类型的总线上
由于总线结构形似一棵树🌳,故有的时候我们也将这些设备、总线、CPU所形成的结构称之为设备树 (Device Tree)
PCI 即 Peripheral Component Interconnect
,是一种连接电脑主板和外部设备的总线标准,其通过多根 PCI bus 完成 CPU 与 多个 PCI 设备间的连接,,在 X86 硬件体系结构中几乎所有的设备都以各种形式连接到 PCI 设备树上;PCIe(PCI express)是新一代的总线标准,它沿用既有的PCI编程概念及信号标准,并且构建了更加高速的串行通信系统标准
在 Linux 下我们可以使用 lspci
指令查看插在当前机器的 PCI bus 上的 PCI 设备,使用 -t
参数查看树形结构,-v
参数可以查看详细信息:
这里展示的结果有 virtio 设备是因为笔者是在阿里云学生机上使用的命令,这类机器一般其实都是用 Qemu 跑的虚拟机
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 $ lspci 00:00.0 Host bridge: Intel Corporation 440FX - 82441FX PMC [Natoma] (rev 02) 00:01.0 ISA bridge: Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II] 00:01.1 IDE interface: Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II] 00:01.2 USB controller: Intel Corporation 82371SB PIIX3 USB [Natoma/Triton II] (rev 01) 00:01.3 Bridge: Intel Corporation 82371AB/EB/MB PIIX4 ACPI (rev 03) 00:02.0 VGA compatible controller: Cirrus Logic GD 5446 00:03.0 Ethernet controller: Red Hat, Inc. Virtio network device 00:04.0 Communication controller: Red Hat, Inc. Virtio console 00:05.0 SCSI storage controller: Red Hat, Inc. Virtio block device 00:06.0 Unclassified device [00ff]: Red Hat, Inc. Virtio memory balloon $ lspci -t -v -[0000:00]-+-00.0 Intel Corporation 440FX - 82441FX PMC [Natoma] +-01.0 Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II] +-01.1 Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II] +-01.2 Intel Corporation 82371SB PIIX3 USB [Natoma/Triton II] +-01.3 Intel Corporation 82371AB/EB/MB PIIX4 ACPI +-02.0 Cirrus Logic GD 5446 +-03.0 Red Hat, Inc. Virtio network device +-04.0 Red Hat, Inc. Virtio console +-05.0 Red Hat, Inc. Virtio block device \-06.0 Red Hat, Inc. Virtio memory balloon
我们还可以使用 lshw -businfo
命令来获取设备信息:
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 $ sudo lshw -businfo [sudo] password for arttnba3: Bus info Device Class Description ========================================================= system Alibaba Cloud ECS bus Motherboard memory 96KiB BIOS cpu@0 processor Intel(R) Xeon(R) Platinum 8163 CPU @ 2.50GHz memory 2GiB System Memory memory 2GiB DIMM RAM pci@0000:00:00.0 bridge 440FX - 82441FX PMC [Natoma] pci@0000:00:01.0 bridge 82371SB PIIX3 ISA [Natoma/Triton II] pci@0000:00:01.1 storage 82371SB PIIX3 IDE [Natoma/Triton II] pci@0000:00:01.2 bus 82371SB PIIX3 USB [Natoma/Triton II] usb@1 usb1 bus UHCI Host Controller usb@1:1 input QEMU USB Tablet pci@0000:00:01.3 bridge 82371AB/EB/MB PIIX4 ACPI pci@0000:00:02.0 display GD 5446 pci@0000:00:03.0 network Virtio network device virtio@0 eth0 network Ethernet interface pci@0000:00:04.0 communication Virtio console virtio@1 generic Virtual I/O device pci@0000:00:05.0 storage Virtio block device virtio@2 /dev/vda disk 42GB Virtual I/O device virtio@2,1 /dev/vda1 volume 39GiB EXT4 volume pci@0000:00:06.0 generic Virtio memory balloon virtio@3 generic Virtual I/O device system PnP device PNP0b00 input PnP device PNP0303 input PnP device PNP0f13 storage PnP device PNP0700 communication PnP device PNP0501 veth073b1a5 network Ethernet interface veth2c8670f network Ethernet interface vethc0202a2 network Ethernet interface veth49e878e network Ethernet interface
II、PCI 设备编号 每个PCI 设备都有着三个编号:总线编号(Bus Number)、设备编号(Device Number)与功能编号(Function Number),作为设备的唯一标识;在此之上还有 PCI 域的概念,一个 PCI 域上最多可以连接 256 根 PCI 总线
当我们使用 lspci
命令查看 PCI 设备信息时,在每个设备开头都可以看到形如 xx:yy.z
的十六进制编号,这个格式其实是 总线编号:设备编号.功能编号
,当我们使用 lspci -v
查看 PCI 设备信息时,在总线编号前面的 4 位数字便是 PCI 域的编号
III、PCI 设备配置空间 每个 PCI 逻辑设备中都有着其自己的配置空间 (configuration space),通常是设备地址空间的前 64 字节(新版的设备还扩展了 0x40~0xFF 这段配置空间),其中存放了一些设备的基本信息,如生厂商信息、IRQ中断号、mem 空间与 io 空间的起始地址与大小等
Intel 芯片组中我们可以使用 IO 空间的 CF8/CFC
地址(端口)来访问 PCI 设备的配置寄存器:
CF8
:CONFIG_ADDRESS ,即 PCI 配置空间地址端口。
CFH
:CONFIG_DATA ,即 PCI 配置空间数据端口。
当我们往 CONFIG_ADDRESS
端口填入对应的设备标识后,就可以从 CONFIG_DATA
端口上读写 PCI 配置空间的内存, CONFIG_ADDRESS
端口的格式如下:
31
位:Enable 位
23:16
位:总线编号
15:11
位:设备编号
10:8
位:功能编号
7:2
位:配置空间寄存器编号
1:0
位:恒为 00
除了通过端口访问外,我们也可以通过 MMIO 的方式访问一个 PCI 设备的地址空间
现在我们来看 PCI 配置空间的结构,PCI 设备分为 Bridge
与 Agent
两类,故配置空间也分为相应的两类
Agent 类型配置空间又被称为 Type 00h
,格式如下图所示:
相应地,Bridge 类型配置空间被称为 Type 01h
,与 Agent 类型配置空间大同小异:
简单介绍几个比较重要的字段:
设备标识相关:
Vendor ID
:生产厂商的 ID,例如 Intel 设备通常为 0x8086
Device ID
:具体设备的 ID,通常也是由厂家自行指定的
Class Code
:类代码,用于区分设备类型
Revision ID
:PCI 设备的版本号,可以看作 Device ID 的扩展
设备状态相关:
设备配置相关:
前面我们讲到 lspci 命令,我们可以使用 -s
来通过指定查看的具体 PCI 设备,通过 -m
查看部分信息,通过 -nn
查看比较详细的信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 $ lspci -vv -s 00:02.0 -m Device: 00:02.0 Class: VGA compatible controller Vendor: Cirrus Logic Device: GD 5446 SVendor: Red Hat, Inc. SDevice: QEMU Virtual Machine $ lspci -vv -s 00:02.0 -nn 00:02.0 VGA compatible controller [0300]: Cirrus Logic GD 5446 [1013:00b8] (prog-if 00 [VGA controller]) Subsystem: Red Hat, Inc. QEMU Virtual Machine [1af4:1100] Control: I/O+ Mem+ BusMaster- SpecCycle- MemWINV- VGASnoop- ParErr- Stepping- SERR+ FastB2B- DisINTx- Status: Cap- 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx- Region 0: Memory at fc000000 (32-bit, prefetchable) [size=32M] Region 1: Memory at febd0000 (32-bit, non-prefetchable) [size=4K] Expansion ROM at 000c0000 [disabled] [size=128K] Kernel driver in use: cirrus Kernel modules: cirrusfb, cirrus
我们还可以直接使用 -x
参数来查看 PCI 设备的配置空间:
1 2 3 4 5 6 $ lspci -s 00:02.0 -x 00:02.0 VGA compatible controller: Cirrus Logic GD 5446 00: 13 10 b8 00 03 01 00 00 00 00 00 03 00 00 00 00 10: 08 00 00 fc 00 00 bd fe 00 00 00 00 00 00 00 00 20: 00 00 00 00 00 00 00 00 00 00 00 00 f4 1a 00 11 30: 00 00 bc fe 00 00 00 00 00 00 00 00 00 00 00 00
在 Linux 当中我们也可以通过 procfs 或 sysfs 这样的文件系统来查看设备的相关配置信息,例如通过 /proc/bus/pci/00/00.0
文件我们同样可以查看 PCI 设备 00:02.0
的配置空间:
1 2 3 4 5 $ cat /proc/bus/pci/00/02.0 | xxd00000000: 1310 b800 0301 0000 0000 0003 0000 0000 ................ 00000010: 0800 00fc 0000 bdfe 0000 0000 0000 0000 ................ 00000020: 0000 0000 0000 0000 0000 0000 f41a 0011 ................ 00000030: 0000 bcfe 0000 0000 0000 0000 0000 0000 ................
通过 /sys/devices/pci0000:00/0000:00:02.0/resource
获取到的信息中每行表示一个地址空间,其中第一行为 MMIO,第二行为 PMIO,三列信息分别为起始地址、终止地址、标志位 ,:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $ sudo cat /sys/devices/pci0000\:00/0000\:00\:02.0/resource 0x00000000fc000000 0x00000000fdffffff 0x0000000000042208 0x00000000febd0000 0x00000000febd0fff 0x0000000000040200 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x00000000000c0000 0x00000000000dffff 0x0000000000000212 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000
通过 /sys/devices/pci0000:00/0000:00:02.0
下的其他文件也可以访问该设备的一些其他资源信息(例如通过 resource0
可以直接访问 MMIO 空间,resource1
则为其 PMIO 空间)
IV、PCI 设备内存 & 端口空间与访问方式 前面我们讲了 PCI 设备与特性和配置相关的配置空间,现在我们来看与 PCI 设备与实际操作相关的内存映射空间与端口映射空间
所有 IO 设备的内存与端口空间需要被映射到对应的地址空间/端口空间中才能访问,这需要占用部分的内存地址空间与端口地址空间,即我们有两种映射外设资源的方式:
MMIO (Memory-mapped I/O):即内存映射 IO。这种方式将 IO 设备的内存与寄存器映射到指定的内存地址空间上,此时我们便可以通过常规的访问内存的方式来直接访问到设备的寄存器与内存
PMIO (Port-mapped I/O):即端口映射 IO。这种方式将 IO 设备的寄存器编码到指定的端口上,我们需要通过访问端口的方式来访问设备的寄存器与内存(例如在 x86 下通过 in
与 out
这一类的指令可以读写端口)。IO 设备通过专用的针脚或者专用的总线与 CPU 连接,这与内存地址空间相独立,因此又称作 isolated I/O
完成映射之后通过相应的内存/端口访问到的便是 PCI 设备的内存/端口地址空间
例如实模式下的 0xA0000 ~ 0xBFFFF
这 128KB 地址空间通常被用作显存的映射,当我们在实模式下读写这块区域时通常便是直接读写显卡上的显存,而并非普通的内存
通过 procfs 的 /proc/iomem
我们可以查看物理地址空间的情况,其中我们便能看到各种设备所占用的地址空间
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 $ sudo cat /proc/iomem 00000000-00000fff : Reserved 00001000-0009fbff : System RAM 0009fc00-0009ffff : Reserved 000a0000-000bffff : PCI Bus 0000:00 000c0000-000c91ff : Video ROM 000c9800-000ca1ff : Adapter ROM 000ca800-000ccbff : Adapter ROM 000f0000-000fffff : Reserved 000f0000-000fffff : System ROM 00100000-7ffdffff : System RAM 1f400000-20200e70 : Kernel code 20200e71-2105843f : Kernel data 2132b000-217fffff : Kernel bss 7ffe0000-7fffffff : Reserved 80000000-febfffff : PCI Bus 0000:00 fc000000-fdffffff : 0000:00:02.0 fc000000-fdffffff : cirrus feb80000-febbffff : 0000:00:03.0 febd0000-febd0fff : 0000:00:02.0 febd0000-febd0fff : cirrus febd1000-febd1fff : 0000:00:03.0 febd2000-febd2fff : 0000:00:04.0 febd3000-febd3fff : 0000:00:05.0 fec00000-fec003ff : IOAPIC 0 fee00000-fee00fff : Local APIC feffc000-feffffff : Reserved fffc0000-ffffffff : Reserved
通过 procfs 的 /proc/ioports
我们可以查看 IO 端口情况,其中便包括各种设备对应的 PMIO 端口:
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 $ sudo cat /proc/ioports 0000-0cf7 : PCI Bus 0000:00 0000-001f : dma1 0020-0021 : pic1 0040-0043 : timer0 0050-0053 : timer1 0060-0060 : keyboard 0064-0064 : keyboard 0070-0071 : rtc0 0080-008f : dma page reg 00a0-00a1 : pic2 00c0-00df : dma2 00f0-00ff : fpu 0170-0177 : 0000:00:01.1 0170-0177 : ata_piix 01f0-01f7 : 0000:00:01.1 01f0-01f7 : ata_piix 0376-0376 : 0000:00:01.1 0376-0376 : ata_piix 03f2-03f2 : floppy 03f4-03f5 : floppy 03f6-03f6 : 0000:00:01.1 03f6-03f6 : ata_piix 03f7-03f7 : floppy 03f8-03ff : serial 0505-0505 : QEMU0001:00 0510-051b : QEMU0002:00 0510-051b : fw_cfg_io 0600-063f : 0000:00:01.3 0600-0603 : ACPI PM1a_EVT_BLK 0604-0605 : ACPI PM1a_CNT_BLK 0608-060b : ACPI PM_TMR 0700-070f : 0000:00:01.3 0700-0708 : piix4_smbus 0cf8-0cff : PCI conf1 0d00-adff : PCI Bus 0000:00 ae0f-aeff : PCI Bus 0000:00 af20-afdf : PCI Bus 0000:00 afe0-afe3 : ACPI GPE0_BLK afe4-ffff : PCI Bus 0000:00 c000-c03f : 0000:00:05.0 c000-c03f : virtio-pci-legacy c040-c05f : 0000:00:01.2 c040-c05f : uhci_hcd c060-c07f : 0000:00:03.0 c060-c07f : virtio-pci-legacy c080-c09f : 0000:00:04.0 c080-c09f : virtio-pci-legacy c0a0-c0bf : 0000:00:06.0 c0a0-c0bf : virtio-pci-legacy c0c0-c0cf : 0000:00:01.1 c0c0-c0cf : ata_piix
二、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 测试成功,设备读写功能正常:
0x04.自定义 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_ONLYQEMU_PROG=qemu-$(TARGET_NAME) QEMU_PROG_BUILD = $(QEMU_PROG) else ifdef CONFIG_A3_PCQEMU_PROG=a3-pc else 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)
🕊🕊🕊