首页 > 其他分享 >NEMU的指令执行步骤

NEMU的指令执行步骤

时间:2024-09-29 17:47:07浏览次数:8  
标签:__ 步骤 mask INSTPAT NEMU 指令 key shift

exec_once()函数覆盖了指令周期的所有阶段: 取指, 译码, 执行, 更新PC

下面学习下函数exec_once()的各个阶段所做的事情

取指

在执行指令之前,需要获取这个指令,我们看下NEMU如何获取一条指令的。

exec_once()接受一个Decode类型的结构体指针s.这个结构体存放“在执行一条指令过程中所需的信息”首先分析下这个Decode结构体.

Decode结构体定义在nemu/include/cpu/decode.h

typedef struct Decode {
  vaddr_t pc;
  vaddr_t snpc; // static next pc
  vaddr_t dnpc; // dynamic next pc
  ISADecodeInfo isa;
  IFDEF(CONFIG_ITRACE, char logbuf[128]);
} Decode;

这里可以看出,除了指令的地址信息pcsnpcdnpc,还包括了一个与ISA相关的结构体抽象ISADecodeInfo.

其具体的定义在nemu/src/isa/$ISA/include/isa-def.h

typedef struct {
  union {
    uint32_t val;
  } inst;
} MUXDEF(CONFIG_RV64, riscv64_ISADecodeInfo, riscv32_ISADecodeInfo);

这个ISADecodeInfo结构体中包含了一个联合体inst,联合体中有一个uint32_t类型的成员val。 RISC-V 32 位架构中,每一条指令的长度都是 32 位,因此这个 val 可以用于存储一条完整的指令。

现在exec_once()函数接收了传入参数s,然后将当前的PC保存到s的成员pcsnpc中。随后调用isa_exec_once()进行指令的执行操作。

函数isa_exec_once()定义在nemu/src/isa/riscv32/inst.c

int isa_exec_once(Decode *s) {
  s->isa.inst.val = inst_fetch(&s->snpc, 4);
  return decode_exec(s);
}

因为inst.val是用来存储一条完整指令的变量,所以推测inst_fetch()函数的功能,是用来取指令的。

下面我们看下函数inst_fetch()的定义(nemu/include/cpu/ifetch.h

static inline uint32_t inst_fetch(vaddr_t *pc, int len) {
  uint32_t inst = vaddr_ifetch(*pc, len);
  (*pc) += len;
  return inst;
}

而函数vaddr_ifetch()的功能就是通过pc所指的客户程序地址,找到对应物理内存中的长度为len的数据。

函数isa_exec_once()pc->snpc的地址作为参数传入到函数vaddr_ifetch()中,所以函数vaddr_ifetch()取完数据后,会根据len(这里是4)来更新s -> snpc,从而让s -> snpc指向下一条指令。

已经获取的指令,将其存放于结构体s关于ISA信息的isa中。至此,取指令流程结束。

译码

随后s带着指令的信息,传入到函数decode_exec()开始译码。

译码的目的是得到指令的操作和操作对象, 这主要是通过查看指令的opcode来决定的. NEMU通过一个模式字符串来指定指令中的opcode.

因为译码部分研读时候发现细节很多,所以为了理解这部分的内容,我这个小节的规划是:先从宏观角度讲译码做了什么,即译码的功能;随后着眼细节,剖析代码的筋骨纹理,看看译码是怎么做到的这个功能。

来不及解释了,我们开始⭐

功能

首先看下,如何获取指令中的opcode.

NEMU定义了用于识别对应opcode的模式匹配规则INSTPAT(意思是instruction pattern)

INSTPAT(模式字符串, 指令名称, 指令类型, 指令执行操作);

宏展开后,首先调用pattern_decode()函数,将一条包含opcode对应匹配规则的字符串,经过转换,作为opcode的判断参数。

然后再将输入的s中的指令信息s->isa.inst.val,经过位操作后,跟上一步骤中的opcode判断参数进行比对。

如果比对成功,则宣告了指令的操作类型已经确定,指令类型的译码工作已经完成。

指令类型确定后,随后便是对操作对象的译码处理decode_operand()。此函数根据传入的指令类型type来进行操作数的译码,译码结果会被保存起来。

decode_operand(s, &rd, &src1, &src2, &imm, TYPE_U);

以上就是宏观角度,屏蔽掉函数内部的复杂粒度,只概述每个函数的输入输出,从简化译码的逻辑。

但是实际的操作,还是需要依靠复杂的逻辑处理和对应c语言特性才能实现。下面,我们就着手细节,从细节上剖析译码的操作流程。

细节

首先看下如何实现的模式匹配。NEMU可以通过一个模式字符串来指定指令中opcode, 例如在riscv32中有如下模式:

INSTPAT_START();
INSTPAT("??????? ????? ????? ??? ????? 00101 11", auipc, U, R(rd) = s->pc + imm);
// ...
INSTPAT_END();

而定义每一条模式匹配规则的INSTPAT是一个宏,其格式为:

INSTPAT(模式字符串, 指令名称, 指令类型, 指令执行操作);

INSTPAT的各个参数的说明,都在PA2部分有详细地说明,这里不再赘述。我们看下这个模式定义后,如何转换为对应的C代码。

我们看下INSTPATINSTPAT_START()INSTPAT_END()其宏定义的具体实现。它们均被定义在nemu/include/cpu/decode.h中。

// --- pattern matching wrappers for decode ---
#define INSTPAT(pattern, ...) do { \
  uint64_t key, mask, shift; \
  pattern_decode(pattern, STRLEN(pattern), &key, &mask, &shift); \
  if ((((uint64_t)INSTPAT_INST(s) >> shift) & mask) == key) { \
    INSTPAT_MATCH(s, ##__VA_ARGS__); \
    goto *(__instpat_end); \
  } \
} while (0)

#define INSTPAT_START(name) { const void ** __instpat_end = &&concat(__instpat_end_, name);
#define INSTPAT_END(name)   concat(__instpat_end_, name): ; }

INSTPAT又使用了另外两个宏INSTPAT_INSTINSTPAT_MATCH, 它们在nemu/src/isa/$ISA/inst.c中定义.

#define INSTPAT_INST(s) ((s)->isa.inst.val)
#define INSTPAT_MATCH(s, name, type, ... /* execute body */ ) { \
  decode_operand(s, &rd, &src1, &src2, &imm, concat(TYPE_, type)); \
  __VA_ARGS__ ; \
}

具体定义如上文所述,下面我们按照其源码,分析下INSTPATINSTPAT_START()INSTPAT_END()的具体逻辑。

首先是宏INSTPAT的各部分含义解析:

  1. pattern_decode 是一个函数,通过解析 pattern(模式)生成 keymaskshift 这三个变量。

  2. if ((((uint64_t)INSTPAT_INST(s) >> shift) & mask) == key)

    • INSTPAT_INST(s) 是一个宏,用于从存放指令执行信息的结构体 s 中提取指令或数据。其逻辑实现为

      #define INSTPAT_INST(s) ((s)->isa.inst.val)
      
    • (uint64_t)INSTPAT_INST(s)将提取到的指令,转换为64位的整数

    • & mask:通过掩码操作保留需要匹配的位,屏蔽掉其他不相关的位。

    • == key:最后将处理后的结果与 key 进行比较,判断当前指令或数据是否符合指定模式。

  3. INSTPAT_MATCH(s, ##__VA_ARGS__)

    • 当2阶段if判断条件成立,宏调用INSTPAT_MATCH,执行与该模式匹配的逻辑

      #define INSTPAT_MATCH(s, name, type, ... /* execute body */ ) { \
        decode_operand(s, &rd, &src1, &src2, &imm, concat(TYPE_, type)); \
        __VA_ARGS__ ; \
      }
      
    • ##__VA_ARGS__ 表示可变参数,允许传递多个参数给 INSTPAT_MATCH,使这个宏更灵活。

  4. goto *(__instpat_end)

    • 匹配成功,代码跳转到预定义的指针__instpat_end所指的地址。其实现在INSTPAT_START

INSTPAT宏的整体作用:

  1. 通过pattern_decode()函数解析传入的指令模式pattern,用与生成匹配的 keymaskshift
  2. s 中提取待处理的指令或数据,并根据 shiftmaskkey 进行模式匹配。
  3. 如果匹配成功,执行 INSTPAT_MATCH 中的操作,并通过 goto *(__instpat_end) 跳转到预定义的位置,可能是为了跳过某些指令或结束当前匹配过程。

分析完毕INSTPAT后,我们具体分析下函数pattern_decode()decode_operand()

首先是将模式字符串解析到变量keymaskshift的函数pattern_decode(),定义在nemu/include/cpu/decode.h中。

static inline void pattern_decode(const char *str, int len,
    uint64_t *key, uint64_t *mask, uint64_t *shift) {
  uint64_t __key = 0, __mask = 0, __shift = 0;

#define macro(i) \
  if ((i) >= len) goto finish; \
  else { \
    char c = str[i]; \
    if (c != ' ') { \
      Assert(c == '0' || c == '1' || c == '?', \
          "invalid character '%c' in pattern string", c); \
      __key  = (__key  << 1) | (c == '1' ? 1 : 0); \
      __mask = (__mask << 1) | (c == '?' ? 0 : 1); \
      __shift = (c == '?' ? __shift + 1 : 0); \
    } \
  }

#define macro2(i)  macro(i);   macro((i) + 1)
#define macro4(i)  macro2(i);  macro2((i) + 2)
#define macro8(i)  macro4(i);  macro4((i) + 4)
#define macro16(i) macro8(i);  macro8((i) + 8)
#define macro32(i) macro16(i); macro16((i) + 16)
#define macro64(i) macro32(i); macro32((i) + 32)

  macro64(0); // 从索引 0 开始解析字符串
  panic("pattern too long"); // 如果解析到这里,表示字符串超长

#undef macro
finish:
  *key = __key >> __shift; // 将 __key 右移 __shift 位
  *mask = __mask >> __shift; // 将 __mask 右移 __shift 位
  *shift = __shift; // 返回移位值
}

里面比较有意思的是宏macro(i)的相关定义,这里学习下。

#define macro(i) \
  if ((i) >= len) goto finish; \
  else { \
    char c = str[i]; \
    if (c != ' ') { \
      Assert(c == '0' || c == '1' || c == '?', \
          "invalid character '%c' in pattern string", c); \
      __key  = (__key  << 1) | (c == '1' ? 1 : 0); \
      __mask = (__mask << 1) | (c == '?' ? 0 : 1); \
      __shift = (c == '?' ? __shift + 1 : 0); \
    } \
  }
  • 边界检查:如果索引 i 超过字符串长度 len,则跳转到 finish 标签,结束解析
  • 字符处理:
    • 获取字符串中索引为i的字符c
    • 如果字符为非空格,继续下面的执行
    • 使用宏Assert()确保字符是01?
    • 根据字符更新__key__mask__shift
      • 1:将 __key 向左移位并设置最低位为 1,同时在 __mask 中将相应位设为 1
      • 0:将 __key 向左移位并设置最低位为 0,同时在 __mask 中将相应位设为 1
      • ?:不影响__Key,在__mask中对应位设置为0,并增加__shift的计数

这里我们单独拎出__key的处理方法,来看看是怎么根据当前字符c来决定这个参数值的

__key = (__key << 1) | (c == '1' ? 1 : 0);
  1. 左移操作__key << 1
  2. 条件表达式(c == '1' ? 1 : 0)
  3. 按位或操作|:按位或是位运算的一种,是将两个数据的二进制表示右对齐后,按位进行运算,两个对应的二进制位中只要一个是1,结果对应位就是1。

举例:如果当前__key值为5(二进制0101

  • 当前字符c1,则__key = 1010 | 1 = 1011
  • 当前字符c0,则__key = 1010 | 0 = 1010

其实这行代码的目的是逐个处理字符串中的字符,并根据字符是否为1,来构建一个二进制数。举一反三,其他两个参数__mask__shift的值获取方式类似。

学习完主要的宏macro(i)后,我们对函数pattern_decode()主要部分进行解析

  • macro(i)
    • 边界检查:解析字符串的每个字符。检查字符是否超出长度len,并使用宏Assert()确保字符是01?
    • 字符处理:根据字符更新 __key__mask__shift
      • '1': 将 __key 向左移位并设置最低位为 1,同时在 __mask 中将相应位设为 1。
      • '0': 将 __key 向左移位并设置最低位为 0,同时在 __mask 中将相应位设为 1。
      • '?': 不影响 __key,在 __mask 中对应位设为 0,并增加 __shift 的计数。

标签:__,步骤,mask,INSTPAT,NEMU,指令,key,shift
From: https://www.cnblogs.com/shangshankandashu/p/18440484

相关文章

  • 轻松上手Linux,掌握这些基础指令就够了
    想要成为Linux高手吗?掌握常用指令是关键!本指南将为你介绍最实用的Linux指令,让你轻松管理你的系统,从文件操作到系统监控,我们将一步步引导你成为Linux的行家里手,快来学习这些必备技能,让你的工作效率飞速提升!目录1、whoami命令语法:whoani功能:显示当前用户名2、pwd命令......
  • 教师提示词指令
    1.课件内容策划指令【背景】:你是一名经验丰富的教师,负责策划和讲授一门关于【课程主题】的【备课科目】课程。你希望通过课件内容的精心设计,激发学生的学习兴趣,帮助他们更好地理解和掌握相关知识点。【目标】:你需要一份完整的课件内容策划,包括以下几个部分:引言:吸引学生注意......
  • Modbus Tcp指令(一)
    1.读取一个或多个保持寄存器的数值如指令0002000000060103F20000060002事务标识符0000协议标识符0006长度标识符(从站号开始到指令结尾共有6个byte)01站号03功能码(读取一个或多个保持寄存器的数值)F200起始寄存器地址0006......
  • 【数据库】Java 中 MongoDB 使用指南:步骤与方法介绍
    MongoDB是一个流行的NoSQL数据库,因其灵活性和高性能而广泛使用。在Java中使用MongoDB,可以通过MongoDB官方提供的Java驱动程序来实现。本文将详细介绍在Java中使用MongoDB的步骤以及相关方法。1.环境准备1.1安装MongoDB首先,确保你的系统中安装了Mongo......
  • Vue 常用的指令用法
    文章目录Vue常用的指令用法一、引言二、指令详解1、v-model2、v-bind3、v-for4、v-if/v-else-if/v-else5、v-show6、v-on7、v-text和v-html三、指令使用技巧四、总结Vue常用的指令用法一、引言Vue.js是一个构建用户界面的渐进式框架,它通过一系列指令来实......
  • sendmail发邮件指南:配置步骤与使用方法?
    sendmail发邮件性能怎么优化?如何用sendmail发邮件?sendmail发邮件系统因其稳定性和灵活性而广泛应用于各种服务器环境中。然而,对于初学者来说,sendmail发邮件的配置和使用可能显得有些复杂。AokSend将详细介绍sendmail发邮件的配置步骤和使用方法。sendmail发邮件:用户别名sen......
  • 脑电数据MNE预处理分析步骤与说明(个人总结版不断完善中)
    1.加载数据:加载原始数据到MNE中。2.加载电极位置信息:加载电极配置。3.查看原始信号:plot查看信号,插值坏导。(无坏导就不需要做)4.滤波:带通滤波0.1-60hz,陷波去除50hz工频干扰。5.独立成分分析:运行ICA分析,画出ICA各成分地形图。6.去除眼电伪迹:根据ICA各成分地形图去除眼电成分,......
  • 妙用编辑器:使用Notepad--宏功能提高维护指令生成生成效率
    应用场景日常维护工作中,需要快速生成一批指令来完成某些操作,比如:快速添加一批节点。目标指令列表如下:ADDNODE:ID=1,NAME="NODE_1";ADDNODE:ID=2,NAME="NODE_2";ADDNODE:ID=3,NAME="NODE_3";ADDNODE:ID=4,NAME="NODE_4";ADDNODE:ID=5,NAME="NODE_5&quo......
  • 要在PBOOTCMS中新增并开启手机端模板,按照以下步骤操作
    要在PBOOTCMS中新增并开启手机端模板,按照以下步骤操作:开启手机版开关登录后台:首先,使用管理员账号登录PBOOTCMS后台管理系统。进入全局配置:在后台菜单中找到并点击“全局配置”或类似的系统设置项。启用手机模板:在全局配置中,找到与“手机模板”或“独立手机版”相关的设置,将其......
  • uniapp [安卓苹果App端] - 详细实现手机蓝牙连接打印机及打印票据小票/标签/面单/热敏
    前言网上的教程乱七八糟,文本提供优质示例代码。在uni-appApp端(安卓APP|苹果APP)开发中,详解实现“手机蓝牙连接并使用蓝牙打印机”,uniAppApp端手机使用蓝牙连接打印机进行打印的相关功能,uniapp苹果安卓app实现开启蓝牙并搜索附近范围的蓝牙打印机对接全流程,支持打印......