# 你所不知道的 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) 來做為了解這個議題的切入點。 ![](https://hackpad-attachments.s3.amazonaws.com/embedded2015.hackpad.com_2q5oxqltYTG_p.299401_1449482197657_undefined) 這篇文章主要的重點為 - 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 :::