# 你所不知道的 C 語言 - 函式呼叫篇
contributed by <`pjchiou`>
---
## Process 和 C 程式的關聯
先從 [Function calls in C: the boring specs](http://gghh.name/dibtp/2015/11/10/function-calls-in-c-the-boring-specs.html) 來做為了解這個議題的切入點。

這篇文章主要的重點為
- How does the C programming language implement function calls?
- What happens when a function returns?
- When a function calls another, how is the state of the former saved in memory?
如果你跟我一樣不是資訊糸的學生,必須先搞懂幾個東西。
- Stack:這是一種先進後出 (First In Last Out, FILO) 的資料結構。
- 認識兩個 register
- esp:永遠指向 stack top (也就是低位處) -
- ebp:指向 begining of current frame 。
- 幾個組合語言指令
- push:把資料存進 stack ,rsp 會向低位移動一單位。
- pop:把資料存 stack 內取出 ,rsp 會向高位移動一單位。
- call: 把當前位址 push 進 stack ,然後跳進某個 procedure 。
- ret: 把 stack 中的資料 pop 出,並跳到該位址。
- leave: [4.2.4節](http://poli.cs.vsb.cz/edu/soj/down/soj-syllabus.pdf)
---
## 從遞迴學習 function call
接下來跟著 [Function calls in C: a practical example](http://gghh.name/dibtp/2015/11/11/function-calls-in-c-practical-example.html) 做點實驗
主要想看的東西有兩個
- 程式進入函式時 stack 的行為。
- 程式離開函式時 stack 的行為。
首先,先看一下 gcd 所在的位置 `disas gcd`
:::info
==0x0000000000400537 <+0>: push %rbp==
==0x0000000000400538 <+1>: mov %rsp,%rbp==
==0x000000000040053b <+4>: sub $0x10,%rsp==
0x000000000040053f <+8>: mov %edi,-0x4(%rbp)
0x0000000000400542 <+11>: mov %esi,-0x8(%rbp)
0x0000000000400545 <+14>: cmpl $0x0,-0x8(%rbp)
0x0000000000400549 <+18>: jne 0x400550 <gcd+25>
0x000000000040054b <+20>: mov -0x4(%rbp),%eax
0x000000000040054e <+23>: jmp 0x400563 <gcd+44>
0x0000000000400550 <+25>: mov -0x4(%rbp),%eax
0x0000000000400553 <+28>: cltd
0x0000000000400554 <+29>: idivl -0x8(%rbp)
0x0000000000400557 <+32>: mov -0x8(%rbp),%eax
0x000000000040055a <+35>: mov %edx,%esi
0x000000000040055c <+37>: mov %eax,%edi
0x000000000040055e <+39>: callq 0x400537 <gcd>
==0x0000000000400563 <+44>: leaveq==
0x0000000000400564 <+45>: retq
:::
更精確地說,我想觀察的有兩個地方
- 0x400537 ~ 0x40053b ,這裡是 function prologue 。
- 0x400563 是 function epilogue 。
於是我設一下中斷點 `break *(0x400537)`後,`run 42 24`。
|中斷點|$rsp|$rbp|
|:-:|:-:|:-:|
|gcd (a=32767, b=-140360048)|0x7fffffffdd08|0x7fffffffdd30|
接下來將指令一個一個慢慢執行,`stepi`後,期望看到的是 **$rsp 往低位移動 8 bytes 且 $rbp 被 push 進 stack**。
這時用 `x/8gx $rsp` 看看 stack 的內容,卻實如同預期。
:::info
0x7fffffffdd00: ==0x00007fffffffdd30== ==0x00000000004005b9==
0x7fffffffdd10: 0x00007fffffffde18 0x0000000300400450
0x7fffffffdd20: 0x0000002affffde10 0x0000000000000018
0x7fffffffdd30: 0x00000000004005e0 0x00007ffff7a05b97
:::
再`stepi`,預期看到的是 **$rbp = $rsp**。
:::info
(gdb) p $rsp
$14 = (void *) 0x7fffffffdd00
(gdb) p $rbp
$15 = (void *) 0x7fffffffdd00
:::
接下來是 $rsp 會往低位移動,給 local variable 空間。然後再過了兩個`stepi`,才看到參數被載入,**為什麼跟 model 有點出入?**
接下來看 function epilogue 的部分,把中斷點改在 *(0x400563) 處。此時的值為
:::info
(gdb) p $rsp
$22 = (void *) 0x7fffffffdc90
(gdb) p $rbp
$23 = (void *) 0x7fffffffdca0
(gdb) x/6gx $rsp
0x7fffffffdc90: 0x0000000000000000 0x0000000600000000
0x7fffffffdca0: 0x00007fffffffdcc0 0x0000000000400563
0x7fffffffdcb0: 0x0000000000050000 0x0000001200000006
:::
leave 預期的行為是
1. $rsp = $rbp
2. pop $rbp
`stepi` 後再看一次值,這裡 $rsp 的值並沒有問題,因為 pop 會使其再往高位移動 8 bytes 。
:::info
(gdb) p $rsp
$24 = (void *) 0x7fffffffdca8
(gdb) p $rbp
$25 = (void *) 0x7fffffffdcc0
(gdb) x/6gx $rsp
0x7fffffffdca8: 0x0000000000400563 0x0000000000050000
0x7fffffffdcb8: 0x0000001200000006 0x00007fffffffdce0
0x7fffffffdcc8: 0x0000000000400563 0x00007fffffffdd30
:::
再來是 ret ,預計會
1. pop
2. 將 procedure 返回到該位置 (也就是前一層的 leave )
所以 $rbp 不會變, $rsp 會再往高位移 8 bytes。
:::info
(gdb) p $rsp
$26 = (void *) 0x7fffffffdcb0
(gdb) p $rbp
$27 = (void *) 0x7fffffffdcc0
:::
---
## 藏在 Heap 裡的細節
為什麼 glibc 可以偵測出上述程式的 “double free or corruption” 呢?
:::info
参考共筆下方的參考資料 [Malloc tutorial](http://danluu.com/malloc-tutorial/)
因為在 malloc 的時候實際上會拿到比要求多的記憶體,而這個多的記憶體可以用來儲存額外的資訊,例如這塊記憶體的大小、是否可用?、下一塊記憶體在哪?
```graphviz
digraph structs {
node[shape=record]
mema [label="<A> A[-1]|A[0]|A[1]|...|A[10]"]
memb [label="<B> B[-1]|B[0]|B[1]|...|B[15]"]
mema:A -> memb:B
}
```
:::
也因此我們可以知道為什麼
- free((void *)ptr),不需要給空間。因為參考這塊**多出來的部分**就可以知道大小。
- 如果每次都 malloc() 一個很小的空間,會造成記憶體使用效率不佳。
第二週的課堂上有一個題目是
```C
int **arr = malloc(3 * sizeof(int *));
for (int i = 0; i < 3; i++)
arr[i] = malloc(4 * sizeof(int));
```
如果要 free() 時,是否需要兩層的迴圈呢?倘若有人只是 free(arr); 會發生什麼事?又,該如何改善?
:::info
如果只 `free(ptr);` 會導致 memory leak,所以要 `free` 兩次,由內而外
```=C
for(int i=0;i<3;i++)
free(arr[i]);
free(arr);
```
應該可以這麼改善
```=C
int *p,*matrix[3];
p = malloc(sizeof(int)*3*4);
for(int i=0;i<3;i++)
matrix[i] = p+i*4;
free(*matrix);
```
原來的樣子:
```graphviz
digraph structs {
node[shape=record]
ptrs [label="<ptr0> arr[0]|<ptr1> arr[1]|<ptr2> arr[2]"]
data0 [label="<d0> arr[0][0]|arr[0][1]|...|arr[0][3]"]
data1 [label="<d1> arr[1][0]|arr[1][1]|...|arr[1][3]"]
ptrs:ptr0 -> data0:d0
ptrs:ptr1 -> data1:d1
}
```
改善後:
```graphviz
digraph structs {
node[shape=record]
ptrs [label="<ptr0> matrix[0]|<ptr1> martix[1]|<ptr2> martix[2]"]
data [label="<d0> p[0]|[1]|...|<d4> p[4]|...|<d8> p[8]|...|p[11]"]
ptrs:ptr0 -> data:d0
ptrs:ptr1 -> data:d4
ptrs:ptr2 -> data:d8
}
```
:::
那麼 matrix 的使用上跟第一種方式無異。測試如下:
```C=1
#include <stdio.h>
#include <stdlib.h>
int main(){
int *p,*matrix[3];
p = malloc(sizeof(int)*3*4);
for(int i=0;i<3*4;i++)
p[i] = i;
for(int i=0;i<3;i++)
matrix[i] = p+i*4;
for(int i=0;i<3;i++)
for(int j=0;j<4;j++)
printf("%d\n",matrix[i][j]);
free(*matrix);
return(0);
}
```
果然印出預期的內容。
:::success
0
1
2
3
4
5
6
7
8
9
10
11
:::