TinyEMU源码分析之虚拟机初始化


本文属于《 TinyEMU模拟器基础系列教程》之一,欢迎查看其它文章。
本文中使用的代码,均为伪代码,删除了部分源码。

1 初始化结构参数

虚拟机的初始化,主要在virt_machine_init函数中完成。
virt_machine_init函数,如下:

static VirtMachine *riscv_machine_init(const VirtMachineParams *p)
{
    RISCVMachine *s;
    VIRTIODevice *blk_dev;
    VIRTIOBusDef vbus_s, *vbus = &vbus_s;
    
	// 初始化结构参数
    s->common.vmc = p->vmc;
    s->ram_size = p->ram_size;
    s->max_xlen = max_xlen;
    s->mem_map = phys_mem_map_init();
    s->mem_map->opaque = s;
    s->mem_map->flush_tlb_write_range = riscv_flush_tlb_write_range;
    s->cpu_state = riscv_cpu_init(s->mem_map, max_xlen);
	
	// 配置RAM地址空间
    /* RAM */
    ram_flags = 0;
    cpu_register_ram(s->mem_map, RAM_BASE_ADDR, p->ram_size, ram_flags);
    cpu_register_ram(s->mem_map, 0x00000000, LOW_RAM_SIZE, 0);
    cpu_register_device(s->mem_map, CLINT_BASE_ADDR, CLINT_SIZE, s,
                        clint_read, clint_write, DEVIO_SIZE32);
    cpu_register_device(s->mem_map, PLIC_BASE_ADDR, PLIC_SIZE, s,
                        plic_read, plic_write, DEVIO_SIZE32);
    cpu_register_device(s->mem_map, HTIF_BASE_ADDR, 16,
                        s, htif_read, htif_write, DEVIO_SIZE32);
    vbus->addr = VIRTIO_BASE_ADDR;
	
	// 初始化设备
    /* virtio console */
    if (p->console) {
        s->common.console_dev = virtio_console_init(vbus, p->console);
        vbus->addr += VIRTIO_SIZE;
    }
    
	...

    if (p->input_device) {
    	// 键盘
		s->keyboard_dev = virtio_input_init(vbus,
											VIRTIO_INPUT_TYPE_KEYBOARD);
		vbus->addr += VIRTIO_SIZE;

		// 鼠标
		s->mouse_dev = virtio_input_init(vbus,
										 VIRTIO_INPUT_TYPE_TABLET);
		vbus->addr += VIRTIO_SIZE;
    }
	
	// 拷贝BIOS和Kernel;手动写入5条指令
    copy_bios(s, p->files[VM_FILE_BIOS].buf, p->files[VM_FILE_BIOS].len,
              p->files[VM_FILE_KERNEL].buf, p->files[VM_FILE_KERNEL].len,
              p->files[VM_FILE_INITRD].buf, p->files[VM_FILE_INITRD].len,
              p->cmdline);
    
    return (VirtMachine *)s;
}

首先,初始化VirtMachineClass、ram大小、max_xlen,以及内存映射初始化等。
然后,在riscv_cpu_init函数中,会完成pc赋初值和TLB初始化(赋值为-1)。

s->pc = 0x1000; 
s->cpu_state = riscv_cpu_init(s->mem_map, max_xlen);

cpu_state的类型为RISCVCPUState结构,该结构中,包含mstatus、mtvec、mscratch等CSR寄存器定义。

我们猜测,第一条指令地址,就是0x1000

初始化结构参数,其实就是把一些参数,保存到RISCVMachine对象中。

2 配置RAM地址空间

我们对本部分代码,进行分析,并结合以下常量定义。

#define LOW_RAM_SIZE   0x00010000 /* 64KB */
#define RAM_BASE_ADDR  0x80000000
#define CLINT_BASE_ADDR 0x02000000
#define CLINT_SIZE      0x000c0000
#define HTIF_BASE_ADDR 0x40008000
#define IDE_BASE_ADDR  0x40009000
#define VIRTIO_BASE_ADDR 0x40010000
#define VIRTIO_SIZE      0x1000
#define VIRTIO_IRQ       1
#define PLIC_BASE_ADDR 0x40100000
#define PLIC_SIZE      0x00400000
#define FRAMEBUFFER_BASE_ADDR 0x41000000

发现代码,构成了,如下的内存地址空间:
在这里插入图片描述
这里,主要是,确定Low Dram、CLINT、HTIF、VBUS、PLIC、High Dram的地址空间范围(申请内存),可以结合上面代码,好好看看,比较简单。

因为,在执行指令时,必须要知道具体的内存空间,是如何分布的,以便正确访问内存。

注意:

RISC-V Linux的入口地址,必须2M对齐,否则kernel无法启动(0x200000)。
因为,linux的setup_vm(),会检查kernel入口地址是否2M对齐,如果不对齐kernel无法启动,但其实我们可以解除这个2M对齐限制,将这部分空间利用起来,关于如何优化这部分内存,可参考《一文实战 | RISC-V Linux入口地址2M预留内存优化》

3 初始化设备

初始化console、net device、block device、filesystem、display device、input device。
不详述,自己看源码。

4 拷贝BIOS和Kernel

在copy_bios函数中,完成拷贝BIOS和Kernel,其代码如下:

static void copy_bios(RISCVMachine *s, const uint8_t *buf, int buf_len,
                      const uint8_t *kernel_buf, int kernel_buf_len,
                      const uint8_t *initrd_buf, int initrd_buf_len,
                      const char *cmd_line)
{
	// 拷贝BIOS到0x80000000
    ram_ptr = get_ram_ptr(s, RAM_BASE_ADDR, TRUE);
    memcpy(ram_ptr, buf, buf_len);

	// 拷贝Kernel到0x80200000
    kernel_base = 0;
    if (kernel_buf_len > 0) {
        /* copy the kernel if present */
        if (s->max_xlen == 32)
            align = 4 << 20; /* 4 MB page align */
        else
            align = 2 << 20; /* 2 MB page align */
        kernel_base = (buf_len + align - 1) & ~(align - 1);
        memcpy(ram_ptr + kernel_base, kernel_buf, kernel_buf_len);
    }
    
	// 创建设备树,并写入内存地址(0x1000+8*8)处
    ram_ptr = get_ram_ptr(s, 0, TRUE);
    fdt_addr = 0x1000 + 8 * 8;
    riscv_build_fdt(s, ram_ptr + fdt_addr,
                    RAM_BASE_ADDR + kernel_base, kernel_buf_len,
                    RAM_BASE_ADDR + initrd_base, initrd_buf_len,
                    cmd_line);

	// 手动写入5条指令
    /* jump_addr = 0x80000000 */
    q = (uint32_t *)(ram_ptr + 0x1000);
    q[0] = 0x297 + 0x80000000 - 0x1000; /* auipc t0, jump_addr */
    q[1] = 0x597; /* auipc a1, dtb */
    q[2] = 0x58593 + ((fdt_addr - 4) << 20); /* addi a1, a1, dtb */
    q[3] = 0xf1402573; /* csrr a0, mhartid */
    q[4] = 0x00028067; /* jalr zero, t0, jump_addr */
}
  • 将bios(bbl64.bin)拷贝到0x80000000地址处(物理地址),本例中bbl64.bin长度为0xd21a。
  • 将kernel(kernel-riscv64.bin)拷贝到0x80200000地址处(物理地址),本例中kernel-riscv64.bin长度为0x3d5324。
  • 创建设备树,并写入内存地址(0x1000+8*8)处(物理地址)。

拷贝BIOS和Kernel的地址,与上图中内存地址空间,一致。

5 设备树

Linux设备树(Device Tree),是一种数据表示方式,用于描述硬件设备的树状结构信息,比如CPU 数量、内存基地址、IIC 接口上接了哪些设备、SPI 接口上接了哪些设备,主要用于嵌入式Linux系统中。

在系统引导启动阶段,将设备树中描述的硬件信息,传递给操作系统,以便进行设备初始化。

  • dts(device tree source):设备树源文件,描述设备信息的;
  • dtc(device tree compiler):设备树编译/反编译/调试工具;
  • dtb(device tree binary):二进制设备树镜像;

在 DTS 文件中,按照指定格式结构配置设备信息,通过 DTC 编译后生成 DTB 文件。U-Boot 和 Linux kernel 运行时,加载 DTB 信息到内存中,解析设备配置。

在TinyEMU源码中,设备树构造,如下所示:

static int riscv_build_fdt(RISCVMachine *m, uint8_t *dst,
                           uint64_t kernel_start, uint64_t kernel_size,
                           uint64_t initrd_start, uint64_t initrd_size,
                           const char *cmd_line)
{
	// 手动写入设备树内容
    s = fdt_init();
    fdt_begin_node(s, "");
    fdt_prop_u32(s, "#address-cells", 2);
    fdt_prop_u32(s, "#size-cells", 2);
    fdt_prop_str(s, "compatible", "ucbbar,riscvemu-bar_dev");
    fdt_prop_str(s, "model", "ucbbar,riscvemu-bare");

	...

    fdt_begin_node(s, "chosen");
    fdt_prop_str(s, "bootargs", cmd_line ? cmd_line : "");
    if (kernel_size > 0) {
        fdt_prop_tab_u64(s, "riscv,kernel-start", kernel_start);
        fdt_prop_tab_u64(s, "riscv,kernel-end", kernel_start + kernel_size);
    }
    size = fdt_output(s, dst);
	
	// 可将设备树,写入文件
#if 0
    {
        FILE *f;
        f = fopen("/tmp/riscvemu.dtb", "wb");
        fwrite(dst, 1, size, f);
        fclose(f);
    }
#endif
    fdt_end(s);
    return size;
}

我们可以,将该数据结构,写入文件/tmp/riscvemu.dtb
并通过执行以下命令,将dtb反编译为dts:

dtc -I dtb -O dts -o riscvemu.dts riscvemu.dtb

需要先安装dtc工具: sudo snap install device-tree-compiler

得到的riscvemu.dts,内容如下:

/dts-v1/;

/ {
	#address-cells = <0x02>;
	#size-cells = <0x02>;
	compatible = "ucbbar,riscvemu-bar_dev";
	model = "ucbbar,riscvemu-bare";

	cpus {
		#address-cells = <0x01>;
		#size-cells = <0x00>;
		timebase-frequency = <0x989680>;

		cpu@0 {
			device_type = "cpu";
			reg = <0x00>;
			status = "okay";
			compatible = "riscv";
			riscv,isa = "rv64acdfimsu";
			mmu-type = "riscv,sv48";
			clock-frequency = <0x77359400>;

			interrupt-controller {
				#interrupt-cells = <0x01>;
				interrupt-controller;
				compatible = "riscv,cpu-intc";
				phandle = <0x01>;
			};
		};
	};

	memory@80000000 {
		device_type = "memory";
		reg = <0x00 0x80000000 0x00 0x8000000>;
	};

	htif {
		compatible = "ucb,htif0";
	};

	soc {
		#address-cells = <0x02>;
		#size-cells = <0x02>;
		compatible = "ucbbar,riscvemu-bar-soc\0simple-bus";
		ranges;

		clint@2000000 {
			compatible = "riscv,clint0";
			interrupts-extended = <0x01 0x03 0x01 0x07>;
			reg = <0x00 0x2000000 0x00 0xc0000>;
		};

		plic@40100000 {
			#interrupt-cells = <0x01>;
			interrupt-controller;
			compatible = "riscv,plic0";
			riscv,ndev = <0x1f>;
			reg = <0x00 0x40100000 0x00 0x400000>;
			interrupts-extended = <0x01 0x09 0x01 0x0b>;
			phandle = <0x02>;
		};

		virtio@40010000 {
			compatible = "virtio,mmio";
			reg = <0x00 0x40010000 0x00 0x1000>;
			interrupts-extended = <0x02 0x01>;
		};

		virtio@40011000 {
			compatible = "virtio,mmio";
			reg = <0x00 0x40011000 0x00 0x1000>;
			interrupts-extended = <0x02 0x02>;
		};

		virtio@40012000 {
			compatible = "virtio,mmio";
			reg = <0x00 0x40012000 0x00 0x1000>;
			interrupts-extended = <0x02 0x03>;
		};
	};

	chosen {
		bootargs = "console=hvc0 root=/dev/vda rw";
		riscv,kernel-start = <0x00 0x80200000>;
		riscv,kernel-end = <0x00 0x805c38f4>;
	};
};

其中启动OS,最重要的2个参数:kernel-start和kernel-end,他们指定了kernel在内存中地址范围。

设备树基址,被保存到a1寄存器(看后文),然后传入BIOS/Bootloader(这里是bbl64.bin),后续bbl64.bin启动kernel时,就知道应该跳转到0x80200000,去取kernel的第一条指令了(kernel-start = 0x80200000)。

6 手动写入5条指令

手动写入的5条指令,翻译过来,就是如下:

    /* jump_addr = 0x80000000 */
    // 从物理地址0x1000位置处开始,手动写入5条指令的机器码
    q = (uint32_t *)(ram_ptr + 0x1000);

    // t0=0x80000000
    q[0] = 0x297 + 0x80000000 - 0x1000; /* auipc t0, jump_addr */

    // a1=PC
    q[1] = 0x597; /* auipc a1, dtb */

    // a1=a1+0x3c
    q[2] = 0x58593 + ((fdt_addr - 4) << 20); /* addi a1, a1, dtb */

    // a0=mhartid
    q[3] = 0xf1402573; /* csrr a0, mhartid */

    // PC=t0
    q[4] = 0x00028067; /* jalr zero, t0, jump_addr */

从物理地址0x1000位置处开始,手动写入5条指令的机器码,一共20字节。

关于a0与a1寄存器的含义:

  • a0 = mhartid:表示硬件线程ID。
  • a1 = a1 + 0x3c:设备树内存基址为0x1040(0x1000+8*8),而a1 = q[1]指令的PC + 0x3c = 0x1004 + 0x3c = 0x1040,正好为设备树基址,因此a1表示设备树基址。
  • 因此,a0与a1,表示后续调用riscv-pk\machine\minit.c中init_first_hart函数的2个参数。

到这里,虚拟机的初始化,就完成了。

  • 18
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
疫情居家办公系统管理系统按照操作主体分为管理员和用户。管理员的功能包括办公设备管理、部门信息管理、字典管理、公告信息管理、请假信息管理、签到信息管理、留言管理、外出报备管理、薪资管理、用户管理、公司资料管理、管理员管理。用户的功能等。该系统采用了MySQL数据库,Java语言,Spring Boot框架等技术进行编程实现。 疫情居家办公系统管理系统可以提高疫情居家办公系统信息管理问题的解决效率,优化疫情居家办公系统信息处理流程,保证疫情居家办公系统信息数据的安全,它是一个非常可靠,非常安全的应用程序。 管理员权限操作的功能包括管理公告,管理疫情居家办公系统信息,包括外出报备管理,培训管理,签到管理,薪资管理等,可以管理公告。 外出报备管理界面,管理员在外出报备管理界面中可以对界面中显示,可以对外出报备信息的外出报备状态进行查看,可以添加新的外出报备信息等。签到管理界面,管理员在签到管理界面中查看签到种类信息,签到描述信息,新增签到信息等。公告管理界面,管理员在公告管理界面中新增公告,可以删除公告。公告类型管理界面,管理员在公告类型管理界面查看公告的工作状态,可以对公告的数据进行导出,可以添加新公告的信息,可以编辑公告信息,删除公告信息
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

百里杨

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

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

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

打赏作者

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

抵扣说明:

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

余额充值