深入理解C語言的函式呼叫過程
先看一個最簡單的程式:
點選(此處)摺疊或開啟
- /*test.c*/
- #include <stdio.h>
- int foo1(int m,int n,int
p)
- {
- int x
= m + n + p;
- return x;
- }
- int main(int argc,char** argv)
- {
- int x,y,z,result;
- x=11;
- y=22;
- z=33;
- result = foo1(x,y,z);
- printf("result=%d\n",result);
- return 0;
- }
點選(此處)摺疊或開啟
- .file "test.c"
- .text
- .globl foo1
- .type foo1, @function
- foo1:
- pushl %ebp
- movl %esp, %ebp
- subl $16, %esp
- movl 12(%ebp), %eax
- movl 8(%ebp), %edx
- leal (%edx,%eax),
%eax
- addl 16(%ebp), %eax
- movl %eax,
-4(%ebp)
- movl -4(%ebp), %eax
- leave
- ret
- .size foo1,
.-foo1
- .section
.rodata
- .LC0:
- .string
"result=%d\n"
- .text
- .globl main
- .type main, @function
- main:
- pushl %ebp
- movl %esp, %ebp
- andl $-16, %esp
- subl $32, %esp
- movl $11, 16(%esp)
- movl $22, 20(%esp)
- movl $33, 24(%esp)
- movl 24(%esp), %eax
- movl %eax, 8(%esp)
- movl 20(%esp), %eax
- movl %eax, 4(%esp)
- movl 16(%esp), %eax
- movl %eax,
(%esp)
- call foo1
- movl %eax, 28(%esp)
- movl $.LC0, %eax
- movl 28(%esp), %edx
- movl %edx, 4(%esp)
- movl %eax,
(%esp)
- call printf
- movl $0, %eax
- leave
- ret
- .size main,
.-main
- .ident
"GCC: (GNU) 4.4.4 20100726 (Red Hat 4.4.4-13)"
- .section .note.GNU-stack,"",@progbits
[root@maple 1]# gcc -g -o test test.s [root@maple 1]# objdump -D test >testbin [root@maple 1]# vi testbin //… 省略部分不相關程式碼 80483c0: ff d0 call *%eax 80483c2: c9 leave 80483c3: c3 ret
080483c4 : 80483c4: 55 push %ebp 80483c5: 89 e5 mov %esp,%ebp 80483c7: 83 ec 10 sub $0x10,%esp 80483ca: 8b 45 0c mov 0xc(%ebp),%eax 80483cd: 8b 55 08 mov 0x8(%ebp),%edx 80483d0: 8d 04 02 lea (%edx,%eax,1),%eax 80483d3: 03 45 10 add 0x10(%ebp),%eax 80483d6: 89 45 fc mov %eax,-0x4(%ebp) 80483d9: 8b 45 fc mov -0x4(%ebp),%eax 80483dc: c9 leave 80483dd: c3 ret
080483de :80483de: 55 push %ebp 80483df: 89 e5 mov %esp,%ebp 80483e1: 83 e4 f0 and $0xfffffff0,%esp 80483e4: 83 ec 20 sub $0x20,%esp 80483e7: c7 44 24 10 0b 00 00 movl $0xb,0x10(%esp) 80483ee: 00 80483ef: c7 44 24 14 16 00 00 movl $0x16,0x14(%esp) 80483f6: 00 80483f7: c7 44 24 18 21 00 00 movl $0x21,0x18(%esp) 80483fe: 00 80483ff: 8b 44 24 18 mov 0x18(%esp),%eax 8048403: 89 44 24 08 mov %eax,0x8(%esp) 8048407: 8b 44 24 14 mov 0x14(%esp),%eax 804840b: 89 44 24 04 mov %eax,0x4(%esp) 804840f: 8b 44 24 10 mov 0x10(%esp),%eax 8048413: 89 04 24 mov %eax,(%esp) 8048416: e8 a9 ff ff ff call 80483c4 804841b: 89 44 24 1c mov %eax,0x1c(%esp) 804841f: b8 04 85 04 08 mov $0x8048504,%eax 8048424: 8b 54 24 1c mov 0x1c(%esp),%edx 8048428: 89 54 24 04 mov %edx,0x4(%esp) 804842c: 89 04 24 mov %eax,(%esp) 804842f: e8 c0 fe ff ff call 80482f4 8048434: b8 00 00 00 00 mov $0x0,%eax 8048439: c9 leave 804843a: c3 ret 804843b: 90 nop 804843c: 90 nop //… 省略部分不相關程式碼 |
用GDB除錯可執行程式test:
在main函式第一條指令執行前我們看一下程式test的棧空間佈局。因為我們最終的可執行程式是通過glibc庫啟動的,在main的第一條指令執行前,其實還有很多故事的,這裡就不展開了,以後有時間再細究,這裡只要記住一點:main函式執行前,其程式空間的棧裡已經有了相當多的資料。我的系統裡此時棧頂指標esp的值是0xbffff63c,棧基址指標ebp的值0xbffff6b8,指令暫存器eip的值是0x80483de正好是下一條馬上即將執行的指令,即main函式內的第一條指令“push %ebp”。那麼此時,test程式的棧空間佈局大致如下:
然後執行如下三條指令:
點選(此處)摺疊或開啟
- 25 pushl %ebp //將原來ebp的值0xbffff6b8如棧,esp自動增長4位元組
- 26 movl %esp,
%ebp //用ebp儲存當前時刻esp的值
- 27 andl $-16, %esp //記憶體地址對其,可以忽略不計
第28條指令“subl $32,%esp”是在棧上為函式裡的本地區域性變數預留空間,這裡我們看到main主函式有4個int型的變數,理論上說預留16位元組空間就可以了,但這裡卻預留了32位元組。GCC編譯器在生成彙編程式碼時,已經考慮到函式呼叫時其輸入引數在棧上的空間預留的問題,這一點我們後面會看到。當第28條指令執行完後棧空間裡的資料和佈局如下:
然後main函式裡的變數x,y,z的值放到棧上,就是接下來的三條指令:
點選(此處)摺疊或開啟
- 29 movl $11, 16(%esp)
- 30 movl $22, 20(%esp)
- 31 movl $33, 24(%esp)
這是三條暫存器間接定址指令,將立即數11,22,33分別放到esp暫存器所指向的地址0xbffff610向高位分別偏移16、20、24個位元組處的記憶體單元裡,最後結果如下:
注意:這三條指令並沒有改變esp暫存器的值。
接下來main函式裡就要為了呼叫foo1函式而做準備了。由於mov指令的兩個運算元不能都是記憶體地址,所以要將x,y和z的值傳遞給foo1函式,則必須藉助通用暫存器來完成,這裡我們看到eax承擔了這樣的任務:
點選(此處)摺疊或開啟
- 32 movl 24(%esp), %eax
- 33 movl %eax, 8(%esp)
- 34 movl 20(%esp), %eax
- 35 movl %eax, 4(%esp)
- 36 movl 16(%esp), %eax
- 37 movl %eax, (%esp)
當foo1函式所需要的所有輸入引數都已經按正確的順序入棧後,緊接著就需要呼叫call指令來執行foo1函式的程式碼了。前面的博文說過,call指令執行時分兩步:首先會將call指令的下一條指令(movl %eax,28(%esp))的地址(0x0804841b)壓入棧,然後跳轉到函式foo1入口處開始執行。當第38條指令“call foo1”執行完後,棧空間佈局如下:
call指令自動將下一條要執行的指令的地址0x0804841b壓入棧,棧頂指標esp自動向低地址處“增長”4位元組。所以,我們以前在C語言裡所說的函式返回地址,應該理解為:當被呼叫函式執行完之後要返回到它的呼叫函式裡下一條馬上要執行的程式碼的地址。為了便於觀察,我們把foo1函式最後生成指令再列出來:
點選(此處)摺疊或開啟
- 3 .globl foo1
- 4 .type foo1, @function
- 5 foo1:
- 6 pushl %ebp
- 7 movl %esp, %ebp
- 8 subl $16, %esp
- 9 movl 12(%ebp), %eax
- 10 movl 8(%ebp), %edx
- 11 leal (%edx,%eax),
%eax
- 12 addl 16(%ebp), %eax
- 13 movl %eax,
-4(%ebp)
- 14 movl -4(%ebp), %eax
- 15 leave
- 16 ret
- 17 .size foo1, .-foo1
接下來第9和第10條指令,也並沒有改變棧上的任何資料,而是將函式輸入引數列表中的的x和y的值分別轉載到eax和edx暫存器,和main函式剛開始時做的事情一樣。此時eax=22、edx=11。然後用了一條leaf指令完成x和y的加法運算,並將運算結果存在eax裡。第12條指令“addl 16(%ebp), %eax”將第三個輸入引數p的值,這裡是實參z的值為33,同樣用暫存器間接定址模式累加到eax裡。此時eax=11+22+33=66就是我們最終要得計算結果。
因為我們foo1()函式的C程式碼中,最終計算結果是儲存到foo1()裡的區域性變數x裡,最後用return語句將x的值通過eax暫存器返回到mian函式裡,所以我們看到接下來的第13、14條指令有些“多此一舉”。這足以說明gcc人家還是相當嚴謹的,C原始碼的函式裡如果有給區域性變數賦值的語句,生成彙編程式碼時確實會在棧上為本地變數預留的空間裡的正確位置為其賦值。當然gcc還有不同級別的優化技術來提高程式的執行效率,這個不屬於本文所討論的東西。讓我們繼續,當第13、14條指令執行完後,棧佈局如下:
將ebp-4的地址處0xbffff604(其實就是foo1()裡的第一個區域性引數x的地址)的值設定為66,然後再將該值複製到eax暫存器裡,等會兒在main函式裡就可以通過eax暫存器來獲取最終的計算結果。當第15條指令leave執行完後,棧空間的資料和佈局如下:
我們發現,雖然棧頂從0xbffff5f8移動到0xbffff60c了,但棧上的資料依然存在。也就是說,此時你通過esp-8依舊可以訪問foo1函式裡的區域性變數x的值。當然,這也是說得通的,因為函式此時還沒有返回。我們看棧佈局可以知道當前的棧頂0xbffff60c處存放的是下一條即將執行的指令的地址,對照反彙編結果可以看到這正是main函式裡的第18條指令(在整個彙編原始檔test.s裡的行號是39)“movl %eax, 28(%esp)”。leave指令其實完成了兩個任務:
1、將棧上為函式預留的空間“收回”;
2、恢復ebp;
也就是說leave指令等價於下面兩條指令,你將leave替換成它們編譯執行,結果還是對的:
點選(此處)摺疊或開啟
- movl %ebp,%esp
- popl %ebp
前面我們也說過,ret指令會自動到棧上去pop資料,相當於執行了“popl %eip”,會使esp增大4位元組。所以當執行完第16條指令ret後,esp從0xbffff60c增長到0xbffff610處,棧空間結構如下:
第39條指令就相當於給main裡的result變數賦值66,如上紅線標註的地方。接下來main函式裡要執行printf("result=%d\n",result)語句了,而printf又是C庫的一個常用的輸出函式,這裡就又會像前面呼叫foo1那樣,初始化棧,然後用“call printf的地址”來呼叫C函式。當40~43這4條指令執行完後,棧裡的資料如下:
點選(此處)摺疊或開啟
- 40 movl $.LC0, %eax
- 41 movl 28(%esp), %edx
- 42 movl %edx, 4(%esp)
- 43 movl %eax, (%esp)
由於此時棧已經用來呼叫printf了,所以棧頂0xbffff610“以上”部分的空間裡就找不到foo1的任何影子了。最後在main函式裡,當第46、47條指令執行完後棧的佈局分別是:
當main函式裡的ret執行完,其實是返回到了C庫裡繼續執行剩下的清理工作。
所以,最後關於C的函式呼叫,我們可以總結一下:
1、函式輸入引數的入棧順序是函式原型中形參從右至左的原則;
2、組合語言裡呼叫函式通常情況下都用call指令來完成;
3、組合語言裡的函式大部分情況下都符合以下的函式模板:
點選(此處)摺疊或開啟
- .globl fun_name
- .type fun_name, @function
- fun_name:
- pushl %ebp
- movl %esp, %ebp
- <函式主體程式碼>
- leave
- ret
如果我們有個函式原型:int funtest(int x,int y int z char* ptr),在彙編層面,當呼叫它時棧的佈局結構一般是下面這個樣子:
而有些資料上將ebp指向函式返回地址的地方,這是不對的。正常情況下應該是ebp指向old ebp才對,這樣函式末尾的leave和ret指令才可以正常工作。
相關文章
- 深入理解 C 語言的函式呼叫過程函式
- C語言函式呼叫棧C語言函式
- 高階語言反彙編程式的函式呼叫過程 (轉)函式
- c語言是如何處理函式呼叫的?C語言函式
- 從彙編角度分析C語言的過程呼叫C語言
- 【C語言】函式的概念和函式的呼叫(引數傳遞)C語言函式
- C語言函式手冊:c語言庫函式大全|C語言標準函式庫|c語言常用函式查詢C語言函式
- 深入理解C語言(一)C語言
- 函式棧幀(呼叫過程)函式
- JavaScript函數語言程式設計之深入理解純函式JavaScript函數程式設計函式
- C語言 execve()函式C語言函式
- C語言常用函式C語言函式
- C語言函式sscanf()的用法C語言函式
- C語言qsort函式的使用C語言函式
- C語言 itoa函式及atoi函式C語言函式
- 深入理解javascript系列(十二):函式與函數語言程式設計(1)JavaScript函式函數程式設計
- 深入理解javascript系列(十三):函式與函數語言程式設計(2)JavaScript函式函數程式設計
- 深入理解函數語言程式設計函數程式設計
- [ASM C/C++] C語言的main 函式ASMC++C語言AI函式
- 詳解C語言函式C語言函式
- tmpnam() - C語言庫函式C語言函式
- tmpfile() - C語言庫函式C語言函式
- C語言時間函式C語言函式
- c語言函式庫(轉)C語言函式
- C語言編譯全過程C語言編譯
- VS2017如何使用C_C++語言呼叫匯編函式C++函式
- 一種面嚮物件語言的方法呼叫過程。物件
- go語言與c語言的相互呼叫GoC語言
- C語言關於指標,gets()和gets_s()函式的理解C語言指標函式
- C語言庫函式及示例C語言函式
- C語言解讀assert函式C語言函式
- C#語言函式遞迴C#函式遞迴
- C語言 sizeof函式詳解C語言函式
- C語言標準函式庫C語言函式
- c#語言-高階函式C#函式
- 函式呼叫發生在SQL呼叫之前還是過程中函式SQL
- 理解JavaScript的函式呼叫和thisJavaScript函式
- 【圖文】函式呼叫過程中棧的變化函式