PA通关-pa2

RTFSC

在PA1中,我们知道调用函数cpu_exec(n)会让CPU执行n步,下面我们通过RTFSC看看这个过程是如何实现的

void cpu_exec(uint64_t n)

emmm可以发现这个函数也是个外壳,真正执行CPU计算的函数是execute(n)

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
// $(NEMU_HOME)/src/cpu/cpu-exec.c
void cpu_exec(uint64_t n) {
g_print_step = (n < MAX_INST_TO_PRINT);
switch (nemu_state.state) {
case NEMU_END: case NEMU_ABORT:
printf("Program execution has ended. To restart the program, exit NEMU and run again.\n");
return;
default: nemu_state.state = NEMU_RUNNING;
}

uint64_t timer_start = get_time();

execute(n);

uint64_t timer_end = get_time();
g_timer += timer_end - timer_start;

switch (nemu_state.state) {
case NEMU_RUNNING: nemu_state.state = NEMU_STOP; break;

case NEMU_END: case NEMU_ABORT:
Log("nemu: %s at pc = " FMT_WORD,
(nemu_state.state == NEMU_ABORT ? ANSI_FMT("ABORT", ANSI_FG_RED) :
(nemu_state.halt_ret == 0 ? ANSI_FMT("HIT GOOD TRAP", ANSI_FG_GREEN) :
ANSI_FMT("HIT BAD TRAP", ANSI_FG_RED))),
nemu_state.halt_pc);
// fall through
case NEMU_QUIT: statistic();
}
}

static void execute(uint64_t n)

这个函数也是个壳子,真正的CPU还要看exec_once()函数,如果调用它,CPU会执行一个周期

1
2
3
4
5
6
7
8
9
10
11
// $(NEMU_HOME)/src/cpu/cpu-exec.c
static void execute(uint64_t n) {
Decode s;
for (;n > 0; n --) {
exec_once(&s, cpu.pc);
g_nr_guest_inst ++;
trace_and_difftest(&s, cpu.pc);
if (nemu_state.state != NEMU_RUNNING) break;
IFDEF(CONFIG_DEVICE, device_update());
}
}

这里声明了一个译码结构体变量struct Decode,趁机解释一波:

1
2
3
4
5
6
7
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是下一条动态指令
  • ISADecodeInfo isa是与ISA相关的译码信息

关于静态指令和动态指令的区别,讲义也有详细介绍

对于顺序执行的指令, 它们的snpcdnpc是一样的; 但对于跳转指令, snpcdnpc就会有所不同, dnpc应该指向跳转目标的指令. 显然, 我们应该使用s->dnpc来更新PC, 并且在指令执行的过程中正确地维护s->dnpc.

所以,当前指令执行结束时,下一条要执行的指令一定是dnpc而不是snpc

static void exec_once(Decode *s, vaddr_t pc)

显然,CPU执行是与ISA有关的,而这个函数就是nemu抽象的与ISA无关的最底层的函数,它会调用与ISA相关的isa_exec_once(),让CPU进行一个周期的计算任务。

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
// $(NEMU_HOME)/src/cpu/cpu-exec.c
static void exec_once(Decode *s, vaddr_t pc) {
s->pc = pc;
s->snpc = pc;
isa_exec_once(s);
cpu.pc = s->dnpc;
#ifdef CONFIG_ITRACE
char *p = s->logbuf;
p += snprintf(p, sizeof(s->logbuf), FMT_WORD ":", s->pc);
int ilen = s->snpc - s->pc;
int i;
uint8_t *inst = (uint8_t *)&s->isa.inst.val;
for (i = ilen - 1; i >= 0; i --) {
p += snprintf(p, 4, " %02x", inst[i]);
}
int ilen_max = MUXDEF(CONFIG_ISA_x86, 8, 4);
int space_len = ilen_max - ilen;
if (space_len < 0) space_len = 0;
space_len = space_len * 3 + 1;
memset(p, ' ', space_len);
p += space_len;

#ifndef CONFIG_ISA_loongarch32r
void disassemble(char *str, int size, uint64_t pc, uint8_t *code, int nbyte);
disassemble(p, s->logbuf + sizeof(s->logbuf) - p,
MUXDEF(CONFIG_ISA_x86, s->snpc, s->pc), (uint8_t *)&s->isa.inst.val, ilen);
#else
p[0] = '\0'; // the upstream llvm does not support loongarch32r
#endif
#endif
}

int isa_exec_once(Decode *s)

rv32版本的isa_exec_once()函数为例:函数inst_fetch()完成取址任务,从虚存地址&s->snpc处取出4个字节(一个字)的数据作为指令,并且还会更新静态PC的值。

除了取址,指令的译码执行写回等动作都在函数inst_fetch()中完成。

1
2
3
4
5
6
7
8
9
10
11
// $(NEMU_HOME)/src/isa/riscv32/inst.c
int isa_exec_once(Decode *s) {
s->isa.inst.val = inst_fetch(&s->snpc, 4);
return decode_exec(s);
}
// $(NEMU_HOME)/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;
}

static int decode_exec(Decode *s)

这一函数牵扯到大量的宏展开,理解起来可能会有些复杂。。放在下一节进行介绍把

通过这些宏,我们就可以很方便地通过结构化程序设计为nemu实现指令,实现代码的解偶,提升可维护性

具体来说,实现新的指令时,只需要在INSTPAT(指令模式字符串, 指令名称, 指令类型, 指令行为)宏中依次填入

  • 指令模式字符串:

    0表示相应的位只能匹配0

    1表示相应的位只能匹配1

    ?表示相应的位可以匹配01

  • 指令名字:仅作为注释

  • 指令类型:六种指令类型

  • 指令行为:C语言编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// $(NEMU_HOME)/src/isa/riscv32/inst.c
static int decode_exec(Decode *s) {
int rd = 0;
word_t src1 = 0, src2 = 0, imm = 0;
s->dnpc = s->snpc;

#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_START();
// Instruction
INSTPAT("??????? ????? ????? ??? ????? 00101 11", auipc , U, R(rd) = s->pc + imm); // add upper immediate to pc
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;
}

为了使指令行为的编写更加简单和容易维护,nemu设置了以下在指令行为中常用的数值:

  • src1src2为两个源操作数的值(word_t
  • rd为目的寄存器的索引(int
  • imm为指令中符号扩展后的立即数(word_t

还实现了以下宏:

  • R(i)宏索引为i的通用寄存器,可作为左/右值
  • Mr(addr,len)宏读取起始为addr处的len个虚存字节, 只可作为右值(word_t
  • Mw(addr,len,data)宏将dataword_t)的最低len字节写入addr处的len个虚存字节, 只可作为左值

译码中用到的宏

打眼一看,decode_exec()函数中指令的译码和执行与五个宏有关:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// $(NEMU_HOME)/include/cpu/decode.h
#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): ; }

// $(NEMU_HOME)/src/isa/riscv32/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__ ; \

例如,我们处理指令0x00000297 0b0000000 00000 00101 00101 11 auipc t0,0,将这五个宏展开后如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 宏INSTPAT_START();
{ const void ** __instpat_end = &&__instpat_end_;

// 宏INSTPAT("??????? ????? ????? ??? ????? 00101 11", auipc , U, R(rd) = s->pc + imm);
do {
uint64_t key, mask, shift;
/* Step 1 */
pattern_decode("??????? ????? ????? ??? ????? 00101 11", 38, &key, &mask, &shift);
if ((((uint64_t)s->isa.inst.val >> shift) & mask) == key) {
{
// 宏INSTPAT_MATCH();
/* Step 2 */
decode_operand(s, &rd, &src1, &src2, &imm, TYPE_U);
/* Step 3 */
R(rd) = s->pc + imm;
}
goto *(__instpat_end);
}
} while (0);

// ... 其他INSTPAT宏

// 宏INSTPAT_END();
__instpat_end_: ; }

Step 1 匹配指令格式

pattern_decode()函数将模式字符串中的01抽取到整型变量key中, mask表示key的掩码, 而shift则表示opcode距离最低位的比特数量, 用于帮助编译器进行优化

通过后面的if判断语句,发现匹配auipc指令的格式

Step 2 进一步译码

再通过decode_operand()函数根据不同的指令类型进行进一步译码,包括提取源寄存器,目的寄存器和立即数

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
enum {
TYPE_I, TYPE_U, TYPE_S, TYPE_R, TYPE_B, TYPE_J,
TYPE_N, // none
};

#define src1R() do { *src1 = R(rs1); } while (0)
#define src2R() do { *src2 = R(rs2); } while (0)

#define immI() do { *imm = SEXT(BITS(i, 31, 20), 12); } while(0)
#define immU() do { *imm = SEXT(BITS(i, 31, 12) << 12, 32); } while(0)
#define immS() do { *imm = SEXT((BITS(i, 31, 25) << 5) | BITS(i, 11, 7), 12); } while(0)
#define immB() do { *imm = SEXT((BITS(i, 31, 31) << 12) | (BITS(i, 30, 25) << 5) | (BITS(i, 11, 8) << 1) | (BITS(i, 7, 7) << 11), 13); } while(0)
#define immJ() do { *imm = SEXT((BITS(i, 31, 31) << 20) | (BITS(i, 30, 21) << 1) | (BITS(i, 20, 20) << 11) | (BITS(i, 19, 12) << 12), 21); } while(0)

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.val;
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_R: src1R(); src2R(); break;
case TYPE_B: src1R(); src2R(); immB(); break;
case TYPE_J: immJ(); break;
}
}

Step 3 执行指令语义

将指令行为copy到代码中即。最后通过goto语句跳转出来

实现指令

在实现这些指令的行为时,需要严格遵循riscv32的指令规范,并且要注意类型转换符号扩展

首先介绍三个常用的宏

1
2
3
#define BITMASK(bits) ((1ull << (bits)) - 1)  // make bitmask like 1111...111 len of mask is `bits`
#define BITS(x, hi, lo) (((x) >> (lo)) & BITMASK((hi) - (lo) + 1)) // similar to x[hi:lo] in verilog
#define SEXT(x, len) ({ struct { int64_t n : len; } __x = { .n = x }; (uint64_t)__x.n; }) // signed extended from x[len-1:0] to x[63:0]

BITMASK(bits)返回bits位unsigne long long类型的掩码

BITS(x, hi, lo)对x进行从高位hi到低位lo的切片,类似verilog中的x[hi:lo],返回值的类型同样时unsigne long long

SEXT(x, len)将输入x的最低len位进行符号扩展至uint64_t

下面解释一下SEXT()宏:

1
2
3
4
5
6
7
8
#define SEXT(x, len) ({ 
struct {
int64_t n : len;
} __x = {
.n = x
};
(uint64_t)__x.n;
})

int64_t n : len 这是结构中的位域,表示只取n的后len位,对应于verilog的[len-1:0]位,然后会将len位的数据进行符号位扩展至int64_t,符号位为n[len-1]的数据。
最后强制类型转换为uint64_t

如果不采用这种方式进行符号位扩展的话,就会按照最高位为1的进行扩展,得到所有高位都是1,这种并不是我们想要的,比如一个10为的二进制数据:0010010101,扩展成16位,就会得到1111111110010101。这种不是我们要的结果。
如果按照上文提供的位结构体实现,就会得到0000000010010101,这才是我们想要的结果。

https://blog.csdn.net/weixin_44617175/article/details/131566639

这里先贴一下通过am-kernel的全部cputest需要的指令

1

对几条指令进行说明

  • 对于逻辑移位,由于源操作数src1src2的类型均为无符号的word_t,所以直接进行移位操作即可。
  • 对于数值移位,则需要将源操作数src1src2强制类型转换为有符号的sword_t再进行移位。
  • 对于**I*类型的指令**(sllisrlisrai),它们的译码与I类型指令有差别,需要注意。
  • 对于load指令,由于是一个小于或等于字长的数据从内存加载到长度等于字长的寄存器中,因此会存在扩展的问题。如果将内存中的数据视为有符号数(即lb、lh、lw、ld指令),需要使用SEXT()宏进行符号扩展;如果将内存中的数据视为无符号数(即lbu、lhu指令),则只需要进行零扩展。
  • 对于store指令,需要将寄存器中小于或等于字长的数据存储到指定空间中,不会出现扩展问题。

DeBug

关于riscv工具链

讲义中介绍的是通过apt下载riscv工具链,以下命令即可实现

1
apt-get install g++-riscv64-linux-gnu binutils-riscv64-linux-gnu

然鹅,我在am-kernel`中跑测试集时却发现:这个工具链下的gcc报错了

1
2
3
4
5
6
# Building add-run [riscv32-nemu]
+ CC tests/add.c
cc1: error: ‘-march=rv32im_zicsr’: unsupported ISA subset ‘z’
make[1]: *** [/home/lyq/Desktop/ysyx-workbench/abstract-machine/Makefile:110: /home/lyq/Desktop/ysyx-workbench/am-kernels/tests/cpu-tests/build/riscv32-nemu/tests/add.o] Error 1
test list [1 item(s)]: add
[ add] ***FAIL***

遂STFW,果不其然,发现一位同仁:

[riscv交叉编译器版本问题_/wordsize.h:28:3: error: #error “rv32i-based targe-CSDN博客](https://blog.csdn.net/u014558361/article/details/135372254#:~:text=cc1%3A error%3A ‘-march%3Drv32im zicsr’%3A unsupported isa subset ‘z’,needs a respin to match the new binutils.)

好吧,果然还是apt下载的工具链版本太低,只能自己下源码编译。。。

下载好工具链后,因为编译的工具链与apt下载的工具链名字不一致,因此需要将$(AM_HOME)/scripts/isa/riscv.h中的riscv64-linux-gnu修改为riscv64-unknown-linux-gnu

吭哧吭哧下好riscv工具链源码,又咣叽咣叽编译好,又出现了一个同仁博客中没有的bug:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Building add-run [riscv32-nemu]
# Building am-archive [riscv32-nemu]
+ CC src/platform/nemu/trm.c
In file included from /home/lyq/Desktop/ysyx-workbench/abstract-machine/am/include/am.h:4,
from /home/lyq/Desktop/ysyx-workbench/abstract-machine/am/src/platform/nemu/trm.c:1:
/opt/riscv64-linux/lib/gcc/riscv64-unknown-linux-gnu/13.2.0/include/stdint.h:9:16: fatal error: stdint.h: No such file or directory
9 | # include_next <stdint.h>
| ^~~~~~~~~~
compilation terminated.
make[2]: *** [/home/lyq/Desktop/ysyx-workbench/abstract-machine/Makefile:110: /home/lyq/Desktop/ysyx-workbench/abstract-machine/am/build/riscv32-nemu/src/platform/nemu/trm.o] Error 1
make[1]: *** [/home/lyq/Desktop/ysyx-workbench/abstract-machine/Makefile:129: am] Error 2
test list [1 item(s)]: add
[ add] ***FAIL***

这啥?找不到stdint.h头文件?遂STFW,在Stack Exchange中发现了解决方案:

One way to fix this error is: restrict gcc in using stdint-gcc.h

This can be done by adding c compiler flag

-ffreestanding

to gcc

For more info on what is freestanding visit here
and implied -fno-builtin visit here

compiling - stdint.h: no such file or directory - Unix & Linux Stack Exchange

虽然还不太懂为啥,但只需要加上-ffreestanding这一个gcc flag就行,于是在$(AM_HOME)/scripts/riscv32-nemu.mk中加入

1
CFLAGS  += -ffreestanding

如此一来,报错消失,继续code