Let‘s get into the world of EL2 [二]
在 ARM64 架构中,共有 EL3 到 EL0 四个异常等级,EL3 异常级别最高。通常操作系统在 EL1,应用程序在 EL0,EL2 则 hypervisor 管理程序。
由于我们要完成的是一个 Type 1 类型的虚拟机管理器,所有的行为都是在 EL2 完成的。我们先不去思考这个在 EL2 的管理者需要完成什么任务,我们首先需要做的是进入这个 EL2 的世界,在这个世界中我们的汇编或者 C 代码。
在这个教学示例中,我采用了 qemu 进行 cortex-a72 仿真,通过 qemu 先加载并 u-boot 程序(u-boot 使用开源代码https://github.com/u-boot/u-boot.git),在 u-boot 中通过 bootm 的方式加载并启动虚拟机管理器的镜像文件X-Hyper_Uimage,这个X-Hyper_Uimage 也只是初始化了程序栈,并跳转到 C 代码处。
既然提到了使用 qemu 进行加载和,那么必然要先了解 qemu 下的物理内存的布局,不然我们都不知道该把我们的镜像放到哪个位置。
我们先考虑如下 qemu 仿真命令:
qemu-system-aarch64 -cpu cortex-a72 -machine virt,gic-version=3 -smp 1 \
-m 512M -nographic \
-bios ./u-boot/u-boot.bin \
-device loader,file=./build/X-Hyper_Uimage,addr=0x40200000,force-raw=on"
仿真的 cpu 为 cortex-a72;
仿真的机器平台 machine 为 virt,其中断控制器版本为 3.0;
仿真多核参数 smp 设置,先设置为 1,之后再扩展多核支持;
设置仿真虚拟硬件的物理内存大小为 512M,可以理解为实际硬件开发板中的外设 DDR 大小;
通过-bios 设置 qemu 要先加载的 u-boot 镜像;
通过-device loader 将X-Hyper_Uimage 加载到物理内存地址0x40200000 处(这里说的物理内存地址其实也是 qemu 仿真环境中给出的虚拟地址而已,这个点要注意);
这条命令通过 QEMU 创建一个基于 Cortex-A72 的虚拟环境,用于指定的 U-Boot ,并通过 U-Boot 加载位于物理内存地址0x40200000 下的一个虚拟机管理器镜像。
那么我们首先需要思考一个问题,那就是在上述的仿真命令下,qemu 给我们模拟出的设备的物理内存地址是怎么样的,它的起始地址是什么?
通过如下命令,我们可以生成当前 qemu virt 平台下的设备树信息:
qemu-system-aarch64 -cpu cortex-a72 -machine virt,gic-version=3,dumpdtb=virt.dtb -smp 1 -m 512M
并通过 dtc 将 dtb 转换为设备树源文件 dts:
dtc -o virt.dts -O dts -I dtb virt.dtb
打开 virt.dts 我们就可以看到仿真的虚拟平台配置了:
/dts-v1/;
/ {
interrupt-parent = <0x8002>;
model = "linux,dummy-virt";
#size-cells = <0x2>;
#address-cells = <0x2>;
compatible = "linux,dummy-virt";
memory@40000000 {
reg = <0x0 0x40000000 0x0 0x20000000>;
device_type = "memory";
};
...
};
这里我们只看内存 memory 信息,其他的都没有粘贴进来,之后用到的时候再分析。从 dts 设备树源文件我们可以知道 qemu 给仿真硬件平台提供的物理内存地址从0x40000000 开始,大小是0x20000000,也就是 512M 大小 。
然后我们就可以构建我们的虚拟机管理器的链接文件 ld 了:
#include "layout.h"
EXTERN(_start)
ENTRY(_start)
SECTIONS
{
/* hyper image link virtual addr, here equal to phys addr */
. = HIMAGE_VADDR;
.text.boot : {
KEEP(*(.text.boot))
}
. = ALIGN(4096);
.text : {
*(.text) *(.text.*)
}
.rodata : {
*(.rodata) *(.rodata.*)
}
.data : {
*(.data) *(.data.*)
}
. = ALIGN(4096);
.bss : {
__bss_start = .;
*(.bss .bss.*)
__bss_end = .;
}
. = ALIGN(4096);
HIMAGE_END = .;
}
因为我们的虚拟机管理器不会开启 EL2 的虚拟地址转换,直接使用物理内存,所以这里HIMAGE_VADDR 的值为0x40200000。
这里有同学可能会问为什么不直接放在物理内存地址起始的位置0x40000000 处呢?这是因为当我们使用上述 qemu 指令仿真时,qemu 会默认把 dtb 加载到物理地址0x40000000 处,并占用一定的内存空间,所以如果我们直接通过-device loader 把镜像加载到0x40000000 会导致 qemu 启动时报 overlap 的错误。
然后我们编写虚拟机管理器的起始代码:
#include "layout.h"
.section .text, "ax"
.global _start
.type _start, function
.align 4
_start:
/* Set stack for c code */
adrp x0, sp_stack
/* Get Current code id */
mrs x1, mpidr_el1
and x1, x1, #0x0f
add x2, x1, 1
mov x3, #SZ_4K
mul x3, x3, x2
add x0, x0, x3
mov sp, x0
bl hyper_init_primary
/* spin here */
b .
这段代码很简单,为了可以跳转到 C 代码,并执行,我们需要设置 EL2 下的栈寄存器 SP,栈内存从哪里来呢,我们在 main.c 中通过初始化全局变量sp_stack(大小为 4K * NCPU),它会占用 bss 段。
#include "layout.h"
__attribute__((aligned(SZ_4K))) char sp_stack[SZ_4K * NCPU] = {0};
int hyper_init_primary()
{
int x;
x = 0x1234;
return 0;
}
最后我们通过编写 CMakeLists.txt 来编译生成 X-Hyper.elf,并通过 objcopy 将 ELF 文件转换为 Binary 文件 X-Hyper,然后再通过mkimage 将 X-Hyper 打包成为 UImage。

$ mkimage -l X-Hyper_Uimage
Image Name:
Created: Tue Nov 5 22:40:43 2024
Image Type: AArch64 Linux Kernel Image (uncompressed)
Data Size: 536 Bytes = 0.52 KiB = 0.00 MiB
Load Address: 40200000
Entry Point: 40200000
UImage Header 中关键的两个值:
Load Address:即 U-boot 加载管理器镜像的内存地址,在 U-boot 中使用 bootm 加载 UImage 后,U-boot 会解析 UImage 的 Header,并根据 Load Address 将 Uimage 的虚拟机管理器镜像文件重新加载到 0x40200000 处;
Entry Point:U-boot 加载完虚拟机管理器镜像文件到指定内存地址后,再根据Entry Point 跳转到 0x40200000 处,也就是虚拟机管理器镜像文件中的的 _start 处开始执行程序;
整个流程如下图所示:

标题
到这里我们就已经进入虚拟机管理器的世界了,我们可以去编写我们想要实现的代码了。
项目构建:
- clone源代码到本地: git clonehttps://gitcode.com/cxjczy1990/X-Hyper.git
- 编译生成u-boot的bin文件: sh build_uboot.sh
- 编译虚拟管理器代码,生成虚拟机管理器镜像: sh run_build.sh
- qemu并加载镜像: sh run_qemu.sh
示例:
$ sh run_qemu.sh
run mode ...
U-Boot 2024.10-rc4 (Nov 06 2024 - 10:58:11 +0800)
DRAM: 512 MiB
Core: 51 devices, 14 uclasses, devicetree: board
Flash: 64 MiB
Loading Environment from Flash... *** Warning - bad CRC, using default environment
In: serial,usbkbd
Out: serial,vidconsole
Err: serial,vidconsole
No USB controllers found
Net: eth0: virtio-net#32
## Booting kernel from Legacy Image at 40200000 ...
Image Name:
Created: 2024-11-06 2:58:40 UTC
Image Type: AArch64 Linux Kernel Image (uncompressed)
Data Size: 536 Bytes = 536 Bytes
Load Address: 40200000
Entry Point: 40200000
Verifying Checksum ... OK
## Flattened Device Tree blob at 5e583d90
Booting using the fdt blob at 0x5e583d90
Working FDT set to 5e583d90
Loading Kernel Image to 40200000
Loading Device Tree to 000000005d47c000, end 000000005d57efff ... OK
Working FDT set to 5d47c000
Starting kernel ...
