instruction

项目工程

Posted by Bruce Lee on 2024-05-08

decode_operand函数

使用BITS宏,从s中的指令二进制串抓取rs1,rs2以及rd的二进制值.

然后根据传入的type类型进行switch跳转,然后执行流水线中的各个操作:
src1R宏,这个宏就是读取rs1存储的寄存器值.
src2R宏,读取rs2存储的寄存器值.
关于src(x)R宏中,调用R宏来做最底层的读取操作.

或调用immI宏:读取I型指令立即数字段.
immU宏:读取U型指令立即数字段.
immS宏:读取S型指令立即数字段.
关于imm(X)宏中调用SEXT宏以及BITS宏来做最底层的操作.

decode_operand函数结束.

关于imm(X)宏和SEXT宏,BITS宏

这些系列宏全部定义在include/macro.h文件中

BITS宏接受三个输入:x表示32位指令二进制串,hi表示高位,lo表示低位.
通过(x) >> (lo),将读取的字段放置最右端
通过& BITMASK((hi) - (lo) + 1), 将比hi还要高的所有二进制位全部置零.

1
(hi) - (lo) + 1

表示要读取的字段的长度.
BITMASK是掩码,接受读取的字段长度,可以制造出一个该长度的"依靠"低位的掩码.

BITMASK中:

<< (bits)```表示将无符号1,类型是unsigned long long,是一个64位.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
我们只需要一个32位的指令二进制串进行操作,为何要使用一个64位呢?
扩展性:使用INSTPAT宏定义指令模板时,我们也是用的64位来定义指令,这是给后续留出足够的扩展余地.
1ull << (bits) - 1 会生成一个低于bits位,全为1,其他全为0的掩码.

SEXT宏解析:
用于抓取立即数字段
定义了一个内嵌int64_t空间的结构体变量__x,其中包含len长度的n字段.其中将x赋值给n字段.
传参x是一个抓取完成的指令字段值.len是这个字段的长度.然后通过```.n = x```, 将这个32位有符号数x赋值给len位的有符号数n,然后n会被有符号扩展为__x,最后将__x.n无符号扩展为64位传出去.
***这里为何进行的是无符号扩展,据我所知,所有的立即数字段都是有符号数,如果抓取出来进行的是无符号扩展,会导致出错.故应该将```(uint64_t)__x.n;```修改为:```(int64_t)__x.n;```


### g_nr_guest_inst变量的解析
表示匹配/执行的指令数

### 关于pc递增
src/cpu文件定义的execute函数声明了Decode变量s,然后传给了exec_once.
src/cpu文件定义的exec_once函数将全局变量pc初始化s->pc,s->snpc,然后调用isa_exec_once函数.
定义在src/isa/riscv32/instc文件的isa_exec_once函数接受这个s,然后读取指令二进制串(调用inst_fetch函数)
在inst_fetch函数中,s->snpc默认递增到下一条指令
然后调用decode_exec函数执行,然后就返回.
在decode_exec函数中,s->snpc赋值给s->dnpc.
然后isa_exec_once函数返回后,就是***cpu.pc = s->dnpc***,s->dnpc是下一条要执行的指令.

#### 关于snpc和dnpc实际差异地方
在src/isa/riscv32/inst.c文件的decode_exec函数中
先是将***s->dnpc = s->snpc***
这是默认,但前执行的指令没有任何跳转,那么下一条真实执行指令默认就是snpc(snpc损失当前指令的下一条物理地址指令)

如果有跳转,那么会在指令exe阶段进行处理,此时dnpc会发生改变,其值自然会到正确的指令地址处.

最后在isa_exec_once退出,来到exec_once函数中,将***cpu.pc = s->dnpc***

### 关于如何添加指令
在src/isa/riscv32/inst.c文件中:
定义"do"宏

在enum枚举中,检查是否是扩展指令类型

添加新指令的立即数抓取宏,源寄存器读取宏(这个应该是不用的)

在decode_operand函数中,进行添加switch跳转条件.

在INSTPAT_START-INSTPAT_END代码块中添加指令模式

### 添加I(ADDi)型指令
检查:enum中,有TYPE_I型
确认:在immI宏中,读取的立即数字段通过查表,是ADDi中的立即数
检查:在decode_operand函数中TYPE_I操作正确

只需要在INSTPAT系列宏代码中添加关于opcode是0010011,funct3是000的指令模式

INSTPAT("??????? ????? ????? 000 ????? 00100 11", addi , I, R(rd) = src1 + imm);


### 关于li隐藏的可能bug
伪指令li,是加载立即数,关于立即数的大小.在12位有符号立即数范围内,那么这个指令会被进一步转换为addi指令,如果超过这个范围的立即数,该伪指令会进而转换为lui+addi指令.

所有对于li伪指令,我们需要对lui指令的实现作出及时的考虑.

### 关于实现jal指令
enum枚举中添加TYPE_J类型

定义immJ宏:
imm[20|10:1|11|19:12]
#define immJ() do {*imm = (SEXT(BITS(i, 31, 31), 1) << 19) | BITS(i, 19, 12) << 11 | BITS(i, 20, 20) << 10 | BITS(i, 30, 21);} while(0)

在decode_operand函数中添加对应的switch选项:
case TYPT_J: immJ();break;

在INSTPAT代码块中实现指令模式:由于jal指令的"do"操作有多个部分,所以需要一个代码块来实现:
{
R(rd) = s->snpc;
s->dnpc = s->pc + (imm << 1);
}
这里的imm << 1;表示将立即数左移一位,这是指令体系结构中的设计,目的是为了节省存储不必要的低位0(地址是4对齐的,低位是0)

### 关于ret伪指令
ret指令是编译器指令,会被展开为jalr x0, 0(x1)

所以,需要实现jalr指令.

jalr是I型指令,太好实现了.

其"do"部分,第一步,将物理地址下一条指令存储在rd中,然后将立即数与src1相加,跳转.

###关于jal中的立即数抓取和左移问题
立即数在进行符号扩展,然后左移一位,为何要左移,原因已经说过.

但是4字节对齐的地址,为何是左移1位而不是2位.GPT4解释:因为指令在编码的时候,已经被左移了一位,剩下的需要移动的一位是在cpu内部进行移动.

### 从行为角度解释为何jal中立即数要左移,jalr中立即数不需要移动
定义中,很清楚,jal中的pc值是"干净"的,而立即数是不干净的,所以需要处理立即数.

jalr中的寄存器值,不一定是"干净"的,哪怕将立即数处理"干净"了.
所有没必要处理立即数.

直接将jalr中的立即数将寄存器值相加,得到跳转地址.

### INSTPAT宏的do传参修改以及INSTPAT\_MATCH中的_\_VA_ARGS_\_宏的修改
在编写jal指令的"do"的时候,我需要将代码以代码块的形式编写.

但是在INSTPAT_MATCH中的变参:

_VA_ARGS_;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
这里的分号会和代码块冲突.
于是我将所有的指令的动作代码都使用{},包含起来,并且其中的每一句动作都是一个语句(添加分号)
然后将INSTPAT_MATCH中的_\_VA_ARGS_\_这里,去掉分号.

### 实现R型指令
对比S型指令,R型指令无需抓取立即数;

在enum枚举中添加TYPE_R型;

无immR宏;

在decode_operand函数中添加switch选项;

在INSTPAT代码块中添加add指令模式;

### 关于add.c程序对应的汇编中地址0x80000098中的指令seqz
seqz是伪指令,需要实现其对应的真实cpu指令sltiu指令:

### 关于add.c程序对应的汇编地址0x80000010中的指令beqz
伪指令,被展开为:beq rs1, x0, offset

如果rs1与x0相等,则pc = pc + sext(offset)
这里的offset处理与jal处理一致,原因也是一样的.

添加beq指令步骤:
在enum枚举中添加TYPE_B型;

编写immB型宏;

#define immB() do { *imm = (SEXT(BITS(i, 31, 31), 1) << 11) |
(BITS(i, 7, 7)) << 10 |
(BITS(i, 30, 25)) << 4 |
(BITS(i, 11, 8));} while(0)

1
2
3
4

在decode_operand函数中添加对应的switch选项;

在INSTPAT代码块中添加指令模式:

INSTPAT(“??? ??? ??? 000 ??? 11000 11”, beq , B, {s->dnpc = (src1 == src2)? (s->pc + (imm << 1)) :
s->dnpc;});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

### fact程序中,添加0x80000094处mul指令
R型指令;

只需要在INSTPAT代码块中写对应的指令模式和"do"代码块

### prime程序中,rem指令的添加
这是R型指令,非常简单,但是在取余的过程中,如果除数是0,如何处理.

这个问题是硬件定义的?在这个riscv计算机中,应该如何定义这个行为,并且如何处理

硬件如何处理,还没有确定,也不知如何实现.姑且信赖编译器,让编译器做第一层简易保护.

指令定义中:将x[rs1]和x[rs2]视为补码并相除,向0舍入,将余数写入x[rd]

c语言中已经自动处理了向0舍入的情况.
### slli指令的添加
这个一个I型指令.

但是这个指令的立即数字段,是移位字段.

该字段在32中是5位,在64中是6位.

但是在32中,其shamt[5]必须是0,就也就是func7字段最后一位必须是0.

与其他I型指令不同的是,这里的shamt是指令24:20字段,如果是64就是25:20字段.

其他的都是31:20字段.

可以这样:

和其他I型指令一样读取31:20立即数字段.然后与0000001 11111'b(64)与运算,

这样不仅保留了funct7字段最低位(为 64考虑),还能做到

### srai指令的实现
和slli指令一样,但是在实现算术右移的时,依赖与编译器的默认功能:

当进行的是有符号数右移操作">>", 那么就是算术右移

如果进行的是无符号数右移操作">>",那么就是逻辑右移.

### 关于实现mulh指令
这个指令需要寄存器数(补码)相乘,取高32位.

可以实现为:

{R(rd) = ((int64_t)src1 * src2) >> 32);}


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !