NJU-ICS2021 | PA0&PA1实验小结
前言
这里简单记录一下自己做PA实验的体会,既是及时做个整理总结,也希望能够帮助到后续做PA实验的同学。
首先声明本人非南大学生,也并非科班学生,之前学过一点CSAPP,了解过部分汇编、计组相关内容,如果没学过相关课程的话,可以看一下中国大学 MOOC 上,南大的计算机系统基础课程,总共有 5 门课,前三门是基础知识,后面两门结合实践,我这里主要学了第 5 门课程,也就是计算机系统基础(五):x86模拟器编程实践。
PA实验也是围绕这个课程开展的,不过这门课在中国大学 MOOC 上可能未开放(有时候因为学期结束了,不能上),所以另外推荐授课老师的B站:我是汪犬人的个人空间_哔哩哔哩_Bilibili,可以从老师主页看到相关录屏课程,然后就是PA实验代码和框架代码:
Guide的仓库及其镜像地址:
- GitHub: http://github.com/ics-nju-wl/icspa-public-guide
- Gitee: https://gitee.com/wlicsnju/icspa-public-guide
实验框架代码及其镜像地址:
PA0 实验环境配置
实验指导上写的很详细,过多内容就不赘述,这里主要记录下自己做实验遇见的一些坑和一些思考
课程推荐的是 Debian ,如果从官网上可以比较难找课程推荐的版本(我最开始就没有找到,也是看了MOOC上讨论区才找到),具体地址如下:
https://www.debian.org/releases/buster/debian-installer/
这是课程对应的Debian 10 buster版本,其他的环境配置跟着实验指导一步一步来就好,暂时没遇见什么问题
PA1
PA1主要是实现数据在 NEMU 模拟器上的表示和运算,包括整数和浮点数的表示、存储和运算
PA1-1 寄存器的模拟
PA1-1 需要需要数据在计算机内部存储形式,也就是实现寄存器,这里框架代码已经给出来了,利用 C 语言中的 union ,看一下这个答案:
|
|
这里用到了 union 联合体,为什么用 union 就可以实现需求呢?
具体需求如下: 我们希望以
cpu.eax
形式访问的和以cpu.gpr[0]._32
形式访问的是同一个模拟寄存器,同时cpu.gpr[0]._16
能够访问到cpu.eax
的低16位也就是ax寄存器
,cpu.gpr[0]._8[0]
访问到cpu.eax
的低8位也就是al
寄存器等。
这里相当于是模拟了 i386 中寄存器的结构
要理解上面为什么可以实现,首先需要找到 union 联合体的在内存储存方式
union 相当于一个结构,它的所有成员相对于基地址的偏移量都为0,此结构空间要大到足够容纳最“宽”的成员 ——摘自K&R C语言p130
我的理解,一个 union 宽度是它的成员中最大的那个,其他的成员和它存放在同一个地址,相对于起始地址偏移量都为0
举个例子:加入现在一个union 有两个成员,如下:
|
|
我们可以知道这个 union 所需要的字节数为一个 int 也就是4个字节,假设现在c = 0x12345615,并且存放在0x80000000这个地址开始位置,那么具体存放内容如下(假设这里为小端存放)
此时,对于 u.a = 0x15
,而 u.b = 0x5615
,相比较起始地址偏移量都为0,char 占用一个字节,则 a 即为 0x15,short 占用两个字节,b 即为 0x5615 了,下面是C程序验证我们的结论
|
|
上面提到一个小端存放,这个在实验指导书上提到:
x86采用小端方式存储数据,它规定了超过一个字节的数据的存储规则:低有效字节放在低地址,高有效字节放在高地址。
这样就比较好理解,为什么下面 union 这个可以模拟出i386 对应寄存器的结构
|
|
_32
在内存中宽度最大,也对应32位的寄存器,例如 eax,而_16
为16为,由于是小端存放,最低有效字节放在低地址,取出16位即为这个值的后16位,也就相当于是寄存器的后16位,同理可以理解 _8[2]
对应的过程
注意另外一点,这里将_32 放在了前面,而不是 _8[2]
根据前面的分析,这里应该也是可以的,但是 union 另外一个特点就是初始化的时候,只能用第一个成员类型的值进行初始化,所以将最宽的成员放在第一位,防止初始化时丢失了后面的精度
下面是代码验证:
|
|
可以看到由于 union 第一个成员位 char a,只能存放 1 个字节,初始化后会损失精度
至此,差不多解释了为什么使用 union 可以实现模拟寄存器的功能
PA1-2 ALU的模拟
这里需要对整数的各个运算来进行模拟,其实实现中比较好实现,直接用 C 语言中对应操作即可,比如说 alu_add 的实现,结果 res = src + dest,包括后续的加减、移位、逻辑和乘除运算基本上都是可以这样来实现
但是最为关键的是需要对标志位进行设置,这就需要结合 i386 手册了(实验指导上给了相应链接 i386 手册: )
PA中需要设置的是CF、PF、ZF、SF 和 OF (AF不需要设置)
- CF:进位标志符,这个主要看运算结果是否超过了最高的有效位,相当于是进位了,或者是看运算是否向高位借位了
这里可以类比十进制,比如说89+83,相加的时候 3 + 9 = 12 > 10 这里要进位,而CF标志位则是看最高位是否发生了进位,或者是两个数相减的时候 如91-87,个位上 1 - 7 不够减,所以需要向十位借位,这种情况 CF 会被设置为 1
- PF:奇偶标志位,看结果中所有bit 位中 1 的个数是否为偶数,偶数 PF = 1,奇数 PF = 0
- ZF:零标志位,看结果是否为 0,如果结果为0 则ZF = 1,否则 ZF = 0
- SF:符号标志位,看结果是否为负,如果为负,SF = 1,否则 SF = 0 (PA中由于模拟的是无符号数,所以看最高有效位是否为 1 来判断结果是否为负数)
- OF:溢出标志位,计算中如果发生溢出则设置为1,溢出包括两种,一个是结果值太大,超过了能表示的最大范围就发生了溢出,另外一个是结果值太小,小于能表示的最小值就发生了溢出
这里注意 CF 和 OF 的区别: 在王爽汇编语言P218书中写到,CF 是针对无符号运算的标志位,OF 是针对有符号的标志位,同样位数情况下无符号的范围,这样就会在解释两个数值相加的时候,如果解释为无符号可能不会产生溢出,而解释为有符号会产生溢出
在PA中模拟的是无符号数,所以两者可能会有区别?其实我自己之前实现的时候,很多时候两个标志位实现逻辑就很类似
另外就是 PF、ZF 和 SF 这三个其实对于每个运算都是一样的,可以直接代码复用
PA1-3 FPU的实现
这个主要实现IEEE 754 单精度浮点数(float)的运算,框架代码已经给出大部分内容了,只需要根据实验手册进行补充即可,最重要的应该就是实现 internal_normalize()
规格化函数
其实这部分代码框架也都设计好了,不过开始没反应过来,自己花了不少时间没用在刀刃上,下面自己在做实验的时候遇见几个坑点:
- 第一个是粘位的处理
最开始我就是判断每次移位之前,判断(sig_grs » 1) 是否大于 1,开始看错了以为是舍入位左边有非零数字,粘位就为1,后来仔细看了代码框架,发现原来的粘位处理操作是 sticky = stickt | (sig_grs & 0x1);
这才发现问题,另外就是注意粘位判断粘位是否为1,要在移位之前进行判断。
-
出现溢出或零的情况 最开始处理溢出或者零的时候,我把sig_grs 直接设置为 P_ZERO_F 或者 P_INF_F 等等这些,然后发现不对,这里应该直接返回,同时要根据符号位,判断正负的情况
-
处理舍入的时候 最开始没弄懂 GRS 位是怎么舍入的,直接将 sig_grs += 0x4 了,我以为只要 GRS 大于0x4 然后就直接进位,后面发现不是怎么回事,这里应该是浮点数默认的向偶数位舍入(这里参考了CSAPP p83),其实就是我们经常听到的四舍六入五成双,举个例子:
对于十进制(这里保留一位):
- 10.04 -> 最后一位是4,直接丢弃,最后结果是 10.0
- 10.06 -> 最后一位是6,大于5直接进位,结果是10.1
- 10.05 -> 最后一位是5,要让进位的成为偶数,5前面的0已经是偶数了,所以这里直接丢弃5,结果是10.0
- 10.15 -> 这里和上面就不一样了,5前面是奇数,所以要进位,结果是10.2
对于二进制,这里 GRS 是3位,所以取半即 0x4(0b100)
- 如果GRS 大于 0x4 直接进位
- 如果 GRS 小于 0x4 直接丢弃后面尾数
- 如果 GRS 等于 0x4,这里需要判断前面一位是否为偶数,偶数直接丢弃 GRS,奇数进行进位
舍入想清楚就基本上很容易解决了 (重新看了MOOC视频,发现老师这里讲的挺清楚的,不过自己最开始看了下视频,如何根据实验手册做的,就把视频内容给忘记了,看来还是需要好好看一下视频的内容)
阶码 exp == 0 && sig_grs » (23 + 3) == 1
在规格化过程中,遇见这种情况,两个非规格化数运算后得到了一个规格化数,指数需要加 1,即 exp = exp + 1
简单分析下为什么要加1,首先 exp == 0,表示是两个非规格化数相加减(因为加减过程中会对阶,两个数应该同时为规格化或者非规格化数),非规格化数基本形式是 0.bbbb…(小数点后面26个位),两个非规格化数相加为规格化数的情况就是相当于,第一位小数都为1,即 0.1bbb… 和 0.1bbb… 相加的情况,此时进位,结果为 1.bbbb…,结果上是一个规格化数,exp 应该等于 1,相当于是右规了一位
小结
PA1的实验大概花了13.5h,感觉这部分内容在框架代码的基础上,查阅i386手册基本上都能实现相应的功能,最大的感受是在浮点数运算的实现时候,对浮点数规格化、舍入等有了更深入的了解