CSAPP:Lab1 -DataLab 超詳解

周小倫發表於2021-01-19

寫在前面

之前考研的時候csapp的書有刷過5,6遍,所以對書本知識還算比較瞭解。恰逢最近在學c++的時候,順帶刷一下大名鼎鼎的csapp實驗。

0. 環境準備

最好準備一個純淨的Linux系統這裡建議使用docker 構建一個centos或者 ubuntu系統

實驗資料的下載
CS:APP3e, Bryant and O'Hallaron
docker上的環境搭建請參考下面的文章
CSAPP:Lab0-搭載環境

拉取centos系統
docker pull centos

建立目錄掛載實現檔案同步
docker container run -it -v /Users/xxxx/yourFilePath:/csapp --name=csapp_env centos /bin/bash

/Users/xxxx/yourFilePath 請替換成你自己想要進行同步的目錄
:/csapp 也請替換成你自己想要命名的目錄

這裡的csapp目錄就是和你本地目錄同步的目錄

同步完成之後可以發現在docker下的csapp目錄和我們的yourFilePath檔案實現了同步


出現類似上面的結果則為配置正確

  • 配置編譯環境
  • 更新yum源
    yum -y update
  • 安裝sudo
    yum install sudo
  • 安裝c/c++編譯環境
    yum install make automake gcc gcc-c++ kernel-devel
  • 安裝gdb
    yum install gdb
  • 準備32位嵌入式c庫
    yum install glibc-devel.i686
  • 閱讀readme 完成配置
    這裡需要先進入剛才對映的檔案目錄csapp檔案然後參考readme檔案
shell    To compile and run the btest program, type:
unix> make btest
unix> ./btest [optional cmd line args]

完成上面的操作之後我們的配置就算完成了。

接下來我們可以在本機的編譯器編寫我們的程式碼。然後在docer中的虛擬容器上編譯和執行我們的程式碼。✅

在編譯器中編寫

在docker容器中編譯和執行

藍色箭頭為編譯。紅色箭頭為執行

注意:每次更改bits.c檔案後都要重新編譯btest。如果需要檢查單個函式的正確性,可以使用-f標誌:

text $ ./btest -f bitXor

dlc程式可以檢測我們有沒有違規,如果執行沒有輸出則沒有問題

text $ ./bits.c

注意每次關閉docker在下一次執行的時候需要先啟動我們的centos。

先找到我們命名為csapp_env容器的容器id

  1. 然後docker start 容器ID啟動我們的容器

  2. 輸入以下命令進入到這個執行中的容器

docker exec -it 容器id /bin/bash

1. 實驗開始

*   IMPORTANT. TO AVOID GRADING SURPRISES:
*   1. Use the dlc compiler to check that your solutions conform
*      to the coding rules.
*   2. Use the BDD checker to formally verify that your solutions produce 
*      the correct answers.
*/

1.1 bitXor

a^b=
1.(a|b)&(~a|~b)
2.~(~a&~b)&~(a&b)
3.(a&~b)|(~a&b)

可以用這三種方式表示異或操作,具體的推導可以自行Google,參考離散數學我自己推了一下發現其實不難

主要是對德摩根律的應用

\[  \begin{align}   A\bigoplus B & = \overline{\overline AB\cup A \overline B} \\             & = \overline{(A \cup \overline B)\cap (\overline A \cup B})  \\             & = ( \overline{(A \cup \overline B)\cap \overline A )\cup ( (A \cup \overline B)\cap B )}\\             & = \overline{(\overline A \overline B) \cup( A B)} \\             & =\overline{(\overline A \overline B)}\cap \overline{( A B)}\\           \end{align} \]

我們選擇第二種操作即可過掉本例

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

1.2 tmin

int tmin(void) {

  return 1<<31;

}

1.3 tmax

題目描述

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

思路

我們考慮四位的最大值x=0111 然後x+1之後就會變成1000 我們對1000 取非 0111 就會重新變回x值

這裡要是可以用等於是不是直接完成了,但是不能用等於,在這可以用一個位運算的小技巧,我們知道自己與自己異或會得到0,也就是說我們可以用異或來判斷等於!((~(x+1)^x)) 判斷這個是否為1即可判斷是否為最大值

這裡有一個例外就是x=-1 由於-1=1111 他利用上面的式子判斷也符合,故要特判-1 利用!!(x+1) 這個操作-1和最大值並不相同

int isTmax(int x) {

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

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

思路

A=1010 A是一個典型的偶數位都是1的數,那隻要一個四位的二進位制數X & A = A 就說明這個二進位制符合條件。那其實只要判斷x & 0xAAAAAAAA == 0xAAAAAAAA 就可以了由於不能直接定義0xAAAAAAAA 我們需要一些位運算的小技巧

int a=0xAA<<8; //0xAA00
int c=a|0xAA; //0xAAAA
int d=c<<16|c; //0xAAAAAAAA

等號的操作可以直接利用a == b 等價於 !((a & b)^b)

int allOddBits(int x) {
    int a=0xAA<<8;
    int c=a|0xAA;
    int d=c<<16|c;
  return !((x&d)^(d));
}

1.5 negate

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

思路

A + ~A = -1A + neg A =0 利用這兩個式子我們可以得到 neg A = ~A + 1

int negate(int x)

  return ~x+1 ;
}

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

思路

我們先看一下0x39 和 0x30的位級表示

0011100100110000 首先那我們必須滿足x>>4==3 然後在滿足後4位位於0-9之間這個題用了一些小技巧

  1. x & 0xF儲存了x的後四位
  2. 用- A是否為負數來判斷後四位的範圍 c=~0xA+1 實現-A
  3. 判斷負數是和0x8000進行與運算是一個正數
int isAsciiDigit(int x) {
    int a=!(x >> 4 ^0x3);
    int b=x&0xF;
    int c=~0xA+1;
    int e=0x80<<4;
    int d=!!((b+c)&(e));
  return  a&d ;
}

1.7 conditional

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

思路

x > 0 return y else return z

我們需要尋找一種方法當x != 0時候 讓x變成0xFFFFFFFF

int a=!!(x^0x0); //a=0 if x=0 else a =1
int b=~a+1;
int c=~(y&~b)+1;
int d=~(z&b)+1;

return y+z+c+d 我們最後這樣返回,上述程式碼的含義其實非常簡單

如果x!=0 那麼 c就會等於-y 我們最後就可以返回z 否則我們就返回y

1.8 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可能會爆int故不能通過這樣簡單的判斷

int a=x>>31&0x1;
int b=y>>31&0x1;
int c1=(a&~b); //表示 x為- y為+
int c2=(~a&b); //表示 x + y -

下面我們計算y-x 這裡需要考慮一些情況

  1. y-x >= 0 也就是第32位為0 flag=y+(~x+1)>>31=0 這時候如果c2 為1 則表示溢位了 c2為1 的情況我們應該返回0 如果c2=0 則我們應該返回1
  2. y-x <0flag=1 返回0

因此有如下程式碼

int e=y+(~x+1); // x-y;
int flag=e>>31; //如果flag 和 c2 不同則說明了溢位了
return c1 |(!c2&!flag);

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

思路

if x!=0 return 0 else return 1 那麼問題就變成了如何判斷x!=0

我們先看一下~x+1>>31 的情況 只要x!=0 那麼他全為-1 只有x=0 的時候為出現0

那麼我們用 x |(~x+1>>31) 如果為-1 則表示x!=0 為 0 則表示x=0

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

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

思路

本題就是要找到從右向左,最左邊的1在第幾位然後加上一位符號位即可,如果是負數的話我們對其取反然後是同樣的操作那麼會有以下幾種情況

  1. x 在[0,1] 我們需要2位
  2. x在[2,3] 我們需要3位
  3. x在[4,7] 我們需要四位
  4. 總結公式 \(2^i\leq x \leqslant 2^{i+1}-1\) 需要i+2

對於高16位我們這樣進行處理

x=(flag&~x)|(~flag&x); //x為非正數則不變 ,x 為負數 則相當於按位取反
int b16=!!(x>>16) <<4; //如果高16位不為0,則我們讓b16=16
x>>=b16; //如果高16位不為0 則我們右移動16位 來看高16位的情況

然後去看高8位下面的處理基本類似

  //下面過程基本類似
  int b8=!!(x>>8)<<3;
  x >>= b8;
  int b4 = !!(x >> 4) << 2;
  x >>= b4;
  int b2 = !!(x >> 2) << 1;
  x >>= b2;
  int b1 = !!(x >> 1);
  x >>= b1;
  int b0 = x;
return b0+b1+b2+b4+b8+b16+1;

建議大家手動模擬一下這個過程

1.11 floatScale2

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

思路

1.首先考慮第一種情況

When argument is NaN, return argument

需要先求出exp

int exp = (uf&0x7f800000)>>23; //23-30 這8位
int sign=uf>>31&0x1; //符號位
int frac=uf&0x7FFFFF;

如果exp=255 並且尾數非0 就是NaN 直接return 就好 其次如果frac 全為0 那麼則表示無窮大 這兩種情況都可以直接return

  1. 如果exp=0 則表示非規格化數

    那麼我們直接返回uf*2 就可就是把frac>>1

  2. 如果exp!=0 && !=255 那麼表示規格化數

那麼我們的修改就先把exp+1

unsigned floatScale2(unsigned uf) {
    unsigned exp = (uf&0x7f800000)>>23;
    unsigned sign=uf>>31&0x1;
    unsigned frac=uf&0x7FFFFF;
    unsigned res;
    if(exp==0xFF)return uf;
    else if(exp==0){
        frac <<= 1;
        res = (sign << 31) | (exp << 23) | frac;
    }
    else{
     exp++;
     res = (sign << 31) | (exp << 23) | frac;
   }
   return res;
}

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

思路

6位IEEE浮點數格式如下

根據上圖我們可以分為三種情況

先計算出E=exp-bias

  1. 如果是小數 E< 0的情況我們直接返回0

  2. 如果是exp=255 的情況直接返回0x80000000u 這裡注意如果超範圍了也會直接返回0x80000000u

    因此可以直接用E>=31 來判斷

  3. 如果是規格化數則我們進行正常處理\(V=(-1)^s \times M \times 2^E\)

    1. 先給尾數補充上省略的1
    2. 判斷E<23 則尾數需要捨去23-E
    3. 根據符號位返回就好
int floatFloat2Int(unsigned uf) {
    unsigned exp = (uf&0x7f800000)>>23;
    int sign=uf>>31&0x1;
    unsigned frac=uf&0x7FFFFF;
    int E=exp-127;
    if(E<0)return 0;
    else if(E >= 31){
        return 0x80000000u;
    }
    else{

        frac=frac|1<<23;
        if(E<23) {//需要舍入
            frac>>=(23-E);
        }else{
            frac <<= (E - 23);
        }

    }
    if (sign)
        return -frac;
    else
        return frac;
}

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

思路

根據上圖我們可以得出幾個邊界

  1. x>127 返回+NAN
  2. x<-148太小返回0
  3. x>=-126規格化數
  4. 否則就是非規格化數
unsigned floatPower2(int x) {
    if(x>127){
        return 0xFF<<23;
    }
    else if(x<-148)return 0;
    else if(x>=-126){
        int exp = x + 127;
        return (exp << 23);
    } else{
        int t = 148 + x;
        return (1 << t);
    }
}

出現上圖的結果即為正確

相關文章