2018 q3 Homework1 === Contributed by < `chenishi` > ### 指標篇 1. Main() call in other function ```c= main() { } char test() { return main(); } ``` 以 gcc 編譯過後只有獲得 `warning: return type defaults to ‘int’ [-Wimplicit-int]` 這樣的對於main本身沒有宣告回傳型別的警告 其執行檔也能夠成功執行,由於這部份可能是因為 `test()` 根本沒有被呼叫到,因此想透過 gdb 跳到 `test()` 實做的記憶體位置直接執行 2. Understanding Array Subscripting ```c= int main() { int arr [] = {1, 1, 2, 3, 5, 8, 13}; } ``` > (gdb) p arr $1 = {1, 1, 2, 3, 5, 8, 13} > (gdb) p *arr $2 = 1 > (gdb) p &arr $10 = (int (*)[7]) 0x7fffffffdd70 > (gdb) x/7 arr 0x7fffffffdd70: 1 1 2 3 0x7fffffffdd80: 5 8 13 想到都用到了 int array,那就順便來看一下 enum 的記憶體規劃 ```c= int main() { enum color{ red, blue, yello, green, orange }; enum color colA = orange; } ``` 看 `gdb` 記憶體狀況沒看到什麼,用 `godbolt` 分析組合語言,結果發現意外的短 ```clike= main: push rbp mov rbp, rsp mov DWORD PTR [rbp-4], 4 ### 只有這行把orange紀錄 mov eax, 0 pop rbp ret ``` 3. String literal ```c= int main() { char str1 [] = "hello world"; char * str2; str2 = "hello world"; return 0; } ``` ```clike= .LC0: .string "hello world" main: push rbp // Line 2 : rbp 在x86_64 assembly 代表 frame pointer (備份當下 call func 的記憶體位置) mov rbp, rsp // Line 2 : 把 rsp(stack pointer) 暫存器內容移動到 rbp movabs rax, 8031924123371070824 // Line 3 : 不少文件都提到他是 "GAS specific" // GAS for "GNU AS", 好處在於支援 GCC inline syntax // 不過還不是很懂後面那串數字表示的意義 ov QWORD PTR [rbp-20], rax // Line 3 mov DWORD PTR [rbp-12], 6581362 // Line 3 mov QWORD PTR [rbp-8], OFFSET FLAT:.LC0 // Line 5 mov eax, 0 pop rbp ret ``` 之前只有就單純 array 與 pointer 間的記憶體上的比較,而後老師在影片中提到「因為兩者記憶體儲存方式不同,導致作為區域變數時表現也會不同」 ```c= #include <stdio.h> char* give_me_ptr() { char* str = "hello world"; return str; } char* give_me_arr() { char str []= "hello world"; return str; } int main() { char* ptr = give_me_ptr(); printf("ptr = %s\n", ptr); char* arr = give_me_arr(); printf("arr = %s\n", arr); } ``` 首先是在寫這段測試碼時,可能是因為太久沒寫陣列(都直接用指標)結果忘記了 c function 不能夠回傳「陣列」而要回傳「指標」 其次是跑出來的結果也是符合預期的 > ptr = hello world arr = (null) 首先是 `give_me_ptr()` 部份 > (gdb) p str $1 = 0x4006d4 "hello world" > (gdb) p *(char*)(0x4006d4) $2 = 104 'h' 我們也可以注意到 'hello world' 這個 string literal 並不是被放在 stack 裡面 > info frame Stack level 0, frame at 0x7fffffffdd90: ... 接著 `continue` 到下一個斷點,`give_me_arr()` 的實做 > (gdb) p str $6 = "hello world" > (gdb) p &str $7 = (char (*)[12]) 0x7fffffffdd60 這邊就可以很清楚的看到 `array` 版本跟 `pointer` 版本的實做差異了 **Array 版本的會把 "hello world" 這樣的 string literal 複製一份放到 stack (可以從記憶體位址得知)** 而回傳一個「指向這個存放於 stack 的 string 的指標」,**有趣的是,這個 stack string 的生命週期在 function call 結束,也就是 return 的現在,就會從 stack 被 pop 出來,導致指標指向一個沒有意義的地方** 而 pointer 版本的,則只是將一個指標指向一個 static storage 的 string literal (C99 6.4.5.5),由於該 string literal 在 function call 結束後生命週期依然存在,因此指標依然有效 ### 函式呼叫篇 1. Double free() ```c= #include <stdlib.h> void double_free(char *ptr) { free(ptr); free(ptr); } void null_free(char *ptr) { ptr = NULL; // memory leak ? free(ptr); } int main() { char *ptr1 = "hello"; char *ptr2 = "world"; double_free(ptr1); null_free(ptr2); } ``` 首先寫完這個測試程式之後,我注意到第11行這樣直接把指向 string literal 的 pointer 改指到 NULL 的行為是不是會造成 memory leak, 這部份可以試試看用 Valgrind 檢測會不會跳錯誤訊息 其次就是我發現在 `double_free()` 的第一次 `free()` 就會產生錯誤,這代表 「在 function 內 free 參數的作法」 可能有問題,同時萬一成功了根據 C 語言傳遞參數的原則,只有副本的記憶體空間會被釋放掉,因此可能也是一個失敗的實做 ```c= #include <stdlib.h> #include <stdio.h> int main() { char *ptr; char *str = "hello world"; ptr = malloc(sizeof(char) * sizeof(str)); // ptr = str; ## 這邊把 ptr 指向新的位址 str // ## 會導致存取不到原本的 malloc() 到的空間 free(ptr); } ``` 後來才發現到單純的把指標指向 string literal 並不會分配記憶體到 heap ,這也是為什麼之前 `free()` 不能成功的主要原因 ```c= #include <string.h> void double_free(char *ptr) { free(ptr); free(ptr); } void null_free(char *ptr) { ptr = NULL; // memory leak ? free(ptr); } int main() { char *ptr1; ptr1 = strdup("hello"); char *ptr2; ptr2 = strdup("world"); double_free(ptr1); null_free(ptr2); } ``` 最終的測試程式 在 `double_free()` 第五行第一次 `free()` 過後 > (gdb) p ptr $1 = 0x602010 "" 可以看到 0x602010 這個並非 stack 內的記憶體空間已經被釋放了 針對這樣子的一個記憶體空間再次 `free()` 會導致 `double free or corruption` 而對於 `null_free()` 則可以看到在第12行時,便指向 `0x0` 也就是 `NULL` 的位址 > (gdb) p ptr $1 = 0x0 這代表針對 `0x0` 位址做的 `free()` 操作是無效的 以上第三版程式透過 `Valgrind` 測試 memory leak > ==4433== LEAK SUMMARY: ==4433== definitely lost: 6 bytes in 1 blocks :::info `2018/10/02 補充` 在 `malloc(3)` man page 裡面也有提到 `if free(ptr) has already been called before, undefined behavior occurs` ::: 2. `Valgrind` Memory Leak 檢查 ```c= #include <stdlib.h> #include <string.h> int main() { char* ptr; ptr = strdup("hello world"); } ``` 為了檢查最基本的 memory leak 而寫的一個小範例 `Valgrind` 結果不出所料,出現 memory leak 了 (12 byte 正好是 string 長度) > ==5426== LEAK SUMMARY: ==5426== definitely lost: 12 bytes in 1 blocks --- ```c= #include <stdlib.h> #include <string.h> int main() { char* ptr; ptr = strdup("hello world"); ptr = strdup("goodbye world"); free(ptr); } ``` 接下來測試像 ``"hello world"`` 這樣無法再被指標存取到的記憶體是不是也被算做 memory leak 透過 `Valgrind` 看來,毫無疑問的也是 memory leak > ==5679== LEAK SUMMARY: ==5679== definitely lost: 12 bytes in 1 blocks --- ```c= #include <stdlib.h> #include <string.h> int main() { char* ptr; ptr = "hello world"; ptr = "goodbye world"; ptr = NULL; } ``` 最後就要切入正題了,究竟「 string literal 會不會跟 memory leak 有關?」 從 `gdb` 來看,執行第6行時 > (gdb) p ptr $1 = 0x400584 "hello world" 執行第7行 > (gdb) p ptr $3 = 0x400590 "goodbye world" 第八行 > (gdb) p ptr $5 = 0x0 單純從上述結果看,string literal 會被放置在一個 static storage,並且從上述操作中,`hello world` 與 `goodbye world` 這兩個 string literal 應該會無法再次被存取 但是跑 `Valgrind` 的結果卻是沒有 memory leak > ==6497== All heap blocks were freed -- no leaks are possible 不過仔細看 `Valgrind` 的訊息強調是在 `heap`,而 string literal 並不存放在 `heap` 這樣的動態記憶體配置空間,這代表可能用 `Valgrind` 無法檢測出這個問題(也有可能這根本不是個問題) ### 附錄: [第一版你所不知道的C語言心得紀錄](https://hackmd.io/n2AHYHRlQdqSgbjbyLSBlQ?both)