實驗說明
《深入理解計算機系統》是卡內基梅隆大學計算機專業的計算機體系課程的標配教材,可以在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的第一個數據實驗,第二個實驗是大名鼎鼎的炸彈實驗,敬請期待~