NJU PA 2022 :激动人心的计算机系统遨游(二)PA2
PA2 简单复杂的机器: 冯诺依曼计算机系统
计算机可不是仅仅只有TRM,首先,在PA1中简单提到的CPU运行过程,在PA2中具体描述成了取值,译码,执行,更新PC这四个步骤,此外还丰富了指令集,裸机(bare-metal)运行时环境(AM)以及设备的抽象(IOE)
PA2对于我来说意义非凡,它帮助我建立了硬件和软件之间的桥梁,让我有了对键盘输入如何输出到我们屏幕上有了清晰直观的认识,还有就是抽象的思想(计算机是一个抽象层),抽象可以说是模块化编程以及跨平台开发的核心了,这也就是面向对象(OOC)编程如今为什么如此受欢迎的原因,RISCV32也是我这次实验新接触的一个架构,在我RTFM之后不禁感叹道,原来汇编语言也可以不用那么复杂,这玩意可比CISC(熟悉的x86)容易上手多了,指令短,清晰,还不用考虑对齐那么RISCV,代价是什么呢(想了解就自行查看指导书去),哦对了,Different Test 在PA2会非常好用,最好先实现一下哦。
做完回头再看PA2后,才发现有好多似曾相识的感觉,比如设备I/O的内存映射就和后面的虚拟映射原理有些类似,一个将物理地址“重定向”到设备的地址空间,一个是将虚拟地址和物理地址按页映射,让CPU误以为自己读取的是物理地址
意外之喜,还能了解游戏编程的一些知识(游戏的运行基本流程)
下面是思考题
YEMU状态机
M代表内存,R代表寄存器
状态格式(PC,R[0],R[1],M[7])
(0,X,X,X) ->(1 , 16(M[6]), X , X) ->(2,16(M[6]),16, X) ->(3,33(M[5]) ,16,X ) ->(4 , 49, 16 ,X )->(5 , 49 , 16 ,49)
从内存中读取指令到寄存器,随后将结果写入内存
RTFSC理解指令执行的过程
计算机是一个进行重复读取指令的机器,而CPU则是对指令进行解析和译码的部件,指令存储在存储器中,由CPU中的pc提取。当一条指令()被提出后,会进行译码,将指令转换为机器语言,随后由ALU执行这些指令(nemu中则用函数来模拟指令的执行效果)
模拟器和DEGUGGER的区别
模拟器能模拟整个硬件的运行流程 而degugger则是在已有的硬件上对程序进行调试。
Motorola 68k系列的处理器都是大端架构的. 现在问题来了, 考虑以下两种情况:假设我们需要将NEMU运行在Motorola 68k的机器上(把NEMU的源代码编译成Motorola 68k的机器码)假设我们需要把Motorola 68k作为一个新的ISA加入到NEMU中在这两种情况下, 你需要注意些什么问题?
68K是大端运行,向内存输入数据后再输出 会出问题 例如NEMU在内存中存的0x1234,在68K上就成为0x4321
为什么会产生这些问题?
大端小端的存储方式
怎么解决它们?
写个字节转换器将小端存储的字节转换为大端字节
(具体一点,实现一个栈,先将小端的内存地址从低到高两位两位划分提取出来存入栈中,随后大端地址按低位从栈的顶部获取数据)
mips32和riscv32的指令长度只有32位, 因此它们不能像x86那样, 把C代码中的32位常数直接编码到一条指令中. 思考一下, mips32和riscv32应该如何解决这个问题?
把常数拆分成两个16位的编码,然后再编码到一条指令
(具体通过位运算来实现,先用0xffff0000和常数相与得到高位,0x0000ffff相与得到低位,最后将高位和低位进行或运算即可)
抽象的好处:
1.减少跨平台功能的代码量,易于维护和迁移
2.代码结构清晰,使用API调用方便
volitale关键字:
迫使该变量每次使用时从其内存地址中调用(防止编译器优化后因为某些指令被省略或合并而发生的错误)
PA2-2
1.我们在am-kernels/tests/cpu-tests/tests/add.c中定义了宏NR_DATA,同时也在add()函数中定义了局部变量c和形参a, b,但你会发现在符号表中找不到和它们对应的表项, 为什么会这样? 思考一下, 什么才算是一个符号(symbol)?
(CSAPP P468)因为局部变量和形参存储在栈中,链接器不会描述其中的符号(why?个人想法:链接器是为了将多个文件揉合成一个可执行文件的过程,只注重于最终能够进行链接的符号,而由于函数中的局部变量和形参会在函数调用结束后被系统释放空间,(局部静态变量(static)不会)),而宏在预处理时会被替换成他所替代的内容,所以也不会出现在符号表中
符号是一个程序中的变量名(拥有物理内存地址)和 函数名
2.在Linux下编写一个Hello World程序, 编译后通过上述方法找到ELF文件的字符串表, 你发现"Hello World!"字符串在字符串表中的什么位置? 为什么会这样?
字符串在字符表中的上方,因为字符串是只读的,优先放在.rodata段中
3. 不匹配的函数调用和返回
如果你仔细观察上文recursion的示例输出, 你会发现一些有趣的现象.
具体地, 注释(1)处的ret的函数是和对应的call匹配的, 也就是说, call调用了f2,而与之对应的ret也是从f2返回; 但注释(2)所指示的一组call和ret的情况却有所不同, call调用了f1, 但却从f0返回; 注释(3)所指示的一组call和ret也出现了类似的现象, call调用了f1, 但却从f3返回.
尝试结合反汇编结果, 分析为什么会出现这一现象
ret返回的函数若在其他函数中,则返回对应的函数,如果没有其他函数则返回自己本身
4.冗余的符号表
可执行文件中丢弃符号表不影响执行,但是可重定位文件中丢弃符号表链接会报错,
可执行文件中因为已经将符号进行重定位所以符号表丢弃无所谓,而可重定位文件中则不一样,因为链接成为可执行文件时符号表中的符号重定位是需要符号表的数据。
5.为什么定义宏__NATIVE_USE_KLIB__之后就可以把native上的这些库函数链接到klib? 这具体是如何发生的? 尝试根据你在课堂上学习的链接相关的知识解释这一现象
定义宏之后 Kilb库变成了强符号
6.为了不损害程序的可移植性, 你编写程序的时候不能再做一些架构相关的假设了, 比如"指针的长度是4字节"将不再成立, 因为在native上指针长度是8字节, 按照这个假设编写的程序, 在native上运行很有可能会触发段错误.当然, 解决问题的方法还是有的, 至于要怎么做, 老规矩, STFW吧.
解决方案(只提供思路):写一个头文件,在其中指定指针的长度大小
7. 思考一下, 如果代码中p指向的地址最终被映射到一个设备寄存器, 去掉volatile可能会带来什么问题?
p地址由于编译器的优化会导致直接使用寄存器中的变量而非内存地址中存储的变量,导致调取失败
PA2-3
一、游戏是如何运行的(打字游戏)
1.初始化
nemu通过IOE1初始化VGA,Timer,键盘还有串口
2.当初始化完毕后,nemu就开始循环播放VGA的画面,并通过串口显示出时间,命中数和FPS(帧数)
3.游戏的逻辑实现则是通过一帧中数据表现进行处理的,比如字母的下落就是根据帧数控制其Y坐标的大小
当你按下一个字母并命中的时候, 整个计算机系统(NEMU, ISA, AM, 运行时环境, 程序) 是如何协同工作, 从而让打字小游戏实现出"命中"的游戏效果?
按下字母时,我们输入的键盘信息会有nemu中的Keyborad获取,游戏程序通过am来获取nemu中键盘设备的数据,具体的操作则是将AM中的代码通过ISA转换成指令后传给nemu,随后游戏程序处理相应的逻辑,如果命中,则游戏程序将字体颜色的信息通过AM中的接口API传给NEMU中的设备,设备将信息处理后又通过ISA将NEMU中的信息传给am,随后AM把处理后的信息传回给游戏程序,nemu将颜色变化通过VGA显示在窗口中。
二、在nemu/include/cpu/ifetch.h中, 你会看到由static inline开头定义的inst_fetch()函数.分别尝试去掉static, 去掉inline或去掉两者, 然后重新进行编译, 你可能会看到发生错误. 请分别解释为什么这些错误会发生/不发生? 你有办法证明你的想法吗?
单独去掉static 和 inline 都不会发生错误,但是当两者同时去掉则报错这是由于在进行链接时由于有多个同名符号,进行重定位时链接器无法具体定位哪个符号而导致报错.
static 静态关键字能让符号存储在静态存储区,函数只能在本文件中调用,可以将其他文件中的同名符号函数区分开来
inline关键字 会让该函数不在栈区调用,程序执行时会将其内容直接作为指令执行,因此该函数不被视为一个符号。
三、在nemu/include/common.h中添加一行volatile static int dummy; 然后重新编译NEMU. 请问重新编译后的NEMU含有多少个dummy变量的实体? 你是如何得到这个结果的?
一个,通过查看nemu中代码的预处理得到的
添加上题中的代码后, 再在nemu/include/debug.h中添加一行volatile static int dummy; 然后重新编译NEMU. 请问此时的NEMU含有多少个dummy变量的实体? 与上题中dummy变量实体数目进行比较, 并解释本题的结果.
两个,因为都是静态变量,只在当前文件中能够被调用,且它们都存储在BSS段(未初始化)中,所以链接时不会发生符号混淆的情况
修改添加的代码, 为两处dummy变量进行初始化:volatile static int dummy = 0; 然后重新编译NEMU. 你发现了什么问题? 为什么之前没有出现这样的问题? (回答完本题后可以删除添加的代码.)
报错了,声明重定义,因为有volatile关键字的存在,使得该变量的数值只能通过内存地址来调用,而此时两个dummy变量都进行了初始化,使得它们都在DATA段中,而static则是要求符号不能重复的,所以报错之前因为没有初始化,所以不触发volatile 关键字调用指定内存。
总结:
PA2-1:
riscv32指令还是非常好理解的,它比x86使用起来更加方便,因为它将所有的指令格式划分为了7类,所以在指令匹配上非常容易实现,唯一有点复杂的是J类型要注意偏移量不是你输入的量
其次就是宏有点多,建议用makefile将每个文件的预编译结果输出出来后能够更直观地看懂整个代码运行的流程
这一part让我具体了解到了ISA在计算机结构中扮演的地位,也亲自体会了指令是如何运行的
PA2-2:
PA2-2实现了基本的字符串库函数还有调试的基础设施,库函数的实现中比较难的是printf的实现,由于个人能力有限,参考了网上的写法完成的
基础设置中较为困难的是ftrace,由于我对文件的输入输出不是很了解这个很好地锻炼到了我读取文件的操作以及对Makefile源码的理解,能够通过修改MAKEFILE1的部分文件
实现读取ELF文件。
PA2-3:
锻炼了我对抽象层之间的关联能力 ,AM是一个封装的API合集(一个库)又称为运行时环境,是为软件提供调用硬件的一层抽象层 ,能够调用NEMU中的设备
目前AM只封装了TRM和IOE有关的API
(在NEMU中)实现硬件功能 -> (在AM中)提供运行时环境 -> (在APP层)运行程序
(ISA)
犯病时刻:
1.2022.7.28 书写bge命令时由于大小比较搞反耗时一个上午
2.找NEMU批处理模式找了一个上午
3.提取ELF中的信息,使用strcmp时忘记加换行符
4.跨文件变量的处理
5.无符号比较只改了移位的忘改了bltu这类的,找了两天,怀疑自己指令是否写错

浙公网安备 33010602011771号