深入理解计算机系统(CSAPP)课后实验CSAPPLAB1——Data Lab
实验说明
《深入理解计算机系统》是卡内基梅隆大学计算机专业的计算机体系课程的标配教材,可以在B站看其配套网课(链接)。课程由书的作者两个人共同执教,比较适合有C语言的基础的同学作为计算机体系构建的课程。但是,仅仅看书收获还是有限的,所以为了加强Coding,而不是纸上谈兵,还需要做这本书配套的实验,全书总共9个实验,本次讲解Lab1。
实验条件准备
实验环境使用Ubuntu,为了减少环境搭建成本,我们使用虚拟机来进行。我之前用过VMWare,但感觉不是很舒服,而且还要找破解版比较麻烦。所以,这次使用VituralBox,这是开源的虚拟机,免费,足够实验使用。
虚拟机环境搭建
首先,去VituralBox官网下载虚拟机安装包(链接),一般是Windows的吧,如果想下载其他版本的,点这个链接。
下载完毕,管理员权限安装,一路点Next就好了。
按照一般配置虚拟机的套路,我们应该去Ubuntu之类的官网下载系统镜像来进行安装。但实际上,这个步骤可以省一省,直接去下载人家配置好环境的虚拟机镜像就好,一键配置妙不妙呀~
我这里是用之前下载的一个清华操作系统课程提供的系统镜像(链接),里面已经配置好了,
虚拟机的管理员密码是1个空格,一般提示输密码就输这个
下载好镜像之后解压缩,注意,这个压缩包格式是.xz(某明星???),这里实测WINRAR和BANDZIP可以解压,其他的没测试过。
解压之后是一个6G多的.vdi文件,在硬盘里新建一个文件夹,把.vdi文件拖进去。然后打开VituralBox,点击创建,系统类型选择Linux,Ubuntu64位,给虚拟机起个名字,然后选择刚刚新的文件夹作为虚拟机目录,点下一步。
现在是选择内存大小,随意,大点没那么卡,小点可以同时开多几个,建议2GB以上,再下一步。
选择用已有的虚拟硬盘文件,然后打开目录,选中刚刚那个.vdi文件,点击创建。然后就可以启动虚拟机了。
Lab1实验文件挂载
进入虚拟机之后,在VituralBox左上角的菜单里,点击设备,点击安装增强功能。Ubuntu里会提示插入镜像,点击Run运行,会跳出命令行,耐心等待安装完毕,命令行里会提示输入Return退出,这时候就可以在虚拟机和本机上共享文件夹和剪贴板了。
点击设备,把拖放和剪贴板共享都设为双向。在电脑上找个地方新建个文件夹,然后打开虚拟机,在左上角的设备里面点击共享文件夹,在跳出的窗口里面点右边的按钮,添加共享文件夹。路径的话就把刚刚那个新建的文件夹的目录输进去(可以右键点属性,复制目录,再粘贴进去),勾选自动挂载和固定分配即可完成共享文件夹设置。
然后进入CSAPP的网站下载书籍配套的实验文件和实验说明(链接),第一个实验是Data Lab,点击下载实验说明,建议打印出来方便看。下载实验代码,解压缩,放到刚刚的共享文件夹就可以了。然后再把实验文件从共享文件夹复制到虚拟机主目录里。
实验要求
The bits.c fifile contains a skeleton for each of the 13 programming puzzles. Your assignment is to complete each function skeleton using only straightline code for the integer puzzles (i.e., no loops or conditionals) and a limited number of C arithmetic and logical operators. Specififically, you are only allowed to use the following eight operators:
! ˜ & ˆ | + << >>
A few of the functions further restrict this list. Also, you are not allowed to use any constants longer than 8 bits. See the comments in bits.c for detailed rules and a discussion of the desired coding style.
大意就是限制只能使用上述运算符,使用的数字也不能超过255(但可以通过位运算得到更大的数字)。只允许使用顺序语句,不能使用选择、循环等,数据类型只能用unsigned和int。
我们只需要更改bits.c文件,里面有13道题(13个函数)。
bits.c里面有完整的说明,最好仔细阅读。
更改完bits.c里面的函数之后,保存,右键点击bits.c,选择properties。复制Location文件路径,然后打开命令行,输入:
cd 鼠标右键粘贴目录
回车即可在命令行进入实验文件目录(事实上会用Linux系统的同学并不需要这样做🐕)
再输入:
./dlc
以使用代码检查工具来检查代码是否符合实验规范。
每次修改文件之后都先输入:
make clean
make btest
以重新用GCC编译文件。
输入:
./btest
以运行所有函数的单元测试
输入:
./btest -f 函数名
可以运行单个函数的单元测试
每个函数都有代码数目限制(Max ops),还有分值(Rating)。
题目解析
一、bitXor
/*
* bitXor - x^y using only ~ and &
* Example: bitXor(4, 5) = 1
* Legal ops: ~ &
* Max ops: 14
* Rating: 1
*/
这道题是手动实现异或操作。
已知对两个位进行异或操作,同0得0,同1得0,不同得1,所以,我们先求出x和y同为0的位:
(~x & ~y)
x与y同为1的位:
(x & y)
相同得0,所以要对上面的位进行取反,整个函数就一句话:
int bitXor(int x, int y) {
return ~(~x & ~y) & ~(x & y);
}
二、tmin
/*
* tmin - return minimum two's complement integer
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 4
* Rating: 1
*/
这道题是送分题,返回补码表示的最小的值。
二进制无符号数的值其实可以看作每一位的数(1或0)乘上2的x次方(从第0位开始)的总和,这个x就是第0位到第31位。
而补码的话就是从第0位加到第30位,最后一位则是负的1或者0乘以2的31次方,所以只要第31位是1,0-30位为0,即可得到补码最小值。
int tmin(void) {
return 1 << 31;
}
三、isTmax
/*
* isTmax - returns 1 if x is the maximum, two's complement number,
* and 0 otherwise
* Legal ops: ! ~ & ^ | +
* Max ops: 10
* Rating: 1
*/
这道题是判断这个数是否为最大的数(2^31 - 1),我习惯使用异或来判断是否相等。
首先求这个最大的数0x7FFFFFFF,可以通过
~(1 << 31)
来得到这个数,与其本身异或,求逻辑非的值,即为结果:
int isTmax(int x) {
return !(x ^ (~(1 << 31)));
}
四、allOddBits
/*
* allOddBits - return 1 if all odd-numbered bits in word set to 1
* where bits are numbered from 0 (least significant) to 31 (most significant)
* Examples allOddBits(0xFFFFFFFD) = 0, allOddBits(0xAAAAAAAA) = 1
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 12
* Rating: 2
*/
这道题是求一个数所有的奇数位是否为1,满足则返回1,否则为0。
Easy,先求出所有奇数位都为1,偶数位为0的数,8位则是0xAA,通过左移可以得到0xAAAAAAAA:
int val1 = 0xAA;
val1 = val1 + (val1 << 8) + (val1 << 16) + (val1 << 24);
将x与0xAAAAAAAA进行位与运算,过滤掉所有偶数位的数据,得到所有奇数位的数据。
再将val1与进行位与运算后得到的值进行异或,一样的话会得到0,取逻辑非则为1,不一样的话会得到一个数,取逻辑非为0。
int allOddBits(int x) {
int val1 = 0xAA;
val1 = val1 + (val1 << 8) + (val1 << 16) + (val1 << 24);
return !(val1 ^ (x & val1));
}
五、negate
/*
* negate - return -x
* Example: negate(1) = -1.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 5
* Rating: 2
*/
这道题只是求负数,所有位取反加1即可。
int negate(int x) {
return ~x + 1;
}
六、isAsciiDigit
/*
* isAsciiDigit - return 1 if 0x30 <= x <= 0x39 (ASCII codes for characters '0' to '9')
* Example: isAsciiDigit(0x35) = 1.
* isAsciiDigit(0x3a) = 0.
* isAsciiDigit(0x05) = 0.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 15
* Rating: 3
*/
判断一个数是否是ASCII码里面的数字,这个有点难想,后来看到别人的思路瞬间豁然开朗。
首先取0x30的负数val1,此时如果把这个数与val1相加,则如果这个数大于等于0x30,则结果大于等于0,而小于0x30的话则结果小于0。已知负数符号位为1,0和正数符号位为0,因此可得到这个数是否大于等于0x30。
int val1 = ~0x30 + 1;
类似的方法,取val2为0x80000000减去0x3a,此时val2符号位为1。将这个数与val2相加,如果这个数大于0x39,则结果会大于或等于0x80000000,即符号位为1,而如果小于0x39,结果会小于0x80000000,符号位为0,取反即可得到想要的结果。
int val2 = (1 << 31) + ~0x3a + 1;
将两个结果都进行逻辑非运算,然后位与运算,即为返回值
int isAsciiDigit(int x) {
int val1 = ~0x30 + 1;
int val2 = (1 << 31) + ~0x3a + 1;
return (!((val1 + x) >> 31)) & (!((val2 + x) >> 31));
}
七、conditional
/*
* conditional - same as x ? y : z
* Example: conditional(2,4,5) = 4
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 16
* Rating: 3
*/
这道题是实现 :?运算符,我一开始是怎么都想不出来怎么做的,只能去参考一下别人的办法。。。
才发现如此简单。。。。
首先,先把x取布尔值,然后取反加一,如果x布尔值为0,则所有位为0,如果布尔值为1,则所有位为1:
int val = ~(!!x) + 1;
然后使用位或运算,左边放val & y,右边放~val & z,这样如果val为全1,则返回y的值,如果为全0,则返回z的值:
int conditional(int x, int y, int z) {
int val = ~(!!x) + 1;
return (val & y) | (~val & z);
}
八、isLessOrEqual
/*
* isLessOrEqual - if x <= y then return 1, else return 0
* Example: isLessOrEqual(4,5) = 1.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 24
* Rating: 3
*/
这道题是实现<=运算符,要分情况讨论
1、符号位一样,判断x - y的符号位即可。
2、符号位不一样,x符号位为0则返回1,符号位为1则返回0。
取两个数符号位相加,0 + 0 = 0; 1 + 1 = 2;第一位都是0,如果两个数符号位相加后第一位为0,则符合相同:
int val1 = (x >> 31) + (y >> 31);
求x - y的符号位,这里加逻辑非是为了结果统一:
int val2 = !((y + (~x) + 1) >> 31);
用上一题位与运算类似的办法来进行结果的选择:
int isLessOrEqual(int x, int y) {
int val1 = (x >> 31) + (y >> 31);
int val2 = !((y + (~x) + 1) >> 31);
int val3 = x >> 31 & 1;
return (val1 & val3) | ((~val1) & val2);
}
九、logicalNeg
/*
* logicalNeg - implement the ! operator, using all of
* the legal operators except !
* Examples: logicalNeg(3) = 0, logicalNeg(0) = 1
* Legal ops: ~ & ^ | + << >>
* Max ops: 12
* Rating: 4
*/
这道题是实现逻辑非,我一开始想0取反加一还是0,但后来试了一下发现0x80000000取反加一也是原来的值,然后想着怎么排除0x80000000,后来参考了一下别人的,发现是我想复杂了。
一个数,取反加一,再与原来的数字进行位或运算,如果是0,那结果还是0,如果不是0,则符号位必为1。将结果右移31位,如果符号位为0,则结果为0,如果符号位为1,则结果为全1。
把上一个结果再加1,0 + 1为1,0xFFFFFFFF + 1 = 0,即为返回值。
int logicalNeg(int x) {
return ((x | (~x + 1)) >> 31) + 1;
}
十、howManyBits
/* howManyBits - return the minimum number of bits required to represent x in
* two's complement
* Examples: howManyBits(12) = 5
* howManyBits(298) = 10
* howManyBits(-5) = 4
* howManyBits(0) = 1
* howManyBits(-1) = 1
* howManyBits(0x80000000) = 32
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 90
* Rating: 4
*/
这道题是求一个数的补码最少可以用多少位来表示,比如12,二进制为0x1100,还有一位符号位,所以返回5。这可能是这次实验里面最难的一道了。我想了很久,但是总会有个别案例不通过,最后去看别人的方法,哇,想不到还可以用二分法~~我太弱了。。。。
首先是0和-1两个特殊情况,都是一位,-1的二进制补码是全1,也就是说,取反之后跟0是一样的,只需要一个符号位就可以表示。而一般的数,取反之后跟取反之前都可以用一样的位数来表示,这道题的方法来自@vhyz:
因此,对传入的数x,如果是正数,就不用动,如果是负数,就取反。可以通过把x右移31位后的值与x进行异或运算,这样如果符号位为0,0和任何数异或不变,如果符号位为1,补码右移的规则是在右移之后的空位补上符号位,即如果符号位为1,则右移31位后的值为0xFFFFFFFF。x与0xFFFFFFFF异或的效果即为取反。
int op = x ^ (x >> 31);
这里设3个变量,
- 如果x == 0,val1 = 1;
- 如果x == -1,val2 = 1;
- 如果x != 0 && x != -1,val3 = 0xFFFFFFFF;
然后就是0和-1之外的情况的操作,这里的方法很巧妙:
-
取op右移16位后的布尔值,这样可以判断高16位是否为0;
-
将刚刚得到的布尔值左移4位,存放在bit_16,如果布尔值为0,bit_16 = 0,如果布尔值为1,则bit_16 = 1;
-
将op右移bit_16位,如果op多于16位,则之后只剩下高16位,否则不变。
-
这时候这个数就只剩下16位要处理了,用同样的方法:
-
- 取op右移8位后的布尔值,这样可以判断高8位是否为0;
- 将刚刚得到的布尔值左移3位,存放在bit_8,如果布尔值为0,bit_8 = 0,如果布尔值为1,则bit_8 = 1;
- 将op右移bit_8位,如果op多于8位,则之后只剩下高8位,否则不变。
把下面的bit_16 , 16, 4 分别换成[bit_8, 8, 3]、[bit_4, 4, 2]、[bit_2, 2, 1]、[bit_1, 1, 0],都运算一遍.
bit_16 = (!!(op >> 16)) << 4;
op = op >> bit_16;
再把bit_xx的值相加,因为不为0,所以还有一位是不用判断必为1的,再有一个符号位为1,所以:
sum = 2 + bit_16 + bit_8 + bit_4 + bit_2 + bit_1;
返回值为:
return (val1) | (val2) | (val3 & sum);
完整代码:
int howManyBits(int x) {
int val1 = !(x ^ 0);
int val2 = !(x ^ (~0));
int val3 = ~(~(val1 | val2) + 1);
int bit_16, bit_8, bit_4, bit_2, bit_1;
int sum;
int op = x ^ (x >> 31);
bit_16 = (!!(op >> 16)) << 4;
op = op >> bit_16;
bit_8 = (!!(op >> 8)) << 3;
op = op >> bit_8;
bit_4 = (!!(op >> 4)) << 2;
op = op >> bit_4;
bit_2 = (!!(op >> 2)) << 1;
op = op >> bit_2;
bit_1 = (!!(op >> 1));
op = op >> bit_1;
sum = 2 + bit_16 + bit_8 + bit_4 + bit_2 + bit_1;
return val1 | val2 | (val3 & sum);
}
十一、floatScale2
/*
* floatScale2 - Return bit-level equivalent of expression 2*f for
* floating point argument f.
* Both the argument and result are passed as unsigned int's, but
* they are to be interpreted as the bit-level representation of
* single-precision floating point values.
* When argument is NaN, return argument
* Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
* Max ops: 30
* Rating: 4
*/
这道题是求浮点数乘二,一般的浮点数乘二只要阶码加一即可,不过我们要考虑几种特殊情况:
- 0乘2
- 无穷大或者NaN乘2
- 非规格数乘2
题目对于浮点数的函数格式要求放宽了不少,可以使用选择,循环,并且常量值可以使用Int范围内的所有数。
首先对浮点数的各部分进行提取:
int exp = uf & 0x7f800000;
int frac = uf & 0x7fffff;
判断是否为无穷大或者NaN:
if (exp == 0x7f800000)
return uf;
判断是否为0或非规格数,非规格数乘2为左移1位:
else if (exp == 0)
frac = frac << 1;
然后就是一般的情况:
else
exp = exp + 0x800000;
最后就是把结果合并:
ret = (uf & 0x80000000) | exp | frac;
值得一提的是,非规格数如果尾数最高位为1时,右移1位会使阶码最低位从0变为1,而这时候恰好就是正确的结果,并不需要额外的处理。这是因为乘2之后完成了进位,刚好规格数在小数点前有一个1,规格数和非规格数从而无缝衔接。
完整的函数:
unsigned floatScale2(unsigned uf) {
int ret;
int exp = uf & 0x7f800000;
int frac = uf & 0x7fffff;
if (exp == 0x7f800000)
return uf;
else if (exp == 0)
frac = frac << 1;
else
exp = exp + 0x800000;
ret = (uf & 0x80000000) | exp | frac;
return ret;
}
十二、floatFloat2Int
/*
* floatFloat2Int - Return bit-level equivalent of expression (int) f
* for floating point argument f.
* Argument is passed as unsigned int, but
* it is to be interpreted as the bit-level representation of a
* single-precision floating point value.
* Anything out of range (including NaN and infinity) should return
* 0x80000000u.
* Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
* Max ops: 30
* Rating: 4
*/
这道题是实现浮点型转整型,需要分情况讨论:
- 浮点数超过整形能表达的最大值
- 浮点数小于1
同上一题一样,先提取浮点数各部分出来:
int exp = 0xff & (uf >> 23);
int frac = 0x7fffff & uf;
int sign = !!(uf >> 31);
按照题目要求,超过最大值返回0x80000000u:
if (exp > 127 + 30)
return 0x80000000u;
小于1,返回0:
if (exp < 127)
return 0;
正常情况,特别的,如果浮点数符号位为1,在得到浮点数的绝对值之后取反加一:
tmp = ((frac >> 23) + 1) << (exp - 127);
if (sign)
return (~tmp) + 1;
else
return tmp;
完整函数:
int floatFloat2Int(unsigned uf) {
int exp = 0xff & (uf >> 23);
int frac = 0x7fffff & uf;
int sign = !!(uf >> 31);
int tmp;
if (exp > 127 + 30)
return 0x80000000u;
if (exp < 127)
return 0;
tmp = ((frac >> 23) + 1) << (exp - 127);
if (sign)
return (~tmp) + 1;
else
return tmp;
}
十三、floatPower2
/*
* floatPower2 - Return bit-level equivalent of the expression 2.0^x
* (2.0 raised to the power x) for any 32-bit integer x.
* The unsigned value that is returned should have the identical bit
* representation as the single-precision floating-point number 2.0^x.
* If the result is too small to be represented as a denorm, return 0.
* If too large, return +INF.
* Legal ops: Any integer/unsigned operations incl. ||, &&. Also if, while
* Max ops: 30
* Rating: 4
*/
这道送分题,求2的x次方,返回浮点数。
三种情况:
- x小于-127,结果为0
- x大于128,结果为无穷大(阶码全为1,指数为0)
- 结果为阶码 = x + 127(阶码的偏移量)
unsigned floatPower2(int x) {
if (x < -127) return 0;
if (x > 128) return 0xff << 23;
return (x + 127) << 23;
}
OK,我们这样就算是做完了CSAPP的第一个数据实验,第二个实验是大名鼎鼎的炸弹实验,敬请期待~
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core GC计划阶段(plan_phase)底层原理浅谈
· .NET开发智能桌面机器人:用.NET IoT库编写驱动控制两个屏幕
· 用纯.NET开发并制作一个智能桌面机器人:从.NET IoT入门开始
· 一个超经典 WinForm,WPF 卡死问题的终极反思
· ASP.NET Core - 日志记录系统(二)
· 博客园 & 1Panel 联合终身会员上线
· 支付宝事故这事儿,凭什么又是程序员背锅?有没有可能是这样的...
· https证书一键自动续期,帮你解放90天限制
· 告别虚拟机!WSL2安装配置教程!!!
· 在线客服系统 QPS 突破 240/秒,连接数突破 4000,日请求数接近1000万次,.NET 多