深入理解计算机系统(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之外的情况的操作,这里的方法很巧妙:

  1. 取op右移16位后的布尔值,这样可以判断高16位是否为0;

  2. 将刚刚得到的布尔值左移4位,存放在bit_16,如果布尔值为0,bit_16 = 0,如果布尔值为1,则bit_16 = 1;

  3. 将op右移bit_16位,如果op多于16位,则之后只剩下高16位,否则不变。

  4. 这时候这个数就只剩下16位要处理了,用同样的方法:

    1. 取op右移8位后的布尔值,这样可以判断高8位是否为0;
    2. 将刚刚得到的布尔值左移3位,存放在bit_8,如果布尔值为0,bit_8 = 0,如果布尔值为1,则bit_8 = 1;
    3. 将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的第一个数据实验,第二个实验是大名鼎鼎的炸弹实验,敬请期待~

posted @ 2020-08-07 01:45  肥皂QAQ  阅读(6840)  评论(6编辑  收藏  举报