计算机可以没有寄存器吗? (建议二周目思考)
如果没有寄存器, 计算机还可以工作吗? 如果可以, 这会对硬件提供的编程模型有什么影响呢?
就算你是二周目来思考这个问题, 你也有可能是第一次听到”编程模型”这个概念. 不过如果一周目的时候你已经仔细地阅读过 ISA 手册, 你会记得确实有这么个概念. 所以, 如果想知道什么是编程模型, RTFM 吧.
Example
可以没有寄存器,通过对 cache 中每个单元进行命名,实际上也能达到寄存器的效果
编程模型(Programming model)是指一种描述计算机程序运行方式的抽象概念。它定义了程序员与计算机系统之间的交互方式,包括如何表示数据、如何组织代码、如何控制程序的执行流程等。
编程模型通常包含一组规则,用于指导程序员创建程序,并定义了程序员需要使用的特定编程语言、API、库和工具集。编程模型的设计可以极大地影响程序的可维护性、可扩展性和可重用性。
常见的编程模型包括面向过程编程(Procedural Programming)、面向对象编程(Object-Oriented Programming)、函数式编程(Functional Programming)、事件驱动编程(Event-Driven Programming)等。每种编程模型都有其独特的特点,可以根据不同的需求选择合适的编程模型来进行编程。
kconfig 生成的宏与条件编译
我们已经在上文提到过, kconfig 会根据配置选项的结果在 nemu/include/generated/autoconf.h
中定义一些形如CONFIG_xxx
的宏, 我们可以在 C 代码中通过条件编译的功能对这些宏进行测试, 来判断是否编译某些代码. 例如, 当CONFIG_DEVICE
这个宏没有定义时, 设备相关的代码就无需进行编译.
为了编写更紧凑的代码, 我们在nemu/include/macro.h
中定义了一些专门用来对宏进行测试的宏. 例如IFDEF(CONFIG_DEVICE, init_device());
表示, 如果定义了CONFIG_DEVICE
, 才会调用init_device()
函数; 而MUXDEF(CONFIG_TRACE, "ON", "OFF")
则表示, 如果定义了CONFIG_TRACE
, 则预处理结果为"ON"
("OFF"
在预处理后会消失), 否则预处理结果为"OFF"
.
这些宏的功能非常神奇, 你知道这些宏是如何工作的吗?
预处理器解析替换
为什么全部都是函数?
阅读init_monitor()
函数的代码, 你会发现里面全部都是函数调用. 按道理, 把相应的函数体在init_monitor()
中展开也不影响代码的正确性. 相比之下, 在这里使用函数有什么好处呢?
面向过程编程的一种编程范式
参数的处理过程
另外的一个问题是, 这些参数是从哪里来的呢?
命令行接收的参数
究竟要执行多久?
在cmd_c()
函数中, 调用cpu_exec()
的时候传入了参数-1
, 你知道这是什么意思吗?
Example
表示一次
yield
,具体可查看cpu_exec()
中对参数n
的调用:
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());}}可以发现,当
n
为 -1 时并不会进入循环
潜在的威胁 (建议二周目思考)
“调用cpu_exec()
的时候传入了参数-1
”, 这一做法属于未定义行为吗? 请查阅 C99 手册确认你的想法.
Example
根据
GPT
的解释:C99 标准中定义了一些行为,如果程序中出现了这些行为,编译器或运行时环境可以采取任意的行动,包括崩溃、输出错误结果、产生不可预测的行为等。这些行为被称为未定义行为(Undefined Behavior)。
以下是一些常见的未定义行为:
- 访问未初始化的变量:未初始化的变量的值是未定义的,它可能包含任意值,包括程序崩溃的值。
- 数组越界访问:访问数组越界是一种未定义的行为,它可能导致程序崩溃、产生不可预测的结果或被攻击者利用。
- 使用空指针:使用空指针是一种未定义的行为,它可能导致程序崩溃或产生不可预测的结果。
- 除数为零:除以零是一种未定义的行为,它可能导致程序崩溃或产生不可预测的结果。
- 同时修改同一变量的多个线程:在多线程程序中,如果多个线程同时修改同一个变量,则可能导致数据竞争,产生不可预测的结果。
在这里传入的
-1
并不属于未定义行为
谁来指示程序的结束?
在程序设计课上老师告诉你, 当程序执行到main()
函数返回处的时候, 程序就退出了, 你对此深信不疑. 但你是否怀疑过, 凭什么程序执行到main()
函数的返回处就结束了? 如果有人告诉你, 程序设计课上老师的说法是错的, 你有办法来证明/反驳吗? 如果你对此感兴趣, 请在互联网上搜索相关内容.
有始有终 (建议二周目思考)
对于 GNU/Linux 上的一个程序, 怎么样才算开始? 怎么样才算是结束? 对于在 NEMU 中运行的程序, 问题的答案又是什么呢?
与此相关的问题还有: NEMU 中为什么要有nemu_trap
? 为什么要有 monitor?
sdb 实现
帮助(1) | help | help | 打印命令的帮助信息 |
---|---|---|---|
继续运行(1) | c | c | 继续运行被暂停的程序 |
退出(1) | q | q | 退出 NEMU |
单步执行 | si [N] | si 10 | 让程序单步执行N 条指令后暂停执行, 当N 没有给出时, 缺省为1 |
打印程序状态 | info SUBCMD | info r info w | 打印寄存器状态 打印监视点信息 |
扫描内存(2) | x N EXPR | x 10 $esp | 求出表达式EXPR 的值, 将结果作为起始内存 地址, 以十六进制形式输出连续的N 个 4 字节 |
表达式求值 | p EXPR | p $eax + 1 | 求出表达式EXPR 的值, EXPR 支持的 运算请见调试中的表达式求值小节 |
设置监视点 | w EXPR | w *0x2000 | 当表达式EXPR 的值发生变化时, 暂停程序执行 |
删除监视点 | d N | d 2 | 删除序号为N 的监视点 |
单步执行
static int cmd_si(char *args) { int step = args == NULL ? 1 : atoi(args); if (step <= 0) { return -1; } cpu_exec(step); return 0;}
打印寄存器
static int cmd_info(char *args) { char subcmd = *args; switch (subcmd) { case 'r': isa_reg_display(); break; case 'w': TODO(); break; default: return -1; } return 0;}
扫描内存
static int cmd_scan(char *args) { char *argn = strtok(args, " "); char *argexpr = argn + strlen(argn) + 1; if (argn == NULL || argexpr == NULL) { return -1; } int nbyte = atoi(argn); if (nbyte < 0) { return -1; } // TODO implement expr function vaddr_t addr = 0x80000000; for (int i = 0; i < nbyte; i++) { vaddr_t current_addr = addr; printf("0x%08x: 0x%08x\n", current_addr, vaddr_read(current_addr, 4)); current_addr += 4; } return 0;}
表达式求值
词法分析
在这里,由于表达式不止数值类型一种,因此在设计 TOKEN
类型时多加了寄存器与变量类型,设计如下:
enum { TK_NOTYPE = 256, TK_EQ, TK_INT, TK_OP, TK_REG, TK_VAR
};
static struct rule { const char *regex; int token_type;} rules[] = {
{" +", TK_NOTYPE}, // spaces {"\\t", TK_NOTYPE}, // tab {"\\+", TK_OP}, // plus {"==", TK_EQ}, // equal {"\\(", '('}, // '(' {"\\)", ')'}, // ')' {"-", TK_OP}, // '-' {"\\*", TK_OP}, // '*' {"/", TK_OP}, // '/' {"[0-9]+", TK_INT}, // integer {"\\$[a-zA-Z0-9]+", TK_REG}, // reg {"[a-zA-Z_]+", TK_VAR}, // variable};
由于写过编译原理,这部分还算简单,需要注意的是我们对 rules
的排序必须严格,因为是通过 switch
语句来判断的。
随后,我们对字符串流一一判断,然后存入到 tokens
列表中:
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: simplify the code switch (rules[i].token_type) { case TK_NOTYPE: break; case ')': tokens[nr_token].type = ')'; strncpy(tokens[nr_token].str, substr_start, substr_len); tokens[nr_token].str[substr_len] = '\0'; ++nr_token; break; case '(': tokens[nr_token].type = '('; strncpy(tokens[nr_token].str, substr_start, substr_len); tokens[nr_token].str[substr_len] = '\0'; ++nr_token; break; case TK_EQ: tokens[nr_token].type = TK_EQ; strncpy(tokens[nr_token].str, substr_start, substr_len); tokens[nr_token].str[substr_len] = '\0'; ++nr_token; break; case TK_OP: tokens[nr_token].type = TK_OP; strncpy(tokens[nr_token].str, substr_start, substr_len); tokens[nr_token].str[substr_len] = '\0'; ++nr_token; break; case TK_VAR: tokens[nr_token].type = TK_VAR; strncpy(tokens[nr_token].str, substr_start, substr_len); tokens[nr_token].str[substr_len] = '\0'; ++nr_token; break; case TK_REG: tokens[nr_token].type = TK_REG; strncpy(tokens[nr_token].str, substr_start, substr_len); tokens[nr_token].str[substr_len] = '\0'; ++nr_token; break; case TK_INT: tokens[nr_token].type = TK_INT; if (substr_len <= 32) { strncpy(tokens[nr_token].str, substr_start, substr_len); tokens[nr_token].str[substr_len] = '\0'; } else { // TODO: handle buffer overflow printf("Expr %.*s is too long to handle", substr_len, substr_start); return false; } ++nr_token; default: break; }
break; } }
if (i == NR_REGEX) { printf("no match at position %d\n%s\n%*.s^\n", position, e, position, ""); return false; } }
return true;}
对于超过 位(超过
str
数组长度,也就是超过缓冲区大小)的内容这里没有处理,思考如何处理缓冲区溢出的情况
递归求值
处理完 token
后,我们开始进行递归求值,实际上文档写的很清楚,包括其具体框架都已给出,我们需要注意的有两个函数:
check_parentheses(int p, int q, bool *success)
find_main_op(int p, int q, bool *success)
第一个函数的行为是:消除由 p
到 q
的 token
所组成表达式的 最外层 括号,并判断括号序列是否合法
第二个函数的行为是:找到主操作符,其方法在文档以给出,记得重点为 优先级最低
在这里我们还需要实现关于负数的操作,我的做法很简单,在
eval
中进行判断是否当前所求值为负数(因为传入的字符流一定存在一个-
符号)
因此,其具体实现为:
bool check_parentheses(int p, int q) { if (tokens[p].type == '(' && tokens[q].type == ')') { int stack = 0; for (int i = p; i <= q; i++) { if (tokens[i].type == '(') { stack++; } else if (tokens[i].type == ')') { if (stack <= 0) { return false; } stack--; } } return stack == 0 && tokens[p].type == '(' && tokens[q].type == ')'; } return false;}
int find_main_op(int p, int q, bool *success) { int main_op = 3, main_op_idx = -1, stack = 0; for (int i = p; i <= q; i++) { if (tokens[i].type == TK_OP || tokens[i].type == '(' || tokens[i].type == ')') { if (tokens[i].type == '(') { stack++; continue; } else if (tokens[i].type == ')') { if (stack <= 0) { return -1; } stack--; continue; } if (stack > 0) { continue; } int op_level = 0; if (tokens[i].str[0] == '+' || tokens[i].str[0] == '-') { op_level = 1; } else if (tokens[i].str[0] == '*' || tokens[i].str[0] == '/') { op_level = 2; } else { *success = false; printf("Invalid operator %s\n", tokens[i].str); return -1; }
if (op_level < main_op) { main_op = op_level; main_op_idx = i; } } } if (stack) { success = false; return -1; } return main_op_idx;}
此 eval
函数如下:
word_t eval(int p, int q, bool *success) { if (p > q) { *success = false; return 0; } else if (p == q) {
int idx = p; if (tokens[idx].type == TK_INT) { return atoi(tokens[idx].str); } else if (tokens[idx].type == TK_REG) { *success = true; return isa_reg_str2val(tokens[idx].str, success); } else if (tokens[idx].type == TK_VAR) { // TODO: handle variable type return 0; }
} else if (check_parentheses(p, q) == true) { return eval(p + 1, q - 1, success); } else if (tokens[p].str[0] == '-' && p == q - 1) { *success = true; return -atoi(tokens[p + 1].str); } else { int op = find_main_op(p, q, success); int val1 = eval(p, op - 1, success); int val2 = eval(op + 1, q, success);
if (!success) { return 0; }
switch (tokens[op].str[0]) { case '+': return val1 + val2; case '-': return val1 - val2; case '*': return val1 * val2; case '/': if (!val2) { success = false; printf("divided by zero\n"); return 0; } return (sword_t)val1 / (sword_t)val2; } } return 0;}
成功时需要将
success
设置为true
,但感觉在最后处理是否能够成功计算时不太对,可能还需要改进