从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.gpr
check_reg_idx
cpu.gpr
的每个含义,在预学习的时候已经接触过了。
对于check_reg_idx
,可见参数为一个int
,那么宏定义gpr
和R
的参数也是int
。
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)
又又冒出来新的宏定义。
如此递归,整理得到:
#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
怎么办。
error: macro "CHOOSE2nd" requires 3 arguments, but only 1 given6 | cout << CHOOSE2nd(1) << endl;
报错信息虽然显示的是三个参数,但其实两个就够了。
MUX_WITH_COMMA
非常细节的逗号:
#define MUX_WITH_COMMA(contain_comma, a, b) CHOOSE2nd(contain_comma a, b)
无论如何都会选中b
,意义何在?接着看看。
MUX_MACRO_PROPERTY
#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)
#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 2int 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)
会展开为:CHOOSE2nd(__P_DEF_macro X, Y)
但似乎还是只返回Y
,为什么会返回X
?看下面的函数:
#define concat_temp(x, y) x ## y
#define concat(x, y) concat_temp(x, y)
调用两个函数,结果是不一样的:
concat_temp(__P_DEF_, A)
:__P_DEF_A
concat(__P_DEF_, A)
:1,
哪来的逗号?
#define __P_DEF_1 X,
- 非常细节的宏定义,
X
后有一个逗号。
- 非常细节的宏定义,
concat(__P_DEF_, A)
的展开结果为:
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
还有一个很费劲的宏定义,出现了三层括号。
#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
这个函数。
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)”,以便编译器做出更好的分支预测和优化。
static inline bool in_pmem(paddr_t addr) {return addr - CONFIG_MBASE < CONFIG_MSIZE;
}
in_pmem
的作用是判断地址是否合法。通过与地址偏移量运算得到。
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
字节的数据,并返回对应的值。
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
逻辑类似。
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
位立即数
文献来源
- https://drive.google.com/file/d/1uviu1nH-tScFfgrovvFCrj7Omv8tFtkp/view?usp=drive_link
- Page26
decode_exec
函数里面嵌套宏定义的写法暂时看不懂。
向上搜索调用链:
int isa_exec_once(Decode *s) {s->isa.inst = inst_fetch(&s->snpc, 4);return decode_exec(s);
}
inst_fetch
调用到vaddr_ifetch
时,可以发现,与vaddr_read
接下来的走向如出一辙。
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
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 $a0INSTPAT("??????? ????? ????? ??? ????? ????? ??", inv , N, INV(s->pc));INSTPAT_END();R(0) = 0; // reset $zero to 0return 0;
}
在decode_exec
的头部,把dnpc
赋值为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_START();
#define INSTPAT_START(name) { const void * __instpat_end = &&concat(__instpat_end_, name);
&&label
是标签地址,官方文档链接:https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html
具体的功能可以写一个函数测试一下:
int main() {void *ptr = &&label1;goto *ptr;printf("hello world\n");
label1:printf("Jumped to label1!\n");return 0;
}
INSTPAT_START()
传入的是空参数,展开的结果为:
{const void *__instpat_end = &&__instpat_end_;
非常细节的大括号,作用需要搭配INSTPAT_END()
来理解:
__instpat_end_ :;
}
强制地提示,INSTPAT_START
应与INSTPAT_END
成对出现。
并且限制了作用域。
INSTPAT_END
放置在函数体结尾。
INSTPAT
#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
#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
中已经右移过了:
finish:*key = __key >> __shift;*mask = __mask >> __shift;*shift = __shift;
指令匹配成功之后,会执行INSTPAT_MATCH
,然后goto
到结尾的位置。
类似一堆if-else
。
INSTPAT_MATCH(s, ##__VA_ARGS__); \goto *(__instpat_end); \
INSTPAT_MATCH
INSTPAT_MATCH
的入参为##__VA_ARGS__
,在参数为空时,会自动去掉前面的逗号,避免编译报错。
#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
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);}
}
第一个参数就是当前正在解码的指令上下文,封装了机器码值、指令地址等参数。
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
的第二个参数auipc
type
,INSTPAT
的第三个参数U
...
,INSTPAT
的第四个参数R(rd) = s->pc + imm)
name
目前来看无关紧要。
展开后会先根据type
取出操作数。
然后展开...
,操作取出的操作数。
总结
INSTPAT_START
和INSTPAT_END
成对出现。
中间处理指令,某条规则匹配成功后,会立即执行并不再继续向下匹配。
INSTPAT
的参数是:
- 匹配规则
- 指令名字
- 指令类型
- 执行语义,传入的应该是一系列函数。
参考
- https://ysyx.oscc.cc/docs/ics-pa/2.2.html#rtfsc-2