title: 深入理解C函數調用機制
copyright: true
tags:
- 函數調用
- 棧幀
- gdb
categories: - C
date: 2019-09-15 15:21:00
寫在前面(未完待續)
C語言是面向過程的一種語言,而函數則作爲解決一個個問題的“過程”,在一個程序中,會出現函數的聲明、定義以及調用,我們已經知道C函數的調用和棧有關,但是在有些程序的debug過程中,如果不瞭解函數調用的底層實現原理,是很痛苦的。所以這裏就以x86-64下的C語言函數調用爲例,至於爲什麼不帶上C++,前面已經說過,C++ is not greater C,C++裏面的構造函數、虛函數,更爲複雜,所以這裏不做討論。
在此之前,需要了解一下:
- 棧幀
棧幀也叫過程活動記錄,可以說每個函數的調用都對應着一個棧幀,棧幀裏保存了函數運行的環境:函數參數、返回地址(下一條指令的地址)、局部變量等。要知道,棧的存儲順序是從高地址往低地址存儲,每個函數的每次調用,都會有屬於自己的棧幀,ebp(32位)/rbp(64位)叫做棧底指針寄存器,指向棧幀的底部(高地址);esp/rsp指向棧幀頂部(低地址) - x86-64下16個通用寄存器
- 每個寄存器的用途並不是單一的。
- %rax 通常用於存儲函數調用的返回結果,同時也用於乘法和除法指令中。在imul 指令中,兩個64位的乘法最多會產生128位的結果,需要 %rax 與 %rdx 共同存儲乘法結果,在div 指令中被除數是128 位的,同樣需要%rax 與 %rdx 共同存儲被除數。
- %rsp 是堆棧指針寄存器,通常會指向棧頂位置,堆棧的 pop 和push 操作就是通過改變 %rsp 的值即移動堆棧指針的位置來實現的。
- %rbp 是棧幀指針,用於標識當前棧幀的起始位置
- %rdi, %rsi, %rdx, %rcx,%r8, %r9 六個寄存器用於存儲函數調用時的6個參數(如果有6個或6個以上參數的話)。
- 被標識爲 “miscellaneous registers” 的寄存器,屬於通用性更爲廣泛的寄存器,編譯器或彙編程序可以根據需要存儲任何數據。
- 這裏還要區分一下 “Caller Save” 和 ”Callee Save” 寄存器,即寄存器的值是由”調用者保存“ 還是由 ”被調用者保存“。當產生函數調用時,子函數內通常也會使用到通用寄存器,那麼這些寄存器中之前保存的調用者(父函數)的值就會被覆蓋。爲了避免數據覆蓋而導致從子函數返回時寄存器中的數據不可恢復,CPU 體系結構中就規定了通用寄存器的保存方式。
如果一個寄存器被標識爲”Caller Save”, 那麼在進行子函數調用前,就需要由調用者提前保存好這些寄存器的值,保存方法通常是把寄存器的值壓入堆棧中,調用者保存完成後,在被調用者(子函數)中就可以隨意覆蓋這些寄存器的值了。如果一個寄存被標識爲“Callee Save”,那麼在函數調用時,調用者就不必保存這些寄存器的值而直接進行子函數調用,進入子函數後,子函數在覆蓋這些寄存器之前,需要先保存這些寄存器的值,即這些寄存器的值是由被調用者來保存和恢復的。
函數調用
函數調用時,caller與callee的棧幀結構如圖:
子函數調用時,執行的操作:
- 父函數將調用參數**從後向前**壓棧
- 將返回地址壓棧保存
- 跳轉到子函數起始地址執行
- 子函數將父函數棧幀起始地址(%rpb) 壓棧
- 將 %rbp 的值設置爲當前 %rsp 的值,即將 %rbp 指向子函數棧幀的起始地址
示例代碼:
testfun.c
void fun(int a, int b, int c) {
int x = 10;
int y = 100;
}
int main() {
fun(1,2,3);
return 0;
}
gcc進行編譯:
$ gcc testfun.c -g -o testfun
gcc生成彙編代碼:
.file "testfun.c"
.text
.globl fun
.type fun, @function
fun:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -20(%rbp)
movl %esi, -24(%rbp)
movl %edx, -28(%rbp)
movl $10, -8(%rbp)
movl $100, -4(%rbp)
nop
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size fun, .-fun
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $3, %edx
movl $2, %esi
movl $1, %edi
call fun
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size main, .-main
.ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.11) 5.4.0 20160609"
.section .note.GNU-stack,"",@progbits
貌似不大好看,使用objdump反彙編看看(只保留關鍵代碼):
testfun: file format elf64-x86-64
00000000004004d6 <fun>:
void fun(int a, int b, int c) {
4004d6: 55 push %rbp
4004d7: 48 89 e5 mov %rsp,%rbp
4004da: 89 7d ec mov %edi,-0x14(%rbp)
4004dd: 89 75 e8 mov %esi,-0x18(%rbp)
4004e0: 89 55 e4 mov %edx,-0x1c(%rbp)
int x = 10;
4004e3: c7 45 f8 0a 00 00 00 movl $0xa,-0x8(%rbp)
int y = 100;
4004ea: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp)
}
4004f1: 90 nop
4004f2: 5d pop %rbp
4004f3: c3 retq
00000000004004f4 <main>:
int main() {
4004f4: 55 push %rbp
4004f5: 48 89 e5 mov %rsp,%rbp
fun(1,2,3); // 對fun(int, int, int)的調用
4004f8: ba 03 00 00 00 mov $0x3,%edx // 3先入棧
4004fd: be 02 00 00 00 mov $0x2,%esi // 然後是2
400502: bf 01 00 00 00 mov $0x1,%ed // 最後是1
400507: e8 ca ff ff ff callq 4004d6 <fun>
return 0;
40050c: b8 00 00 00 00 mov $0x0,%eax
}
400511: 5d pop %rbp
400512: c3 retq
400513: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
40051a: 00 00 00
40051d: 0f 1f 00 nopl (%rax)
這個就好看多了,有對應的彙編代碼和C源碼,對於我這個彙編菜鳥來說真是太人性化了。不過這還是不夠,我們還有寄存器沒有觀察。萬能的gdb,這件事就交給你了。
$ gdb testfun -tui // 使用tui界面
接着,
$ layout regs // 分配一個寄存器界面佈局
然後,
set disassmele-next-line on // 實時顯示反彙編代碼
接着,在fun()的調用位置打上斷點,
$ b 7 //也就是我們的fun(1,2,3);
$ b *fun // 注意 *fun是彙編級別的fun()函數地址
接着,單步運行,
$ ni // 注意,ni和si都是相對於彙編代碼的單步運行,n和s只是相對於C代碼的單步運行;
// 再者,n和s都有單步運行的功能,只不過s直接會進入函數調用的內部
1. main
首先,3進入%edx寄存器
然後,2進入%esi寄存器
接着,1進入%edi寄存器
然後對fun()函數進行調用
0x00000000004004f8 <main+4>: ba 03 00 00 00 mov $0x3,%edx
0x00000000004004fd <main+9>: be 02 00 00 00 mov $0x2,%esi
0x0000000000400502 <main+14>: bf 01 00 00 00 mov $0x1,%edi
=> 0x0000000000400507 <main+19>: e8 ca ff ff ff callq 0x4004d6 <fun>
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-va6Ih0xh-1571063957951)(http://px1awapyv.bkt.clouddn.com/step1.png)]
2. fun
=> 0x00000000004004d6 <fun+0>: 55 push %rbp
0x00000000004004d7 <fun+1>: 48 89 e5 mov %rsp,%rbp
0x00000000004004da <fun+4>: 89 7d ec mov %edi,-0x14(%rbp)
0x00000000004004dd <fun+7>: 89 75 e8 mov %esi,-0x18(%rbp)
0x00000000004004e0 <fun+10>: 89 55 e4 mov %edx,-0x1c(%rbp)
0x00000000004004f1 <fun+27>: 90 nop
0x00000000004004f2 <fun+28>: 5d pop %rbp
0x00000000004004f3 <fun+29>: c3 retq