本篇幅,我将带领大家去用基于上一篇幅的代码基础来 bring-up 一个 Linux 操作系统:
我们要做的第一件事是编译一个 Linux,获得其内核镜像:
wget https://github.com/torvalds/linux/archive/refs/tags/v6.12.tar.gz
tar -xvzf v6.12.tar.gz
cd linux-6.12
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
cp arch/arm64/config/defconfig .config
make menuconfig
make -j16
为了经可能缩小 Image 的大小,我们在make menuconfig 时尽可能将一些外设驱动全部删除。
获取内核镜像后,我们还需要创建对应的设备树 dtb:
qemu-system-aarch64 -cpu cortex-a72 -machine virt,gic-version=3,dumpdtb=virt.dtb -smp 2 -m 128M -nographic
然后我们修改设备树如下,我们只保留关键的分支,之后用到再扩展:
/dts-v1/;/ {interrupt-parent = <0x8003>;model = "linux,dummy-virt";#size-cells = <0x2>;#address-cells = <0x2>;compatible = "linux,dummy-virt";psci {migrate = <0xc4000005>;cpu_on = <0xc4000003>;cpu_off = <0x84000002>;cpu_suspend = <0xc4000001>;method = "hvc";compatible = "arm,psci-1.0", "arm,psci-0.2", "arm,psci";};memory@40000000 {reg = <0x0 0x80000000 0x0 0x4000000>;device_type = "memory";};platform-bus@c000000 {interrupt-parent = <0x8003>;ranges = <0x0 0x0 0xc000000 0x2000000>;#address-cells = <0x1>;#size-cells = <0x1>;compatible = "qemu,platform", "simple-bus";};pl011@9000000 {clock-names = "uartclk", "apb_pclk";clocks = <0x8000 0x8000>;interrupts = <0x0 0x1 0x4>;reg = <0x0 0x9000000 0x0 0x1000>;compatible = "arm,pl011", "arm,primecell";};intc@8000000 {phandle = <0x8003>;reg = <0x0 0x8000000 0x0 0x10000 0x0 0x80a0000 0x0 0xf60000>;#redistributor-regions = <0x1>;compatible = "arm,gic-v3";ranges;#size-cells = <0x2>;#address-cells = <0x2>;interrupt-controller;#interrupt-cells = <0x3>;its@8080000 {phandle = <0x8004>;reg = <0x0 0x8080000 0x0 0x20000>;#msi-cells = <0x1>;msi-controller;compatible = "arm,gic-v3-its";};};cpus {#size-cells = <0x0>;#address-cells = <0x1>;cpu-map {socket0 {cluster0 {core0 {cpu = <0x8002>;};core1 {cpu = <0x8001>;};};};};cpu@0 {phandle = <0x8002>;reg = <0x0>;enable-method = "psci";compatible = "arm,cortex-a72";device_type = "cpu";};cpu@1 {phandle = <0x8001>;reg = <0x1>;enable-method = "psci";compatible = "arm,cortex-a72";device_type = "cpu";};};timer {interrupts = <0x1 0xd 0x4 0x1 0xe 0x4 0x1 0xb 0x4 0x1 0xa 0x4>;always-on;compatible = "arm,armv8-timer", "arm,armv7-timer";};apb-pclk {phandle = <0x8000>;clock-output-names = "clk24mhz";clock-frequency = <0x16e3600>;#clock-cells = <0x0>;compatible = "fixed-clock";};chosen {stdout-path = "/pl011@9000000";rng-seed = <0x1a8d6e59 0x3644466e 0x24b258f8 0x1ae5e04d 0x96e22649 0x142857c2 0xdb1aa43d 0xa75edb65>;kaslr-seed = <0x5addae6b 0x7d35b193>;};
};
这里最关键的一个修改是 memory 的范围,这个范围和我们对 guest 的配置要一致,这块可以看之前的关于 stage2 转换的文章。
然后我们将上述 dts 转换为 dtb:
dtc -I dts -O dtb -o virt.dtb virt.dts
最后我们将内核镜像和 dtb 通过 ld 打包到 X-Hyper 镜像中:
set(Guest_VM_Image "${PROJECT_BINARY_DIR}/../linux/image.o \ ${PROJECT_BINARY_DIR}/../linux/virt.dtb.o")
set(X_HYPER_LINK "${CMAKE_LINKER} -pie -Map X_Hyper.map -T${LSCRIPT} -L${LINK_PATH} \-lx_hyper_libs -o X-Hyper.elf ${Guest_VM_Image}")
在 IPA mapping 中,我们需要将 dtb 进行映射:
LOG_INFO("-->Create dtb range mapping for guest\n");for(p = 0; p < vm_config->guest_dtb->image_size; p += PAGESIZE) {char *page = alloc_one_page();if(page == NULL) {abort("Unable to alloc a page");}if(vm_config->guest_dtb->image_size - p > PAGESIZE) {copy_size = PAGESIZE;} else {copy_size = vm_config->guest_dtb->image_size - p;}/* copy the guest image content from X-Hyper image to pages */memcpy(page, (char *)vm_config->guest_dtb->start_addr + p, copy_size);create_guest_mapping(pgt, vm_config->dtb_addr + p, (u64)page, PAGESIZE, S2PTE_NORMAL | S2PTE_RW);}
最后我们需要通过 x0 寄存器告诉内核这个 dtb 在物理内存(IPA) 的位置:
if(vcpuid == 0) { /* If it is the primary virtual cpu, set the dtb address (ipa) */vcpu->regs.x[0] = vm->dtb;}
完成上述操作后,我们就可以重新编译 X-Hyper,然后运行 qemu 了,这个过程会出现各种问题,我们带着问题来一个个解决:
debug 过程:
问题 1:
Linux 调用 hvc 下发了一个 0x80000000 的 function ID,但是这个 psci 的 function id 为0x80000000 并没有在 psci 的协议中找到对应的值?这块后面可以在深入研究一下。
u64 vpsci_trap_smc(vcpu_t *vcpu, u64 funid, u64 target_cpu, u64 entry_addr)
{if(vcpu == NULL) {abort("vpsci_trap_smc with NULL vcpu");}switch(funid) {case PSCI_VERSION:return vpsci_version();case PSCI_MIGRATE_INFO_TYPE:return (s64)vpsci_migrate_info_type();case PSCI_SYSTEM_OFF:LOG_WARN("Unsupported PSCI CPU OFF\n");break;case PSCI_SYSTEM_RESET:LOG_WARN("Unsupported PSCI CPU RESET\n");break;case PSCI_SYSTEM_CPUON:return (s64)vpsci_cpu_on(vcpu, funid, target_cpu, entry_addr);case PSCI_FEATURE: /* Linux will use this funid to get the PSCI FEATURE *//* fake it */return 0;case 0x80000000: /* TODO:要弄清楚这个function id的作用 */return 0;default:abort("Unknown function id : %p from hvc/smc call", funid);return -1;}return -1;
}
问题 2:
Linux 访问 GIC Distributor 的 0x800ffe8 地址:
翻阅 GICv3 的数据手册,我们知道 offset = 0xffe8 是 GICD_PIDR2
case GICD_PIDR2: /* Linux gic driver will read gicd_pidr2 */*val = GICD_READ32(GICD_PIDR2);goto finished;
问题 3:
Linux 启动过程中卡死,通过 LOG 发现是在配置完成 GICR 之后,但是通过跟踪 GICR 在 Hypervisor 中的读写发现没有什么问题,继续跟踪 Linux 发现是由于 Linux 读到的cntfrq_el0 寄存器值为 0,导致 Linux 时钟配置发生问题,原因是我们在 Hypervisor 中没有初始化cntfrq_el0,这样的话在restore_sysreg 中会把 0 值赋值给cntfrq_el0,所以我们需要对cntfrq_el0 进行初始化工作:
vcpu_t *create_vcpu(vm_t *vm, int vcpuid, u64 entry)
{u64 cnt;vcpu_t *vcpu = vcpu_alloc();if(vcpu == NULL) {abort("Unable to alloc a vcpu");}vcpu->core_name = "Cortex-A72";vcpu->vm = vm;vcpu->cpuid = vcpuid;vcpu->regs.spsr = SPSR_M(5) | SPSR_DAIF; /* used to set the spsr_el2 */vcpu->regs.elr = entry; /* used to set the elr_el2 */vcpu->sys_regs.mpidr_el1 = vcpuid; /* used to fake the mpidr_el1 */vcpu->sys_regs.midr_el1 = 0x410FD081; /* used to fake the core to cortex-a72 */read_sysreg(cnt, cntfrq_el0);vcpu->sys_regs.cntfrq_el0 = cnt; /* 初始化cntfrq_el0 */
问题 4:
EL1 发生同步异常,异常编号 0x18:
通过 ESR_EL2 的描述,当异常编号为 0x18 时,说明在 Linux 中执行了敏感指令的 MSR 或 MRS,并且在当前系统状态下会被 trap 到 EL2 处理,然后我们读取的 ISS 为 0x3a3276:
通过 ISS 我们可以得到系统寄存器编码:
原来这是一个 ICC_SGI1R_EL1 的写请求,这个寄存器干嘛用的呢?其实就是 Linux 用来发送核间中断 IPI 的,写入这个寄存器就可以给指定的 Core 发送中断请求,所以我们接下来要做两件事:
- 在 EL1 的同步异常处理中处理 0x18 的异常编号,并通过 ISS 得到访问的寄存器的寄存器编码;
- 我们先只处理ICC_SGI1R_EL1,得到其写入的值后通过虚拟中断注入到指定的 core;
case 0x18:/* trapped by read/write system register */if(vsysreg_handler(vcpu, esr_iss) < 0) {abort("Unknow system register trap");}vcpu->regs.elr += 4;break;
int vsysreg_handler(vcpu_t *vcpu, u64 iss)
{int write_not_read = !(iss & 1);/* The Rt value from the issued instruction, the general-purpose register used for the transfer. */int rt = (iss >> 5) & 0x1F;iss = iss & ~(0x1F << 5);switch(iss) {case VSYSREG_ICC_SGI1R_EL1:vgicv3_generate_sgi(vcpu, rt, write_not_read);return 0;}LOG_WARN("Unable to handler a system register\n");return -1;
}
int vgicv3_generate_sgi(struct vcpu *vcpu, int rt, int wr)
{u64 regs_sgi = vcpu->regs.x[rt];u16 target = regs_sgi & 0xFFFF;u8 intid = (regs_sgi >> 24) & 0xF;bool irm = (regs_sgi >> 40) & 0x1;write_sysreg(ICC_SGI1R_EL1, regs_sgi);return 1;
}
这里vgicv3_generate_sgi 先简化了直接把写的值写入ICC_SGI1R_EL1,但是这样其实是有问题,大家可以自己思考一下为什么?等后面支持多个 vm 后我们再修改。
上述问题都解决后,我们再次运行:
Linux 可以起来了☺☺:
最后在加载 rootfs 的时候失败,因为我们现在压根没有根文件系统,这块后面篇幅再继续了,今天到此为止。
项目构建:
- clone 源代码到本地:git clone GitCode - 全球开发者的开源社区,开源代码托管平台;
- 编译生成 u-boot 的 bin 文件:sh build_uboot.sh;
- 编译虚拟机 Guest OS 镜像:cd ./guest; sh build_vm.sh;
- 编译虚拟机管理器代码,生成虚拟机管理器镜像:sh run_build.sh;
- 运行 qemu 并加载镜像:sh run_qemu.sh (直接运行);