TinyEMU源码分析之虚拟机初始化
- 1 初始化结构参数
- 2 配置RAM地址空间
- 3 初始化设备
- 4 拷贝BIOS和Kernel
- 5 手动写入5条指令
- 6 体验第一条指令的执行
本文属于《 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的地址空间范围(申请内存),可以结合上面代码,好好看看,比较简单。
因为,在执行指令时,必须要知道具体的内存空间,是如何分布的,以便正确访问内存。
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。
拷贝BIOS和Kernel的地址,与上图中内存地址空间,一致。
5 手动写入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个参数。
6 体验第一条指令的执行
通过目前源码的分析,可以得知,以下大致启动流程:
- 第一条指令,从0x1000处,开始取指执行。
- 然后,以上这5条指令运行完毕,最后一条指令,设置PC=0x80000000,该地址,正是我们bbl64.bin,在内存中的基地址。
- 也就是说,下一条指令,将跳转到bbl64.bin,执行指令。
- 等待bbl64.bin执行完毕,再开始执行kernel-riscv64.bin。
我们可以在glue函数的,s->pc = GET_PC()位置处,打上断点,检查第一条指令的PC,的确是0x1000;并且依次取出的指令,的确为这5条指令。
static void no_inline glue(riscv_cpu_interp_x, XLEN)(RISCVCPUState *s, int n_cycles1)
{
for(;;) {
// 获取PC
s->pc = GET_PC();
addr = s->pc;
ptr = (uint8_t *)(s->tlb_code[tlb_idx].mem_addend +
(uintptr_t)addr);
code_ptr = ptr;
//根据PC获取一条指令机器码
insn = get_insn32(code_ptr);
}
}
上述启动执行流程,如下图所示: