CS:APP 實驗DataLab (滿分原創,解析詳細)

實驗及其環境

Data Lab [Updated 12/16/19]
環境: Ubantu
gcc版本: 4.8.5

常用命令

make:編譯所有文件
./dlc bits.c: 測試使用的操作符是否合法
./btest -h //測試命令的功能列表,按照說明使用即可
如./btest -f bitXor,測試bitXor是否能通過所有的測試用例

在data-handout的readme文件中對上述命令有具體說明,在此不多贅述

題解

第一關

1.bitXor

/* 
 * bitXor - x^y using only ~ and & 
 *   Example: bitXor(4, 5) = 1
 *   Legal ops: ~ &
 *   Max ops: 14
 *   Rating: 1
 */

大意:使用~和&來完成^運算符。

大致思路是先完成( ~x & y) | ( x & ~y)運算,最後利用摩根定律,使用~和&運算符來替代 | 操作符。

int bitXor(int x, int y) {
  return ~(~(~x & y) & ~( x & ~y));
}

2.tmin

/* 
 * tmin - return minimum two's complement integer 
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 4
 *   Rating: 1
 */

大意:求Tmin

送分題,直接移位

int tmin(void) {

  return 1 << 31;

}

3.isTmax

/*
 * isTmax - returns 1 if x is the maximum, two's complement number,
 *     and 0 otherwise 
 *   Legal ops: ! ~ & ^ | +
 *   Max ops: 10
 *   Rating: 1
 */

大意:判斷給出的x是否是Tmax。

如果x == Tmax,則有!~((x + 1) ^ x ) = 1,當然 x == -1時,該等式也成立。因此特判一下 x != -1,此時!!(x + 1)應該爲0x1,等式成立只有這兩種情況了。

int isTmax(int x) {
  return !~((x + 1) ^ x ) &!!(x + 1);
}

第二關

1.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
 */

大意:如果x中所有的奇數位都爲1則返回1,反之返回0.(x位數範圍爲0-31),偶數位可以不用管。

取其中的一個字節10101010,十進制爲170,將其生成爲一個所有奇數位都爲1的數字mask(10101010 10101010 10101010 10101010),拿這個數字和x來比較,如果x == mask,則必有(x&mask) ^ mask == 0(a^a == 0, x&mask是爲了去掉偶數位)

int allOddBits(int x) {
  int mask = 170 + (170 << 8) + (170 << 16) + (170 << 24);
  return  !((x & mask) ^ mask);
}

2.negate

/* 
 * negate - return -x 
 *   Example: negate(1) = -1.
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 5
 *   Rating: 2
 */

大意:求相反數

書本中給有這個公式

int negate(int x) {
  return ~x + 1;
}

第三關

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
 */

大意:判斷給定的x是否是ascii碼,即0x30 <= x <= 0x39返回1,繁殖返回0

先算出0x30等於十進制的48,而0x39位十進制的57,所以轉化一下,即求 x-48>=0 && x-58<0(-運算符使用第二關第三題可以計算),再通過取反並移31位。如果x是在此區間範圍,!((x + ~48 + 1) >> 31) = 0x1, ((x + ~58 + 1) >> 31 = 0x1,通過&連接即可

int isAsciiDigit(int x) {
  return (!((x + ~48 + 1) >> 31)) & ((x + ~58 + 1) >> 31);
}

2.conditional

/* 
 * conditional - same as x ? y : z 
 *   Example: conditional(2,4,5) = 4
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 16
 *   Rating: 3
 */

大意:實現三目運算符 x ? y : z

這題可以這樣想,既然x!=0返回y,則有一部分應該是0xffffffff & y,此時應該無視z,那麼另外一個部分就可以是0x0 & z,然後通過|操作符連接。即返回(0xffffffff & y) | (0x0&z)
那麼問題來了,x!=0時,則需將x轉化爲0xfffffff,反之則轉化爲0x0;我是用移位操作來完成的,類似二分的遞增。

int conditional(int x, int y, int z) {
  int bits = !!x;
  bits += bits << 1;
  bits += bits << 2;
  bits += bits << 4;
  bits += bits << 8;
  bits += bits << 16;
  return (bits & y) | (~bits & z);
}

看到網上有將x轉化爲0xfffffff更神仙的方法:

int conditional(int x, int y, int z) {
   return ((!x+~1+1)&y)|((~!x+1)&z);
}

3.isLessOrEqual

/* 
 * isLessOrEqual - if x <= y  then return 1, else return 0 
 *   Example: isLessOrEqual(4,5) = 1.
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 24
 *   Rating: 3
 */

大意: x<= y時返回1,反正返回0

先得到x, y和 y - x 的符號位,再分三種情況討論即可
①x < 0, y < 0, y - x >= 0
②x > 0, y > 0, y - x >= 0
③y >=0, x < 0

int isLessOrEqual(int x, int y) {
  int negX = x >> 31;
  int negY = y >> 31;
  int gte = !((y + ~x + 1) >> 31);
  return (negX & negY & gte) | (!negX & !negY & gte) | (negX & !negY);
}

第四關

1.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,其他的數都不等,那麼可以利用這個特點,來對x進行一個比較和判斷,當 x == 0時,(~-0) & (~+0) 的符號位一定爲1, 若是其他的數,符號位則不爲1. 最後右移31位,&1去除其他的位可以得到結果

int logicalNeg(int x) {
  return (((~(~x + 1)) & (~x)) >> 31) & 1;
}

2.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
 */

大意:返回最少用來表示x的位數.

可以說是最難的一題了

比如說howManyBits(12) 返回 5,12的需要最少的二進制是 01100,高位的0可以去掉,此時需要的位數是5位。但是如果用1100就不行,因爲這時表示的是-4。表示-5的需要最少的二進制爲 1001,返回4

可以先做一個預處理,把負的都取反,比如上面的-5,取反後爲(1…1)0110,這時就可以把-5當成正數來處理了。

接下來這一部分比較頭疼,我也是想了很久纔想明白,主要是使用了一個二分的思想

核心思想是,查看x的後16bit是否都是1,如果是這樣的話,那麼x至少要16位才能表示;如果後16位都是1,那麼將x右移16位,反之則右移0位,使用(!!(x >> 16)) << 4可以使得s16變爲0或16;接下來,再依次對x的後8位,4位,2位,1位進行處理。

最後特殊情況需要注意,比如當x輸入是0時,這是s1 - s16都是0,此時s16 + s8 + s4 + s2 + s1 = 0, 但實際上都需要返回1,嘗試在之前的結果加上x+1,此時返回的剛好等於1;再看另一種情況, x是0x01需要返回2,此時s1 - s16均爲0, 最後的x = 1,x+1 =2,也成立。所以最後應該返回s16 + s8 + s4 + s2 + s1 + x + 1而不是s16 + s8 + s4 + s2 + s1

int howManyBits(int x) {
  int s16, s8, s4, s2, s1;
  int sign = (x >> 31) & 1;
  int mask = ~sign + 1;

  x = (mask & ~x) | (~mask & x);
  
  s16 = (!!(x >> 16)) << 4;
  x >>= s16;

  s8 = (!!(x >> 8)) << 3;
  x >>= s8;

  s4 = (!!(x >> 4)) << 2;
  x >>= s4;

  s2 = (!!(x >> 2)) << 1;
  x >>= s2;

  s1 = (!!(x >> 1)) << 0;
  x >>= s1;

  return s16 + s8 + s4 + s2 + s1 + x + 1;
}

3.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
 */

大意:給定一個位表示的浮點數f,返回2*f的位表示

這裏面牽扯到的是浮點數的知識了,翻書可以知道float是32位表示,其中符號位sign佔1位,階碼位exp佔8位,尾數frac佔23位。所以可使用掩碼操作,先分別得到sign,exp,frac的位表示。

接下來就很明朗了,直接將這個數分爲規格化數(normalize)和非規格化數(denormolize)討論即可,對sign,exp,frac進行計算,最後返回sign | exp | frac

①exp = 0的時候,十進制的值表示爲2EM{^E*M},這時候uf*2,也就是2E2M{^E*2M},直接frac左移一位就可以了(非規格化的值時,M=f)。
②exp !=0 且 uf不爲無窮時,十進制2E{^E}M{*M}乘2就變成了2E+1{^{E+1}}M{*M},又E=expbias{E = exp - bias},所以只需要將exp加1就可以完成計算了,此外,還要注意一下溢出的情況特判。

unsigned floatScale2(unsigned uf) {
  unsigned sign = uf & 0x80000000;
  unsigned exp = uf & 0x7f800000;
  unsigned frac = uf & 0x007fffff;
  if( exp == 0x0){
    frac <<= 1;
  }else if(exp!= 0x0 && exp != 0x7f800000){
    exp += 0x00800000;
    // return ∞
    if(exp == 0x7f800000) return sign | exp;
  }

  return sign | exp | frac;
}

4.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
 */

大意:將float轉化爲int,其中float給出的是位表示。

還是先分別取其中的sign,exp和frac三個位。先計算出abs=2EMabs = 2{^E}*M,然後返回sign == 0 ? abs : -abs;

可以先在草稿紙上算一算,uf<1時肯定要返回0,所以計算uf==1時的臨界值,即2EM==1{2^E*M == 1},此時E是0,根據E=exp127{E = exp - 127} 可得exp爲127,所以分下面幾種情況討論

①當exp<127時,向下取整進而返回0;
②這時再來考慮一下溢出的情況,int最大值爲23112^{31}-1所以這個時候exp127==31{exp - 127} == 31 剛好溢出,所以當exp > 157的時候返回一個0x80000000u
③最後來考慮可以正常計算的情況,先求得abs的值,然後再來考慮frac位的情況。由於abs=2E(1+f){abs = 2^E*(1+f)},所以只需要在原來的基礎上加上f2E{f*2^E},這就很好辦了,使用一個循環,依次計算frac的每一位即可。


int floatFloat2Int(unsigned uf) {
  int sign = uf >> 31;
	int exp = (uf >> 23) & 0xff;
	int frac = uf & 0x007fffff;
	int E;
	int cnt;
	int abs;
	if (exp < 127) {
		return 0;
	}
	else if (exp > 157) {
		return 0x80000000u;
	}
	E = exp - 127;

	//2^E * M
	cnt = -23;
	abs = 1 << E;
	while (frac > 0) {
		abs += (cnt + E >= 0) ? (1 << (cnt + E)) : 0;
		cnt++;
		frac >>= 1;
	}

	return !sign ? abs : ~abs + 1;
}

5.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的位級表示

也是一系列的特判,計算float可以表示的範圍。

①最小的邊界值爲2E=223126=21492^E=2^{-23-126}=2^{-149},所以x<-149時直接返回0
②最大的值爲2254127=2E1272^{254-127}=2^{E-127},所以x>127時返回一個正無窮就行
③這時再來計算最小的規格化值,也就是exp等於1,frac=0時x的邊界值。這時十進制是21127=21262^{1-127}=2^{-126},所以當x<-126時,exp=0,frac=-x
④最後的情況就是exp!=0了,這時位數frac=0,由於2x=2exp127{2^x = 2^{exp-127}},所以exp = x+127,通過掩碼操作和移位,得到exp部分的位表示。

unsigned floatPower2(int x) {
    unsigned sign = 0x0;
    unsigned exp;
    unsigned frac;
    if(x < -149) return 0;
    else if(x > 127) return 0x7f800000u;
    else if( x < -126 ){
      exp = 0x0;
      frac = ~x + 1;
    }else{
      frac = 0x0;
      exp = ((x + 127) & 0xff) << 23;
    }
    return sign | exp | frac;
}

運行結果

舒服了
在這裏插入圖片描述

在這裏插入圖片描述

小結

這個實驗寫了兩天,文章寫了一天(猝。有部分題還是有難度的。推薦vscode上的一個插件Romote-SSH,通過這個插件連接遠程服務器,直接在vscode上寫的。

剩下的部分和書接下來再看,有一說一csapp這本書真有意思,接下來再繼續完成bomb實驗。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章