申明,本人并非是南京大学的学生,此实验一共完成其中四部分,试验环境为Manjaro(并非PA0中间的docker) + SpaceVim, 此文为记录试验的部分心得。
PA0
正如在本课程试验中间反复提到的基础设施的重要性,比如PA1 中间gdb的构建,PA2 中间的diff test 创建,同样的搭建一个良好的试验环境也可以让自己注意力集中关键的部分。
方案一:vs code
方案二: vim
不推荐不熟悉vim的用户使用此方案。
无论采用哪一个方案,务必保证可以实现符号跳转,全局符号搜索和文件搜索的功能。
PA1
PA1 主要作用是熟悉工程的架构,有一个平缓的学习曲线,和后面试验关系不大(我完成了PA4,才把这一部分补充完整的, 个人感觉debug 主要依赖于diff test), 试验的难点主要有两个方面:
- 表达式的求值
- watchpoint 的链表操作
表达式的求值
其中表达式的求值更加麻烦,我采用的方案并不是实验指导书中间的策略,而是使用的逆波兰表达式求值。
使用逆波兰表达式写了一半的时候突然发现似乎没有办法处理单目表达式的, 也就是说对于如下的表达式都是没有办法处理
12 +-12 // 没有办法正确的处理 -12
*0x4000000 + 12 // 无法处理取地址符
其实处理的办法很简单,在使用逆波兰表达式求值之前遍历一遍表达式,如果一个运算符的左侧不是数值那么改operator必定是单目的。
参看此链接实现普通表达式到逆波兰表达式的装换,
https://en.wikipedia.org/wiki/Shunting-yard_algorithm
但是这一个算法在本实验中间有两点需要做出两个说明:
- 不用实现函数
- 为了方便实现,需要把所有需要符号设置为右结合的,也就是
(the operator at the top of the operator stack has equal precedence and is left associative) 这一个条件不使用
关于结合性, 使用如下例子
12 + 13 - 14
// + - 都是左结合,对应的Reverse Polish 表达式为 12 13 14 + -
// 所有表达式设置为右结合,结果为 12 13 14 - +
表达式求值中间有一个非常坑的地方,那就是expr.c 中间定义了一个宏表示可以处理的最长的符号个数,对于如下测试样例:
(1 + (3 * 2) + (($eax - $eax) + (*$eip - 1 **$eip) + (0x5 -- 5 + *0x1000005 - *0x1000005)) * 4)
恰好超过了限制,这一个bug 非常难以发现。所以我建议大家做实验的时候首先将改宏设置为初始值的10倍
watchpoint 的链表操作
文档中间对于这一个实现描述已经非常的清楚了,每次CPU 执行的时候都检查数值watchpoint的求值是否发生变化,实现难度关键是链表的操作,如果不小心很容易造成死循环和segment fault, 我的建议是多写assert
其他的收获
Makefile的书写
先分析Makefile.git
define git_commit
-@git add .. -A --ignore-errors
-@while (test -e .git/index.lock); do sleep 0.1; done
-@(echo "> $(1)" && echo $(STUID) && id -un && uname -a && uptime && (head -c 20 /dev/urandom | hexdump -v -e '"%02x"') && echo) | git commit -F - $(GITFLAGS)
-@sync
endef
在此处定义git_commit函数,GNU Make 中间默认定义少量用于用于字符串处理函数
https://www.gnu.org/software/make/manual/html_node/Functions.html
下面是一个Makefile 自定义函数的教程:
https://coderwall.com/p/cezf6g/define-your-own-function-in-a-makefile
其中下面这一个最麻烦
-@(echo "> $(1)" && echo $(STUID) && id -un && uname -a && uptime && (head -c 20 /dev/urandom | hexdump -v -e '"%02x"') && echo) | git commit -F - $(GITFLAGS)
其运行结果如下:
> run
U201514545
shen
Linux shen-pc 4.20.0-1-MANJARO
#1 SMP PREEMPT Mon Dec 24 08:20:48 UTC 2018 x86_64 GNU/Linux
00:56:37 up 1:21, 1 user, load average: 0.79, 0.65, 0.47
d85114835383b139f02b1c1923a8ae33eaa5dc49
&&
分割开来都是命令,|
分割则是重定向使用,整个表达式的作用就是将电脑的信息作为commit的内容,不是很懂为什么需要包含随机数到commit 中间,可能是为了发防止作弊吧。
再分析Makefile:
首先这是一个很不错的小模板,我不是很熟悉Makefile的语法,所以我认为以下几个知识点还是值的学习的。
- 几种不同的赋值
https://stackoverflow.com/questions/448910/what-is-the-difference-between-the-gnu-makefile-variable-assignments-a - include
- 自动创建依赖(目前没有搞清楚是怎么创建出来的那些依赖文件的)
- override(没有查看)
- .DEFAULT_GOAL(没有查看)
Nemu工程的源码分析
Nemu是i386模拟器,相当于计算机硬件基础,主要实现的功能是
decode(PA2), exec(PA2), 模式设备(借助SDL2库), gdb(本轮试验完成的) 和diff test(基础设施PA2)
├── include
├── Makefile
├── Makefile.git
├── README.md
├── runall.sh
├── src
│ ├── cpu
│ │ ├── decode
│ │ │ ├── decode.c
│ │ │ └── modrm.c
│ │ ├── exec
│ │ │ ├── all-instr.h
│ │ │ ├── arith.c
│ │ │ ├── cc.c
│ │ │ ├── control.c
│ │ │ ├── data-mov.c
│ │ │ ├── exec.c
│ │ │ ├── logic.c
│ │ │ ├── prefix.c
│ │ │ ├── relop.c
│ │ │ ├── special.c
│ │ │ └── system.c
│ │ ├── intr.c
│ │ └── reg.c
│ ├── device
│ │ ├── device.c
│ │ ├── io
│ │ │ ├── mmio.c
│ │ │ └── port-io.c
│ │ ├── keyboard.c
│ │ ├── serial.c
│ │ ├── timer.c
│ │ └── vga.c
│ ├── main.c
│ ├── memory
│ │ └── memory.c
│ ├── misc
│ │ └── logo.c
│ └── monitor
│ ├── cpu-exec.c
│ ├── debug
│ │ ├── expr.c
│ │ ├── ui.c
│ │ └── watchpoint.c
│ ├── diff-test
│ │ ├── diff-test.c
│ │ ├── diff-test.h
│ │ └── ref.c
│ └── monitor.c
└── tools
├── gen-expr
│ ├── gen-expr.c
│ └── Makefile
└── qemu-diff
├── include
│ ├── common.h
│ └── protocol.h
├── Makefile
└── src
├── diff-test.c
├── gdb-host.c
└── protocol.c
在接下来试验PA2中间我会首先分析试验的难点,坑点,然后分析部分源代码和实现的技术。