# 2019q3 Homework1 (review) contributed by < `davinais` > ###### tags: `sysprog` `sysprog2019` ## 作業要求 * 從 [課程簡介和注意須知](https://docs.google.com/presentation/d/1Wo22s5aYuuyry97-z2MMmUmdxhjD0wKTdhxIW5oqjvo/edit?usp=sharing), [第 1 週測驗題](https://hackmd.io/@sysprog/HksKVpUIr) 選出你感興趣的 3 個題組進行作答,並且回答附帶的「延伸問題」 * 應當包含 [課程簡介和注意須知](https://docs.google.com/presentation/d/1Wo22s5aYuuyry97-z2MMmUmdxhjD0wKTdhxIW5oqjvo/edit?usp=sharing) 的 Page 9 到 Page 16 裡頭的 C 語言程式設計議題 * 比照 [課前測驗參考解答: Q1](https://hackmd.io/s/ByzoiggIb) 和 [對 Linked List 進行氣泡排序](https://hackmd.io/s/BklGm1MhZ) 的模式來撰寫共筆,需要詳細分析自己的想法、參閱的材料 (包含 C 語言規格書的章節),以及==進行相關實驗== * 閱讀 [第一週課程進度](http://wiki.csie.ncku.edu.tw/sysprog/schedule) 所有材料,記錄所見所聞並提問,若有不能理解的部分,請標註出來。善用 HackMD 的語法 `:::info` 和 `:::` 標註你的提問 :::info 像是這樣標註提問 ::: ## 問題討論 ### 問題 1 [課程簡介和注意須知](https://docs.google.com/presentation/d/1Wo22s5aYuuyry97-z2MMmUmdxhjD0wKTdhxIW5oqjvo/edit?usp=sharing) Page 11 考慮以下程式碼: ```c #include <stdio.h> int main() { return (*******puts)("Hello"); } ``` 能否編譯執行?若可,為什麼? #### 初步思考 說來慚愧,雖然以前就有先讀了一些 [你所不知道的C語言](/@sysprog/HJpiYaZfl?type=view) ,也知道 Function Pointer,但是一開始看到還是傻住了。只能說沒有讀得很精熟,必須要再重新複習一遍。 回歸正題,當我們直接拿去編譯後,我們發現編譯是會正確通過的,也能夠正常執行,顯然 `(*******puts)("Hello")` 應該是與 `puts("Hello")` 等價的。 可以拿以下的例子來實驗,我們直接印出 `func` 等,看看能不能編譯以及會出現什麼結果: ```c #include <stdio.h> void func() { printf("In func!\n"); } int main(void) { printf(" func: %p\t\t *func: %p\n", func, *func); printf("**func: %p\t\t***func: %p\n", **func, ***func); func(); (*func)(); //*func(); /* error: indirection requires pointer operand ('void' invalid) */ return 0; } ``` 結果編譯正常,而且印出來的位址都一樣,看起來我們不管加了幾個`*`,都可以呼叫到同個函式。不過加上 `*` 後在呼叫時如果沒有補上括號將 `func` 包起來,那麼就會因為結合順序的關係先與後方的括號結合, 至於為什麼呢?不妨讓我們看看原本的規格書標準吧。 #### Function designator 首先,先來讓我們了解一下 function designator ,函式指定符,其實就是函數的名字啦。在 C99 規格書裡面其實是有詳細規範 function designator 的: > A function designator is an expression that has function type. > Except when it is the operand of the `sizeof` operator or the unary `&` operator, **a function designator with type ‘‘*function returning type*’’ is converted to an expression that has type ‘‘*pointer to function returning type*’’.** > [name=[6.3.2.1-4] in C99 ] 也就是說除了 單元 `&` 取址運算以及 `sizeof` 運算,所有的 function designator with type 'function returning type' 都會被轉成 pointer to function 的表達式。 我們現在知道了函數名稱代表了什麼,那麼我們在前面加上一堆 `*` ,或者是在 function designator 經過 單元 `&` 運算子運算之後會變成什麼呢?於是我們勢必也得熟悉一下這兩個運算子。 #### Address and indirection operators > #### Constraints > 1. The operand of the unary `&` operator shall be **either a function designator, the result of a `[]` or unary `*` operator,** or an lvalue that designates an object that is not a bit-field and is not declared with the `register` storage-class specifier. > 2. The operand of the unary `*` operator **shall have pointer type**. > > [name=[6.5.3.2] in C99 ] 首先我們得知了 單元`&`運算子 可以接受 function designator 以及 單元`*`運算子的結果 作為運算元,而 單元`*`運算子 則只接受 pointer type 作為運算元。 再來看看他們代表的意義,以及他們的運算結果會是什麼: > #### Semantics > 3. The unary `&` operator yields the address of its operand. **If the operand has type ‘‘*type*’’, the result has type ‘‘pointer to *type*’’.** **If the operand is the result of a unary `*` operator, *neither that operator nor the `&` operator is evaluated and the result is as if both were omitted*, except that the constraints on the operators still apply and the result is not an lvalue.** Similarly, if the operand is the result of a `[]` operator, neither the `&` operator nor the unary `*` that is implied by the `[]` is evaluated and the result is as if the `&` operator were removed and the `[]` operator were changed to a `+` operator. Otherwise, the result is a pointer to the object or function designated by its operand. > 4. The unary `*` operator denotes indirection. **If the operand points to a function, the result is a function designator**; if it points to an object, the result is an lvalue designating the object. If the operand has type ‘‘pointer to *type*’’, the result has type ‘‘*type*’’. If an invalid value has been assigned to the pointer, the behavior of the unary * operator is undefined. > > [name=[6.5.3.2] in C99 ] 有點多,不過基本上有關於本題的部分只要看粗體就可以了。 首先規格書提到了 單元`&`運算子 結果將產生其運算元的位址,即為取址。如果該運算元有 ‘‘*type*’’ 型態,則結果將會有 ‘‘pointer to *type*’’ 型態;而如果運算元是 單元`*`運算子 的結果,那麼基本上 `&` 跟 `*` 都不求值,類似於直接忽略,但是結果不會是 lvalue。 :::info 我原本以為是抵銷的,結果才發現結果有些差異,並不是完全相等,像是這樣一來他就沒辦法再用 `&` 取址。 ::: 再來 單元`*`運算子 代表的是 indirection,也有人稱他為 dereference。這裡提到如果運算元 points to a function 的話,那麼結果會是 function designator。 如此一來便可以解釋 `(*******puts)("Hello")` 的運作邏輯了,首先我們必須先對 `(*******puts)` 求值,而我們能將該段程式碼看成 `(*(*(*(*(*(*(*(puts))))))))` 來依序拆解: 1. `(puts)`: `puts` 是 function designator,根據 C99[6.5.3.2-4],我們可以將它轉換成 pointer to function return type。 2. `(*(puts))`: 單元`*`運算子碰到了 points to a function,結果輸出 function designator,能被轉換成 pointer to function return type。 3. `(*(*(*(*(*(*(*(puts))))))))`: 後面就是一直重複第二步,因此最後輸出的還是 function designator。 可以知道 `(*******puts)` 跟 `puts` 的結果輸出並沒有什麼不同,因此可以正常被呼叫。 解決了 `(*******puts)` 的問題,不過其他情況下會如何呢?稍微做一點實驗試試看: ```c= #include <stdio.h> int times = 0; void func() { printf("[%d] In func!\n", ++times); } int main(void) { func(); (*func)(); (&func)(); (&*func)(); (*&func)(); //(&&*func)(); /* error: expected identifier */ (&*&func)(); (&***func)(); (****&func)(); (*&*&*&func)(); (&*&*&*&func)(); void (*fptr)(void); fptr = func; fptr(); (*fptr)(); //(&fptr)(); /* error: called object type 'void (**)(void)' is not a function or function pointer */ (&*fptr)(); (*&fptr)(); void (**fptrptr)(void); fptrptr = &fptr; //(fptrptr)(); /* error: called object type 'void (**)(void)' is not a function or function pointer */ (*fptrptr)(); (********fptrptr)(); return 0; } ``` 我們可以發現雖然 `(&*&func)()` 可以被正確的解讀(因為`&*`大致上可視為忽略,剩下`(&func)()` 視為 pointer to function,但不可作為 lvalue );但是 `(&&*func)()` 右側的 `(&*func)()` 雖然可以當作 function designator,但是無法當作 lvalue,因此無法再用 `&` 取址,跳出 expected identifier 的錯誤。 :::info 不過其實我沒有很明白為什麼是 expected identifier,因為規格書好像沒有特別寫到 `&` 不能接受 lvalue ? ::: ### 測驗 2 #### 題目 考慮以下 C 程式可取得以 `sizeof(int)` 為 alignment 基礎的整數空間,也就是說,當 `sizeof(int)` 為 `4` 的時候: * 給定 `char arr[13];`,`INT_SIZE_OF(arr)` 要得到 `16`; * 給定 `short int s;`,`INT_SIZE_OF(s)` 要得到 `4` ```c #include <stdint.h> #define INT_SIZE_OF(n) \ ((sizeof(n) + sizeof(int) + X) & ~(sizeof(int) + Y)) ``` `X` = ? `Y` = ? :::warning `X` = -1, `Y` = -1 ::: #### 運作原理 這其實與測驗的第1題 `align4(x)` 有些關係,雖然我一開始以為 round-up 是一捨二入所以選了 `K` = 2,~~真是可惜~~。 原本第1題的 align 巨集長的像下面這樣: ```c #define align4(x) (((x) + 3) & (-4)) ``` 沒辦法直觀看出來,沒關係,把他轉成二進位來看看。 - $-4_{10} = 11...1100_{2}$ (二補數情況下) 從 -4 已經能看出,其實這就是一個 mask ,除去後面兩位數字只保留前面的部分,與先右移兩位再左移兩位的情況相同。至於為什麼要 +3 ?這是為了讓後面兩位在 1 以上時直接進位($1+3=4$,所以只要在 1 以上就會進位),最後我們再去除後面兩位數字,便可以得到 align 過後的數字了。 那麼在這裡的 `INT_SIZE_OF(n)` 能否依樣畫葫蘆呢?當然是可以! #### 依樣畫葫蘆 先觀察 `align4(x)` ,這個巨集的目的是要做 4-byte alignment,因此只要 $x \mod 4 \gt 0$ 的話就需要多一個 4-byte 來儲存,以至於會進位。為了讓他在 $\gt 0$ 時會進位,我們可以加上一個數字,讓 $x \div 4$ 的餘數與他相加之後能夠進位,最後捨棄末兩位便能達成效果。 因此: - 選擇 $+3$ , 為了使餘數不為 0 時進位,只要確保餘數在 1 以上時會進位即可,**選擇 $4-1=3$ ,意即 $+3$ 即可達成效果**。 - Mask 選擇 $-4$ 作 AND,這是因為 $-4_{10} = 11...1100_{2}$,拿來做 AND 運算剛好就能捨棄掉末兩位,**其中 $-4$ 其實就是我們要做的 N-byte alignment 中的 N 取負數**。 依此類推,我們這裡要以 `sizeof(int)` 作為 alignment 基礎,也依照上述方式選擇 `X` 與 `Y` 即可: - `X`: 需確保餘數在 1 以上會進位,選擇 $\text{sizeof(int)}-1$ ,因此 `X` = -1。 - `Y`: 將 `sizeof(int)` 取負數作為 Mask,二補數的計算方式為 $\neg x+1$ 或者是 $\neg(x-1)$,對照可知 `Y` = -1。 ### 測驗 3 #### 題目 考慮以下 C 程式碼: ```c #include <stdbool.h> bool func(unsigned int x) { return x && ((x & (~x + 1)) == x); } ``` 可改寫為以下等價的程式碼: ```c bool func(unsigned int x) { unsigned int unknown = 0; while (x && unknown <= 1) { if ((x & Z) == 1) unknown++; x >>= 1; } return (unknown == 1); } ``` 請補完程式。 :::warning Ans: Z = 1 ::: #### 初步思考 每次當我看到關於 bitwise 的應用或題目,只要不是能直觀理解用途的,都感到傻眼以及讚嘆「這種東西到底是怎麼想出來的?」 回到正題,剛開始看到 bitwise 的部份,我只知道 `~x + 1` 是將 `x` 取 2 補數。在什麼情況之下 `x` 自身與他的 2 補數做 AND 運算會與自身相等,最後再判斷 `x` 是否非 0 後輸出結果? 在考試當下感到相當慌張,代了幾個數字都是 `False` 於是憑藉著一點對於補數的印象「碰巧」猜到了答案,事後檢討才發現實際上到底是在算什麼。 #### 代入數字 既然完全看不出來在幹嘛,那就直接代數字來看看吧,代多一點總能知道他在幹嘛了吧? 試著將 `x` 代入由 0 到 16 的數字 | $x_{10}$ | $x_2$ | $(\neg x+1)_2$ | $x_2 \wedge (\neg x+1)_2$ | | ---:|:-------:|:-------:|:-------:| | 0 | $00000$ | $00000$ | $00000$ | | 1 | $00001$ | $11111$ | $00001$ | | 2 | $00010$ | $11110$ | $00010$ | | 3 | $00011$ | $11101$ | $00001$ | | 4 | $00100$ | $11100$ | $00100$ | | 5 | $00101$ | $11011$ | $00001$ | | 6 | $00110$ | $11010$ | $00010$ | | 7 | $00111$ | $11001$ | $00001$ | | 8 | $01000$ | $11000$ | $01000$ | | 9 | $01001$ | $10111$ | $00001$ | | 10 | $01010$ | $10110$ | $00010$ | | 11 | $01011$ | $10101$ | $00001$ | | 12 | $01100$ | $10100$ | $00100$ | | 13 | $01101$ | $10011$ | $00001$ | | 14 | $01110$ | $10010$ | $00010$ | | 15 | $01111$ | $10001$ | $00001$ | | 16 | $10000$ | $10000$ | $10000$ | 我們可以發現,當 `x` 為 2 的指數倍時,`x` 自身與 2 補數做 AND 的結果會與自身相同,而最後再判斷 `x` 是否為 0 將 0 剔除。可以大膽猜測本函數便是判斷「`x` 是否為 2 的非 0 指數倍」。 #### 原理探討 原理到底是什麼呢?我並不是很確定,但是當數字是 2 的正指數倍時,除了代表該數字的 bit 為 $1$ ,其他都會是 $0$ ;而其的 2 補數則是位於該位(含自身)左方所有的 bit 都會是 $1$ ,兩者做 AND 剛好就只剩下原本數字的 bit。 其他的數字則不盡然,如**奇數的話最低位必定為 1** (因為原本的數字最低位就是$1$,做 NOT 之後再加 `1` 最低位仍然會是 $1$),因此除了 `1` 以外的數字都不會與自身相等,而 `1` 恰好為 2 的指數倍。 **偶數的話則會出現的是該數字擁有的為 2 指數倍之最大因數**,如 `14` 的話會出現 `2` , `12` 的話會出現 `4`,這可以直接的從二進位理解,當從某位數到最低位的數值皆為 $0$ 時,代表右移並不會有餘數,那麼我們便能知道他擁有的為 2 指數倍的最大因數了。我們也能發現當 `x` 自身即為 2 的指數倍時,剛好便是特例,最大因數即為自身。 #### 適用負數? 那麼負數能否正確判斷呢?原本的 `x` 我們使用 `unsigned int` ,代表我們不必在意正負,但是如果只能用在正數未免有點侷限。這裡直接用正數的表即可反推負數的表,畢竟負數的 2 補數就是他的正數表示方式,很遺憾的是並沒有辦法,光是正數的 sign bit 必定為 0 就代表一定會失敗了。不過如果改成: 1. 採用 OR 而非 AND;或 2. AND 完的結果與其 2 補數比較而非與自身比較 應該就有辦法判斷負數了,因此我們若將原函數改成(採用第二種方法): ```c #include <stdbool.h> bool func(unsigned int x) { return x && (((x & (~x + 1)) == x) || (x & (~x + 1)) == (~x + 1)); } ``` 應該就能將適用範圍擴展到負數。 #### 改寫 回到改寫後的程式碼: ```c= bool func(unsigned int x) { unsigned int unknown = 0; while (x && unknown <= 1) { if ((x & Z) == 1) unknown++; x >>= 1; } return (unknown == 1); } ``` 如何找到 `Z` 讓它也能判斷 `x` 是否為非 0 的 2 指數倍?我們先發現到我們回傳的值: ```c=8 return (unknown == 1); } ``` 發現 `unknown` 為 1 時將會回傳 `true`,而同時我們知道在 2 的指數倍中,只有一個位數會是 $1$。那麼或許 `unknown` 代表的就是 $1$ 的個數?(這不就是 pop-count 嗎?) ```c=3 while (x && unknown <= 1) { if ((x & Z) == 1) unknown++; x >>= 1; } ``` 觀察迴圈,當 `x` 為 0 以及 `unknown` 超過 1 時會中止迴圈,並且 `unknown` 只會在 `((x & Z)) == 1` 時增加,那麼這行一定就是關鍵判斷式了,由於後面我們能看到每次迭代中, `x` 都被右移一位,可以猜測該行便是用來判斷目前的最低位是否為 $1$。 經過這樣簡單推理之後,即可得出 `Z` 應該要是 `1` 才能在最末位為 $1$ 時「必定」輸出 `1` 的結果。 #### 以 const-time 實作 popcount 既然剛剛都提到了 pop-count,那麼就以 pop-count 作為例子吧。 pop-count,全名是 population count,是拿來計算二進位下一個數字有多少個 0 問題,在計算 0-1稀疏矩陣或者是[字串間的漢明距離](https://zh.wikipedia.org/wiki/%E6%B1%89%E6%98%8E%E8%B7%9D%E7%A6%BB)等場合下十分有用。 最簡單的實作方法其實就像是題目改寫之後的那樣: ```c= int popcount(unsigned int x) { unsigned int ones = 0; while (x) { if ((x & 1) == 1) ones++; x >>= 1; } return ones; } ``` 這方法相當的直覺,但是有非 constant-time 的 loop 存在,可能會因為 side-attack 等造成一些資安威脅。 讓我們試著將 popcount 改寫成 constant-time 看看: 1. constant-time 有一種最簡單的方式,直接做一個 look-up table,用空間換取時間: ```c #define CHAR_BIT 8 // 依照 char 的位元數 #define BIT2(n) n, n+1, n+1, n+2 #define BIT4(n) BIT2(n), BIT2(n+1), BIT2(n+1), BIT2(n+2) #define BIT6(n) BIT4(n), BIT4(n+1), BIT4(n+1), BIT4(n+2) #define BIT8(n) BIT6(n), BIT6(n+1), BIT6(n+1), BIT6(n+2) #define TBL_LEN (1<<CHAR_BIT) static const unsigned char TABLE[TBL_LEN] = { #if (CHAR_BIT==8) BIT8(0) #elif (CHAR_BIT==7) BIT6(0), BIT6(1) #else # error "BITX to be added here." #endif }; #undef TBL_LEN int lookup_popcnt(unsigned int n) { return TABLE[n & UCHAR_MAX]+ TABLE[(n>>CHAR_BIT) & UCHAR_MAX]+ TABLE[(n>>(CHAR_BIT*2)) & UCHAR_MAX]+ TABLE[(n>>(CHAR_BIT*3)) & UCHAR_MAX]; } ``` 2. 也能夠運用平行加法的技巧來實現: ```c= #define POW2(c) (1u<<(c)) #define MASK(c) (((unsigned int)(-1))/(POW2(POW2(c))+1u)) #define COUNT(x,c) ((x)&MASK(c)) + (((x)>>(POW2(c)))&MASK(c)) int parallel_popcnt(unsigned int n) { n=COUNT(n,0); n=COUNT(n,1); n=COUNT(n,2); n=COUNT(n,3); n=COUNT(n,4); /* n=COUNT(n,5); for 64-bit integers */ return n ; } ``` 由2位1組開始,分別取出每組的高位與低位並且相加,再將1的個數直接存放在該組的位置。隨後合併相鄰的兩組繼續進行迭代,直到完成為止。 以下作為示範,假設原本有 8 位元數字 $10110101$,這裡以 $\hat x$表示高位, $\check x$ 表示低位: 1. 先兩兩分組: $\hat1\check0|\hat1\check1|\hat0\check1|\hat0\check1$ 2. 以遮罩取出高位以及低位: $1|1|0|0$ 與 $0|1|1|1$ 3. 兩者相加,代表該組 1 的個數: $01|10|01|01$ 4. 重新迭代,四四分組: $\hat0\hat1\check1\check0|\hat0\hat1\check0\check1$ 5. 以遮罩取出高位以及低位: $01|01$ 與 $10|01$ 6. 兩者相加,代表該組 1 的個數: $0011|0010$ 7. 再次迭代,八八分組: $\hat0\hat0\hat1\hat1\check0\check0\check1\check0$ 8. 以遮罩取出高位以及低位: $0011$ 與 $0010$ 9. 相加,代表該組 1 的個數: $00000101$ 10. 迭代完成,因此 $10110101$ 總共有 $00000101_2=5_{10}$ 個 1 若數字是 $2^k$ 位元,則只需要 $k$ 次迭代必定可完成。 ![](https://i.imgur.com/IQgIEN5.png) *本方法的圖解,圖片引用自[1]* 3. 不過現在部份 CPU 也已經有 popcnt 指令了,可以直接在組語呼叫就好了