TinyEMU源码分析之访存处理

TinyEMU源码分析之访存处理

  • 1 访存指令介绍
  • 2 指令译码
  • 3 地址转换
    • 3.1 VA与PA
    • 3.2 VA转PA
  • 4 判断地址空间范围
  • 5 执行访存操作
    • 5.1 访问RAM内存
    • 5.2 访问非RAM(设备)内存
  • 6 访存处理流程图

本文属于《 TinyEMU模拟器基础系列教程》之一,欢迎查看其它文章。

1 访存指令介绍

访存指令,主要有,如下这些:
在这里插入图片描述在这里插入图片描述
在RISC-V架构中,CPU在处理与内存访问相关的这些指令时,会发出对某地址的访问。这些指令通常涉及加载(Load)和存储(Store)操作,用于从内存中读取数据或将数据写入内存。

本文旨在,通过分析访存指令的执行,以理解CPU在执行指令时,是如何发出以及处理这些地址请求的。

2 指令译码

我们以ld指令(读取)为例,进行说明。

指令形式:ld rd, imm(rs1)
功能说明:rd = M[rs1+imm][0:63],表示从内存地址(rs1+imm)中,加载一个64位值到寄存器rd

取指译码,是在riscv_cpu_template.h的glue函数中完成,代码如下:

static void no_inline glue(riscv_cpu_interp_x, XLEN)(RISCVCPUState *s,
                                                   int n_cycles1)
{
    for(;;) {
		...

		// 取指
		insn = get_insn32(code_ptr); 
		
		// 译码执行
        funct3 = (insn >> 12) & 7;
        imm = (int32_t)insn >> 20;
        addr = s->reg[rs1] + imm;
        switch(funct3) {
	    ...
        case 3: /* ld */
            {
                uint64_t rval;
                if (target_read_u64(s, &rval, addr))
                    goto mmu_exception;
                val = (int64_t)rval;
            }
            break;
        }
        ...
        s->reg[rd] = val;
	}
}

我们在riscv_cpu.c的target_read_slow函数中,打断点:

b riscv_cpu.c:308 if addr>=0x80000000

再通过调用堆栈,回溯到glue函数中。
当TinyEMU对ld指令,进行译码执行时:

  • 从机器码insn中,提取imm;
  • 从机器码insn中,提取rs1,并计算出访存地址addr = reg[rs1] + imm。

然后,通过调用target_read_u64函数,读取addr地址内容,放入rd寄存器中。

这里,例子中,各值如下:

  • insn == 0xa767b783
  • rs1 == 0xf
  • imm == 0xfffffa76
  • addr == 0x80006d88

这里得到的addr,可能是虚拟地址,也可能是物理地址,我们统一当成虚拟地址看待即可,后续会进行转换。

3 地址转换

3.1 VA与PA

虚拟地址(Virtual Address):

  • 处理器生成的地址,用于在软件层面访问内存。虚拟地址空间是程序看到的内存视图,它可能远大于实际的物理内存大小。虚拟地址的主要目的是提供内存保护(通过隔离不同进程的地址空间)和简化内存管理(通过允许操作系统透明地管理物理内存)。

  • 在 RISC-V 系统中,虚拟地址通常通过内存管理单元(MMU)进行转换,以映射到物理地址。MMU 负责执行虚拟到物理地址的转换,同时检查访问权限和页面有效性。

物理地址(Physical Address):

  • 内存芯片实际使用的地址,用于定位特定的内存位置。物理地址空间是实际可用的 RAM 的大小,它受到硬件和操作系统的限制。

  • 在 RISC-V 中,当处理器需要访问内存时,它会生成一个虚拟地址。然后,MMU 会将这个虚拟地址转换为物理地址,处理器使用这个物理地址来访问实际的 RAM。

只有进入OS阶段,开启MMU后,才支持VA;在此之前,所有的访问全都为PA。

3.2 VA转PA

target_read_u64函数,是通过宏来定义的,在riscv_cpu_priv.h中:

#define TARGET_READ_WRITE(size, uint_type, size_log2)                   \
static inline __exception int target_read_u ## size(RISCVCPUState *s, uint_type *pval, target_ulong addr)                              \
{\
    uint32_t tlb_idx;\
    tlb_idx = (addr >> PG_SHIFT) & (TLB_SIZE - 1);\
    if (likely(s->tlb_read[tlb_idx].vaddr == (addr & ~(PG_MASK & ~((size / 8) - 1))))) { \
        *pval = *(uint_type *)(s->tlb_read[tlb_idx].mem_addend + (uintptr_t)addr);\
    } else {\
        mem_uint_t val;\
        int ret;\
        ret = target_read_slow(s, &val, addr, size_log2);\
        if (ret)\
            return ret;\
        *pval = val;\
    }\
    return 0;\
}\
...

TARGET_READ_WRITE(64, uint64_t, 3)

首先,会查询TLB中,是否有addr(VA)缓存:

  • 有的话,直接取出addr对应的PA。
  • 没有的话,则调用target_read_slow函数,继续查询。

在target_read_slow函数中,再调用get_phys_addr函数查询。

int target_read_slow(RISCVCPUState *s, mem_uint_t *pval,
                     target_ulong addr, int size_log2)
{
	...
	// part1
	if (get_phys_addr(s, &paddr, addr, ACCESS_READ)) {
		s->pending_tval = addr;
		s->pending_exception = CAUSE_LOAD_PAGE_FAULT;
		return -1;
	}
	...
}

如果查询失败,会触发page fault异常,处理器接受到该异常后,会自动创建VA对应的页表。

static int get_phys_addr(RISCVCPUState *s,
                         target_ulong *ppaddr, target_ulong vaddr,
                         int access)
{
	// 当前是否运行在M模式
    if (priv == PRV_M) {
        *ppaddr = vaddr;
        return 0;
    }

	// 读取satp寄存器
    mode = (s->satp >> 60) & 0xf;
    if (mode == 0) {
        /* bare: no translation */
        *ppaddr = vaddr;
        return 0;
    } else {
        /* sv39/sv48 */
        levels = mode - 8 + 3;
        pte_size_log2 = 3;
        vaddr_shift = MAX_XLEN - (PG_SHIFT + levels * 9);
        if ((((target_long)vaddr << vaddr_shift) >> vaddr_shift) != vaddr)
            return -1;
        pte_addr_bits = 44;
    }

	// 页表查询
    pte_addr = (s->satp & (((target_ulong)1 << pte_addr_bits) - 1)) << PG_SHIFT;
    pte_bits = 12 - pte_size_log2;
    pte_mask = (1 << pte_bits) - 1;
    for(i = 0; i < levels; i++) {
        vaddr_shift = PG_SHIFT + pte_bits * (levels - 1 - i);
        pte_idx = (vaddr >> vaddr_shift) & pte_mask;
        pte_addr += pte_idx << pte_size_log2;
        if (pte_size_log2 == 2)
            pte = phys_read_u32(s, pte_addr);
        else
            pte = phys_read_u64(s, pte_addr);
        //printf("pte=0x%08" PRIx64 "\n", pte);
        if (!(pte & PTE_V_MASK))
            return -1; /* invalid PTE */
        paddr = (pte >> 10) << PG_SHIFT;
        xwr = (pte >> 1) & 7;
        if (xwr != 0) {
            if (xwr == 2 || xwr == 6)
                return -1;
            /* priviledge check */
            if (priv == PRV_S) {
                if ((pte & PTE_U_MASK) && !(s->mstatus & MSTATUS_SUM))
                    return -1;
            } else {
                if (!(pte & PTE_U_MASK))
                    return -1;
            }
            /* protection check */
            /* MXR allows read access to execute-only pages */
            if (s->mstatus & MSTATUS_MXR)
                xwr |= (xwr >> 2);

            if (((xwr >> access) & 1) == 0)
                return -1;
            need_write = !(pte & PTE_A_MASK) ||
                (!(pte & PTE_D_MASK) && access == ACCESS_WRITE);
            pte |= PTE_A_MASK;
            if (access == ACCESS_WRITE)
                pte |= PTE_D_MASK;
            if (need_write) {
                if (pte_size_log2 == 2)
                    phys_write_u32(s, pte_addr, pte);
                else
                    phys_write_u64(s, pte_addr, pte);
            }
            vaddr_mask = ((target_ulong)1 << vaddr_shift) - 1;
            *ppaddr = (vaddr & vaddr_mask) | (paddr  & ~vaddr_mask);
            return 0;
        } else {
            pte_addr = paddr;
        }
    }
    return -1;
}

这里有2种情况:

  • 如果运行在M模式下(运行固件/bios/bootloader),尚未启用MMU时,VA==PA,无需转换。
  • 如果运行在S模式下(运行OS),MMU已启用,则读取satp寄存器,并根据该寄存器中,保存的第一级页表基址,进行页表查询,最后得到PA。

具体页表查询原理,我们暂时不关心,只需要理解:

  • OS启动过程中,会创建第一级页表,并将其基址保存到satp寄存器,开启MMU。
  • 页表,是由OS建立的,保存在物理内存中,我们查询页表,其实就是在遍历内存,以便得到VA对应的PA。

4 判断地址空间范围

int target_read_slow(RISCVCPUState *s, mem_uint_t *pval,
                     target_ulong addr, int size_log2)
{
	...
	// part2
	pr = get_phys_mem_range(s->mem_map, paddr);
	...
}
PhysMemoryRange *get_phys_mem_range(PhysMemoryMap *s, uint64_t paddr)
{
    PhysMemoryRange *pr;
    int i;
    for(i = 0; i < s->n_phys_mem_range; i++) {
        pr = &s->phys_mem_range[i];
        if (paddr >= pr->addr && paddr < pr->addr + pr->size)
            return pr;
    }
    return NULL;
}

上面转换得到的PA,然后,再调用get_phys_mem_range函数,判断PA到底属于以下地址空间中哪个范围。

在这里插入图片描述
也就是说,上面得到的PA地址,可能为0x0~0x88000000范围内的任意地址。

5 执行访存操作

int target_read_slow(RISCVCPUState *s, mem_uint_t *pval,
                     target_ulong addr, int size_log2)
{
	...
	// part3
	else if (pr->is_ram) {
		tlb_idx = (addr >> PG_SHIFT) & (TLB_SIZE - 1);
		ptr = pr->phys_mem + (uintptr_t)(paddr - pr->addr);
		s->tlb_read[tlb_idx].vaddr = addr & ~PG_MASK;
		s->tlb_read[tlb_idx].mem_addend = (uintptr_t)ptr - addr;
		switch(size_log2) {
		...
		case 3:
			ret = *(uint64_t *)ptr;
			break;
		}
	} else {
		offset = paddr - pr->addr;
		if (((pr->devio_flags >> size_log2) & 1) != 0) {
			ret = pr->read_func(pr->opaque, offset, size_log2);
		}
#if MLEN >= 64
		else if ((pr->devio_flags & DEVIO_SIZE32) && size_log2 == 3) {
			/* emulate 64 bit access */
			ret = pr->read_func(pr->opaque, offset, 2);
			ret |= (uint64_t)pr->read_func(pr->opaque, offset + 4, 2) << 32;
			
		}
#endif
	}
	*pval = ret;
}

这里分为2个分支,看PA是否为RAM地址(Low Dram和High Dram)。

  • 访问RAM内存
  • 访问非RAM(设备)内存

5.1 访问RAM内存

若PA为RAM地址时,进行以下计算:

ptr = pr->phys_mem + (uintptr_t)(paddr - pr->addr);

(1)计算出欲访问物理地址paddr,相对于RAM物理基址pr->addr的偏移,也就是paddr - pr->addr
(2)然后,再加上模拟器分配给RAM的内存基址(其实是Host机器的虚拟地址),也就是pr->phys_mem + (uintptr_t)(paddr - pr->addr)
(3)得到的结果ptr,就是指令欲访问VA,对应的ram内存地址。此时,还会将VA与PA对应关系,更新到TLB中,下次就可以直接查询TLB,而不用查页表了。

最后,通过指针,就可以取出相应长度内容了,通过pval将值返回。

5.2 访问非RAM(设备)内存

若PA为设备地址时,执行以下操作:

  • 计算出欲访问物理地址paddr,相对于该设备物理基址pr->addr的偏移,也就是offset = paddr - pr->addr
  • 然后,调用该设备对应的read_func函数,ret = pr->read_func(pr->opaque, offset, ...),以便读取该offset处的内容。
  • 最后,读取到的值,通过pval返回。

比如CLINT,在riscv_machine_init函数中,划分地址空间时,就将clint_read、clint_write函数指针进行了保存(读写函数、设备地址范围等,进行了捆绑),以便在后续,需要处理该设备地址范围内访问时,可以直接调用该函数。

static VirtMachine *riscv_machine_init(const VirtMachineParams *p)
{
	...
	// 划分地址空间
    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);
	...
}

比如,PA为0x2004000,写操作的话,那么就是,表示向CLINT中mtimecmp(定时器比较寄存器)写入一个值。mtimecmp是一个内存映射寄存器,定义在CLINT中断控制器中。

clint_write函数定义,如下:

static void clint_write(void *opaque, uint32_t offset, uint32_t val,
                      int size_log2)
{
	...
    switch(offset) {
    case 0x4000:
        m->timecmp = (m->timecmp & ~0xffffffff) | val;
        riscv_cpu_reset_mip(m->cpu_state, MIP_MTIP);
        break;
	...
    }
}

此时,计算出的offset为0x4000,然后调用riscv_cpu_reset_mip函数,将val值写入了mip寄存器。

其他的设备,也是类似的原理。

6 访存处理流程图

因此,归根到底,CPU在执行指令时,发出的访问地址:

  • 如果在RAM范围内,表示访问物理内存;
  • 如果在设备范围内,表示访问该设备内部的寄存器或内存资源。

关于访存的处理过程,整理为流程图,如下:
在这里插入图片描述

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

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

相关文章

数据结构排序算法

排序也称排序算法(SortAlgorithm)&#xff0c;排序是将一组数据&#xff0c;依指定的顺序进行排列的过程。 分类 内部排序【使用内存】 指将需要处理的所有数据都加载到内部存储器中进行排序插入排序 直接插入排序希尔排序 选择排序 简单选择排序堆排序 交换排序 冒泡排序快速…

两阶段提交进阶

两阶段提交之进阶 上一节我们讲了&#xff0c;两阶段提交逻辑上的表现&#xff0c;其实较为肤浅&#xff0c;并且偏向理论&#xff0c;可能大家都能看懂&#xff0c;但是如果放入实际的mysql应用中并联系事务和日志进行分析&#xff0c;又会怎么样呢&#xff1f; 这次就专门分…

Unity类银河恶魔城学习记录13-1 p142 Save system源代码

Alex教程每一P的教程原代码加上我自己的理解初步理解写的注释&#xff0c;可供学习Alex教程的人参考 此代码仅为较上一P有所改变的代码 【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili FileDataHandler.cs using System; using System.IO; using UnityEngine; p…

软考133-上午题-【软件工程】-软件项目估算

一、COCOMO 估算模型 COCOMO 模型是一种精确的、易于使用的成本估算模型。 COCOMO 模型按其详细程度分为&#xff1a;基本 COCOMO 模型、中级 COCOMO 模型和详细 COCOMO 模型。 1&#xff09;基本 COCOMO 模型 基本 COCOMO 模型是一个静态单变量模型&#xff0c;用于对整个软…

内衣裤洗衣机如何选购?掌握这六个挑选技巧,轻松选购!

这两年内衣裤洗衣机可以称得上较火的小电器&#xff0c;小小的身躯却有大大的能力&#xff0c;一键可以同时启动洗、漂、脱三种全自动为一体化功能&#xff0c;在多功能和性能的提升上&#xff0c;还可以解放我们双手的同时将衣物给清洗干净&#xff0c;让越来越多小伙伴选择一…

node基础 第二篇

01 ffmpeg开源跨平台多媒体处理工具&#xff0c;处理音视频&#xff0c;剪辑&#xff0c;合并&#xff0c;转码等 FFmpeg 的主要功能和特性:1.格式转换:FFmpeg 可以将一个媒体文件从一种格式转换为另一种格式&#xff0c;支持几乎所有常见的音频和视频格式&#xff0c;包括 MP…

Node Version Manager(nvm):轻松管理 Node.js 版本的利器

文章目录 前言一、名词解释1、node.js是什么&#xff1f;2、nvm是什么&#xff1f; 二、安装1.在 Linux/macOS 上安装2.在 Windows 上安装 二、使用1.查看可安装的node版本2.安装node3. 查看已安装node4.切换node版本5.其它 总结 前言 Node.js 是现代 Web 开发中不可或缺的一部…

docker-compose 安装MongoDB续创建用户及赋权

文章目录 1. 问题描述2. 分析2.1 admin2.2 config2.3 local 3. 如何连接3.解决 1. 问题描述 在这一篇使用docker-compose创建MongoDB环境的笔记里&#xff0c;我们创建了数据库&#xff0c;但是似乎没有办法使用如Robo 3T这样的工具去连接数据库。连接的时候会返回这样的错误&…

c语言,单链表的实现----------有全代码!!!!

1.单链表的定义和结构 单链表是一种链式的数据结构&#xff0c;它用一组不连续的储存单元存反线性表中的数据元素。链表中的数据是以节点的形式来表示的&#xff0c;节点和节点之间相互连接 一般来说节点有两部分组成 1.数据域 &#xff1a;数据域用来存储各种类型的数据&…

基于SpringBoot+Vue的疾病防控系统设计与实现(源码+文档+包运行)

一.系统概述 在如今社会上&#xff0c;关于信息上面的处理&#xff0c;没有任何一个企业或者个人会忽视&#xff0c;如何让信息急速传递&#xff0c;并且归档储存查询&#xff0c;采用之前的纸张记录模式已经不符合当前使用要求了。所以&#xff0c;对疾病防控信息管理的提升&a…

windows 如何安装 perl ?

链接&#xff1a;https://strawberryperl.com/ 我们选择安装 “草莓 perl” 下载后根据引导安装就行了

node.jd版本降级/升级

第一步.先清空本地安装的node.js版本 按健winR弹出窗口&#xff0c;键盘输入cmd,然后敲回车&#xff08;或者鼠标直接点击电脑桌面最左下角的win窗口图标弹出&#xff0c;输入cmd再点击回车键&#xff09; 进入命令控制行窗口&#xff0c;输入where node&#xff0c;查看本地…

双指针的引入和深入思考(持续更新中)

目录 1.引入双指针 2.使用场景 3.例题引入 1.引入双指针 当我们需要维护某个区间性质的或者是求满足某些性质的区间的长度时&#xff0c;对于一个区间是由左右端点的&#xff0c;我们有简单的枚举左右端点的O()的时间的做法&#xff0c;当时在大多数题目中是不可行的&#…

DataX案例,MongoDB数据导入HDFS与MySQL

【尚硅谷】Alibaba开源数据同步工具DataX技术教程_哔哩哔哩_bilibili 目录 1、MongoDB 1.1、MongoDB介绍 1.2、MongoDB基本概念解析 1.3、MongoDB中的数据存储结构 1.4、MongoDB启动服务 1.5、MongoDB小案例 2、DataX导入导出案例 2.1、读取MongoDB的数据导入到HDFS 2…

论文笔记:Does Writing with Language Models Reduce Content Diversity?

iclr 2024 reviewer评分 566 1 intro 大模型正在迅速改变人们创造内容的方式 虽然基于LLM的写作助手有可能提高写作质量并增加作者的生产力&#xff0c;但它们也引入了算法单一文化——>论文旨在评估与LLM一起写作是否无意中降低了内容的多样性论文设计了一个控制实验&…

Kubernetes部署应用利器Helm详解

文章目录 一、helm概述&安装1.为什么需要Helm2.Helm介绍3.Helm架构4.部署Helm客户端5.Helm基本使用5.1 创建Chart示例 二、Helm 应用部署、升级1.创建项目&#xff08;chat所需目录、文件&#xff09;2.创建/拷贝项目的yaml文件到templates目录下3.使用Helm进行部署项目4.H…

第十五届蓝桥杯复盘python大学A组——试题B 召唤数学精灵

按照正常思路解决&#xff0c;由于累乘消耗大量时间&#xff0c;因此这不是一个明智的解决方案。 这段代码执行速度非常慢的原因在于它试图计算非常大的数的阶乘&#xff08;累乘&#xff09;&#xff0c;并且对于每一个i的值都执行这个计算。阶乘的增长是极其迅速的&#xff…

49.HarmonyOS鸿蒙系统 App(ArkUI)Tab导航组件的使用

HarmonyOS鸿蒙系统 App(ArkUI)Tab导航组件的使用 图片显示 Row() {Image($r(app.media.leaf)).height(100).width(100)Image($r(app.media.icon)).height(100).width(100) } 左侧导航 import prompt from ohos.prompt; import promptAction from ohos.promptAction; Entry C…

vue2知识点1 ———— (vue指令,vue的响应式基础)

vue2的知识点&#xff0c;更多前端知识在主页&#xff0c;还有其他知识会持续更新 Vue 指令 Vue指令是Vue.js中的一个重要概念&#xff0c;用于向DOM元素添加特定行为或功能。Vue指令以v-开头&#xff0c;例如v-bind、v-if、v-for等。 v-bind 动态绑定属性 用法&#xff1a…

windows ubuntu 子系统:肿瘤全外篇,2. fq 数据质控,比对。

首先我们先下载一组全外显子测序数据。nabi sra库&#xff0c;随机找了一个。 来自受试者“16177_CCPM_1300019”(SRR28391647, SRR28398576)的样本“16177_CCPM_1300019_BB5”的基因组DNA配对端文库“0369547849_Illumina_P5-Popal_P7-Hefel”的Illumina随机外显子测序 下载下…
最新文章