# 2018q3 Homework4 (assessment) contributed by <`krimson8`> ## 開發環境 ``` Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian CPU(s): 6 On-line CPU(s) list: 0-5 Thread(s) per core: 1 Core(s) per socket: 6 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 158 Model name: Intel(R) Core(TM) i5-8400 CPU @ 2.80GHz Stepping: 10 CPU MHz: 800.012 CPU max MHz: 4000.0000 CPU min MHz: 800.0000 BogoMIPS: 5616.00 Virtualization: VT-x L1d cache: 32K L1i cache: 32K L2 cache: 256K L3 cache: 9216K NUMA node0 CPU(s): 0-5 ``` ## 2018q3 第 4 週測驗題 (上) ### 測驗 `1` ```clike #include <stdint.h> int64_t abs64(int64_t x) { int64_t y = x A1 (A2 - 1); return (x A3 y) - y; } ``` * A1: >> * A2: 64 * A3: ^ #### 解題思路 首先很直覺的認爲 A2 是 64,因爲要取絕對值於 sign-bit 一定有關係 這裏先附上3-bit 的 signed integer 列表 |decimal |binary | |-- |-- | |3 |011 | |2 |010 | |1 |001 | |0 |000 | |-1 |111 | |-2 |110 | |-3 |101 | |-4 |100 | 照着這個思路,要提取出這個 sign-bit 應該是要用 shift ,那麼sign bit 就會出現在 第一個 bit 內,但是那時候考慮到 sign extension (6.5.7 - 5: If E1 has a signed type and a negative value, the resulting value is implementation-defined.) 會讓前面的所有bit 都變得跟 第一個bit 一樣,一時緊張選擇了錯誤的答案 `&`. 其實前面的所有bit 和 sign bit 一樣正是想要的結果,繼續探討 A3,使用排除的方式,根據兩種情況的結果 * 若原本傳入的值就是正數,則與 `000` 進行 `|` 和 `^` 能保持原本的數值 * 若是負數,`return` 哪一行的 `-y` 實際意義就是 `+1`,從上表得知,將負數 -n 的後兩個位元與 11 做 `^` 的結果便是位元提換,替換的結果剛好是 n-1,因此 `+1` 便可以得到正確的數值 根據 C 語言規格書,對有號數做 right-shift 都是 implementation-defined,但是目前看到大多數的 compiler 支援的都是 arithmetic shift,也就是對空白處補上 sign-bit。 #### 潛在的 underflow/ overflow 問題 因爲在 64-bit 有號數裏面的有效表示範圍爲 $-2^{64}$ 到 $2^{64} - 1$ 因此若傳入進去的值是$-2^{64}$ 時,則程式的執行結果不正確,但是考量下列情況 ``` x = -4 100 ^ 111 ------ 011 + 001 ------ 100 (-4) ``` 並沒有發生 overflow/underflow 的情況 #### 探討 pseudo-number generator 與 上述程式的可行性 ```clike static uint64_t r = 0xdeadbeef int64_t rand64() { r ^= r >> 12; r ^= r << 25; r ^= r >> 27; return (int64_t) (r * 2685821657736338717); } ``` 使用這隻程式應該是可以在有限的時間內窮舉所有的可能,但是所耗費的時間實在是太長,因此目前的想法是帶入幾個 corner case 來測試 ## 2018q3 第 4 週測驗題 (中) #### 探討 TCO 參考自己的 [hw1](https://hackmd.io/c/H1-NYOEFX/%2Fs%2FHkCht_EtX)↓↓↓ 遞迴函式每一次的函式呼叫都會建立 **stack** 並且把 **{參數,區域變數,返回位置}** 這些 **原函式的執行狀態** 保存起來,再把他推到 **系統堆疊** 的頂端。 **理論上的尾端遞迴的優化** - ([來源](https://stackoverflow.com/questions/19854007/what-is-the-advantage-of-using-tail-recursion-here)) 其中有一片解答說明 tail recursion 可以很輕易的 轉化爲 iterative 的形式,也就是說編譯器有進行優化的話可以不佔用更多的 stack frame,但是其實很多編譯器並沒有這麼做 另一篇解答更好的說明了 TCO 的原理 ``` This optimization is possible because the compiler knows that once the tail recursive call is made, no previous copies of variables will be needed, because there is no more code to execute. If, for instance, a print statement followed a recursive call, the compiler would need to know the value of the variable to be printed after the recursive call returns, and thus the stack frame cannot be reused. ``` 也就是說若是像計算 費氏數列 的 recursive 函式若是 tail recursion 的話是可以優化成iterative 的形式的,而老師原本的例子 ```clike= void g(int *p); void f(void) { int x = 3; g(&x); } void g(int *p) { printf("%d\n", *p); } ``` 此段程式碼不能夠被優化的原因是因爲 x 原本存在於 `f()`的 stack frame 內,但是若要優化的話則表示 `g()` 將繼續使用那個 stack,因此 x 的位址內的內容將不再是 x,導致程式出錯 參考 [這篇文章](https://david.wragg.org/blog/2014/02/c-tail-calls-1.html) ```clike= int *global_var; void f(void) { int x = 3; global_var = &x; ... /* The compiler cannot perform TCO here, * unless it can establish that g does not * dereference the pointer in global_var. */ g(); } ``` ``` ......it's possible that the compiler can perform some analysis to establish that the called function does not in fact dereference the pointer to the local variable. But given the compilation model typically used by C compilers, it is optimistic to expect them to perform such analysis. ``` 也就是說,如果我們使用的編譯器有辦法去判定 `g()` 內有沒有去做對 `global_var` 的 dereference,就有辦法在這裏施加 TCO #### gcc 的 TCO 行爲 改寫實驗的程式,讓 `g()` 沒有去做 x 的 dereference ```clike= #include <stdio.h> void g(); void f(void) { int x = 3; g(); } void g() { int a = 1; printf("hello %d\n", a); } int main() { f(); return 0; } ``` 編譯時期打開 先是 -O0,查看 .s 檔 [線上看](https://godbolt.org/z/AmZQQb) ``` f: pushq %rbp movq %rsp, %rbp subq $16, %rsp movl $3, -4(%rbp) movl $0, %eax call g nop leave ret .LC0: .string "hello %d\n" g: pushq %rbp movq %rsp, %rbp subq $16, %rsp movl $1, -4(%rbp) movl -4(%rbp), %eax movl %eax, %esi movl $.LC0, %edi movl $0, %eax call printf nop leave ret main: pushq %rbp movq %rsp, %rbp call f movl $0, %eax popq %rbp ret ``` 可以看到 f 和 g 的呼叫都有牽涉到 ``` pushq %rbp movq %rsp, %rbp ... ret ``` 也就證明了 GCC 在沒有優化的時候是有讓每一個 function call 都建立一個 stack frame 的 若開啓 -O2 ``` .LC0: .string "hello %d\n" f: movl $1, %esi movl $.LC0, %edi xorl %eax, %eax jmp printf g: movl $1, %esi movl $.LC0, %edi xorl %eax, %eax jmp printf main: subq $8, %rsp movl $1, %esi movl $.LC0, %edi xorl %eax, %eax call printf xorl %eax, %eax addq $8, %rsp ret ``` f 和 g 內都沒有繼續做 新增 stack frame 的動作 :::warning 疑問 code [線上看](https://godbolt.org/z/S9Vi07) 題目原本的例子 ```clike= #include <stdio.h> void g(int *p); void f(void) { int x = 3; g(&x); } void g(int *p) { printf("%d\n", *p); } int main() { f(); } ``` 編譯出來的結果如下 ``` .LC0: .string "%d\n" f: movl $3, %esi movl $.LC0, %edi xorl %eax, %eax jmp printf g: movl (%rdi), %esi xorl %eax, %eax movl $.LC0, %edi jmp printf main: subq $8, %rsp movl $3, %esi movl $.LC0, %edi xorl %eax, %eax call printf xorl %eax, %eax addq $8, %rsp ret ``` 看起來是有進行 TCO !! 然而若修改程式碼內的 `g()` ```clike= void g(int *p) { printf("%d\n", *p); printf("%d\n", *p); } ``` 則從新編譯的結果變成 ``` .LC0: .string "%d\n" g: pushq %rbx movl (%rdi), %esi movq %rdi, %rbx xorl %eax, %eax movl $.LC0, %edi call printf movl (%rbx), %esi movl $.LC0, %edi xorl %eax, %eax popq %rbx jmp printf f: subq $24, %rsp leaq 12(%rsp), %rdi movl $3, 12(%rsp) call g addq $24, %rsp ret main: subq $24, %rsp leaq 12(%rsp), %rdi movl $3, 12(%rsp) call g xorl %eax, %eax addq $24, %rsp ret ``` 突然就沒有 TCO 了 因此這裏的猜測是,當`g()` 內只有一個 `printf()` 的時候,程式碼的流程如下 * `main` 呼叫 `f()` * `f()`的stack frame 被建立出來 * 到了呼叫 `g()` 的時候,編譯器判斷 `g()` 內只需要 dereference `x` 一次,因此直接把 `f()` 的 stack frame 重用,也就是做 TCO,變成 `printf` 使用的stack frame,結束了也是return 回一樣的地址,也就是 `main()` 當`g()` 內有兩個 `printf()` 的時候,程式碼的流程如下 * `main` 呼叫 `f()` * `f()`的stack frame 被建立出來 * 到了呼叫 `g()` 的時候,編譯器判斷 `g()` 內需要 dereference `x` 兩次,而且第一個 `printf()` return 的地址是 `g()` 內,然後再一次dereference `x`,也就是說 `f()`的stack frame 並沒有重用,而且編譯器額外建立了 `g()` 和 第一個`printf()` 的 stack frame,第二個 `printf()` 的 stack frame 與 `g()` 共用,結束後直接 jump 回 `f()` 感謝`aben20807` 發現這個問題並且一起探討 ::: #### __builtin_return_address 持續更新中 ## 2018q3 第 4 週測驗題 (中) #### 原題目 ```clike int main() { int *ptr = 0; return *ptr; } ``` 根據 6.3.2.3 -- 3 >An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant. If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function. > ([維基](https://en.wikipedia.org/wiki/Null_pointer))對 null pointer 做 dereference 是 undefined behaviour。找不太到C 規格書內這一段的定義。 #### 題目一 ```clike= int main() { return *((int *) 0); } ``` 根據 6.6 - 9 >An address constant is a null pointer, a pointer to an lvalue designating an object of static storage duration, or a pointer to a function designator; it shall be created explicitly using the unary & operator or an integer constant cast to pointer type, or implicitly by the use of an expression of array or function type. The array-subscript [] and member-access . and -> operators, the address & and indirection * unary operators, and pointer casts may be used in the creation of an address constant, but the value of an object shall not be accessed by use of these operators. > :::info 疑問 這一段有一句 it shall be created explicitly using the unary & operator or an integer constant cast to pointer type,所以上述程式碼其實是可行的,只是因爲 0 是 null constant,被cast 成 pointer 了過後成了 null pointer,不知道這樣的解釋對不對? ::: #### 題目二 ```clike= int main() { return &*((int *) 0); } ``` 根據 6.5.3.3 註解 84 > ...&*E is equivalent to E (even if E is a null pointer), and &(E1[E2]) to ((E1)+(E2)). It is always true that if E is a function designator or an lvalue that is a valid operand of the unary & operator, *&E is a function designator or an lvalue equal to E. If *P is an lvalue and T is the name of an object pointer type, *(T)P is an lvalue that has a type compatible with that to which T points. > 因此若題1 的解釋成立的話,就代表這段程式碼只是在return 一個 null pointer,並沒有對 null pointer 做 dereference ``` test.c: In function ‘main’: test.c:1:21: warning: return makes integer from pointer without a cast [-Wint-conversion] int main() { return &*((int *) 0); } ^~~~~~~~~~~~~ ``` 因爲是return 一個 null pointer,且 constant value 爲0,但是main 的 return type 是int,因此顯示形態錯誤 #### 題目三 ```clike= #include <stddef.h> int main() { return &*NULL; } ``` 根據 6.5.3.3 註解 84 > ...&*E is equivalent to E (even if E is a null pointer),... 因此只是return NULL,跟題2 一樣 #### 題目四 ```clike= #include <stddef.h> int main() { return &*(*main - (ptrdiff_t) **main); } ``` :::warning 更新中 :::