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)