深入理解C函數調用機制(未完待續)

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的棧幀結構如圖:

子函數調用時,執行的操作:

  1. 父函數將調用參數**從後向前**壓棧
  2. 將返回地址壓棧保存
  3. 跳轉到子函數起始地址執行
  4. 子函數將父函數棧幀起始地址(%rpb) 壓棧
  5. 將 %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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章