前言
本人反正是慕名而来上ICS这门课,可惜只有这个学期有空,所以遗憾只能选择重修补修班了。并且由于课程冲突,不得不选择免修不免考。
ICSPA这套框架也传了十几年迭代了三个大版本了,在这个vibe coding盛行的时代,我也试图在AI的辅助之下高效地完成这个PA实验,在节省手工时间的同时也能对计算机框架架构有深入的了解。所以形成了这篇帖子,谈一谈我是怎么完成这个实验的。
平台与环境配置
x86架构NEMU,无需本地环境,直接在服务器端测试。使用IDE为vscode,AI agent使用github copilot,模型为gemini3.1pro,学生认证premium。
x86架构是cisc架构,模拟效率低下,并且包含了一些框架的祖传问题,可玩性是不如riscv架构NEMU的。
虽然说无需本地环境,但是服务器也只有网页环境的IDE。因此最佳方法是在本地写,上传到服务器做测试。常规的vscode ssh,sftp之类的连接上传方法就不用尝试了,一是服务器没有开放对home文件夹以及其父级文件夹的写入权限,二是服务器是IA32架构,缺少相关依赖库。因此最合适的办法是使用git通过ssh上传。
这里提一嘴啊不要git push -f啊,因为提交时候会在pa_nju文件夹下生成score(很显然我不知道),-f会覆盖这个文件夹,导致最终上传时候看不到之前的成绩。
另外在上传之前记得清理git的记录,否则你会在提交时候提交一个GB级别的文件。

总体感受
先说结论,AI确实可以节省一些排错和指令编写的重复工作的时间,但是我完成实验的总时长还是超过了传说中老师预估的50小时。并且实话实说AI不是万能的,有一些细节需要自己去查手册,AI的知识库当中对于手册的解读是不完全的,因此如果一味地tab补全,反而会增加排错时长。不过反过来讲,训练debug能力也是很重要的一环。不过无论使用AI与否,完成PA离完整理解代码框架和x86计算机系统还是相差甚远的,而我觉得AI真正的价值就在这里,可以帮我们更轻松地去解构这个复杂项目,深入理解NEMU的架构。
不管是传统手搓还是vibe coding,先读文档先读文档!!!文档给的思路很清晰,顺着来就能很好地理解。
PA1 数据的表示、存取和运算(ALU与FPU)
大体工作是完成ALU和外挂的FPU,ALU部分大致做三件事情,求出运算结果,设置标志位,返回按目标长度截取过的结果。FPU部分大致是两件事情,补齐已经写完框架的规格化函数和补全运算函数。
这里的工作可以参考前人代码写一个函数,剩下的tab是没有问题的。
PA2 程序的执行
2-1 指令集
个人认为工作量最大的一部分。大致需要实现一整张opcode表(当然最简单的办法是抄opcode_ref.c,注意这个ref里面是缺了几条指令的,暂时不影响这一部分实验的完成,后续留给大家debug了),实现每一个基础指令,修改条件跳转宏。
实现指令部分依旧可以参考前人代码写一个函数,然后剩下的工作敲tab是可取的(因为重复性工作比较多,当然可以通过带参数的宏解决),但是不一定是可行的。下面我举一个例子。
make_instr_func(add_r2rm_v)
{
OPERAND rm, r;
rm.data_size = data_size;
r.data_size = data_size;
int len = 1;
len += modrm_r_rm(eip + 1, &r, &rm);
operand_read(&rm);
operand_read(&r);
rm.val = alu_add(r.val, rm.val, data_size);
operand_write(&rm);
return len;
}
AI在读取了这一个函数作为模板之后,你去让他写add_r2rm_b指令,然后他就会非常天真的继续给你写rm.val = alu_add(r.val, rm.val, data_size)。
这其实就说明了我们完成这部分内容最需要关注的东西,操作数类型,操作数大小,操作数读取方式(相应的指令长度大小)。
如果这一部分出错其实还好办,运行make test_pa-2-1之后会告诉你HIT BAD TRAP at 0x0003004b(举例),然后通过反汇编回去看这个地址对应的指令是什么(比如lea)就可以了。如果每个测试用例都在同一个指令处dump,那么回去好好研究一下这个指令就行。
笨人不幸还遇到了第二种情况。其中两个测试用例在一处地方dump,其他测试用例都正常,对应的HIT BAD TRAP位置是一个mov指令。很显然mov指令会出现在各个测试用例,那么为什么只有这两个会dump呢?与ai激情搏斗四小时发现笨人两个condition写反了。所以某种程度提醒大家,在debug上AI或许能给我们思路,但是绝对不能取代我们的工作。
PA2-2 ELF的装载
完成两件事,修改testcase/Makefile中LDFLAGS使得测试用例的起始位置位于物理地址0x100000,然后实现kernel中的loader()函数。loader()函数要做的工作是两个,拷贝测试用例,清零p_memsz - p_filesz字节的内容。
PA2-3 完善调试器
交给AI自由发挥环节。
PA3 存储管理
这一大部分的流程就是依次实现从虚拟地址到线性地址再到物理地址的三步转换,我们会按照从后往前的顺序分别接触到,对应的就是Cache,Page和Segment的加入。
同样地,AI在这里的工作是可取但不一定可行的。
举一个例子,AI根本不会关注到跨Page读写的问题。
if(wordAddr + len <= 64) {
int line = find_hit_line(set, targetTag);
if(line == -1) {
line = select_fill_line(set);
fill_line_from_mem(line, targetTag, set);
}
uint32_t ret = 0;
memcpy(&ret, &(cache[line].data[wordAddr]), len);
return ret;
//AI只会帮你写这部分内容,如果你也没有意识到跨页读写问题的话,恭喜你将喜提kernel dump
}
else {}
所以如果在这里纯粹地使用AI一定要小心,但是在这里让AI帮你找bug还是非常可行的。
PA4 异常、中断与I/O
PA4-1 & PA4-2
请记得先做PA4-0!!!后面带图形的测试用例都需要使用图形转发,否则会kernel dump。
讲真梦回PA2-1,补充了一些先前没有实现的指令,但是除此之外中断部分涉及复杂的函数调用过程,我觉得这一部分非常适合使用AI来理清整个逻辑链。
PA4-3
打字小游戏是已经做完了的,如果按照教程当中依次实现各个模块(在这里AI还是非常好用的),你的PAL理论上可以以一帧能玩的帧率运行启动画面。至于游戏内容部分,得益于古早的代码框架和低效的模拟系统(可以关闭Cache提升性能),目前没有资料表明可以轻松的做出来。另外Audio部分在远程转发时候是不可用的。
接下来
接下来可以交给AI大刀阔斧修改框架代码啦!(雾,还是去做更全能的RISCV架构NEMU吧)