返回列表

PA1 学习笔记

16 分钟阅读
📈 130

PA1.1

IFDEF(CONFIG_DEVICE, init_device()); 表示,如果定义了 CONFIG_DEVICE,才会调用 init_device() 函数;而 MUXDEF(CONFIG_TRACE, "ON", "OFF") 则表示,如果定义了 CONFIG_TRACE,预处理结果为 "ON""OFF" 在预处理后会消失),否则预处理结果为 "OFF"

x86 的物理内存是从 0 开始编址的,但对于一些 ISA 来说却不是这样,例如 mips32 和 riscv32 的物理地址均从 0x80000000 开始。因此对于 mips32 和 riscv32,其 CONFIG_MBASE 将会被定义成 0x80000000。将来 CPU 访问内存时,我们会将 CPU 将要访问的内存地址映射到 pmem 中的相应偏移位置,这是通过 nemu/src/memory/paddr.c 中的 guest_to_host() 函数实现的。例如如果 mips32 的 CPU 打算访问内存地址 0x80000000,我们会让它最终访问 pmem[0],从而可以正确访问客户程序的第一条指令。这种机制有一个专门的名字,叫地址映射,在后续的 PA 中我们还会再遇到它。

“一生一芯”的代码跟踪日志位于另一个分支

如果你参加“一生一芯”,请通过 git log tracer-ysyx 查看代码跟踪日志。

在命令提示符后键入 c 后,NEMU 开始进入指令执行的主循环 cpu_exec()(在 nemu/src/cpu/cpu-exec.c 中定义)。cpu_exec() 又会调用 execute(),后者模拟了 CPU 的工作方式:不断执行指令。具体地,代码将在一个 for 循环中不断调用 exec_once() 函数,这个函数的功能就是我们在上一小节中介绍的内容:让 CPU 执行当前 PC 指向的一条指令,然后更新 PC。

readline()

static char *rl_gets()

str_end 是指针,str 指向首元素,str 向后偏移 n,指向最后一个填充的 0。

Decode 是模拟 PC,ISA 存的是指令内容(in binary)。

修复 q 的报错:

#define concat_temp(x, y) x ## y:连接。

regsR(i)

vaddr_ifetch()

  • return value: vaddr
  • parameters: pc

inst 就是指令(little endian)。

Result of BITS is x[hi:lo], and bits start from index of 0.

RISC-V instruction type。

扫描内存,即访问 Memory 的内容?可能的思路:指令是从 Memory 取的,所以看一下指令怎么被取的。

x done!

PA1.2

TODO

int regexec(const regex_t *restrict preg, const char *restrict string,
    size_t nmatch, regmatch_t pmatch[restrict], int eflags);
  • preg 是编译好的 pattern。
  • string 是待匹配的 raw string。
  • nmatch 设为 1 表示不关心子串。
  • pmatch 存放匹配结果:so(start offset)、eo(end offset)。

static 函数只能在当前文件被调用。

grep -r "#define TODO" *

从表达式求值窥探编译器

你在程序设计课上已经知道,编译是一个将高级语言转换成机器语言的过程。但你是否曾经想过,机器是怎么读懂你的代码的?回想你实现表达式求值的过程,你是否有什么新的体会?

首先把要执行的命令装入内存;然后,对于命令,先做词法分析,将语句识别为 token(s);然后调用对应的函数(有开发者写的,也有库函数)。

事实上,词法分析也是编译器编译源代码的第一个步骤,编译器也需要从你的源代码中识别出 token。这个功能也可以通过正则表达式来完成,只不过 token 的类型更多、更复杂而已。这也解释了为什么可以在源代码中插入任意数量的空白字符(包括空格、tab、换行)而不会影响程序的语义;也可以将所有源代码写到一行里面,编译仍然能够通过。

一个和词法分析相关的有趣应用是语法高亮。把源代码看作一个字符串输入到语法高亮程序中,在循环中识别出一个 token 之后,根据 token 类型用不同颜色将它的内容重新输出一遍即可。如果打算将高亮的代码输出到终端,可以使用 ANSI 转义码的颜色功能

在表达式求值的递归求值过程中,逻辑上其实做了两件事情:第一件事是根据 token 分析表达式的结构(属于 BNF 中的哪一种情况),第二件事才是求值。它们在编译器中也有对应的过程:语法分析就好比分析表达式的结构,只不过编译器分析的是程序的结构,例如哪些是函数、哪些是语句等。程序的结构要比表达式的结构更复杂,因此编译器一般会使用一种标准框架来分析程序的结构。若有兴趣,可以看看 C 语言语法的 BNF。

和表达式最后的求值相对的,在编译器中就是代码生成。ICS 理论课会有专门的章节来讲解 C 代码和汇编指令的关系。即使不了解代码具体是怎么生成的,仍然可以理解它们之间的关系,因为 C 代码天生就和汇编代码有密切联系。高水平 C 程序员的思维甚至可以在 C 代码和汇编代码之间相互转换。若要深究代码生成过程,也不难猜到它是用递归实现的:例如要生成一个函数的代码,就先生成其中每一条语句的代码,然后通过某种方式将它们连接起来。

我们通过表达式求值的实现来窥探编译器的组成,是为了落实一个道理:学习汽车制造专业不仅仅是为了学习开汽车,而是要学习发动机怎么设计。我们也强烈推荐在将来修读“编译原理”课程,深入学习“如何设计发动机”。

  • 如何保证表达式进行无符号运算?
  • 如何随机插入空格?在 gen_rand_expr 中加入一个生成空格的步骤。
  • 如何生成长表达式,同时不会使 buf 溢出?
  • 如何过滤求值过程中有除 0 行为的表达式?

过滤除 0 行为的表达式

乍看之下这个问题不好解决,因为框架代码只负责生成表达式,而检测除 0 行为至少要对表达式进行求值。结合前两个蓝框题的回答(前提是对它们的理解都足够深入),就会找到解决方案,而且解决方案不唯一。

.code.c 里处理。每次声明变量的时候,compiler 会报错,使用 SIG,只要一报错,就不 printf

一个 bug

  • 负号和减号都是 -,如何区分它们?
  • 负号是个单目运算符,分裂的时候需要注意什么?

PA1.3

池的核心思想:预分配和重用

池的核心思想是预先准备好一定数量的资源,并将这些资源组织起来进行管理。当程序需要使用资源时,就从池中获取一个空闲资源;当资源使用完毕后,并不立即销毁它,而是将它放回池中,标记为空闲状态,以便下次再次使用。

温故而知新

框架代码中定义 wp_pool 等变量的时候使用了关键字 staticstatic 在此处的含义是什么?为什么要在此处使用它?

For security.

Linked list corner case: