QEMU Device Tree —— 传递流程

dtb创建

  • 我们以下面的设备树信息为例,介绍设备树的创建过程:
{   /* 根节点信息 */
    interrupt-parent = <0x00008001>;
    #size-cells = <0x00000002>;
    #address-cells = <0x00000002>;
    compatible = "linux,dummy-virt";
    
    /* gpio-keys信息 */
    gpio-keys {
        #address-cells = <0x00000001>;
        #size-cells = <0x00000000>;
        compatible = "gpio-keys";
        poweroff {
            gpios = <0x00008003 0x00000003 0x00000000>;
            linux,code = <0x00000074>;
            label = "GPIO Key Poweroff";
        };
    };
    
    /* pl011串口芯片信息 */
    pl011@9000000 {
        clock-names = "uartclk", "apb_pclk";
        clocks = <0x00008000 0x00008000>;
        interrupts = <0x00000000 0x00000001 0x00000004>;
        reg = <0x00000000 0x09000000 0x00000000 0x00001000>;
        compatible = "arm,pl011", "arm,primecell";
    };
 };

api

libfdt api

  • dtb一词在qemu中被fdt(Flat Device Tree)一词取代,以下所有提到fdt的地方指代dtb,linux平台为设备树的创建提供了libfdt库,由libfdt-devel rpm包提供,这个库提供了fdt操作的各种接口,部分如下:
  1. fdt_create:初始化设备树
  2. fdt_add_subnode:设备树中添加节点
  3. fdt_setprop_cell:设置设备树属性键值对,值为数字
  4. fdt_setprop_string:设置设备属性键值对,值为字符串

wrapper api

qemu对libfdt的接口做了二次封装,部分接口举例如下:

  1. create_device_tree:初始化一颗设备树
  2. qemu_fdt_setprop_cell:设置设备树的节点的属性,值为数字
  3. qemu_fdt_setprop_string:设置设备树的节点的属性,值为字符串
  4. qemu_fdt_add_subnode:添加一个子节点

创建流程

根节点创建

  • qemu在初始化virt架构板级信息时调用machvirt_init函数,首先创建设备树的基本属性,如下:
machvirt_init
    create_fdt
static void create_fdt(VirtMachineState *vms)
{
    ....
    void *fdt = create_device_tree(&vms->fdt_size); 						/* 1 */
    vms->fdt = fdt;
    /* Header */
    qemu_fdt_setprop_string(fdt, "/", "compatible", "linux,dummy-virt"); 	/* 2 */
    qemu_fdt_setprop_cell(fdt, "/", "#address-cells", 0x2); 				/* 3 */
    qemu_fdt_setprop_cell(fdt, "/", "#size-cells", 0x2);
    ....
}
1. 初始化设备树,返回一个设备树内存地址
2. 往设备树根节点添加compatible = "linux,dummy-virt"键值对
3. 往设备树根节点添加#address-cells = <0x00000001>和#address-cells = <0x00000002>键值对
  • 经过以上流程后,设备树信息如下:
{
    #size-cells = <0x00000002>;
    #address-cells = <0x00000002>;
    compatible = "linux,dummy-virt";
}

设备节点创建

  • 设备树基本属性设置后,一些基本硬件对应的设备在创建后,也会随之设置其设备树属性,比如gpio-key,如下:
machvirt_init
    create_gpio
static void create_gpio(const VirtMachineState *vms)
{
    ....
    qemu_fdt_add_subnode(vms->fdt, "/gpio-keys");
    qemu_fdt_setprop_string(vms->fdt, "/gpio-keys", "compatible", "gpio-keys");
    qemu_fdt_setprop_cell(vms->fdt, "/gpio-keys", "#size-cells", 0);
    qemu_fdt_setprop_cell(vms->fdt, "/gpio-keys", "#address-cells", 1);

    qemu_fdt_add_subnode(vms->fdt, "/gpio-keys/poweroff");
    qemu_fdt_setprop_string(vms->fdt, "/gpio-keys/poweroff", "label", "GPIO Key Poweroff");
    qemu_fdt_setprop_cell(vms->fdt, "/gpio-keys/poweroff", "linux,code", KEY_POWER);
    qemu_fdt_setprop_cells(vms->fdt, "/gpio-keys/poweroff", "gpios", phandle, 3, 0);
    ....
}
  • 以上流程生成如下设备树节点信息:
gpio-keys {
    #address-cells = <0x00000001>;
    #size-cells = <0x00000000>;
    compatible = "gpio-keys";
    poweroff {
        gpios = <0x00008003 0x00000003 0x00000000>;
        linux,code = <0x00000074>;
        label = "GPIO Key Poweroff";
    };
};

dtb传递

数据结构

arm_boot_info

  • 设备树信息dtb生成之后,qemu会在最后阶段将其加载到内存。在arm架构下,qemu定义了一个arm_boot_info用于存放启动的信息,这些信息可能是qemu命令行配置的,也可能是qemu自己维护,数据结构如下:
struct arm_boot_info {
    uint64_t ram_size;									/* 1 */
    const char *kernel_filename;						/* 2 */
    const char *kernel_cmdline;							/* 3 */
    const char *initrd_filename;						/* 4 */
    const char *dtb_filename;							/* 5 */
    hwaddr loader_start;								/* 6 */
    hwaddr dtb_start;									/* 7 */
    hwaddr dtb_limit;									
    ....
    hwaddr initrd_start;								/* 8 */
    hwaddr initrd_size;									
    ....
};
1. 虚拟机内存大小
2. 内核镜像的文件路径,通常是linux内核编译出的镜像:arch/arm64/boot/Image,通过-kernel参数指定
3. 传递给内核的命令行参数,通过-append指定
4. 内存根文件系统ramdisk.img路径,通过-initrd指定
5. 设备树dtb路径,qemu传递给kernel的设备树dtb信息有两个来源,一个是qemu自动生成的dtb传递给kernel,另一个是qemu去用户指定路径下
加载dtb文件,然后传递给kernel。这里保存的是第二种方式下用户指定的路径,通过-dtb参数可以指定
6. cpu运行时的起始物理地址,这段地址用来存放固件代码,其作用是加载dtb地址和kernel地址到寄存器,后文中会提到
7. dtb被加载到内存后,它所在的虚机物理地址以及长度
8. ramdisk.img文件被加载到内存后,它所在的虚机物理地址及大小

VirtMachineState

  • 对于virt 架构的arm板,还有一个数据结构用来保存板级相关的信息,它继承自MachineState,qemu中定义的所有类型架构都继承自MachineState,如下:
typedef struct {
    MachineState parent;
    ......
    struct arm_boot_info bootinfo;     	/* 1 */
    void fdt;                         	/* 2 */
    int fdt_size;                   	/* 3 */
    ......
} VirtMachineState;
1. 指向虚拟机的启动信息
2. 保存虚拟机dtb信息的内存地址(HVA)
3. 保存虚拟机dtb信息的大小

ARMInsnFixup

  • ARMInsnFixup是一个临时的数据结构,它用于存放虚机引导过程中,跳转到kernel之前的一段固件代码,固件代码是ARMv8汇编指令对应的机器码,结构体的insn就用于存放这个机器码
typedef struct ARMInsnFixup {
    uint32_t insn;
    FixupType fixup;
} ARMInsnFixup;

dtb加载

  • qemu生成dtb的过程就是在一段内存区间内不断添加设备树的信息,包括节点和属性信息,最终这段内存区域存放着dtb的内容,在host侧看来这段内存区间就是qemu申请的虚拟内存区间
machvirt_init
	vms->bootinfo.get_dtb = machvirt_dtb;
		vms->machine_done.notify = virt_machine_done;
			virt_machine_done
				arm_load_dtb
int arm_load_dtb(hwaddr addr, const struct arm_boot_info *binfo,
                 hwaddr addr_limit, AddressSpace *as)
{
	......
    if (binfo->dtb_filename) {										/* 1 */
        char *filename;										
        filename = qemu_find_file(QEMU_FILE_TYPE_BIOS, binfo->dtb_filename);
        fdt = load_device_tree(filename, &size);					
    } else {
        fdt = binfo->get_dtb(binfo, &size);							/* 2 */
    }
	......
	qemu_fdt_dumpdtb(fdt, size);									/* 3 */
	rom_add_blob_fixed_as("dtb", fdt, size, addr, as);				/* 4 */
	......
1. 如果命令行指定了dtb文件路径,将器加载到内存中
2. 如果命令行没有指定,通过get_dtb获取,get_dtb的对应实现machvirt_dtb,它返回qemu保存fdt的内存地址,存放在VirtMachineState的fdt成员中。
3. 判断qemu命令行参数,如果指定了dumpdtb选项,将dtb信息打印出来,然后退出
4. 封装一个qemu Rom对象并设置其物理地址,将dtb的内容拷贝到Rom的数据区域,然后将Rom插入到全局的roms链表中。roms链表最终会被qemu整
个映射到虚机的物理地址空间,从而让虚机可以访问包括设备树在内的rom数据。
  • 打印fdt指向的内存内容前40字节如下,可以看到,前40字节就是设备树规范中定义的头部信息,header是大端字节对齐,高位在低内存地址。内存中保存的dtb信息和磁盘文件上一致:
    在这里插入图片描述

地址传递

传递规范

  • 在设备树规范中,初始化系统硬件的组件(比如固件,bootloader,hypervisors)被称为启动程序(boot programe),启动程序在硬件初始化完成后需要将控制权交给另外一个程序(比如kernel,bootloader)被称为客户程序(client program),设备树规范就是在boot programe和client programe之间定义一组接口用于传递不容易被自动探测的硬件信息。boot programe负责初始化并搜集硬件信息,生成设备树dtb然后传递给client program。client program解析dtb并根据其提供的信息加载对应的设备驱动,使用这些设备。在两者之间,如何传递设备树的内存地址信息,需要一个约定。内核的Documentation/arm64/booting.rst定义了启动规范,boot program需要在启动过程中完成如下几个任务:
  1. 设置并初始化系统内存
  2. 设置设备树信息
  3. 解压内核镜像
  4. 跳转到内核镜像,移交控制权
  • 其中描述了内核设备树地址如何传递给内核:通过寄存器x0。
    Primary CPU general-purpose register settings:
    x0 = physical address of device tree blob (dtb) in system RAM.
    x1 = 0 (reserved for future use)
    x2 = 0 (reserved for future use)
    x3 = 0 (reserved for future use)
  • 上面的规范是针对aarch64架构,对于32-bit arm,是通过r2寄存器传递的dtb地址。

传递流程

  • qemu在加载好所有镜像后(包括kernel Image,ramdisk.img,dtb),跳转到kernel之前,这中间还有一件事情需要做,就是加载一个临时生成的bootloader固件到内存,然后跳转到这个固件执行代码,最终通过这个固件跳转到内核。这个固件做了三个动作,一是将dtb的地址加载到x0寄存器,二是将内核地址加载到x4寄存器,三是跳转到kernel入口地址。至此才算移交控制权给kernel。
  • dtb和kernel加载的地址可能每次有变化,因此固件的内容是由qemu动态生成的,其信息存放在bootloader_aarch64变量中,内容是aarch64架构汇编指令对应的机器码,如下:
static const ARMInsnFixup bootloader_aarch64[] = {
    { 0x580000c0 }, /* ldr x0, arg ; Load the lower 32-bits of DTB /    			/* 1 */
    { 0xaa1f03e1 }, /* mov x1, xzr */                                    			/* 2 */
    { 0xaa1f03e2 }, /* mov x2, xzr */
    { 0xaa1f03e3 }, /* mov x3, xzr */
    { 0x58000084 }, /* ldr x4, entry ; Load the lower 32-bits of kernel entry */	/* 3 */
    { 0xd61f0080 }, /* br x4 ; Jump to the kernel entry point /     				/* 4 */
    { 0, FIXUP_ARGPTR_LO }, /* arg: .word @DTB Lower 32-bits */						/* 5 */
    { 0, FIXUP_ARGPTR_HI}, /* .word @DTB Higher 32-bits */						
    { 0, FIXUP_ENTRYPOINT_LO }, /* entry: .word @Kernel Entry Lower 32-bits */		/* 6 */
    { 0, FIXUP_ENTRYPOINT_HI }, /* .word @Kernel Entry Higher 32-bits */
    { 0, FIXUP_TERMINATOR }
};
1. 将dtb的物理地址加载到x0寄存器中,0x580000c0是ldr指令对应机器码(0b1011000000000000000000011000000)
2. 将x1,x2,x3写入0
3. 将内核的入口地址加载到x4寄存器中,0x58000084同上,是ldr指令对应机器码(0b1011000000000000000000010000100)
4. 跳转到内核
5. 存放dtb的物理内存地址(GPA)
6. 存放kernel的入口地址(GPA)
  • 参考ARMv8-A_Architecture_Refernce_Manual的C5章节,查找ldr指令对应机器码格式如下:
    在这里插入图片描述在这里插入图片描述
  • 分析第一条汇编指令机器码,提取关键字段如下:
  1. opc: 01,表示操作64-bit变量
  2. imm19: 110,立即数为6
  3. Rt: 0,要加载的寄存器是x0
  4. label: 表示当前pc指针的偏移offset,寄存器的值从PC + offset的地方取值加载。它的值是imm19 * 4 = 6 * 4 = 24。
  • 上面这条汇编指令的含义,就是从相对pc指针偏移的24字节的地方,取64字节内容,加载的x0寄存器中,再对比bootloader_aarch64数组的内容,可以确定第一条指令的意思,就是从数组的第6,7个元素中读取值然后加载到x0寄存器中。从注释中也可以确认,第6,7个元素分别放的就是dtb地址的低32bit和高32bit。
  • bootloader_aarch64存放的是汇编指令的机器码,它被作为固件加载到虚机的物理地址空间,其加载流程如下:
arm_setup_direct_kernel_boot													
	primary_loader = bootloader_aarch64;										/* 5 */
	write_bootloader("bootloader", info->loader_start, 							/* 6 */
						primary_loader, fixupcontext, as);
		rom_add_blob_fixed_as(name, code, len * sizeof(uint32_t), addr, as);
11. 将bootloader固件代码的指针放到primary_loader中
12. 封装成Rom,添加到roms全局链表中,动作与dtb的加载类似,从这里可以看到,loader_start保存的虚机物理地址,存放的是固件代码,主要用于
加载dtb地址和内核地址,并跳转
  • primary_loader,kernel Image,dtb,ramdisk都被加载到内存后,一切准备就绪,最后的动作就是让cpu跳转到到primary_loader处执行固件代码,那怎么跳转过去呢?通过初始化cpu的pc寄存器,将其设置为primary_loader的地址,然后运行cpu,那么虚拟机cpu自然就从primary_loader开始运行,流程如下:
arm_load_kernel
	qemu_register_reset(do_cpu_reset, ARM_CPU(cs))		/* 7 */
		do_cpu_reset,								
			if (cs == first_cpu) {
				cpu_set_pc(cs, info->loader_start)		/* 8 */
				......
			}
arm_cpu_set_pc											/* 9 */
	ARMCPU *cpu = ARM_CPU(cs);			
    CPUARMState *env = &cpu->env;
	env->pc = value;									
13. qemu在加载内存镜像时,注册了cpu复位的回调函数。当cpu复位时,会触发do_cpu_reset
14. do_cpu_reset中有一个动作就是设置pc指针,这里将loader_start保存的虚机物理地址设置成pc指针,这个地址在arm64架构下就是
primary_loader固件的起始地址,将其作为cpu复位时pc寄存器的初始值,那么当cpu运行时,第一条读取的指令就是这个固件存放的指令
15. 每个架构下pc指针对应的cpu寄存器不同,qemu对应的定义了不同的数据结构,对于arm就是ARMCPU这个数据结构。
  • 以上所有这些设置好之后,qemu中虚机pc的值存放的时primary_loader固件的物理地址,当cpu进入guest态时,将这个pc值加载的cpu对应的物理pc寄存器中,然后开始cpu开始运行,执行固件代码,固件的动作是加载dtb和kernel的物理内存地址,然后跳转到kernel入口,启动虚机。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

享乐主

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值