# 2018q3 Homework4 (assessment) ###### tags: `C語言` contributed by < `asd757817` > ## [第一題](https://hackmd.io/s/HyyxpJE5X#2018q3-%E7%AC%AC-4-%E9%80%B1%E6%B8%AC%E9%A9%97%E9%A1%8C-%E4%B8%8A) 利用二補數的特性完成取絕對值,不需要透過 if 判斷 ```C #include <stdint.h> int64_t abs64(int64_t x) { int64_t y = x A1 (A2 - 1); return (x A3 y) - y; } ``` - 二補數轉換:將數字所有位元反向運算後再+1 - 絕對值計算: sign bit 為 0 → 運算後 bit 不變 sign bit 為 1 → 所有 bit 做反向運算 +1 因為不使用 if 判斷正負,所以必須藉由 sing bit 達成正負的判斷,推測 y 所進行的運算為取得 sign bit,經由 bit 右移最大位數 -1 可以取得,傳入的 x 為 64 bit,故 A1: >> 、 A2: 64 當 x >= 0 → y = 0 ; x < 0 → y = -1 思考```reutrn (x A3 y) - y```,當 x < 0 : - y = -1 - -1 的二進位表示為 0xffffffff - x 與 y 進行運算後等同於反向運算 - x 以 -1 帶入檢驗:0xffffffff A3 0xffffffff +1 會等於 0x01 → A3: ^ ( XOR ) x 以 1 帶入檢查: - y = 0 - (1 ^ 0) - 0 = 1 也符合條件 ANS: A1:>> A2:64 A3:^ C99 6.5.7 (5) > The result of E1 >> E2 is E1 right-shifted E2 bit positions. If E1 has an unsigned type or if E1 has a signed type and a nonnegative value, the value of the result is the integral part of the quotient of E1 / $2^{E2}$. ==If E1 has a signed type and a negative value, the resulting value is implementation-defined.== 有號負數進行 right-shift 的結果會根據工具不同而有不同的結果,這個題目是預期 right-shift 時會補 sign bit 的數值(正數補 0、負數補 1)來實作,以 gcc 編譯執行的結果是相符的,但在某些機器上執行可能因為補的數值不同而造成錯誤。 ### overflow/underflow 因為使用的是二補數,在 n bits 系統的數值範圍介於 $2^{n-1}-1$ ~ $- 2^{n-1}$,負數會比正數多一個,當傳入最小負數時就會出現 overflow。 ## [第二題](https://hackmd.io/s/Syl6me49Q#2018q3-%E7%AC%AC-4-%E9%80%B1%E6%B8%AC%E9%A9%97%E9%A1%8C-%E4%B8%AD) tail call: 一個函式最後執行的動作是呼叫另一個函式 一般函式執行中的 stack 變化: ```graphviz digraph structs{ stack1 [label="{<f0> Caller Frame|<f1> Previous Frame}" shape=record]; stack2 [label="{<f0> Called Frame|<f1> Caller Frame|<f2> Previous Frame}" shape=record] stack3 [label="{<f0> Caller Frame|<f1> Previous Frame}" shape=record]; stack4 [label="{<f0> Previous Frame}" shape=record]; stack1 -> stack2 -> stack3 -> stack4 } ``` 當函式呼叫另一個函式時會將原本函式的資料、return address 保存在 stack 中,並在 stack 規劃出下一個函式所使用的範圍,在部分 tail call 的例子中這樣其實是浪費記憶體空間的,以下列程式碼為例: ```C int f(){ int a = 1; int b = 2; return g(a+b); } ``` f() 函式實際上等同於 return g(2),呼叫 f() 的結果等於直接呼叫 g(2),但是在未最佳化的運作中,執行 g(2) 時 f() 的內容 a、b 還會被保存在 stack 裡,但實際上這些資訊是不需要被保存的,因此 TCO 的效果就是不保留 f() 用不到的資訊,只保留 g(3) 所需要的部分。 [reference](http://www.ruanyifeng.com/blog/2015/04/tail-call.html) 簡易測試 TCO: 1.c ```C #include "stdio.h" int f(); int g(int); int main(){ int x = f(); printf("%d\n", x); return 0; } int f(){ int a=1; int b=2; return g(a+b); } int g(int x){ return x; } ``` 比較不做最佳化與 O1 最佳化的差異 ```shell gcc -o 1.s -S 1.c gcc -o 1_O1.s -O1 -S 1.c ``` 不做最佳化: ``` main: .LFB2: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $16, %rsp movl $0, %eax call f movl %eax, -4(%rbp) movl -4(%rbp), %eax movl %eax, %esi movl $.LC0, %edi movl $0, %eax call printf movl $0, %eax leave .cfi_def_cfa 7, 8 ret .cfi_endproc f: .LFB3: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $16, %rsp movl $1, -8(%rbp) movl $2, -4(%rbp) movl -8(%rbp), %edx movl -4(%rbp), %eax addl %edx, %eax movl %eax, %edi call g leave .cfi_def_cfa 7, 8 ret .cfi_endproc g: .LFB4: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl %edi, -4(%rbp) movl -4(%rbp), %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE4: .size g, .-g .ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609" .section .note.GNU-stack,"",@progbits ``` O1 最佳化: ``` main: .LFB38: .cfi_startproc subq $8, %rsp .cfi_def_cfa_offset 16 movl $3, %edx movl $.LC0, %esi movl $1, %edi movl $0, %eax call __printf_chk movl $0, %eax addq $8, %rsp .cfi_def_cfa_offset 8 ret .cfi_endproc f: .LFB39: .cfi_startproc movl $3, %eax ret .cfi_endproc .LFE39: .size f, .-f .globl g .type g, @function g: .LFB40: .cfi_startproc movl %edi, %eax ret .cfi_endproc .LFE40: .size g, .-g .ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609" .section .note.GNU-stack,"",@progbits ``` 不做最佳化時 main 函式內呼叫 f、f 函式內呼叫 g,最佳化以後 f 函式內就沒有呼叫 g 了 --- 修改程式碼 ```C #include "stdio.h" int f(); int g(); int * g_ptr; int main(){ int x = f(); printf("%d\n", x); return 0; } int f(){ int a=1; g_ptr = &a; return g(); } int g(){ return 10; } ``` 不做最佳化 ```shell main: .LFB2: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $16, %rsp movl $0, %eax call f movl %eax, -4(%rbp) movl -4(%rbp), %eax movl %eax, %esi movl $.LC0, %edi movl $0, %eax call printf movl $0, %eax leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE2: .size main, .-main .globl f .type f, @function f: .LFB3: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $16, %rsp movq %fs:40, %rax movq %rax, -8(%rbp) xorl %eax, %eax movl $1, -12(%rbp) leaq -12(%rbp), %rax movq %rax, g_ptr(%rip) movl $0, %eax call g movq -8(%rbp), %rdx xorq %fs:40, %rdx je .L5 call __stack_chk_fail .L5: leave .cfi_def_cfa 7, 8 ret .cfi_endproc g: .LFB4: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl $10, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE4: .size g, .-g .ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609" .section .note.GNU-stack,"",@progbits ``` O1 最佳化 ``` main: .LFB38: .cfi_startproc subq $24, %rsp .cfi_def_cfa_offset 32 movq %fs:40, %rax movq %rax, 8(%rsp) xorl %eax, %eax leaq 4(%rsp), %rax movq %rax, g_ptr(%rip) movl $10, %edx movl $.LC0, %esi movl $1, %edi movl $0, %eax call __printf_chk movq 8(%rsp), %rcx xorq %fs:40, %rcx je .L2 call __stack_chk_fail .L2: movl $0, %eax addq $24, %rsp .cfi_def_cfa_offset 8 ret .cfi_endproc .LFE38: .size main, .-main .globl f .type f, @function f: .LFB39: .cfi_startproc subq $24, %rsp .cfi_def_cfa_offset 32 movq %fs:40, %rax movq %rax, 8(%rsp) xorl %eax, %eax leaq 4(%rsp), %rax movq %rax, g_ptr(%rip) movq 8(%rsp), %rdx xorq %fs:40, %rdx je .L5 call __stack_chk_fail .L5: movl $10, %eax addq $24, %rsp .cfi_def_cfa_offset 8 ret .cfi_endproc .LFE39: .size f, .-f .globl g .type g, @function g: .LFB40: .cfi_startproc movl $10, %eax ret .cfi_endproc .LFE40: .size g, .-g .comm g_ptr,8,8 .ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609" .section .note.GNU-stack,"",@progbits ``` 觀察 f 函式實際的行為,在這次測驗中因為 g 函式內沒有使用到 g_ptr,因此可以進行 TCO 且不影響程式。 --- 再次修改程式 ```C #include "stdio.h" int f(); int g(); int * g_ptr; int main(){ int x = f(); printf("%d\n", x); return 0; } int f(){ int a=1; g_ptr = &a; return g(); } int g(){ int b = 20 + *g_ptr; return b; } ``` 不做最佳化的情況與先前類似,只探討 O1 最佳化後 f 函式的行為: ```shell f: .LFB39: .cfi_startproc subq $24, %rsp .cfi_def_cfa_offset 32 movq %fs:40, %rax movq %rax, 8(%rsp) xorl %eax, %eax movl $1, 4(%rsp) leaq 4(%rsp), %rax movq %rax, g_ptr(%rip) movl $0, %eax call g movq 8(%rsp), %rdx xorq %fs:40, %rdx je .L3 call __stack_chk_fail .L3: addq $24, %rsp .cfi_def_cfa_offset 8 ret .cfi_endproc .LFE39: .size f, .-f .section .rodata.str1.1,"aMS",@progbits,1 .LC0: .string "%d\n" .text .globl main .type main, @function ``` 可發現這個例子中 f 函式呼叫了 g 函式,因為回傳值 b 參照了 g_ptr 所指向的內容,而 g_ptr 指向的目標在 f 函式內,這時候 f 函式的 stack frame 必須保留,因此不能進行 TCO。 --- 繼續改 ```C #include "stdio.h" int f(); int g(); int * g_ptr; int main(){ int x = f(); printf("%d\n", x); return 0; } int f(){ int a=1; g_ptr = &a; return g(); } int g(){ int a = 1 + *g_ptr; int b = 20; return b; } ``` ```shell f: .LFB39: .cfi_startproc subq $24, %rsp .cfi_def_cfa_offset 32 movq %fs:40, %rax movq %rax, 8(%rsp) xorl %eax, %eax leaq 4(%rsp), %rax movq %rax, g_ptr(%rip) movq 8(%rsp), %rdx xorq %fs:40, %rdx je .L5 call __stack_chk_fail .L5: movl $20, %eax addq $24, %rsp .cfi_def_cfa_offset 8 ret .cfi_endproc .LFE39: .size f, .-f .globl g .type g, @function ``` g 函式一樣有參照 g_ptr 指向的目標,但是因為**回傳值的運算與 g_ptr**無關,所以還是可以進行 TCO。 題目: ```C= int *global_var; void f(void) { int x = 3; global_var = &x; ... /* Can the compiler perform TCO here? */ g(); } ``` 思考程式註解,在第 8 行能否施加 TCO 呢?選出最適合的解釋。 ANS: 題目所給的三個案答案都有錯誤 * `(a)` 編譯器不可能施加 TCO * `(b)` 編譯器一定可施加 TCO * `(c)` 只要函式 `g` 沒有對 `global_var` 指標作 dereference,那麼 TCO 就有機會 從測試結果可知 g 即使對 `global_var` 取值,但回傳值的計算與之無關時還是可以進行 TCO,因此進行 TCO 的判斷不是函式有沒有對 `global_var` 取值,而是回傳值的計算是否需要對 `global_var` 取值,若回傳值運算與 `global_var` 無關就可以進行 TCO。 ## [第三題](https://hackmd.io/s/By7Lwz4qm#2018q3-%E7%AC%AC-4-%E9%80%B1%E6%B8%AC%E9%A9%97%E9%A1%8C-%E4%B8%8B) **ptr1.c** ```C int main() { return *((int *) 0); } ``` 0 在程式中的意思為 NULL,經過 ( int \*) 強制轉型後變為 NULL pointer,對 NULL pointer 取值會出現 Segmentation fault。 實際執行: ```C gcc -o 1 1.c && ./1 [1] 7248 segmentation fault (core dumped) ./1 ``` **ptr2.c** ```C int main() { return &*((int *) 0); } ``` 根據 C99 規格書 6.5.3.2 註記 87 > Thus, ==&*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. return 可簡化為 return (int \*) 0 ,0 強制轉型成 pointer to integer 此時是 NULL pointer,因為回傳型態為 int 所以會直接將指標視為 int 回傳,這在編譯時會出現警告,NULL pointer 的數值表示是 0,所以執行的結果相當於 return 0。 實際執行: ```C gcc -o 1 1.c && ./1 1.c: In function ‘main’: 1.c:43:9: warning: return makes integer from pointer without a cast [-Wint-conversion] return &*((int *) 0); ``` **ptr3.c** ```C #include <stddef.h> int main() { return &*NULL; } ``` 對指標進行 &* 運算等於指標本身,因此程式可以簡化成 return NULL,NULL 等型態是指標,實際上的數值是 0,因為回傳型態是 int,會直接將指標內容作為 int 回傳,因此程式等同於 retrun 0。 實際執行: ```C gcc -o 1 1.c && ./1 1.c: In function ‘main’: 1.c:44:10: warning: dereferencing ‘void *’ pointer return &*NULL; ^ 1.c:44:9: warning: return makes integer from pointer without a cast [-Wint-conversion] return &*NULL; ``` ==**注意**==:&* 一起運算時才可傳入 NULL pointer 若將程式改為 return \*NULL、return &NULL ```shell # return *NULL gcc -o 1 1.c && ./1 1.c: In function ‘main’: 1.c:44:9: warning: dereferencing ‘void *’ pointer return *NULL; ^ 1.c:44:9: error: void value not ignored as it ought to be # return &NULL gcc -o 1 1.c && ./1 1.c: In function ‘main’: 1.c:44:9: error: lvalue required as unary ‘&’ operand return &NULL; ^ ``` - 對 NULL pointer 取值是一個 Undefined Behavior,可編譯執行,但結果無法預測 - 對 NULL pointer 做 & 運算違反了 & 運算的限制 * C99 6.5.3.2 Address and indirection operators > Constraints 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. **ptr4.c** ```C #include <stddef.h> int main() { return &*(*main - (ptrdiff_t) **main); } ``` &* 的運算可直接省略,直接觀察 (\*main - (ptrdiff_t) \*\*main) * 在 C99 規格書中提到兩個指標相減: > When two pointers are subtracted, both shall point to elements of the same array object,or one past the last element of the array object; the result is the difference of the subscripts of the two array elements.The size of the result is implementation-defined,and its type (a signed integer type) is ptrdiff_t defined in the <stddef.h> header. * 對 ptrdiff_t 型態的描述: > which is the signed integer type of the result of subtracting two pointers 兩指標間的距離會因為型態的不同而有所差異,如兩個 int 指標相差一個單位,在記憶體中差了 4 bytes;但 char 指標差一個單位卻是差 1 byte,ptrdiff_t 型態解決了這一個問題,他代表的結果就是該指標相差的單位數量。 以程式檢驗 ptrdiff_t 功能: 1.c ```C #include "stdio.h" #include "stdlib.h" #include "stdint.h" #include "stddef.h" int main(){ int num[] = {1,2,3,4,5,6}; int *n = n; int *m = n + 1; ptrdiff_t o = m - n; printf("%ld\n", o); } ``` 輸出結果: ```shell gcc -o 1 1.c && ./1 0x40073d 0x400741 1 ``` 指標 n、m 所指向的型態為 int,因此在進行運算時一個單位是 4 bytes,0x40073d、0x400741 兩個數值正好差 4,若型態改為 char 記憶體的位置只會差 1 bytes;而 ptrdiff_t 是用來計算兩個指標相差幾個"單位",不是記憶體相差的 byte 數,因此 o 的數值為 1。 直接執行 ptr4.c: ```shell gcc -o 1 1.c && ./1 1.c: In function ‘main’: 1.c:42:12: warning: return makes integer from pointer without a cast [-Wint-conversion] return &*(*main - (ptrdiff_t) **main); ``` main 是 function designator \*main 與 \*\*main 兩者也都是 function designator,所存的數值是 main function 在記憶體中的位置,```(ptrdiff_t) **main``` 把 main 的記憶體位置轉換成指標操作時移動的單位,再以 \*main 相減得到 0,這時候得到的結果是一個內容為 0 的 function designator,程式再將 function designator 轉換成 pointer to function returning type (因為記憶體地址是 0 → NULL pointer),最後程式將 NULL pointer 的數值 0 直接作為 int 回傳,因此出現型態轉換的警告。 ==注意== 若程式改為: ```C #include <stddef.h> int main() { return &*(*main - (ptrdiff_t) **main); } ``` ```shell gcc -o 1 1.c && ./1 1.c: In function ‘main’: 1.c:28:10: error: invalid type argument of unary ‘*’ (have ‘long int’) return &*(*main - **main); ``` ```(*main - **main)``` 指標與指標相減,直接使用指標所存的值(記憶體位置)相減,得到的結果為純量,因此無法做 * 或 & 運算,透過 ptrdiff_t 將 \*\*main 轉換成純量,整個運算變成指標減一個純量,得到的結果仍然是指標,所以可以做 &、* 運算。 ==轉型成 int、long 可以得到相同的結果,但 int 因長度不同會有警告出現==