Introduction to CSAPP(八):Datalab

2020-07-19更新

非常感谢@阿楠(at不到放上他的主页好啦)对于最后一题需要对denormal的处理的提醒,非常感谢他的分享。

因为有你,知识的分享才有意义;因为有你们,开源才有意义。

虚拟环境

虚拟环境搭建面向纯小白用户。

首先,安装一下docker,不管你是Mac还是windows,它都有docker这个东西的。

Mac安装参考:

Install Docker Desktop for Macdocs.docker.com图标

Windows安装参考:

Install Docker Desktop on Windowsdocs.docker.com图标

然后,在命令行中运行:

docker run -d -p 9912:22 --name datalab yansongsongsong/csapp:datalab

这个时候你就有了一个可以操作的实验环境了。如果你熟悉命令行更改文件,你可以执行

docker exec -it datalab /bin/zsh

进入实验环境。

如果你不够熟悉命令行,希望在一个GUI的代码编辑器中方便地更改它,那么我推荐VSCode。

关于VSCode的安装,可以参考:

Download Visual Studio Code - Mac, Linux, Windowscode.visualstudio.com图标

安装好之后,需要安装一个插件 remote ssh

在插件市场搜索这个插件并安装


然后按ctrl + shift + P 或者command + shift + P呼出 vscode 的命令洁面,敲remote ssh

选择 connect to host


之后选择 add host

选择新加ssh host


输入命令

ssh root@127.0.0.1 -p 9912


然后一路回车即可
最后会在右下角看到ssh已经添加的消息,点击建立连接即可

之后就可以连接到这个实验环境了


密码是THEPASSWORDYOUCREATED
随后一路回车即可。最后我们在左侧文件夹栏,选择打开root下的datalab-handout文件夹,这样就可以直接通过vscode来修改与保存代码了。

手工搭建

如果你有一个linux的环境,那最好不过了。我使用的是64位的机器。可惜的是mac是不能build这个作业的,所以我们只能使用centos或者ubuntu的机器。如果没有的话,可以参考上面的虚拟环境搭建。

这里想说的是,这个作业的目标程序是32位的程序。因此在64位机器上,构建时会出现一些问题,在32位机器上构建是否还会有其他问题,我就不得而知了,希望有时间可以测试一下。

在手工搭建的这里只说一些可能遇到的坑:

  1. Make之后报错说没有这个指令
    运行apt-get install build-essential

2. make之后有头文件之类的报错:

 In file included from btest.c:16:0:
 /usr/include/stdio.h:27:10: fatal error: bits/libc-header-start.h: No such file or directory
  #include <bits/libc-header-start.h>
           ^~~~~~~~~~~~~~~~~~~~~~~~~~
不一定仅仅只是这个,包括一些btest依赖的头文件的报错

两个解决方案

    • 运行apt-get installbuild-essential module-assistant gcc-multilib g++-multilib 或者 yum install glibc-devel.i686 libstdc++-devel.i686
这是因为makefile里它的目标文件是32位的,64位机器上没办法直接直接编译目标为32位的程序文件,会抛缺少对应的库
    • 直接修改makefile文件,将CFLAGS = -O -Wall -m32改为CFLAGS = -O -Wall -m64

Lab分析

Datalab主要是希望我们结合对整型、浮点数的编码理解,通过使用基本的位操作、基本运算等操作,实现一系列目标。

Datalab的实验形式是,让我们补充bits.c里面的函数,运行评分程序,获得最高的分数。

Lab可以在csapp.cs.cmu.edu/3e/lab里下载,在虚拟实验环境中我已经为大家下载好了。如果你想要手动下载的话

lab代码csapp.cs.cmu.edu

labs的托管网页中有着好多个综述性的介绍,比如:

课程目标与工具介绍csapp.cs.cmu.edu
lab课程简介csapp.cs.cmu.edu

另外代码中也有一个README,也是讲着差不多的综述性质的介绍。比较推荐看这个课程目标与工具介绍

下面我大概介绍一下这个代码结构。

datalab-handout 代码结构
  • bits.c:你实际操作的文件,里面有着大量的注释,需要你自己去阅读。每道题上的注释就是出题者对你的要求,要求包括你需要修改这个函数以便完成的目标、你在这个函数中所能使用的操作符种类、能使用的操作符的数目等。
  • btest.c:编写好了的用于测试你写的函数是否正确的代码文件,编译它可以获得一个可运行的二进制文件,运行它可以跑一些测试用例,来让你对自己的代码进行测试。
  • dlc:一个可运行的二进制文件,是魔改过的gcc编译器,用来检测你的代码风格。是的,在这个lab中,出题人对你的代码风格进行了限制,他要求你将声明全部放在表达式之前,否则会判错。
  • driver.pl:一个可运行的文件,用来进行最终的分数判定
  • fshow.c&ishow.c:两个工具,编译后可以得到两个可运行的二进制文件,分别可以输出你输入的float数和integer数的内存表示形式,比如标志位是什么,阶码是多少等

用法详情还是需要你自己去阅读代码中的README文件,加油。总的来说,你需要做的是:

  1. 阅读bits.c的注释与代码
  2. 修改它
  3. 命令行运行./dlc -e bits.c查看自己用了多少操作符,以及是否有代码风格问题
  4. 运行make clean && make btest编译文件
  5. 运行./btest检查自己是否做对了
  6. return 1 直到全部做完
  7. 最终运行./driver.pl获得打分

题目解析

题目解析我就不打算在这里详细地说了,直接放出答案,和部分注释以供大家参考。

 /* 
  * bitXor - x^y using only ~ and & 
  *   Example: bitXor(4, 5) = 1
  *   Legal ops: ~ &
  *   Max ops: 14
  *   Rating: 1
  */
 int bitXor(int x, int y) {
   int nand = ~(x & y);
   int a = ~(x & nand);
   int b = ~(y & nand);
 
   return ~(a & b);
 }
 /* 
  * tmin - return minimum two's complement integer 
  *   Legal ops: ! ~ & ^ | + << >>
  *   Max ops: 4
  *   Rating: 1
  */
 int tmin(void) {
   return 1 << 31;
 }
 //2
 /*
  * isTmax - returns 1 if x is the maximum, two's complement number,
  *     and 0 otherwise 
  *   Legal ops: ! ~ & ^ | +
  *   Max ops: 10
  *   Rating: 1
  */
 int isTmax(int x) {
   // TODO: analysis process
   // return !(x & 0x80000000)/* is negative? 0: 1 */ & !~(x | (x + 1))/* is Tmax? 1: 0 */; 
   int i = x + 1;
   x = x + i;
   x = ~x;
   i = !i;
   x = x + i;
   return !x;
 }
 /* 
  * 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
  */
 int allOddBits(int x) {
   // return !~(((x & 0xAAAAAAAA) >> 1) | x);
   int m = 0xAA;
   m = (m << 8) | m;
   m = (m << 8) | m;
   m = (m << 8) | m;
   m = (m << 8) | m;
   return !((x & m) ^ m);
 }
 /* 
  * negate - return -x 
  *   Example: negate(1) = -1.
  *   Legal ops: ! ~ & ^ | + << >>
  *   Max ops: 5
  *   Rating: 2
  */
 int negate(int x) {
   return (~x + 1);
 }
 //3
 /* 
  * 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
  */
 int isAsciiDigit(int x) {
   // TODO: bit ops can determine range
   int sign = 0x1<<31;
   int upperBound = ~(sign|0x39);
   int lowerBound = ~0x30;
   upperBound = sign&(upperBound+x)>>31;
   lowerBound = sign&(lowerBound+1+x)>>31;
   return !(upperBound|lowerBound);
 }
 /* 
  * conditional - same as x ? y : z 
  *   Example: conditional(2,4,5) = 4
  *   Legal ops: ! ~ & ^ | + << >>
  *   Max ops: 16
  *   Rating: 3
  */
 int conditional(int x, int y, int z) {
   // TODO: bit ops can equal with conditional operator
   int flag = (~!!x) + 1;
   return (flag & y) | (~flag & z);
 }
 /* 
  * isLessOrEqual - if x <= y  then return 1, else return 0 
  *   Example: isLessOrEqual(4,5) = 1.
  *   Legal ops: ! ~ & ^ | + << >>
  *   Max ops: 24
  *   Rating: 3
  */
 int isLessOrEqual(int x, int y) {
   int sign = !(x>>31)^!(y>>31);      // is 1 when signs are different
   int a = sign & (x>>31);            // diff signs and x is neg, gives 1
   int b = !sign & !((y+(~x+1))>>31); // same signs and difference is positive or = 0, gives 1
   return a | b;
 }
 //4
 /* 
  * 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 
  */
 int logicalNeg(int x) {
   // x and its two's complement are Additive Inverse
   // | opr and >> make it be -1(D) when x is not 0;
   // it will be 0 when x is 0
   return ((x|(~x+1))>>31)+1;
 }
 /* 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
  */
 int howManyBits(int x) {
   int b16,b8,b4,b2,b1,b0;
   int sign=x>>31;
   x = (sign&~x)|(~sign&x);//如果x为正则不变,否则按位取反(这样好找最高位为1的,原来是最高位为0的,这样也将符号位去掉了)
 
   // 不断缩小范围
   b16 = !!(x>>16)<<4;//高十六位是否有1
   x = x>>b16;//如果有(至少需要16位),则将原数右移16位
   b8 = !!(x>>8)<<3;//剩余位高8位是否有1
   x = x>>b8;//如果有(至少需要16+8=24位),则右移8位
   b4 = !!(x>>4)<<2;//同理
   x = x>>b4;
   b2 = !!(x>>2)<<1;
   x = x>>b2;
   b1 = !!(x>>1);
   x = x>>b1;
   b0 = x;
   return b16+b8+b4+b2+b1+b0+1;//+1表示加上符号位
 }
 //float
 /* 
  * 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
  */
 unsigned floatScale2(unsigned uf) {
   // 0[111 ..11 1]000 ..00
   // 1 is where exp is. to get exp
   int exp_mask = 0x7f800000;
   // 1[000 ..00 0]111 ..11
   // 1 is where exp is. to overlay exp
   int anti_exp_mask = 0x807fffff;
   int exp = (uf&exp_mask)>>23;
   int sign = uf&(1<<31);
   if(exp==0) return uf<<1|sign;
   if(exp==255) return uf;
   exp++;
   if(exp==255) return exp_mask|sign;
   return (exp<<23)|(uf&anti_exp_mask);
 }
 /* 
  * 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
  */
 int floatFloat2Int(unsigned uf) {
   int sign = uf>>31;
   int exp = ((uf&0x7f800000)>>23)-127;
   // complements shadowed 1 ahead of number
   int frac = (uf&0x007fffff)|0x00800000;
   // ignore sign and judge 0
   if(!(uf&0x7fffffff)) return 0;
 
   // overflow
   if(exp > 31) return 0x80000000;
   // decimal cast to 0
   if(exp < 0) return 0;
 
   // 23 is the number of frac bits 32 - 9 = 23
   // we need to complement 0
   if(exp > 23) frac <<= (exp - 23);
   else frac >>= (23 - exp);
 
   // check whether overflow
   if(!((frac >> 31) ^ sign)) return frac; // sign is same with before
   // sign is 1 and sign is different from before, overflow
   else if(frac >> 31) return 0x80000000; 
   // sign is different from before and now is positive, get the minus
   else return ~frac + 1;
 }
 /* 
  * 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
  */
unsigned floatPower2(int x) {
  if (x >= -149 && x <= -127) {
    // for denormal number
    int i = 149 + x;
    return 1 << i;
  } else {
    // 2.0^x = (1.0 * 2^1)^x = 1.0 * 2^x
    // x cant be bigger than 255
    int exp = x + 127;
    if(exp <= 0) return 0;
    if(exp >= 255) return 0x7f800000; // Positive infinity
    return exp << 23;
  }
}

其他想说的

这个实验本质上是考察你对基本数据的内存表示的理解以及对位运算的应用。leetcode上有很多位运算相关的题目,而这种题目一般是相当tricky的,所以如果你做不出来,也不用太过气馁。因为有些题目是有点类似数字电路中让你实现某个逻辑电路门般的存在,属于会者不难,难者不会的范畴。

另外,由于课程经常更新,实际上datalab的题目还有很多。本篇文章涉及到的datalab实验是18年11月2日的版本。这里有一个博主整理了很多其他的题目,我放在这里以飨读者。

《深入理解计算机系统/CSAPP》Data Labwww.hh-yzm.com图标

另外,对于技巧性很高的位运算,我在这里给出一些位运算相关的连接,供大家学习。

位运算有什么奇技淫巧?www.zhihu.com图标斯坦福到小书:Bit Twiddling Hacksgraphics.stanford.edu

另外,还有一些同学在做题到时候会发现最后一题一直都会判错不过的情况,请将makefile里的m32改位m64。是的这个有点无奈。。如果有同学在32位机器上运行成功了,望告知。。

reference

GCC在64位系统上编译32位程序遇到的问题notes.maxwi.com图标《深入理解计算机系统/CSAPP》Data Labwww.hh-yzm.com图标【读厚 CSAPP】I Data Labwdxtub.com图标https://github.com/Amano-Sei/MyCSAPPsolution/raw/master/datalab-handout.targithub.com
csapp-Datalablittlecsd.net图标

编辑于 07-19

文章被以下专栏收录