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

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字节。
到这里,虚拟机的初始化,就完成了。

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); 
	}
}

上述启动执行流程,如下图所示:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/474389.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java 面向对象编程进阶七(字符串 String )

目录 字符串 String String 基础 String 类和常量池 String 类常用的方法 String 类常用方法一 String 类常用方法二 字符串相等的判断 字符串 String String 是我们开发中最常用的类&#xff0c;我们不仅要掌握 String 类常见的方法&#xff0c;对于 String 的 底层实现…

Vue.js+SpringBoot开发智能教学资源库系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 数据中心模块2.2 课程档案模块2.3 课程资源模块2.4 课程作业模块2.5 课程评价模块 三、系统设计3.1 用例设计3.2 数据库设计3.2.1 课程档案表3.2.2 课程资源表3.2.3 课程作业表3.2.4 课程评价表 四、系统展示五、核心代…

多进程数据库不适合作为hive的元数据库

简介 “今天发现一个比较奇怪的现象&#xff0c;因为博主不熟悉mysql&#xff0c;所以在安装hive的使用了postgresql作为hive的元数据库&#xff0c;在测试几个连接工具对hive进行链接&#xff0c;后面再测试的时候发现链接不上了&#xff0c;并且报错日志如下&#xff1a;” …

【记录39】html element-ui 加载

环境 html使用element-ui组件、用vue框架搭建 方法一&#xff1a; 方法二&#xff08;推荐&#xff09; 将相关资源下载下来&#xff0c;在对应的html文件中相对路径引入。注意&#xff1a;css加载放在js之前

导航栏还是侧栏?flutter 跨平台适配指南

介绍 引言&#xff1a;Flutter 的跨平台特性 Flutter 是由 Google 开发的一款跨平台应用开发框架&#xff0c;它具有许多优点&#xff0c;包括性能优异、开发效率高以及良好的用户体验等。其中&#xff0c;最引人注目的特性之一就是其出色的跨平台能力。通过编写一套代码&…

数学建模(熵权法 python代码 例子)

目录 介绍&#xff1a; 模板&#xff1a; 例子&#xff1a;择偶 极小型指标转化为极大型&#xff08;正向化&#xff09;&#xff1a; 中间型指标转为极大型&#xff08;正向化&#xff09;&#xff1a; 区间型指标转为极大型&#xff08;正向化&#xff09;&#xff1a…

电脑设备管理器在哪?这里有详细指南!

电脑设备管理器是Windows操作系统中一个重要的工具&#xff0c;它允许用户查看和管理计算机中的硬件设备。无论是查找设备驱动程序、识别硬件问题还是管理设备属性&#xff0c;设备管理器都是一个不可或缺的工具。在本文中&#xff0c;我们将介绍三种常见的方法&#xff0c;以分…

Docker容器化技术(docker-compose安装部署案例)

docker-compose编排工具 安装docker-compose [rootservice ~]# systemctl stop firewalld [rootservice ~]# setenforce 0 [rootservice ~]# systemctl start docker[rootservice ~]# wget https://github.com/docker/compose/releases/download/v2.5.0/docker-compose-linux-…

Python——pgzero游戏打包exe执行时报错

Python——pgzero游戏打包exe执行时报错 记录一次使用 pgzero 开发游戏&#xff0c;使用 pyinstaller 打包时报错 目录结构&#xff1a; 1. 第一次报错 打包指令&#xff1a; pyinstaller -Fw .\code.py结果打开报错 报错如下&#xff1a; Traceback (most recent call…

【机器学习300问】39、高斯分布模型如何实现异常检测?

一、异常检测是什么&#xff1f; &#xff08;1&#xff09;举几个例子 ① 信用卡交易异常检测 在信用卡交易数据分析中&#xff0c;如果某个用户的消费习惯通常是小额且本地化消费&#xff0c;那么突然出现一笔大额且跨国的交易就可能被标记为异常。 ② 电机温度异常检测 在电…

软考 系统架构设计师系列知识点之云原生架构设计理论与实践(1)

所属章节&#xff1a; 第14章. 云原生架构设计理论与实践 第1节 云原生架构产生背景 云原生&#xff08;Cloud Native&#xff09;是近几年云计算领域炙手可热的话题&#xff0c;云原生技术已成为驱动业务增长的重要引擎。同时&#xff0c;作为新型基础设施的重要支撑技术&…

新一代 数据安全管理平台是什么,对于各行各业的公司又有什么价值?

新一代数据安全管理平台&#xff0c;在数据安全建设体系中可发挥“全局风险治理、统一指挥调度”的重要作用&#xff0c;为用户提供跨数据类型、存储孤岛和生态系统集成数据的产品和服务&#xff0c;从而实现更简单、一致的数据安全。 数据安全管理平台是以数据访问行为分析为基…

Redis如何设置键的生存时间或过期时间

键的生存时间或过期时间 概述。 通过EXPIRE命令或者PEXIPIRE命令&#xff0c;客户端可以以秒或者毫秒精度为数据库中的某个键设置生存时间(Time To Live,TTL)&#xff0c;在经过指定的秒数或者毫秒数之后&#xff0c;服务器就会自动删除生存时间为0的键: 127.0.0.1:6379>…

Oracle:ORA-01830错误-更改数据库时间格式

1,先把报错SQL语句拿出来执行&#xff0c;看看是不是报的这个错 ORA-01830: 日期格式图片在转换整个输入字符串之前结束 2&#xff0c;然后查看默认日期格式是不是“YYYY-MM-DD HH24:MI:SS”&#xff08;正确格式&#xff09;。&#xff1b; 执行&#xff1a; SELECT * FRO…

美团24秋招第一套笔试试题

小美的字符串匹配度 我的代码&#xff08;16/20&#xff09; import java.util.Scanner;// 注意类名必须为 Main, 不要有任何 package xxx 信息 public class Main {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextL…

Apache Superset

前言 最近在准备一个小的项目&#xff0c;需要对 Hive 的数据进行展示&#xff0c;所以想到了把 Hive 的数据导出到 MySQL 然后用 Superset 进行展示。 Superset 1.1 Superset概述 Apache Superset是一个现代的数据探索和可视化平台。它功能强大且十分易用&#xff0c;可对接…

表格固定行固定列问题

效果图 代码&#xff1a; 1.第一部分&#xff1a;表格固定行&#xff1a;用合计行来实现 <el-tableclass"fixedRowcol":data"tableData"bordershow-summarysum-text"合计"ref"table"></el-table> 2.第二部分&#xff1…

Docker 【通过Dockerfile构建镜像】【docker容器与镜像的关系】

文章目录 前言一、前期的准备工作二、上手构建一个简单的镜像三、DcokerFile1 指令总览2 指令详情 四、Dockerfile文件规范五、docker运行build时发生了什么?六、调试手段1. 修改镜像打包后&#xff0c;如何验证新内容已更新至镜像 七、Dockerfile优化方案 前言 docker构建镜…

【Spark编程基础】实验三RDD 编程初级实践(附源代码)

目录 一、实验目的二、实验平台三、实验内容1.spark-shell 交互式编程2.编写独立应用程序实现数据去重3.编写独立应用程序实现求平均值问题 一、实验目的 1、熟悉 Spark 的 RDD 基本操作及键值对操作&#xff1b; 2、熟悉使用 RDD 编程解决实际具体问题的方法 二、实验平台 …

burpsuit教程汉化+Repeater(非常详细),从零基础入门到精通,看完这一篇就够了

前言&#xff1a;释疑解惑 《BP使用教程一》发布后&#xff0c;后台收到了许多小伙伴的私信问BP是怎么汉化的&#xff0c;在这里统一为大家解答一下。 BP的汉化依赖于汉化jar包&#xff0c;在启动时引入汉化包即可&#xff0c;废话不多说&#xff0c;直接上命令&#xff1a; …