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;
这里可以看出,除了指令的地址信息pc
、snpc
和dnpc
,还包括了一个与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
的成员pc
和snpc
中。随后调用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代码。
我们看下INSTPAT
、INSTPAT_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_INST
和INSTPAT_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__ ; \
}
具体定义如上文所述,下面我们按照其源码,分析下INSTPAT
、INSTPAT_START()
和INSTPAT_END()
的具体逻辑。
首先是宏INSTPAT
的各部分含义解析:
-
pattern_decode
是一个函数,通过解析pattern
(模式)生成key
、mask
和shift
这三个变量。 -
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
进行比较,判断当前指令或数据是否符合指定模式。
-
-
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
,使这个宏更灵活。
-
-
goto *(__instpat_end)
- 匹配成功,代码跳转到预定义的指针
__instpat_end
所指的地址。其实现在INSTPAT_START
中
- 匹配成功,代码跳转到预定义的指针
INSTPAT
宏的整体作用:
- 通过
pattern_decode()
函数解析传入的指令模式pattern
,用与生成匹配的key
、mask
和shift
。 - 从
s
中提取待处理的指令或数据,并根据shift
、mask
和key
进行模式匹配。 - 如果匹配成功,执行
INSTPAT_MATCH
中的操作,并通过goto *(__instpat_end)
跳转到预定义的位置,可能是为了跳过某些指令或结束当前匹配过程。
分析完毕INSTPAT
后,我们具体分析下函数pattern_decode()
和decode_operand()
。
首先是将模式字符串解析到变量key
、mask
和shift
的函数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()
确保字符是0
、1
或?
- 根据字符更新
__key
、__mask
和__shift
1
:将__key
向左移位并设置最低位为 1,同时在__mask
中将相应位设为 10
:将__key
向左移位并设置最低位为 0,同时在__mask
中将相应位设为 1?
:不影响__Key
,在__mask
中对应位设置为0
,并增加__shift
的计数
- 获取字符串中索引为
这里我们单独拎出__key
的处理方法,来看看是怎么根据当前字符c
来决定这个参数值的
__key = (__key << 1) | (c == '1' ? 1 : 0);
- 左移操作
__key << 1
- 条件表达式
(c == '1' ? 1 : 0)
- 按位或操作
|
:按位或是位运算的一种,是将两个数据的二进制表示右对齐后,按位进行运算,两个对应的二进制位中只要一个是1,结果对应位就是1。
举例:如果当前__key
值为5
(二进制0101
)
- 当前字符
c
为1
,则__key = 1010 | 1 = 1011
- 当前字符
c
为0
,则__key = 1010 | 0 = 1010
其实这行代码的目的是逐个处理字符串中的字符,并根据字符是否为1
,来构建一个二进制数。举一反三,其他两个参数__mask
和 __shift
的值获取方式类似。
学习完主要的宏macro(i)
后,我们对函数pattern_decode()
主要部分进行解析
- 宏
macro(i)
- 边界检查:解析字符串的每个字符。检查字符是否超出长度
len
,并使用宏Assert()
确保字符是0
、1
或?
- 字符处理:根据字符更新
__key
、__mask
和__shift
:- '1': 将
__key
向左移位并设置最低位为 1,同时在__mask
中将相应位设为 1。 - '0': 将
__key
向左移位并设置最低位为 0,同时在__mask
中将相应位设为 1。 - '?': 不影响
__key
,在__mask
中对应位设为 0,并增加__shift
的计数。
- '1': 将
- 边界检查:解析字符串的每个字符。检查字符是否超出长度