Datalab

陈风破浪的帆發表於2024-05-12

Datalab

前言

該實驗是《深入理解計算機系統》(英文縮寫CSAPP)課程附帶實驗——Lab1:Data Lab,對應書中第二章內容(資訊的表示和處理),是所有實驗中的第一個實驗,

**實驗目的 **

datalab實驗提供了一個資料夾,我們的目的只是改寫bits.c中的15個函式,使其完成相應的功能即可。至於其他檔案是用來編譯、測試,並且限制你使用一些禁止的運算子

實驗說明

Students implement simple logical, two’s complement, and floating point functions, but using a highly restricted subset of C. For example, they might be asked to compute the absolute value of a number using only bit-level operations and straightline code. This lab helps students understand the bit-level representations of C data types and the bit-level behavior of the operations on data.
學生實現簡單的邏輯、二進位制補碼和浮點數,但使用c的一個高度受限的子集。例如,他們可能被要求僅使用位級操作和直線程式碼計算一個數字的絕對值。本實驗幫助學生理解C資料型別的位級表示和資料操作的位級行為。

目標是修改bits.c的副本,使其透過btest中的所有測試,而不違反任何編碼準則。

makefile -生成btest、fshow和show
readme -此檔案
bits.c -你將修改並提交的檔案
bits.h -標頭檔案
btest.c -主要的btest程式
btest.h -用於構建btest
decl.c -用於構建btest
tests.c - 用於構建btest
test-header.c - 用於構建btest
dlc* -規則檢查編譯器二進位制(資料實驗室編譯器)
driver .pl* -使用btest和dlc自動升級位的驅動程式
Driverhdrs.pm -可選的“擊敗教授”比賽的標頭檔案
fshow.c -用於檢查浮點表示的工具
ishow.c -用於檢查整數表示的實用程式

一、不允許允許的操作

  1. 使用任何控制結構,如if, do, while, for, switch等。

  2. 定義或使用任何宏。

  3. 在此檔案中定義任何其他函式。

  4. 呼叫任何庫函式。

  5. 使用任何其他的操作,如&&, ||, -, or ? :

  6. 使用任何形式的casting

  7. 使用除int以外的任何資料型別。這意味著你不能使用陣列、結構等。[1]

二、實驗環境

將整個檔案拖入Linux環境32位環境中,開啟終端;
1.make clean 清除所有編譯檔案
2.make 編譯所有檔案 (如果bits.c有問題編譯不成功)
3…/btest bits.c 測試bits.c
最終結果:(函式正確情況下)

image-20240512155435723

三、函式實現

需要注意的點:

  1. 按位右移>> :
    對有符號數都是算數右移(最高位是0補0,最高位是1補1)
    對無符號數都是邏輯右移(最高位補0)
  2. 提取符號的方法:x&1 (格式化該數,得到其符號)

題目列表

![d4c2e1283b653bebcf387793c7d2603](C:\Users\86155\Documents\WeChat Files\wxid_9k0twos4vlbj22\FileStorage\Temp\d4c2e1283b653bebcf387793c7d2603.jpg)



1.bitXor(x,y)

只使用兩種位運算實現異或操作。

  • 這裡可以用一個真值表
image-20240512160542746
  • 思路:

摩根定律

a^b=
1.(a|b)&(~a|~b)
2.~(~a&~b)&~(a&b)
3.(a&~b)|(~a&b)
image-20240512160542746
  • 程式碼
/* 
 * bitXor - x^y using only ~ and & 
 *   Example: bitXor(4, 5) = 1
 *   Legal ops: ~ &
 *   Max ops: 14
 *   Rating: 1
 */
int bitXor(int x, int y) {
  return ~(~x&~y)&~(x&y);
}

2.tmin()

使用位運算獲取對2補碼的最小 int 值。這個題目也是比較簡單。

/* 
 * tmin - return minimum two's complement integer 
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 4
 *   Rating: 1
 */
int tmin(void) {
  return 0x1<<31;
}
  • 思路

C 語言中 int 型別是32位,即4位元組數。補碼最小值就是符號位為1,其餘全為0。所以只需要得到這個值就行了,我採用的是對數值 0x1 進行移位運算,得到結果。


3.isTmax

判斷x是不是補碼錶示的最大值

  • 思路

要我們判斷輸入的x是不是最大的補碼整數。首先,最大補碼整數為:符號位為0,其餘位為1(其實就是最小補碼取反)。所以我麼可以先構造最大整數:max = ~(0x1<<31)。

假設x為TMax(01111111,8位),則x+1得到TMin(10000000),再加x,取反再取非,即可返回1,而其餘值返回0
但因為當x=-1時,最後返回值也為1,所以要排除這種情況

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


4.allOddBits(x)

  • 思路

這個題目還是比較簡單的,採用掩碼方式解決。首先要構造掩碼,使用移位運算子構造出奇數位全1的數 mask ,然後獲取輸入 x 值的奇數位,其他位清零(mask&x),然後與 mask 進行異或操作,若相同則最終結果為0,然後返回其值的邏輯非。

判斷所有奇數位是否都為1,這裡的奇數指的是位的階級是2的幾次冪。重在思考轉換規律,如何轉換為對應的布林值。

所有奇數位為1則返回1。其中位的編號從0(最低有效位)到31(最高有效位)。

想要判斷一個位是否為1很簡單:設計一種掩碼,需要檢測的位設定為1,其餘位設定為0,然後和要檢測的數按位與,最後判斷結果和掩碼是否相等即可。

本題的難點是怎麼構造這樣一個補碼。因為常數被限制在0~255,即我們只能設定最低的8位。先看8位,奇數位為1偶數位為0即0xAA(10101010),採用位移的方式使32位中每個8位都符合這個標準。結果如下:

/* 
 * 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) {
    int A = 0xA;
    int AA= A|(A<<4);
    int AAA=AA|(AA<<8);
    int mask=AAA|(AAA<<16);
  return !((x&mask)^mask);
}

5.negate

返回-x

思路:
一個數的相反數為其二進位制位表示的按位取反,再+1,即-x=(~x)+1

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

6.isAsciiDigit(x)

計算輸入值是否是數字 30-39 的 ASCII 值。這個題讓我認識到了位級操作的強大。

思路:
要使0x30 <= x <= 0x39,則x-0x30>=0 且 0x39-x>=0
即運算結果的符號位為0,可以用移位操作(有符號數右移為算術右移)
另,減法運算可以看作x+(-x), -x=~x+1

/* 
 * 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) {
    //0x30 -> 110000
    //0x39 -> 111001
    //leading zero ->26
    int a = x>>6;
    int cond1 = !a;

    //11xxxx
    int b = x>>4;
    int cond2 = !(b^0b11);

    //111001 1001 -> 9
    int c = x&(0xf);
    int res = c-(0xA);
    int cond3 =!!(res>>31);
  return

7.conditional(x, y, z)

使用位級運算實現C語言中的 x?y:z三目運算子。又是位級運算的一個使用技巧。

  • 思路:
    用倒推的思路,返回值二選一,return結果一定是用 | 連線
    而一個返回y,一個返回z,返回原值可以用補碼全1(即-1)和&來實現,返回0可用0和&來實現
    定義中間量condition=-1或0,condition需要與x相關聯,則可以用!!x和取相反數的操作來實現
    當 x!=0時,!!x=1, condition=~(!!x)+1=-1
    當 x= 0時,!!x=0, condition=~(!!x)+1= 0
/* 
 * 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) {
	int mask = ((!!x)<<31)>>31;
  	return (mask&y)|((~mask)&z) ;
}

8.isLessOrEqual

如果x<=y,則返回1,否則返回0

當相同符號時,直接相減判斷是否為負數即可。
當前者為正,後者為負時,直接返回0,前者為負後者為正時返回1。

  • 思路

​ 透過位運算實現比較兩個數的大小,無非兩種情況:一是符號不同正數為大,二是符號相同看差值符號。

/* 
 * 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
 *   !(x^y)
 *
 *   2.x+ y-
 *   signX = x>>31&0x1;
 *   signY = y>>31&0x1;
 *   !signX&&!signY
 *
 *   3.x- y+
 *   signX&&!signY;
 *
 *   4.x+ y+   x- y-
 *   x+(~y)+1 //x-y
 *   ((x+(~y)-1)>>31)&0x1
 */
int isLessOrEqual(int x, int y) {
    int cond1 = !(x^y);
    int signX = (x>>31)&1;
    int signY = (y>>31)&1;
    int cond2 = !((!signX)&(signY));
    int cond3 = signX&(!signY);
    int cond4 = ((x+(~y)+1)>>31)&0b1;

  return cond1 |( cond2 & (cond3 | cond4)) ;
}

9.logicalNeg

實現“!”運算子

使用位級運算求邏輯非 !

  • 思路

​ 邏輯非就是非0為1,非非0為0。利用其補碼(取反加一)的性質,除了0和最小數(符號位為1,其餘為0), 外其他數都是互為相反數關係(符號位取位或為1)。0和最小數的補碼是本身,不過0的符號位與其補碼符號位位或為0,最小數的為1。利用這一點得到解決方法。

/* 
 * 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) {
    int negx = (~x)+1;
    int sign = (negx|x)>>31;
  return sign+1 ;
}

10. howManyBits

求值:“一個數用補碼錶示最少需要幾位?”

例子:

howManyBits(12) = 5

howManyBits(298) = 10

howManyBits(-5) = 4

howManyBits(0) = 1

howManyBits(-1) = 1

howManyBits(0x80000000) = 32

這裡我們以12為例

0000 0000 0000 0000 0000 0000 0000 1100

實際上要得到12只需要4+1(符號位)=5位即可

這裡我們可以看出,計算一個數最小需要多少位元位的問題轉化成了從最高位到最低位尋找第一個資料為1的問題

這裡我們使用類似於二分的思想

先看高低16位(判斷是否存在資料為1的位元位),在看8位,4,2,1......

這裡用 flag = !!(x>>16);

如果flag結果為1,表示高16位中存在一位或者多位資料為1的位元位

為0表示高16位中不存在資料為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
 *            hiowManyBits(0)  = 1
 *            howManyBits(-1) = 1
 *            howManyBits(0x80000000) = 32
 *  Legal ops: ! ~ & ^ | + << >>
 *  Max ops: 90
 *  Rating: 4
 */
int howManyBits(int x) {
	int isZero = !x;
	int flag = x>>31;
	int mask = ((!!x)<<31)>>31;
	x = (flag & (~x)) | ((~flag) & x );
	int bit_16,bit_8,bit_4,bit_2,bit_1,bit_0;

	bit_16 =(!((!!(x>>16))^(0x1)))<<4;
	x>>=bit_16;
	bit_8 =(!((!!(x>>8))^(0x1)))<<3;
	x>>=bit_8;
	bit_4 =(!((!!(x>>4))^(0x1)))<<2;
	x>>=bit_4;
	bit_2 =(!((!!(x>>2))^(0x1)))<<1;
	x>>=bit_2;
	bit_1 =(!((!!(x>>1))^(0x1)));
	x>>=bit_1;
	bit_0 =x;

	int ret = bit_16 + bit_8 + bit_4 + bit_2 + bit_1 + bit_0 + 1;

	return isZero | (mask & ret) ;
}

11.unsigned floatScale2(unsigned uf)

求浮點數乘以2的結果

根據IEEE浮點規則我們知道

$$V = ((-1)^ s ) * M * ( 2 ^ E )$$

⚠️這裡需要注意的是函式傳入的引數以及返回值都是無符號整型數

也就是說變數uf雖然是一個無符號整型數,但在題目中我們需要把它的二進位制表示解析成一個單精度浮點數

思路1

image-20240512160542746

看了圖,思考一下如何將一個無符號整型數解析成單精度浮點數呢?

A:根據單精度浮點數的定義來劃分變數uf的32個bit位

其中uf二進位制表示的最高位(31位)表示的是符號位s

第23~30位表示階碼欄位(exp)

第0~22位表示小數字段(frac)

我們需要從uf中提取符號欄位、階碼欄位和小數字段

那麼獲取階碼欄位exp將uf與0x7F800000(如下)按位“與運算”,再將結果右移23位即可

0     1111 1111(exp)   0000.....0000

即 exp = (0x7F800000 & uf ) >>23

這樣就解析出階碼欄位exp,獲取其他欄位的方式同理

由此,我們將無符號整型數解析成單精度浮點數

根據IEEE浮點規則我們知道

$$V = ((-1)^ s ) * M * ( 2 ^ E )$$

根據階碼exp的值分類討論

1.當exp = 0xFF時,表示特殊值,當為特殊值時我們知道有兩種情況。一種(當frac不等於0)表示不是一個數(NaN),另一種(frac等於0)表示無窮(根據符號位表示正無窮還是負無窮)。

    當為NaN時根據題目要求,直接返回uf即可

    當為無窮時,無窮 * 2 結果還是無窮,返回uf

2.當exp = 0時,表示非規格化的數,由於非規格化的數表示數值0或者非常接近0的數

    當frac = 0 時,此時表示0 ,0乘以任何數都為0,所以直接返回uf(注意當符號位不同時,正零與負零是有區別的,但由於0怎麼乘都為0,所以這裡我們不做討論,直接返回uf,不能返回零)

    當frac != 0 時,此時表示非常接近0的數,針對這情況只需將小數字段乘以2即可(左移一位)

3.當exp 既不等於0,也不等於0xFF時,此時表示規格化的數

    對於這種情況乘以2只需要對 exp + 1 即可

    (具體原因可以看上面的IEEE浮點表示規則)

    不過這裡還有一種特殊情況要出處理,當exp = 254,此時雖然是一個規格化的數,但階碼欄位加1之後會超出規格化所能表示的範圍,針對這種情況,我們需要返回無窮(無窮分為正無窮和負無窮,這裡根據符號位判斷即可)

思路2

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 則表示非規格化數
image-20240512160542746

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

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

image-20240512160542746

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

最終用或操作和移位操作將這三個欄位拼成一個32bit的數返回即可

程式碼

//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) {
	//s,expr,frac
	unsigned s = (uf >> 31) & (0x1);
	unsigned expr = (uf>>23) & (0xff);
	unsigned frac = (uf & 0x7fffff);

	//0
	if(expr == 0 && frac == 0)
		return uf;
	//inifity or nor na number
	if(expr == 0xff)
		return uf;
	//denormalize
	if(expr == 0)
	{
		//E = expr - basic
		//    expr - 127 = -127
		// frac
		frac <<= 1;
		return (s << 31 ) | frac;
	 }
	//normalize
	expr++; //即乘以二
	//E = expr - 127
	return (s << 31) | (expr << 23) | (frac);
}

12.int floatFloat2Int(unsigned uf)

把單精度浮點數強制轉化成整型數

  • 思路

​ 6位IEEE浮點數格式如下

image-20240512160542746

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

先計算出E=exp-bias

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

  2. 如果是exp=255 的情況直接返回0x80000000u 這裡注意如果超範圍了也會直接返回0x80000000u
    因此可以直接用E>=31 來判斷

  3. 如果是規格化數則我們進行正常處理 $$𝑉=(−1)𝑠×𝑀×2𝐸$$

    1. 先給尾數補充上省略的1
    2. 判斷E<23 則尾數需要捨去23-E
    3. 根據符號位返回就好
1.當E等於0時,此時表示的數不是整數,所以只需返回0即可

由於浮點數所能表示的範圍遠大於整型數(32位整型數只能表示 -2^31  ~  2^31 -1 )

               當  E = 31, s = 1, V = -2^31
    
                   E = 31, s = 0, V = 2^31 
                       
                   此時浮點數表示的數超過了整型所能表示的最大值

2.綜上,
	當E >31 時   浮點數所表示的數值超過了整型數能表示的最大值,此時函式返回0x80000000u即可

3.E在 0 ~ 31 之間如何轉換   

       此時根據exp的值可以知道,資料為規格化的數
    
       由於E的取值範圍在0 ~ 31,而小數字段的長度是23位,所以E的範圍又要分成兩種情況討論
    
       0<= E <= 23        
       對小數字段進行截斷處理(如果E = 23,直接返回小數字段,E= 22,捨棄小數字段最後一位-->小數字段右移一位)
    
        24 <= E <= 31      E = 24(小數字段左移一位),E = 25(左移兩位)
        最後看符號位,若為負數則取反加一,正數直接返回即可
/* 
 * 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) {
	unsigned s = (uf >> 31) & (0x1);
        unsigned expr = (uf>>23) & (0xff);
        unsigned frac = (uf & 0x7fffff);
	
	//0
	if(expr == 0 && frac == 0)
	       return 0;
	//Ini / NaN 無窮大或無窮小
	if(expr == 0xff)
		return 1 << 31;
        //denormalize 非規格化
	if(expr ==0)
	{
	   //M 0.1111  < 1
	   //E = 1 - 127 = -126
	   return 0;
   	}
	//normalize 規格化
	//M 1.xxxxx(2)
	int E = expr - 127;
	frac = frac | (1 << 23);
	if(E > 31)
		return 1 << 31;
	else if(E < 0)
	 	return 0;
	//10 -> 1. < 2
	//M * 2^E
	if(E >= 23)
		frac <<= (E - 23);
	else
		frac >>= (23 - E);
	if(s) //負數
		return (~frac) + 1;
	return frac;
}

13 floatPower2(X)

求 $$2.0^𝑥$$

  • 思路

2.0的位級表示( 1.0×21 ):符號位:0,指數:1+127=128,frac=1.0-1=0。 2.0𝑥=(1.0×21)𝑥=1.0×2𝑥 ,所以x就當做真正的指數的。

這個比較簡單,首先得到偏移之後的指數值e,如果e小於等於0(為0時,結果為0,因為2.0的浮點表示frac部分為0),對應的如果e大於等於255則為無窮大或越界了。否則返回正常浮點值,frac為0,直接對應指數即可。

![55aee3ce17ba362a59f6936fcc1de1f](C:\Users\86155\Documents\WeChat Files\wxid_9k0twos4vlbj22\FileStorage\Temp\55aee3ce17ba362a59f6936fcc1de1f.jpg)

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

  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);
    }
}
  • 程式碼
/* 
 * 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){
		return 0;
	}else if(x < -126){
		//E = x
		//E = 1 -127 = -126
		int shift = 23 + (x +126);
		return (1 << shift;)
	}else if(x <= 127){
		//x = expr - bias
		int expr = x + 127;
		return expr << 23;
	}else{	
		return (0xFF) << 23;
	}
*/
	int exp;
	unsigned ret;

	if(x <-149) return 0;
	if(x >127) return (0xff <<23);

	if(x <-126) return 0x1 << (x +149);
	exp = x + 127;
        ret = exp << 23;
	return ret;       
}

  1. (浮點運算的規則沒那麼嚴格,具體看每個函式上方的規則說明) ↩︎