PA1

本文是在完成后才整理的文章,因而可能会有所遗漏部分修改内容,经过整理仍旧比较混乱,可阅读性很低,只作为学习记录参考

且本人能力有限,代码可能会出现一些未检查出来的bug

计算机可以没有寄存器吗?

理论可行,这里指的是ISA的通用寄存器,例如堆栈机等

NEMU阶段

配置系统kconfig

使用make menuconfig打开需要的即可。

makefile阅读

$@代指冒号前面

$<是冒号后面的

cc一般是编译器

cflags是编译参数

%:表示通配符,用于匹配任何字符,包括空字符。例如,%.c 匹配所有以 .c 结尾的文件名。

可以先查看make过程中都运行了哪些命令,然后反过来理解$(CFLAGS)等变量的值。 为此, 我们可以键入make -nB, 它会让make程序以”只输出命令但不执行”的方式强制构建目标. 运行后, 你可以看到很多形如

init_monitor()函数

初始化函数init_monitor(),将客户程序读入到客户计算机中。

`代码如下:

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
void init_monitor(int argc, char *argv[]) {
/* Perform some global initialization. */

/* Parse arguments. */
parse_args(argc, argv);

/* Set random seed. */
init_rand();

/* Open the log file. */
init_log(log_file);

/* Initialize memory. */
init_mem();

/* Initialize devices. */
IFDEF(CONFIG_DEVICE, init_device());

/* Perform ISA dependent initialization. */
init_isa();

/* Load the image to memory. This will overwrite the built-in image. */
long img_size = load_img();

/* Initialize differential testing. */
init_difftest(diff_so_file, img_size, difftest_port);

/* Initialize the simple debugger. */
init_sdb();

#ifndef CONFIG_ISA_loongarch32r
IFDEF(CONFIG_ITRACE, init_disasm(
MUXDEF(CONFIG_ISA_x86, "i686",
MUXDEF(CONFIG_ISA_mips32, "mipsel",
MUXDEF(CONFIG_ISA_riscv,
MUXDEF(CONFIG_RV64, "riscv64",
"riscv32"),
"bad"))) "-pc-linux-gnu"
));
#endif

/* Display welcome message. */
welcome();
}

解析函数parse_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
29
static int parse_args(int argc, char *argv[]) {
const struct option table[] = {
{"batch" , no_argument , NULL, 'b'},
{"log" , required_argument, NULL, 'l'},
{"diff" , required_argument, NULL, 'd'},
{"port" , required_argument, NULL, 'p'},
{"help" , no_argument , NULL, 'h'},
{0 , 0 , NULL, 0 },
};
int o;
while ( (o = getopt_long(argc, argv, "-bhl:d:p:", table, NULL)) != -1) {
switch (o) {
case 'b': sdb_set_batch_mode(); break;
case 'p': sscanf(optarg, "%d", &difftest_port); break;
case 'l': log_file = optarg; break;
case 'd': diff_so_file = optarg; break;
case 1: img_file = optarg; return 0;
default:
printf("Usage: %s [OPTION...] IMAGE [args]\n\n", argv[0]);
printf("\t-b,--batch run with batch mode\n");
printf("\t-l,--log=FILE output log to FILE\n");
printf("\t-d,--diff=REF_SO run DiffTest with reference REF_SO\n");
printf("\t-p,--port=PORT run DiffTest with port PORT\n");
printf("\n");
exit(0);
}
}
return 0;
}

getopt_long()的功能和使用方法是:

  1. 函数原型:

    1
    2
    3
    int getopt_long(int argc, char * const argv[],
    const char *optstring,
    const struct option *longopts, int *longindex);

    argv:选项元素。以-开头,然后紧跟一个选项字符。当重复调用函数getopt_long的时候,函数会连续返回选项元素

  2. 对于里面的option结构体的table,是选项表,其实现方法为

    1
    2
    3
    4
    5
    6
    struct option {
    const char *name;
    int has_arg;
    int *flag;
    int val;
    };

    name是选项的名称;

    has_arg:

    若为0或者no_argment,则不需要参数。

    若为1或者required_argument,则需要参数

    flag: 如果为 NULL,返回 val。否则,将 val 存储到 flag 指向的位置,并返回 0

    val:如果 flag 为 NULL,则返回该值,否则存储在 flag 指向的变量中

剩余都是一些初始化的内容。

load_img() 函数

将客户程序镜像加载到模拟内存的起始位置(RESET_VECTOR)。

c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static long load_img() {
if (img_file == NULL) {
Log("No image is given. Use the default build-in image.");
return 4096; // built-in image size
}

FILE *fp = fopen(img_file, "rb");
Assert(fp, "Can not open '%s'", img_file);

fseek(fp, 0, SEEK_END);
long size = ftell(fp);

Log("The image is %s, size = %ld", img_file, size);

fseek(fp, 0, SEEK_SET);
int ret = fread(guest_to_host(RESET_VECTOR), size, 1, fp);
assert(ret == 1);

fclose(fp);
return size;
}
  • 若未指定镜像文件,则使用内置镜像(大小为 4096 字节)。
  • 使用 guest_to_host(RESET_VECTOR) 将客户机物理地址转换为主机内存指针,然后直接读取文件内容至该区域。

这里就是Monitor的初始化工作,它的功能就是设置好ISA和默认程序,初始化内存和cpu状态。

strtok()

1
2
#include <string.h>
char *strtok(char *str, const char *delim);

参数

  • str: 要分割的字符串。在第一次调用时,传入要分割的字符串;后续调用时,传入 NULL,表示继续分割同一个字符串。
  • delim: 分隔符字符串。strtok() 会根据这个字符串中的任意一个字符来分割 str

返回值

返回指向下一个标记的指针。如果没有更多的标记,则返回 NULL。

注意事项
修改原字符串: strtok() 会修改传入的字符串,将分隔符替换为 \0(空字符)。因此,原始字符串会被破坏。

不可重入: strtok() 使用静态缓冲区来保存状态,因此它不是线程安全的。如果在多线程环境中使用,可以考虑使用 strtok_r()(可重入版本)。

连续分隔符: 如果字符串中有连续的分隔符,strtok() 会忽略它们,并返回下一个有效的标记。

模拟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
32
33
34
35
36
37
38
39
40
// 译码相关代码
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;


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); //FMT_WORD : "0x%08x"
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
}

关于优雅的的退出

是因为cmd_q返回值为-1所以调用了return is_exit_status_bad(),直接exit(0)就行了。

基础设施

单步执行

代码实现

1
2
3
4
5
6
7
8
9

static int cmd_si(char *args) {
int n = 1;
if (args != NULL) {
sscanf(args, "%d", &n);
}
cpu_exec(n);
return 0;
}

这里我用了sscanf来弄这个,不过这里我没有做负数的检查什么的。

打印寄存器

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static int cmd_info(char *args) {
char *sub = strtok(args, " ");
if (sub == NULL) {
printf("info: missing subcommand (r, w)\n");
return 0;
}
if (strcmp(sub, "r") == 0) {
isa_reg_display();
} else if (strcmp(sub, "w") == 0) {
display_watchpoints();
} else {
printf("Unknown info subcommand '%s'\n", sub);
}
return 0;
}

这里我是把两者都实现了关于w和r的命令,关于w要后面再说

1
2
3
4
5
6
7
8
void isa_reg_display() {
for (int i = 0; i < 32; i++) {
printf("%-3s:0x%08x ", regs[i], cpu.gpr[i]);
if ((i + 1) % 8 == 0) {
printf("\n");
}
}
}

直接通过isa这个接口来进行输出所有寄存器就行了。

扫描内存

代码实现

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

static int cmd_x(char *args){
if (args == NULL) {
printf("Usage: x N EXPR\n");
return 0;
}

char *n_str = strtok(args, " ");
if (n_str == NULL) {
printf("Missing N\n");
return 0;
}
int n = atoi(n_str);
if (n <= 0) {
printf("N must be positive\n");
return 0;
}

char *expr_str = strtok(NULL, "");
if (expr_str == NULL) {
printf("Missing EXPR\n");
return 0;
}
// 前面是解析 N 和表达式
bool success = false;
word_t addr = expr(expr_str, &success);
if (!success) {
printf("Invalid expression\n");
return 0;
}

for (int i = 0; i < n; i++) {
word_t val = paddr_read(addr + i * 4, 4);
printf("0x%08x: 0x%08x\n", addr + i * 4, val);
}

return 0;
}

设计思路:先用 expr() 求出起始地址,再循环调用 paddr_read() 读取物理内存,每次读 4 字节(一个 word)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
word_t expr(char *e, bool *success) {
if (!make_token(e)) {
*success = false;
return 0;
}
for (int i = 0; i < nr_token; i++) {
if (tokens[i].type == '*') {
if (i == 0) {
tokens[i].type = TK_DEREF; // 行首的 * 一定是解引用
} else {
int prev = tokens[i-1].type;
if (prev == '(' || prev == '+' || prev == '-' ||
prev == '*' || prev == '/' || prev == TK_EQ ||
prev == TK_NEQ || prev == TK_AND) {
tokens[i].type = TK_DEREF; // 前面是运算符或 '(' -> 解引用
}
}
}
}

return eval(0, nr_token - 1, success);

}

设计主要解决两个问题:词法解析区分乘号与解引用运算符

  1. 词法分析:调用 make_token(e) 将输入字符串 e 切分成一个 token 序列(存于全局数组 tokens[] 中),每个 token 包含类型(如数字、运算符、括号等)和值。如果词法分析失败(例如出现非法字符),直接返回 0 并标记 success = false
  2. 二义性消除:遍历所有 token,根据上下文将 '*' 重新分类为 乘法(二元运算符)或 解引用(一元运算符)。
  3. 表达式求值:调用 eval(0, nr_token - 1, success) 对 token 序列进行递归下降求值,得到最终结果。

区分*的两种作用

* 既可以表示乘法(如 a * b),也可以表示指针解引用(如 *p*(p+1))。词法分析器无法单靠一个字符判断其含义,必须借助上下文。

如果 * 前面的 token 是运算符左括号,那么它就是一个一元解引用运算符;否则它就是二元乘法运算符。

make_token

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
static bool make_token(char *e) {

int position = 0;
int i;
regmatch_t pmatch;

nr_token = 0;

while (e[position] != '\0') {
/* Try all rules one by one. */
for (i = 0; i < NR_REGEX; i ++) {
if (regexec(&re[i], e + position, 1, &pmatch, 0) == 0 && pmatch.rm_so == 0) {
char *substr_start = e + position;
int substr_len = pmatch.rm_eo;

Log("match rules[%d] = \"%s\" at position %d with len %d: %.*s",
i, rules[i].regex, position, substr_len, substr_len, substr_start);

position += substr_len;

/* TODO: Now a new token is recognized with rules[i]. Add codes
* to record the token in the array `tokens'. For certain types
* of tokens, some extra actions should be performed.
*/

switch (rules[i].token_type) {
case TK_NOTYPE: // 忽略空白、注释等无用字符
break;

case TK_HEX:
case TK_DEC:
case TK_REG: // 复制匹配到的字符串(最多31字符,留1给'\0')
tokens[nr_token].type = rules[i].token_type;
{
int len = (substr_len < 31) ? substr_len : 31;
strncpy(tokens[nr_token].str, substr_start, len);
tokens[nr_token].str[len] = '\0';
}
nr_token++;
break;

default:
tokens[nr_token].type = rules[i].token_type;
tokens[nr_token].str[0] = substr_start[0];
tokens[nr_token].str[1] = '\0';
nr_token++;
break;
}
break;
}
}

if (i == NR_REGEX) {
printf("no match at position %d\n%s\n%*.s^\n", position, e, position, "");
return false;
}
}

return true;
}

通过正则表达式来记录token,这个是最终的版本,一开始肯定不是这样的,这是经过一次次完善出来的

函数依赖一个全局的规则表 rules[],每个规则包含:

  • 正则表达式字符串(如 "[0-9]+" 匹配十进制数)
  • 对应的 token 类型(如 TK_DEC
  • 预编译的正则表达式 re[i]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static struct rule {
const char *regex;
int token_type;
} rules[] = {

/* TODO: Add more rules.
* Pay attention to the precedence level of different rules.
*/

{" +", TK_NOTYPE}, // spaces
{"0[xX][0-9a-fA-F]+", TK_HEX},
{"[0-9]+", TK_DEC},
{"\\+", '+'}, // plus
{"\\$[a-zA-Z0-9]+", TK_REG}, // regs
{"-", '-'},
{"\\*", '*'},
{"/", '/'},
{"\\(", '('},
{"\\)", ')'},
{"==", TK_EQ}, // equal
{"!=", TK_NEQ},
{"&&", TK_AND},
};

这些规则按照优先级顺序存储在数组中。匹配时按顺序尝试,第一个完整匹配(pmatch.rm_so == 0 表示从当前位置开始匹配)的规则胜出,从而自然处理了关键字的优先级(例如 "0x" 前缀应在普通数字之前匹配)。

eval

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

static word_t eval(int p, int q, bool *success) {
if (p > q) {
*success = false;
return 0;
}

// 处理一元负号
if (tokens[p].type == '-') {
// 判断是否为一元负号:表达式开头 或 前一个token是左括号或运算符
bool is_unary = (p == 0);
if (!is_unary) {
int prev_type = tokens[p-1].type;
if (prev_type == '(' || prev_type == '+' || prev_type == '-' ||
prev_type == '*' || prev_type == '/') {
is_unary = true;
}
}
if (is_unary) {
if (p == q) { // 单独的负号,非法
*success = false;
return 0;
}
word_t val = eval(p+1, q, success);
if (!(*success)) return 0;
*success = true;
return -val;
}
}

// 处理指针解引用
if (tokens[p].type == TK_DEREF) {
if (p == q) {
*success = false;
return 0;
}
word_t addr = eval(p+1, q, success);
if (!(*success)) return 0;
*success = true;
return paddr_read(addr, 4);
}

// 处理括号
if (check_parentheses(p, q)) {
return eval(p+1, q-1, success);
}

// 单个数字
if (p == q) {
if (tokens[p].type == TK_HEX) {
char *endptr;
word_t val = strtoul(tokens[p].str, &endptr, 16);
*success = true; return val;
} else if (tokens[p].type == TK_DEC) {
char *endptr;
word_t val = strtoul(tokens[p].str, &endptr, 10);
*success = true; return val;
} else if (tokens[p].type == TK_REG) {
word_t val = isa_reg_str2val(tokens[p].str, success);
return val;
} else {
*success = false; return 0;
}
}

// 找主运算符(二元)
int op = find_main_op(p, q);
if (op == -1) {
*success = false;
return 0;
}

word_t val1 = eval(p, op-1, success);
if (!(*success)) return 0;
word_t val2 = eval(op+1, q, success);
if (!(*success)) return 0;

switch (tokens[op].type) {
case '+': *success = true; return val1 + val2;
case '-': *success = true; return val1 - val2;
case '*': *success = true; return val1 * val2;
case '/':
if (val2 == 0) { *success = false; return 0; }
*success = true; return val1 / val2;
case TK_EQ: *success = true; return (val1 == val2) ? 1 : 0;
case TK_NEQ: *success = true; return (val1 != val2) ? 1 : 0;
case TK_AND: *success = true; return (val1 && val2) ? 1 : 0;
case TK_DEREF: {
uint32_t addr = val1;
*success = true;
return paddr_read(addr, 4);// 从模拟内存读取 4 字节
}
default:
*success = false;
return 0;
}
}

思路就是eval(p, q) 计算从 tokens[p]tokens[q] 这一子表达式的值,采用分治法

  1. 处理一元运算符(负号 - 和 解引用 TK_DEREF
  2. 处理括号(递归剥离最外层括号)
  3. 处理数字、寄存器
  4. 找到主运算符(最低优先级的二元运算符),递归计算左右子表达式,再应用运算

负号 '-' 在词法分析时被标记为普通运算符,需要根据上下文区分一元负号还是二元减法。

ps:此处负号的处理仍有问题,会把整个后面都取负号。

判断规则:

  • 位于表达式开头(p == 0)→ 一元负号
  • 前一个 token 是左括号或运算符 → 一元负号
  • 否则为二元减法(后面由主运算符逻辑处理)

解引用也是一元运算符,其操作数是后续的子表达式(应计算出地址),然后从客户机物理内存中读取 4 字节的值。

find_main_op

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

static int find_main_op(int p, int q) {
int op_pos = -1;
int best_pri = 100;
int bracket_depth = 0;

for (int i = p; i <= q; i++) {
if (tokens[i].type == '(') {
bracket_depth++;
} else if (tokens[i].type == ')') {
bracket_depth--;
} else if (bracket_depth == 0) {
int pri = -1;
int type = tokens[i].type;

// 设置优先级(数值越小优先级越高)
if (type == TK_EQ || type == TK_NEQ) pri = 3; // 最低优先级
else if (type == TK_AND) pri = 4; // 稍高
else if (type == '+' || type == '-') pri = 2; // 加减
else if (type == '*' || type == '/') pri = 1; // 乘除

if (pri != -1) {
if (op_pos == -1 || pri > best_pri) {
best_pri = pri;
op_pos = i;
}
else if (pri == best_pri && i > op_pos) {
op_pos = i;
}
}
}
}
return op_pos;
}

扫描 tokens[p..q] 区间,忽略括号内的内容,找出优先级最低(即 pri 数值最大)的二元运算符,并返回其下标。若找不到任何运算符则返回 -1。这个结果将被 eval 函数用于递归求值。

同优先级处理:当遇到优先级相同(pri == best_pri)的运算符时,选择最右边的那个(i > op_pos),这实现了左结合。例如 a - b - c 被解释为 (a - b) - c 而不是 a - (b - c)

eval 函数中,当表达式不是一元运算符、括号或值时,会调用 find_main_op(p, q) 获得主运算符位置 op,然后递归计算 eval(p, op-1)eval(op+1, q),最后根据 tokens[op].type 执行运算。

表达式求值

见上面

表达式生成器

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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
/***************************************************************************************
* Copyright (c) 2014-2024 Zihao Yu, Nanjing University
*
* NEMU is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
*
* See the Mulan PSL v2 for more details.
***************************************************************************************/

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <assert.h>
#include <string.h>

static char buf[65536] = {};
static char code_buf[65536 + 128] = {};
static char *code_format =
"#include <stdio.h>\n"
"int main() { "
" unsigned result = %s; "
" printf(\"%%u\", result); "
" return 0; "
"}";

static char *p = buf;
static int depth = 0;
#define MAX_DEPTH 10

static uint32_t choose(uint32_t n) {
return rand() % n;
}

static void gen_num() {
uint32_t num = rand() % 65536;
p += sprintf(p, "%u", num);
}
static void gen_op() {
char op = "+-*/"[choose(4)];
*p++ = op;
}

static void gen_rand_expr_rec() {
if (depth >= 3) {
gen_num();
return;
}
depth++;
switch (choose(3)) {
case 0:
gen_num();
break;
case 1:
*p++ = '(';
gen_rand_expr_rec();
*p++ = ')';
break;
default:
gen_rand_expr_rec();
gen_op();
if (*(p-1) == '/') {
char *save = p;
gen_rand_expr_rec();
if (p - save == 1 && *save == '0') {
p = save;
p += sprintf(p, "1");
}
} else {
gen_rand_expr_rec();
}
break;
}
depth--;
}

void gen_rand_expr() {
buf[0] = '\0';
p = buf;
depth = 0;
gen_rand_expr_rec();
*p = '\0';
}

int main(int argc, char *argv[]) {
int seed = time(0);
srand(seed);
int loop = 1;
if (argc > 1) {
sscanf(argv[1], "%d", &loop);
}
for (int i = 0; i < loop; i++) {
gen_rand_expr();

sprintf(code_buf, code_format, buf);

FILE *fp = fopen("/tmp/.code.c", "w");
assert(fp != NULL);
fputs(code_buf, fp);
fclose(fp);

int ret = system("gcc /tmp/.code.c -o /tmp/.expr");
if (ret != 0) continue;

fp = popen("/tmp/.expr", "r");
assert(fp != NULL);

int result;
ret = fscanf(fp, "%d", &result);
pclose(fp);

printf("%u %s\n", result, buf);
}
return 0;
}

整体思路

  1. 随机生成表达式:递归构造一个合法的算术表达式,限制深度避免溢出。(虽然还是有溢出)
  2. 嵌入 C 程序:将表达式字符串填入一个 C 程序的 main 函数模板中。
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
static void gen_rand_expr_rec() {
if (depth >= 3) { // 达到深度限制,生成数字
gen_num();
return;
}
depth++;
switch (choose(3)) { // 随机选择三种产生式
case 0: gen_num(); break;
case 1: *p++ = '('; gen_rand_expr_rec(); *p++ = ')'; break;
default:
gen_rand_expr_rec(); // 左操作数
gen_op(); // 运算符
if (*(p-1) == '/') { // 若为除法,避免除0
char *save = p;
gen_rand_expr_rec(); // 右操作数
if (p - save == 1 && *save == '0') {
p = save;
p += sprintf(p, "1");
}
} else {
gen_rand_expr_rec(); // 右操作数
}
break;
}
depth--;
}

监视点

1
2
3
4
5
6
7
wp_pool[32](静态数组)

├── free_ 链表(空闲监视点)
│ wp_pool[0] → wp_pool[1] → ... → wp_pool[31] → NULL

└── head 链表(已使用监视点)
wp_n → wp_m → ... → NULL

每个监视点存储:

  • NO:编号

  • expr:表达式字符串(动态分配)

  • old_val:上次求值结果

    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
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132

    #include "sdb.h"

    #define NR_WP 32

    typedef struct watchpoint {
    int NO;
    struct watchpoint *next;
    char *expr;
    word_t old_val;

    } WP;

    static WP wp_pool[NR_WP] = {};
    static WP *head = NULL, *free_ = NULL;
    static void free_wp(WP *wp);

    void init_wp_pool() {
    int i;
    for (i = 0; i < NR_WP; i ++) {
    wp_pool[i].NO = i;
    wp_pool[i].next = (i == NR_WP - 1 ? NULL : &wp_pool[i + 1]);
    }

    head = NULL;
    free_ = wp_pool;
    }

    static WP* alloc_wp() {
    if (free_ == NULL) {
    printf("No free watchpoint slots.\n");
    return NULL;
    }
    WP *wp = free_;
    free_ = free_->next;
    wp->next = NULL;
    wp->expr = NULL;
    return wp;
    }

    bool free_wp_by_no(int no) {
    WP *prev = NULL;
    WP *curr = head;

    while (curr) {
    if (curr->NO == no) {
    // 从链表中移除
    if (prev) {
    prev->next = curr->next;
    } else {
    head = curr->next;
    }
    // 释放监视点
    free_wp(curr);
    printf("Watchpoint %d deleted\n", no);
    return true;
    }
    prev = curr;
    curr = curr->next;
    }
    return false;
    }

    bool check_watchpoints() {
    WP *wp = head;
    bool triggered = false;

    while (wp) {
    bool success;
    word_t new_val = expr(wp->expr, &success);

    if (success && new_val != wp->old_val) {
    printf("\nWatchpoint %d: %s\n", wp->NO, wp->expr);
    printf("Old value = %u (0x%08x)\n", wp->old_val, wp->old_val);
    printf("New value = %u (0x%08x)\n", new_val, new_val);
    wp->old_val = new_val;
    triggered = true;
    }
    wp = wp->next;
    }

    return triggered;
    }


    void display_watchpoints() {
    if (head == NULL) {
    printf("No watchpoints.\n");
    return;
    }

    printf("Num Expression\n");
    WP *wp = head;
    while (wp) {
    printf("%d %s\n", wp->NO, wp->expr);
    wp = wp->next;
    }
    }

    static void free_wp(WP *wp) {
    if (wp->expr) {
    free(wp->expr);
    wp->expr = NULL;
    }
    wp->next = free_;
    free_ = wp;
    }

    WP* new_wp(char *expr_str) {
    WP *wp = alloc_wp();
    if (wp == NULL) return NULL;

    bool success = false;
    word_t val = expr(expr_str, &success);
    if (!success) {
    printf("Invalid expression: %s\n", expr_str);
    free_wp(wp);
    return NULL;
    }

    wp->expr = strdup(expr_str);
    wp->old_val = val;
    wp->next = head;
    head = wp;

    printf("Watchpoint %d: %s\n", wp->NO, wp->expr);
    return wp;
    }
    WP* get_wp_head() {
    return head;
    }

    init_wp_pool()

    初始化对象池,将 wp_pool 中的节点串成一条空闲链表,活跃链表置空。

    c

    1
    2
    3
    4
    5
    6
    7
    8
    9
    void init_wp_pool() {
    int i;
    for (i = 0; i < NR_WP; i ++) {
    wp_pool[i].NO = i;
    wp_pool[i].next = (i == NR_WP - 1 ? NULL : &wp_pool[i + 1]);
    }
    head = NULL;
    free_ = wp_pool;
    }
    • 每个节点的 NO 赋值为其下标。
    • 节点按数组顺序链接:wp_pool[i]next 指向 wp_pool[i+1],最后一个指向 NULL
    • free_ 指向第一个空闲节点(wp_pool[0])。

    alloc_wp()

    从空闲链表中分配一个监视点节点。

    c

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    static WP* alloc_wp() {
    if (free_ == NULL) {
    printf("No free watchpoint slots.\n");
    return NULL;
    }
    WP *wp = free_;
    free_ = free_->next;
    wp->next = NULL;
    wp->expr = NULL;
    return wp;
    }
    • 如果 free_ 为空(已用完所有监视点),报错返回 NULL
    • 取下 free_ 指向的第一个节点,并更新 free_ 指向下一个。
    • 初始化新节点的 next = NULLexpr = NULL(调用者负责填充)。

    free_wp()

    将节点放回空闲链表(释放资源)。

    c

    1
    2
    3
    4
    5
    6
    7
    8
    static void free_wp(WP *wp) {
    if (wp->expr) {
    free(wp->expr);
    wp->expr = NULL;
    }
    wp->next = free_;
    free_ = wp;
    }
    • 先释放 wp->expr 占用的动态内存(若存在),并置空。
    • wp 插入空闲链表头部(头插法)。

    free_wp_by_no()

    根据监视点编号删除一个监视点。

    c

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    bool free_wp_by_no(int no) {
    WP *prev = NULL;
    WP *curr = head;
    while (curr) {
    if (curr->NO == no) {
    // 从活跃链表中移除
    if (prev) {
    prev->next = curr->next;
    } else {
    head = curr->next;
    }
    free_wp(curr); // 放回空闲链表
    printf("Watchpoint %d deleted\n", no);
    return true;
    }
    prev = curr;
    curr = curr->next;
    }
    return false;
    }
    • 遍历活跃链表 head,找到编号为 no 的节点。
    • 调整前驱节点的 next 指针将其跳过。
    • 调用 free_wp 将节点放回空闲池并释放表达式字符串。
    • 返回 true 表示删除成功,否则 false

    new_wp()

    创建一个新的监视点。

    c

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    WP* new_wp(char *expr_str) {
    WP *wp = alloc_wp();
    if (wp == NULL) return NULL;

    bool success = false;
    word_t val = expr(expr_str, &success);
    if (!success) {
    printf("Invalid expression: %s\n", expr_str);
    free_wp(wp);
    return NULL;
    }

    wp->expr = strdup(expr_str);
    wp->old_val = val;
    wp->next = head;
    head = wp;

    printf("Watchpoint %d: %s\n", wp->NO, wp->expr);
    return wp;
    }

步骤

  1. 分配一个空闲节点。
  2. 调用 expr() 对表达式字符串进行预求值,验证其合法性。
    • 若不合法,释放节点并报错返回。
  3. 复制表达式字符串(strdup)到 wp->expr
  4. 保存当前求值结果到 old_val
  5. 头插法将新节点插入活跃链表(next = head; head = wp)。
  6. 打印成功信息并返回节点指针。

check_watchpoints()

遍历所有监视点,重新求值并与旧值比较,若改变则输出信息并返回 true

c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bool check_watchpoints() {
WP *wp = head;
bool triggered = false;

while (wp) {
bool success;
word_t new_val = expr(wp->expr, &success);

if (success && new_val != wp->old_val) {
printf("\nWatchpoint %d: %s\n", wp->NO, wp->expr);
printf("Old value = %u (0x%08x)\n", wp->old_val, wp->old_val);
printf("New value = %u (0x%08x)\n", new_val, new_val);
wp->old_val = new_val;
triggered = true;
}
wp = wp->next;
}
return triggered;
}
  • 依次对每个活跃监视点调用 expr(wp->expr, &success) 求新值。
  • 如果求值成功且新值与 old_val 不同,则打印新旧值(十进制和十六进制),更新 old_val,并设置 triggered = true
  • 返回值表示是否有监视点被触发(模拟器可据此暂停执行)。

display_watchpoints()

打印所有已设置的监视点。

c

1
2
3
4
5
6
7
8
9
10
11
12
void display_watchpoints() {
if (head == NULL) {
printf("No watchpoints.\n");
return;
}
printf("Num Expression\n");
WP *wp = head;
while (wp) {
printf("%d %s\n", wp->NO, wp->expr);
wp = wp->next;
}
}
  • 简单遍历活跃链表,输出编号和表达式。

get_wp_head()

返回活跃链表头指针。

c

1
2
3
WP* get_wp_head() {
return head;
}

isa_reg_str2val()

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
word_t isa_reg_str2val(const char *s, bool *success) {
if (s[0] == '$') s++;

if (strcmp(s, "pc") == 0) {
*success = true;
return cpu.pc;
}

for (int i = 0; i < 32; i++) {
if (strcmp(s, regs[i]) == 0) {
*success = true;
return cpu.gpr[i];
}
}

if (s[0] == 'x') {
int idx = atoi(s + 1);
if (idx >= 0 && idx <= 31) {
*success = true;
return cpu.gpr[idx];
}
}

*success = false;
return 0;
}
  1. 去除可选的前缀 $:如果字符串以 $ 开头,则跳过该字符,得到纯寄存器名。这样用户既可以使用 $pc 也可以使用 pc 来访问程序计数器。
  2. 特殊处理程序计数器 pc:若字符串是 "pc",则直接返回全局 CPU 状态中的 cpu.pc,并设置成功标志为 true
  3. 匹配通用寄存器:遍历预定义的寄存器名数组 regs[32](该数组与 cpu.gpr[32] 的索引一一对应),找到匹配项后返回相应通用寄存器的值。
  4. 支持数字索引形式:如果字符串以 'x' 开头,则尝试将后续字符解析为整数(例如 x0x31)。若解析得到的索引在 0~31 范围内,则直接返回 cpu.gpr[idx]
  5. 失败处理:如果以上均未匹配,则设置 *success = false,并返回 0。

execute()(在cpu-exec.c里面)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

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 (check_watchpoints()) {
nemu_state.state = NEMU_STOP;// 触发则停止模拟器
if (nemu_state.state != NEMU_RUNNING) break; // 跳出循环
}


if (nemu_state.state != NEMU_RUNNING) break;
IFDEF(CONFIG_DEVICE, device_update());
}
}

关于前面测试的事情

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

static int parse_args(int argc, char *argv[]) {
const struct option table[] = {
{"batch" , no_argument , NULL, 'b'},
{"log" , required_argument, NULL, 'l'},
{"diff" , required_argument, NULL, 'd'},
{"port" , required_argument, NULL, 'p'},
{"help" , no_argument , NULL, 'h'},
{"test" , required_argument, NULL, 't'},
{0 , 0 , NULL, 0 },
};
int o;
while ( (o = getopt_long(argc, argv, "-bhl:d:p:t:", table, NULL)) != -1) {
switch (o) {
case 'b': sdb_set_batch_mode(); break;
case 'p': sscanf(optarg, "%d", &difftest_port); break;
case 'l': log_file = optarg; break;
case 'd': diff_so_file = optarg; break;
case 't': test_file = optarg; break;
case 1: img_file = optarg; return 0;
default:
printf("Usage: %s [OPTION...] IMAGE [args]\n\n", argv[0]);
printf("\t-b,--batch run with batch mode\n");
printf("\t-l,--log=FILE output log to FILE\n");
printf("\t-d,--diff=REF_SO run DiffTest with reference REF_SO\n");
printf("\t-p,--port=PORT run DiffTest with port PORT\n");
printf("\t-t,--test=FILE run expression test from FILE\n");
printf("\n");
exit(0);
}
}
return 0;
}

我修改了这个参数选取,增加了-t来测试,往初始化里面加了一个专门测试的入口

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
void init_monitor(int argc, char *argv[]) {
/* Perform some global initialization. */

/* Parse arguments. */
parse_args(argc, argv);
if (test_file != NULL) {
init_sdb();
sdb_expr_test(test_file);
exit(0);
}
/* Set random seed. */
init_rand();

/* Open the log file. */
init_log(log_file);

/* Initialize memory. */
init_mem();

/* Initialize devices. */
IFDEF(CONFIG_DEVICE, init_device());

/* Perform ISA dependent initialization. */
init_isa();

/* Load the image to memory. This will overwrite the built-in image. */
long img_size = load_img();

/* Initialize differential testing. */
init_difftest(diff_so_file, img_size, difftest_port);

/* Initialize the simple debugger. */
init_sdb();

IFDEF(CONFIG_ITRACE, init_disasm());

/* Display welcome message. */
welcome();
}

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
void sdb_expr_test(const char *filename) {
FILE *fp = fopen(filename, "r");
if (!fp) {
printf("Cannot open file: %s\n", filename);
return;
}

char line[65536];
int total = 0, passed = 0, failed = 0;
printf("Running expression tests from %s ...\n", filename);

while (fgets(line, sizeof(line), fp)) {
line[strcspn(line, "\n")] = '\0'; // 去除换行符
if (line[0] == '\0') continue; // 跳过空行

char *space = strchr(line, ' '); // 查找空格分隔符
if (!space) continue; // 无空格则格式错误,跳过
*space = '\0'; // 将空格替换为 '\0' 以分割字符串
//ps:有潜在问题,里面随机加的空格也会变成\0
unsigned long expected = strtoul(line, NULL, 10); // 前半部分转为数字
char *expr_str = space + 1; // 后半部分即表达式字符串


bool success;
uint32_t result = expr(expr_str, &success);

total++;
if (success && result == expected) {
passed++;
} else {
failed++;
printf("[FAIL] %s\n", expr_str);
printf(" expected: %u, got: %u\n", (unsigned)expected, result);
}
}
fclose(fp);
printf("Total: %d, Passed: %d, Failed: %d\n", total, passed, failed);
}

riscv32有哪几种指令格式?

R-type, I-type, S-type, U-type 四种核心格式,并在随后添加了基于立即数处理的变种 B-typeJ-type。因此,总共有 6种 基本指令格式

LUI指令的行为是什么?

具体来说,该指令会把一个 20位的立即数 加载到目的寄存器 rd高20位,而寄存器的低12位则被填充为0

mstatus寄存器的结构是怎么样的?

mstatus 是一个 XLEN 位的 CSR (Control and Status Register)。它包含了许多控制处理器状态的字段,例如中断使能(MIE等)、异常处理前的特权级(MPP)、以及浮点单元状态(FS)等。具体位域布局可在手册“Machine Status Registers”章节的详细说明中找到

Shell命令

  • 代码行数统计方法
    • 统计所有 .c 和 .h 文件行数(含空行和注释):使用 find nemu/ -name "*.c" -or -name "*.h" | xargs cat | wc -l 命令。
    • 统计所有 .c 和 .h 文件代码行数(除去空行):使用 find nemu/ -name "*.c" -or -name "*.h" | xargs cat | grep -v '^[[:space:]]*$' | wc -l 命令。
    • 统计PA1阶段新增或修改的代码行数:使用 git diff pa0 --shortstat -- . ":(exclude)nemu/tools" 命令。
    • 将统计命令集成到Makefile中:可以在 nemu/Makefile 文件中增加一个 count 目标,这样在终端中直接执行 make count 即可自动统计并输出结果。

编译选项解析

nemu/scripts/build.mk 文件中可以看到 -Wall-Werror 这两个选项。它们的含义是:

  • -Wall-W 表示Warning,all 表示“所有”。它用于开启GCC的大部分常见警告信息,帮助发现潜在的代码隐患。
  • -Werror-Werror 的含义是将所有编译警告(Warning)当作错误(Error)处理。只要代码触发了任何警告,编译过程就会失败并停止,强迫开发者修复所有警告。