深入理解計算機系統(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的第一個數據實驗,第二個實驗是大名鼎鼎的炸彈實驗,敬請期待~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM