从src/isa/riscv32/inst.c出发。
向上搜索,理解宏定义的含义。
R(i)
#define R(i) gpr(i)
R(i):访问第i号通用寄存器
会被替换为:
#define gpr(idx) (cpu.gpr[check_reg_idx(idx)])

分为两个部分:
cpu.gprcheck_reg_idx
cpu.gpr的每个含义,在预学习的时候已经接触过了。

对于check_reg_idx,可见参数为一个int,那么宏定义gpr和R的参数也是int。
cpp
static inline int check_reg_idx(int idx) {
IFDEF(CONFIG_RT_CHECK, assert(idx >= 0 && idx < MUXDEF(CONFIG_RVE, 16, 32)));
return idx;
}
先看IFDEF:
#define IFDEF(macro, ...) MUXDEF(macro, __KEEP, __IGNORE)(__VA_ARGS__)
又冒出来新的宏定义。
需要找MUXDEF:
#define MUXDEF(macro, X, Y) MUX_MACRO_PROPERTY(__P_DEF_, macro, X, Y)
又又冒出来新的宏定义。
如此递归,整理得到:
cpp
#define IFDEF(macro, ...) MUXDEF(macro, __KEEP, __IGNORE)(__VA_ARGS__)
#define MUXDEF(macro, X, Y) MUX_MACRO_PROPERTY(__P_DEF_, macro, X, Y)
#define MUX_MACRO_PROPERTY(p, macro, a, b) MUX_WITH_COMMA(concat(p, macro), a, b)
#define MUX_WITH_COMMA(contain_comma, a, b) CHOOSE2nd(contain_comma a, b)
#define CHOOSE2nd(a, b, ...) b
CHOOSE2nd
递推的终点是:#define CHOOSE2nd(a, b, ...) b
从宏的名字和定义可以看出,这个宏的作用是:从可变参数中选择第二个参数。
测试一下,如果参数小于2怎么办。
plain
error: macro "CHOOSE2nd" requires 3 arguments, but only 1 given
6 | cout << CHOOSE2nd(1) << endl;
报错信息虽然显示的是三个参数,但其实两个就够了。
MUX_WITH_COMMA
非常细节的逗号:
#define MUX_WITH_COMMA(contain_comma, a, b) CHOOSE2nd(contain_comma a, b)
无论如何都会选中b,意义何在?接着看看。
MUX_MACRO_PROPERTY
cpp
#define concat_temp(x, y) x ## y
#define concat(x, y) concat_temp(x, y)
#define CHOOSE2nd(a, b, ...) b
#define MUX_WITH_COMMA(contain_comma, a, b) CHOOSE2nd(contain_comma a, b)
#define MUX_MACRO_PROPERTY(p, macro, a, b) MUX_WITH_COMMA(concat(p, macro), a, b)
经测试,无论前两个参数是啥,结果都是第四个参数。
- 这个宏的作用是?接着看看

MUXDEF
#define MUXDEF(macro, X, Y) MUX_MACRO_PROPERTY(__P_DEF_, macro, X, Y)
cpp
#include <iostream>
using namespace std;
#define concat_temp(x, y) x ## y
#define concat(x, y) concat_temp(x, y)
#define CHOOSE2nd(a, b, ...) b
#define MUX_WITH_COMMA(contain_comma, a, b) CHOOSE2nd(contain_comma a, b)
#define MUX_MACRO_PROPERTY(p, macro, a, b) MUX_WITH_COMMA(concat(p, macro), a, b)
#define __P_DEF_0 X,
#define __P_DEF_1 X,
#define __P_ONE_1 X,
#define __P_ZERO_0 X,
#define MUXDEF(macro, X, Y) MUX_MACRO_PROPERTY(__P_DEF_, macro, X, Y)
#define A
#define B 1
#define C 2
int main() {
cout << MUXDEF(A, 1, 2) << endl;
cout << MUXDEF(B, 1, 2) << endl;
cout << MUXDEF(C, 1, 2) << endl;
cout << MUXDEF(1, 1, 2) << endl;
cout << MUXDEF(0, 1, 2) << endl;
}
经测试,当拼接后的__P_DEF_macro有定义时,会返回X,否则返回Y。
到这里,输出的结果就不再是固定的了。
回头看一下,依次展开:
MUXDEF(macro, X, Y) MUX_MACRO_PROPERTY(_P_DEF, macro, X, Y) MUX_WITH_COMMA(concat(_P_DEF, macro), X, Y) CHOOSE2nd(__P_DEF_macro X, Y)
MUXDEF(macro, X, Y)会展开为:CHOOSE2nd(__P_DEF_macro X, Y)
但似乎还是只返回Y,为什么会返回X?看下面的函数:
cpp
#define concat_temp(x, y) x ## y
#define concat(x, y) concat_temp(x, y)
调用两个函数,结果是不一样的:
concat_temp(__P_DEF_, A):__P_DEF_Aconcat(__P_DEF_, A):1,
哪来的逗号?
#define __P_DEF_1 X,- 非常细节的宏定义,
X后有一个逗号。
- 非常细节的宏定义,
concat(__P_DEF_, A)的展开结果为:
cpp
concat(__P_DEF_, A)
concat_temp(__P_DEF_, 1)
__P_DEF_1
X,
这个的X,与MUX_WITH_COMMA省略的逗号结合。
如果A被定义为0或1,那么展开后,contain_comma a会变成X,a,使a成为第二个元素。
实际效果为宏定义下的?:三元运算符。
再回头看,那个流程图展开是有问题的。
宏定义不会递推到最后一层再展开,参考concat(__P_DEF_, A)的展开过程,A在第一步就展开了,它的展开结果会影响下一步展开。
对于整条链路的入口:MUXDEF(CONFIG_RVE, 16, 32))
- 如果定义了
CONFIG_RVE为1或0,那么编译16,否则32
IFDEF
还有一个很费劲的宏定义,出现了三层括号。
cpp
#define __IGNORE(...)
#define __KEEP(...) __VA_ARGS__
#define IFDEF(macro, ...) MUXDEF(macro, __KEEP, __IGNORE)(__VA_ARGS__)
有一个非常关键的关键字:__VA_ARGS__
会取出可变参数的值,也就是...的部分。
比如IFDEF(A, cout<<1<<endl;),会先展开为:
MUXDEF(A, __KEEP, __IGNORE)(cout<<1<<endl;)
前文已经知道,MUXDEF在第一个参数定义为1或0时,会编译为第二个参数。
那么就变成了:
__KEEP(cout<<1<<endl;)
__KEEP会编译为参数列表,也就是:cout<<1<<endl;
第三个括号等第二个括号解析完成后作为参数传入。
总结
IFDEF(CONFIG_RT_CHECK, assert(idx >= 0 && idx < MUXDEF(CONFIG_RVE, 16, 32)));
- 作用是判断,是否检查寄存器越界访问
R(i)
- 作用是取出第
i个寄存器的值。
一串宏定义的作用是判断取值的时候要不要检查。
Mr/Mw
#define Mr vaddr_read
这个函数在预学习的时候也用到过,现在顺着这个函数把宏定义捋一下。
首先是Mr后面没带括号,是给vaddr_read这个函数起了个别名。

vaddr_read是调用了paddr_read这个函数。
c
word_t paddr_read(paddr_t addr, int len) {
if (likely(in_pmem(addr))) return pmem_read(addr, len);
IFDEF(CONFIG_DEVICE, return mmio_read(addr, len));
out_of_bound(addr);
return 0;
}
现在又出现了多个宏定义。
likely
#define likely(cond) __builtin_expect(cond, 1)
告诉编译器,cond的值期望为1。
__builtin_expect(expr, expected) 的返回值就是 expr 的值本身。
它的作用不是改变值,而是告诉编译器你"预期这个值通常为 expected(通常是 0 或 1)",以便编译器做出更好的分支预测和优化。
cpp
static inline bool in_pmem(paddr_t addr) {
return addr - CONFIG_MBASE < CONFIG_MSIZE;
}
in_pmem的作用是判断地址是否合法。通过与地址偏移量运算得到。
c
static word_t pmem_read(paddr_t addr, int len) {
word_t ret = host_read(guest_to_host(addr), len);
return ret;
}
pmem_read的作用是从客户机的物理内存地址addr开始,读取len字节的数据,并返回对应的值。
cpp
static inline word_t host_read(void *addr, int len) {
switch (len) {
case 1: return *(uint8_t *)addr;
case 2: return *(uint16_t *)addr;
case 4: return *(uint32_t *)addr;
IFDEF(CONFIG_ISA64, case 8: return *(uint64_t *)addr);
default: MUXDEF(CONFIG_RT_CHECK, assert(0), return 0);
}
}
len只有1,2,4,8四种取值。也就是取出addr开始的1,2,4,8个字节的数据。
default: MUXDEF(CONFIG_RT_CHECK, assert(0), return 0);
- 如果定义了
CONFIG_RT_CHECK,那么非法的len会触发断言 - 如果未定义
CONFIG_RT_CHECK,那么非法的len会被忽略,返回0
host_write的函数体与host_read逻辑类似。
cpp
static inline void host_write(void *addr, int len, word_t data) {
switch (len) {
case 1: *(uint8_t *)addr = data; return;
case 2: *(uint16_t *)addr = data; return;
case 4: *(uint32_t *)addr = data; return;
IFDEF(CONFIG_ISA64, case 8: *(uint64_t *)addr = data; return);
IFDEF(CONFIG_RT_CHECK, default: assert(0));
}
}
imm*

这段宏定义在下面的decode_operand()中使用。
BITS
#define BITS(x, hi, lo) (((x) >> (lo)) & BITMASK((hi) - (lo) + 1)) // similar to x[hi:lo] in verilog
- 提取
x的第hi到lo位(闭区间)
运算分为两个部分:((x) >> (lo))与BITMASK((hi) - (lo) + 1)
先把低位干掉,然后取出新的地位。
BITMASK
#define BITMASK(bits) ((1ull << (bits)) - 1)
生成低bits位全是1的掩码。
ull避免溢出。
SEXT
#define SEXT(x, len) ({ struct { int64_t n : len; } __x = { .n = x }; (uint64_t)__x.n; })
写个程序测试一下功能。

({ ... })
- 这是
GCC和Clang支持的一种语法糖,用于将一个代码块作为一个表达式返回值。不能在标准C中使用。
在语法糖内部,有两条语句:
struct { int64_t n : len; } __x = { .n = x };struct { int64_t n : len; }定义了一个匿名结构体,变量n只取第n位。__x = { .n = x }创建了一个结构体变量__x,.n被赋值为x,高位会被截断。
(uint64_t)__x.n;- 把阶段的位域强转为
uint64_t,并作为表达式结果。
- 把阶段的位域强转为
那么SEXT的作用就是:
- 将
x看作一个len位的有符号整数,对其进行"符号扩展"为64位整数,并以uint64_t类型返回其值。
immI
#define immI() do { *imm = SEXT(BITS(i, 31, 20), 12); } while(0)
- 取出
32位指令中的位段[31:20] - 将它作为
12位 有符号立即数 符号扩展成64位 - 然后赋值给
*imm
immu
#define immU() do { *imm = SEXT(BITS(i, 31, 12), 20) << 12; } while(0)
- 取出
32位指令中的位段[31:12] - 然后左移
12位形成最终的32位立即数
imms
#define immS() do { *imm = (SEXT(BITS(i, 31, 25), 7) << 5) | BITS(i, 11, 7); } while(0)
- 取出:高
7位:i[31:25]和低5位:i[11:7] - 将高
7位符号扩展,再左移5位 - 与低
5位做按位或,合并成完整的12位立即数
文献来源

decode_exec
函数里面嵌套宏定义的写法暂时看不懂。
向上搜索调用链:
cpp
int isa_exec_once(Decode *s) {
s->isa.inst = inst_fetch(&s->snpc, 4);
return decode_exec(s);
}
inst_fetch调用到vaddr_ifetch时,可以发现,与vaddr_read接下来的走向如出一辙。

cpp
int isa_exec_once(Decode *s) {
s->isa.inst = inst_fetch(&s->snpc, 4);
return decode_exec(s);
}
static inline uint32_t inst_fetch(vaddr_t *pc, int len) {
uint32_t inst = vaddr_ifetch(*pc, len);
(*pc) += len;
return inst;
}
isa_exec_once的作用是,取出从&s->snpc处,长为4字节的指令。
- 也就是
32位指令。
并更新snpc为下一个位置。
snpc在PA2手册中有提到:
snpc是下一条静态指令, 而dnpc是下一条动态指令. 对于顺序执行的指令, 它们的snpc和dnpc是一样的; 但对于跳转指令, snpc和dnpc就会有所不同, dnpc应该指向跳转目标的指令. 显然, 我们应该使用s->dnpc来更新PC, 并且在指令执行的过程中正确地维护s->dnpc
decode_exec
c
static int decode_exec(Decode *s) {
s->dnpc = s->snpc;
#define INSTPAT_INST(s) ((s)->isa.inst)
#define INSTPAT_MATCH(s, name, type, ... /* execute body */ ) { \
int rd = 0; \
word_t src1 = 0, src2 = 0, imm = 0; \
decode_operand(s, &rd, &src1, &src2, &imm, concat(TYPE_, type)); \
__VA_ARGS__ ; \
}
INSTPAT_START();
INSTPAT("??????? ????? ????? ??? ????? 00101 11", auipc , U, R(rd) = s->pc + imm);
INSTPAT("??????? ????? ????? 100 ????? 00000 11", lbu , I, R(rd) = Mr(src1 + imm, 1));
INSTPAT("??????? ????? ????? 000 ????? 01000 11", sb , S, Mw(src1 + imm, 1, src2));
INSTPAT("0000000 00001 00000 000 00000 11100 11", ebreak , N, NEMUTRAP(s->pc, R(10))); // R(10) is $a0
INSTPAT("??????? ????? ????? ??? ????? ????? ??", inv , N, INV(s->pc));
INSTPAT_END();
R(0) = 0; // reset $zero to 0
return 0;
}
在decode_exec的头部,把dnpc赋值为snpc。表示默认下一条指令就在下一个字节的位置。
中间两端宏定义暂时看不懂,但是好在暂时没有调用,只是定义:
c
#define INSTPAT_INST(s) ((s)->isa.inst)
#define INSTPAT_MATCH(s, name, type, ... /* execute body */ ) { \
int rd = 0; \
word_t src1 = 0, src2 = 0, imm = 0; \
decode_operand(s, &rd, &src1, &src2, &imm, concat(TYPE_, type)); \
__VA_ARGS__ ; \
}
可以接着往下看:

INSTPAT_START
第二条要执行的语句是:INSTPAT_START();
#define INSTPAT_START(name) { const void * __instpat_end = &&concat(__instpat_end_, name);
&&label是标签地址,官方文档链接:https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html
具体的功能可以写一个函数测试一下:
c
int main() {
void *ptr = &&label1;
goto *ptr;
printf("hello world\n");
label1:
printf("Jumped to label1!\n");
return 0;
}
INSTPAT_START()传入的是空参数,展开的结果为:
c
{
const void *__instpat_end = &&__instpat_end_;
非常细节的大括号,作用需要搭配INSTPAT_END()来理解:
c
__instpat_end_ :;
}
强制地提示,INSTPAT_START应与INSTPAT_END成对出现。
并且限制了作用域。
INSTPAT_END放置在函数体结尾。
INSTPAT
cpp
#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)
这段宏定义的内容是定义了一段代码,do...wihile保证按期望运行。
uint64_t key, mask, shift;声明了一些变量
pattern_decode(pattern, STRLEN(pattern), &key, &mask, &shift);
进到这个函数看下是如何运作的。
pattern_decode

定义了一堆宏定义,看起来比较复杂。
macro
c
#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); \
} \
}
if ((i) >= len) goto finish;
len定义自:pattern_decode(pattern, STRLEN(pattern), &key, &mask, &shift);
也就是str的长度。
思考一下,macro64展开后能覆盖0-63,但字符串长度是64,支持的最大长度是63还是64?
可以写个程序测试下。

字符串长度为64时输出了pattern too long。
pattern_decode函数的作用是,从一个长度为len的字符串str中解析出三种信息:
key:把所有'0'和'1'字符组成一个位串,表示匹配值mask:每一位如果是'0'或'1'则为1,如果是'?'则为0,表示哪些位需要匹配shift:表示尾部连续'?'的数量,这些位会被右移舍弃掉
回到INSTPAT
if ((((uint64_t)INSTPAT_INST(s) >> shift) & mask) == key)
这里为什么key不用右移?
因为pattern_decode中已经右移过了:
cpp
finish:
*key = __key >> __shift;
*mask = __mask >> __shift;
*shift = __shift;
指令匹配成功之后,会执行INSTPAT_MATCH,然后goto到结尾的位置。
类似一堆if-else。
c
INSTPAT_MATCH(s, ##__VA_ARGS__); \
goto *(__instpat_end); \
INSTPAT_MATCH
INSTPAT_MATCH的入参为##__VA_ARGS__,在参数为空时,会自动去掉前面的逗号,避免编译报错。
c
#define INSTPAT_MATCH(s, name, type, ... /* execute body */ ) { \
int rd = 0; \
word_t src1 = 0, src2 = 0, imm = 0; \
decode_operand(s, &rd, &src1, &src2, &imm, concat(TYPE_, type)); \
__VA_ARGS__ ; \
}
发现name和type即使传入空置也不会影响目前的编译。
decode_operand
c
static void decode_operand(Decode *s, int *rd, word_t *src1, word_t *src2, word_t *imm, int type) {
uint32_t i = s->isa.inst;
int rs1 = BITS(i, 19, 15);
int rs2 = BITS(i, 24, 20);
*rd = BITS(i, 11, 7);
switch (type) {
case TYPE_I: src1R(); immI(); break;
case TYPE_U: immU(); break;
case TYPE_S: src1R(); src2R(); immS(); break;
case TYPE_N: break;
default: panic("unsupported type = %d", type);
}
}
第一个参数就是当前正在解码的指令上下文,封装了机器码值、指令地址等参数。
c
uint32_t i = s->isa.inst;
int rs1 = BITS(i, 19, 15);
int rs2 = BITS(i, 24, 20);
*rd = BITS(i, 11, 7);
分别取出源寄存器1、源寄存器2和目的寄存器。
这三个寄存器的位置是固定的,在RSICV官方手册中的出处:

还是这张图。
每个寄存器不一定都能用到。但是每种类型的指令,只要用到了,位置就是固定的。
有个细节,上面的代码取寄存器的时候,只有rd是指针解引用赋值,其他参数是局部变量,对函数外暂时没有产生影响。
对于I型指令,需要immI、rs1和rd。
对于U型指令,需要immU和rd。
对于S型指令,需要immS、rs2、rs1和rd。
rd对应手册中的imm[4:0],可以发现位置完全一样。
对于R型指令,看手册定义,格式与S型一致,猜测后续执行时会复用S型指令的逻辑。
到这里,decode_operand函数的意义已经非常明确了:
- 根据不同的指令类型,取出操作数。
VA_ARGS
把可变参数展开。
结合已有代码:INSTPAT("??????? ????? ????? ??? ????? 00101 11", auipc , U, R(rd) = s->pc + imm);
首先会尝试与字符串模板匹配:"??????? ????? ????? ??? ????? 00101 11"
如果匹配成功,会展开INSTPAT_MATCH:
s,在decode_exec函数入参中传入name,INSTPAT的第二个参数auipctype,INSTPAT的第三个参数U...,INSTPAT的第四个参数R(rd) = s->pc + imm)
name目前来看无关紧要。
展开后会先根据type取出操作数。
然后展开...,操作取出的操作数。
总结

INSTPAT_START和INSTPAT_END成对出现。
中间处理指令,某条规则匹配成功后,会立即执行并不再继续向下匹配。
INSTPAT的参数是:
- 匹配规则
- 指令名字
- 指令类型
- 执行语义,传入的应该是一系列函数。