
本文以记录个人学习CSAPP的过程,使用blog的方式记录能更专注的思考,不至于走马观花式的做实验。
实验说明
在动手实验之前建议仔细阅读实验提供的文档以及bits.c
前的注释。本实验对应CSAPP的第二章,是对位运算的处理和应用。在解决这些问题过程中,会对位运算有更深刻的理解。实验中提供13个函数接口,我们需要根据函数前注释的要求实现相应的功能,同时满足所要求的限制条件。
环境配置
我的实验环境是Debian(x86_64),编译32位的程序需要使用 sudo apt-get install gcc-multilib
下载gcc-multilib
,这是一个能够实现不同位数的编译器。下载后可以正常make
编译。
实验中的工具dlc
和driver.pl
中可用-h
参数查看使用手册
一些小坑
- 使用
dlc
时出现bits.c:xx:parse error bits.c:xx:undeclared variable xx
是因为该编译器仅仅支持C89的语法,需要将变量全部在代码块的开头声明。 - 使用
dlc
时出现bits.c:284: Warning: suggest parentheses around arithmetic in operand of x
是因为运算符优先级肯存在歧义,建议为优先运算块加括号
整数规则细节
表达式规则
- 整数的范围在0~255之间,不能使用大整数
- 只能使用局部变量和参数
- 允许使用单目运算符
! ~
- 允许使用双目运算符
& ^ | + << >>
禁止使用
- 非顺序执行的语句
- 宏
- 调用函数
- 使用不在规定范围内的操作符
- 使用强制类型转换
- 使用非
int
类型,使用数组,结构体等
对运行机器的规定
- 使用补码,32位整数表示
- 按算术方式执行右移
- 如果移位数
大于31
或小于0
, 移位有不可预测的行为
整数具体题解
1. bitXor
根据异或运算的表达式
可知代码如下
1 | /* |
2. tmin
根据补码规则,最高位符号位为1时为负数,CSAPP上有一张图很好解释补码的规则
所以根据这个规则我们知道最小的补数就是最高位为1,其余为0的数,实现代码如下
1 | /* |
3. isTmax
- 同样根据上面的图可知,最大的数和最小的数在补数规则中是完全去反的规则,由此可以找出最大的数。要想不通过
==
比较两个数,我们可以采用异或后结果是否等于0的方式判断,因为只有相等才会异或为0。 - 但是本题禁止使用
<<
,代表我们需要采用另一种方式实现。我们令最大的数0x7fffffff
为a
。发现a+a = 0xffffffe
(相当于右移一位),对其去反的结果为1。顺着这个思路我们只需要找到其他满足~(x+x+1) == 0
的数排除即可。不难思考,将0xffffffe
右移的两种情况中的另一种为0xffffffff
(也就是-1),即-1 + 1 == 0
,因此判断逻辑不难表达如下
1 | /* isTmax - returns 1 if x is the maximum, two's complement number, |
4. allOddBits
题目需要找到所有奇数位都是1的数,可以通过移动一位后,判断或运算是否所有位都是1来判断结果,其中全位都是1就是-1,可以再简化判断
1 | /* |
5. negate
直接使用补码定义即可
1 | /* |
6. isAsciiDigit
主要思路是首先通过判断高位(4到7位)的为0011b
,可以同时判断其他位是否为0,然后判断最后一位,观察发现(可以通过卡诺图辅助),满足条件的如下:满足0xxx (x表示任意)或者是8或9,实现代码如下
1 | /* |
上述的符号数为13,符合规定。但是观察发现,每次都需要使用!
,我们可以通过摩根公式化简flag1
,筛选掉不要的值,观察发现,在11xxb
和1x1xb
是需要剔除的元素,代码简化后如下,只需要11个符号
1 | int isAsciiDigit(int x) { |
7. conditional
我们需要通过任何非1的数生成出0xffffffff
(也就是-1),同时0还是0。这个算法可以把思路转化为通过1生成0,通过0生成-1,那么也就-1对数减1即可。
具体可见代码注释
1 | /* |
8. isLessOrEqual
本题需要分为两种情况,两个数异号和同号,
异号的情况是如果相减会溢出,我们通过符号位的特点判断结果,观察发现,异号时结果和x的符号位同步
同号的情况相减判断符号位即可,这里的技巧是由于0和正数的符号位相同,因此采用y - x >= 0
判断会简化过程
最后将两种情况通过掩码合并。
1 | /* |
9. logicalNeg
本题需要充分利用符号位,由于最终需要实现的结果是逻辑非,如何识别出0就十分关键。在allOddBits
一题最后我们通过为-1 + 1
和0 + 1
实现能让全1转换为0,全0转换为1的方法,本题同样借鉴这个思路。而0的特点是,其补码和就是其本身,具有同样特性的还有0x80000000
(也就是前面找到的最小数),通过观察发现他们的区别在于最小数最小数和其补数做或运算后最高位为1,而0做相同运算后结果为0。再利用其符号右移的特性,能构造出需要的0xAAAAAAAA
(全1)或0
。
1 | /* |
10. howManyBits
首先我们可以通过给出的几组示例发现,对于任何数,相当于查看其原码表示的最小位数,我们可以先将负数转化为于之对应的补数,两者的最小表示位数相同,计算后记作y
。
接着我们利用二进制数的表示思路来处理,也就是说,我们可以通过16、8、4、2、1这几个数表示1到31的任意数字,也就意味我们只要验证处理这几次就可以表示出位数来
处理时有技巧,我们从利用能取大的先取大的原则,不断缩小y(进行移位),如果不能移位则用小数再试。具体见代码
1 | /* howManyBits - return the minimum number of bits required to represent x in |
至此10道整数问题结束
浮点数规则细节
可以使用
- 可以使用循环和条件控制
- 使用
int
和unsigned
,并可以使用任何运算符号
禁止使用
- 宏
- 定义或调用函数
- 使用非
int
和unsigned
类型的数据 - 使用任何浮点运算符号
浮点数具体题解
1. floatScale2
只要熟悉浮点的规则就可以模拟出这个过程。
首先处理特殊情况(INF,NAN),其特点为M
为0xff
,此时返回这个数即可。
然后处理非标准数,需要确定是否会变成标准数,只要看E
是否超过0x7fffff
(也就是E所能表示的最大值即可)。
1 | /* |
floatFloat2Int
本题需要熟悉计算从浮点数转换为整数的规则,经过分析可以分为以下几种情况,
- 特殊值 NaN 和 infinity 的情况,会返回
0x80000000u
- 非标准化的情况会返回0,因为非标准化表示的是小于1的数
- 实际阶数 a 满足
a < 0
的数同样返回0,因为表示的数同样小于1 - 实际阶数 a 满足
0 <= a <= 23
的情况,保留整数部分的数,具体计算规则见代码 - 实际阶数 a 满足
a > 23
,根据题目要求返回0x80000000u
其中非特殊情况还要加上符号位。
1 | /* |
3.floatPower2
一个小坑 :出现Timed out after 10 secs (probably infinite loop)
的情况,应该是虚拟机性能不佳,绝对时间的超时。可以使用参数-T 20
将执行限制时间改为20秒即可。
本题需要实现一个浮点数对2的幂计算,我们容易想到的是,浮点数本身计数方式就是以2的幂次形式,因此通过改变其中M
的部分就可以实现大部分浮点运算。很容易想到的是当超出最大能表示数时返回0x7f800000
(即无限大),当小到无法表示时,输出0。
但是事实上如果仅仅考虑阶数的问题,其实并没有得到正确答案,尽管这样做可以通过官方测试案例,但并不是正确答案。其实不应只靠考虑标准浮点数,非标准浮点数同样也应该考虑在内,我们把x + 121
记作M
,也就是真实阶数数,当阶数在0 =< M <= 23
时能用非标准浮点数表示,详见代码如下。
1 | /* |
为了验证函数的正确性,我写了一个验证程序mytest.c
在项目目录下简单测试
1 |
|
我们通过gcc -m32 -o mytest mytest.c
编译后并执行./mytest
。结果没有abort
,一定程度验证正确性。
总结
最后附上最终测试结果
陆陆续续用了一周时间完成Lab1,我认为对计算机系统级的数据处理方式和技巧都有所学习,其中很多答案可能不是最优答案,欢迎大家留言讨论,共同学习。