日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

C語言函數調用底層實現原理分析_C 語言

作者:Real返璞歸真 ? 更新時間: 2023-06-16 編程語言

前言

C語言程序執行實質上的函數的連續調用

運行程序時,系統通過程序入口調用main函數,在main函數中又不斷調用其它函數。

程序的每個進程都包括一個調用棧結構(Call Stack)

調用棧的作用:

  • 傳遞函數參數
  • 保存返回地址
  • 臨時保存寄存器原有值(保存現場)

寄存器分配

寄存器指CPU中可以進行高速運算的緩沖區。用于存放程序執行中用到的數據和指令。

Intel 32位結構寄存器(IA32)包含8個通用寄存器,每個寄存器4個字節(32位)

通用寄存器按照AT&T語法,寄存器名以**%e**開頭。

若按照Intel語法,寄存器名直接按e開頭。

通用寄存器包括:EAX、EBX、ECX、EDX、ESI、EDI、ESP、EBP

數據寄存器:EAX、EBX、ECX、EDX

變址寄存器:ESI、EDI

指針寄存器:ESP、EBP

X86架構中,EIP寄存器指向下一條待執行的命令地址

ESP是棧指針寄存器,指向當前棧幀的棧頂

EBP是棧幀基址寄存器,指向當前棧幀的基地址

不同架構的cpu寄存器名前綴不同。

例如:x86架構的寄存器用字母e作為前綴(extended),表明寄存器大小是32位。

x86_64架構用字母r作為前綴,表明寄存器大小是64位。

ABI協議規定了寄存器、堆棧的使用規則以及參數傳遞規則。用于約束硬件與系統之間的通信協議。編譯器必須按照ABI給出的寄存器功能定義,將C程序轉為匯編程序。

寄存器使用約定

寄存器是唯一能被被所有函數共享的資源。因此,在函數中調用其它函數時,需要考慮到數據的保存與覆蓋問題(即防止被調函數直接修改寄存器導致主調函數的數據被覆蓋)。

IA32采用了統一的寄存器使用約定,所有函數必須遵守。

  • EAX、ECX、EDX為主調函數保存寄存器,即在調用被調函數之前,主調函數如果希望保存這三個寄存器的數據,需要將數據保存到堆棧中,然后調用被調函數。
  • EBX、ESI、EDI是被調函數保存寄存器,被調函數如果向使用這三個寄存器,需要先將其中的數據保存到堆棧中,然后操作寄存器,最后將堆棧中的數據還原
  • EBP和ESP指向當前的棧,每個函數對應一個棧幀。被調函在返回前,需將主調函數的棧幀還原。即恢復到調用前的狀態。

棧幀結構

注意,程序的棧從高地址向低地址增長!

函數調用由堆棧進行處理,每個函數都單獨在堆棧中占用一塊連續的區域。這塊區域叫做每個函數的棧幀。棧幀是堆棧的邏輯片段

棧幀中保存 傳入的參數 局部變量 和 用于返回上一棧幀的信息。

棧幀的邊界由EBP和ESP決定。EBP指向棧幀的底部(高地址)ESP指向棧頂地址(低地址)。ESP可以看作是EBP的偏移量,始終指向棧幀的頂部。

EBP為幀基指針,ESP為棧頂指針。

函數調用棧演示如下:

參數2
參數1
主調函數返回地址(EIP)
主調函數棧幀基址(EBP)
被調函數保存寄存器(可選)
局部變量1
局部變量2

函數被調用時,壓棧的順序:

參數2 -> 參數1 -> 主調函數返回地址 -> 主調函數棧幀基址 -> 被調函數保存寄存器(可選) -> 局部變量 -> 局部變量2

注意,參數是從右向左依次入棧。

參數壓棧完成后,緊接著被壓入的是EIP指針所指向的地址,也就是主調函數下一個要執行的命令的地址。(用于被調函數執行完后繼續執行程序)

然后,將主調函數EBP棧幀基地址壓入棧幀,用于還原現場。并把ESP賦值給EBP,使EBP成為被調函數的棧幀基地址

繼續,改變SP的值,給被調函數局部變量預留空間

這時候,EBP指向被調函數的棧底,向上是主調函數返回地址,向下是局部變量。該地址還保存主調函數的棧幀基址

函數調用結束后,EBP賦值給ESP,使ESP指向被調函數棧底,釋放被調函數局部變量。再將主調函數棧幀基地址彈出給EBP,并彈出返回地址到EIP

堆棧操作

函數調用流程

函數調用時的具體操作:

  1. 主調函數按照約定,將參數壓入棧中。(x86將參數壓入棧幀,x86_64具有16個通用寄存器,前六個參數通常由寄存器保存,其余參數壓入棧中。)
  2. 主調函數將控制權轉給被調函數,返回地址(EIP)保存在棧中(在call指令中執行)。
  3. 被調函數設置棧幀基址,即用ESP給EBP賦值。
  4. 若有必要,保存被調函數希望保持的寄存器的數據。
  5. 被調函數修改棧頂指針,為局部變量預留空間。并向低地址方向開始存放局部變量和臨時變量。
  6. 被調函數執行任務,若被調函數返回值,一般存放在EAX中。
  7. 棧頂指針指向EBP,釋放局部變量空間。
  8. 恢復4中保存的主調函數寄存器中的數據。并恢復3中的棧幀基址。
  9. 被調函數控制權交還給主調函數(ret指令),也可能清除參數。
  10. 主調函數得到控制器,可能將棧上的參數清除。

函數調用常用命令

壓棧(push):棧頂指針減小4個字節,以字節為單位將數據壓入棧中。(不足補0)

出棧(pop):棧頂指針數據被取回,ESP增大4個字節

調用(call):將EIP(call的下一條指令地址)壓入棧幀,然后EIP指向被調函數代碼開始處。

離開(leave):恢復主調函數棧幀,等價于 mov ebp esp 、pop ebp

返回(ret):與call對應,從棧頂彈出返回地址給EIP。繼續執行程序。

C調用約定典型的函數序函數跋如下:

指令序列 含義
函數序(prologue) push %ebp 將主調函數棧基指針ebp壓棧,即保存舊棧幀基址以便函數返回時恢復舊棧幀。
mov %esp %ebp 將主調函數棧頂指針賦值給ebp,此時,ebp執行被調函數棧幀底部。
sub %esp 將棧頂指針下移,為局部變量開辟空間,n通常為16的倍數,以便于字節對齊進行編譯優化。
push 可選,如有必要,被調函數保存某些寄存器的值(ebx,edi,esi)
函數跋(epilogue) pop? 可選,如有必要,被調函數恢復某些寄存器的值(ebx,edi,esi)
mov %ebp %esp* 恢復主調函數棧頂指針esp,將其指向被調函數棧底。局部變量空間被釋放,但數據未清除。
pop %ebp 恢復主調函數棧幀基地址,此時,esp指向返回地址存放處。
ret 從棧中彈出返回地址到eip,繼續執行主調函數。再由主調函數恢復棧。
*:這兩條指令序列也可以由leave實現,具體方式由編譯器決定。

C語言函數調用的兩種壓棧方式:

壓棧方式一 壓棧方式二
push 4push 3push 2push 1call CdeclDemoadd $16, %ebp sub $16, %espmov $4, 12(%esp)mov $3, 8(%esp)mov $2, 4(%esp)mov $1, (%esp)call CdeclDemo

兩種壓棧方式區別:

方式一是傳統方式,一個參數一個參數的壓棧,然后調用,最后釋放棧

方式二是預先開辟空間,然后將參數復制到空間,最后沒有回收空間

函數調用約定

創建棧幀最重要的步驟是參數的傳遞。函數選擇特定調用約定,以特定方式進行參數傳遞。調用約定還規定在函數調用結束后,由主調函數還是被調函數對棧進行清理。

函數調用約定包括以下方面:

  • 函數參數傳遞順序和方式
  • 棧的維護方式
  • 名字修飾策略

常見調用約定

cdecl調用約定

別名 C調用約定,C/C++編譯器默認調用約定

所有非C++成員函數,和未使用stdcall、fastcall聲明的函數默認都是cdecl調用。

參數按照從右向左的順序入棧,主調函數負責清空棧返回值保存在EAX中。

cdecl調用支持可變參數函數,對于C函數,名字修飾是在函數名前加 _

對于C++,除非使用**extern"C"**修飾,否則有不同的名字修飾方法。

stdcall調用約定(微軟命名)

Pascal程序缺省調用方式,WinAPI也多采用該調用約定。

參數從右向左入棧,被調函數負責清空棧返回值保存在EAX

stdcall僅適用于參數個數固定的函數,因為被調函數無法知道棧上參數個數。

C函數中,stdcall的名字修飾是在名字前加_,在名字后加@和參數大小。

fastcall調用約定

stdcall的變形,通常使用ECX、EDX寄存器傳遞前兩個DWORD(四字節雙字)類型或更少的字節的函數參數,其余從右向左入棧。

被調函數負責清空棧中參數。返回值保存在EAX中。

函數名兩邊使用@修飾,并在后面用十進制表示參數列表大小(字節)

thiscall調用約定

C++類的非靜態成員函數必須接收一個主調對象的指針(this指針),并頻繁的使用該指針。編譯器默認使用thiscall調用約定提高調用效率。

參數按照從右向左的順序入棧。

若參數數目固定,this指針通過ECX傳遞,被調函數負責清理堆棧。

若參數數目不固定,this指針在所有參數入棧后再入棧主調函數清理堆棧。

thiscall不是C++關鍵字,不能用于修飾函數,只能由編譯器使用。

naked call調用約定

naked call調用,編譯器不產生保存和恢復寄存器的代碼。也不能使用return語句。

只能使用內嵌的匯編返回結果。用于某些特殊場合,如非C/C++上下文中的函數,程序員需自行編寫初始化和清棧的內嵌匯編指令。

pascal調用約定

Pascal語言調用約定,參數從右向左入棧。只支持固定數量參數。

被調函數清理堆棧,函數名稱無修飾且全部大寫。

上述約定的特點:

調用方式 stdcall(Win32) cdecl fastcall thiscall(C++) naked call
參數壓棧順序 從右至左 從右至左 自定義,Arg1在ecx,Arg2在edx 從右至左,this指針在ecx 自定義
參數位置 棧 + 寄存器 棧,寄存器ecx 自定義
負責清棧函數 被調函數 主調函數 被調函數 被調函數 自定義
支持可變參數 自定義
函數名字格式 _name@number _name @name@number 自定義
參數表開始特征 “@@YG” “@@YA” “@@YI” 自定義
注:C++因支撐函數重載、命名空間和成員函數等語法特征,采用更為復雜的名字修飾策略。C++函數修飾名以"?“開始,后面緊跟函數名、參數表開始標識和按照類型代號拼出的返回值參數表。例如,函數int Function(char *var1,unsigned long)對應的stdcall修飾名為”?Function@@YGHPADK@Z"。

Windows下可直接在函數聲明前添加關鍵字__stdcall、__cdecl或__fastcall等標識確定函數的調用方式,如int __stdcall func()。

Linux下可借用函數attribute 機制,如int attribute((stdcall)) func()。

被調函數CalleeFunc分別聲明為cdecl、stdcall和fastcall約定時,匯編代碼比較:

cdecl stdcall fastcall
主調函數職責 sub $0xc, %espmov $0x33, 0x8(%esp)mov $0x22, 0x4(%esp)mov $0x11,(%esp)call 8048354 sub $0xc, %espmov $0x33, 0x8(%esp)mov $0x22, 0x4(%esp)mov $0x11,(%esp)call 8048354 sub $0xc, %esp sub $0x4,%esp movl $0x33,(%esp) mov $0x22,%edx mov $0x11,%ecx call 8048354 sub $0x4,%esp
被調函數職責 push %ebpmov %ebp %espmov 0xc(%ebp), %eaxadd 0x8(%ebp), %eaxadd 0x10(%ebp), %eaxpop %ebpret push %ebpmov %ebp %espmov 0xc(%ebp), %eaxadd 0x8(%ebp), %eaxadd 0x10(%ebp), %eaxpop %ebpret $0xc 執行ret指令并清理參數占用的堆棧(棧頂指針上移參數個數*4=12個字節,以釋放壓棧的參數) push %ebp mov %esp,%ebp sub $0x8,%esp mov %ecx,0xfffffffc(%ebp) mov %edx,0xfffffff8(%ebp) mov 0xfffffff8(%ebp),%eax add 0xfffffffc(%ebp),%eax add 0x8(%ebp),%eax leave ret $0x4 //ret <壓棧參數字節數>。若參數不超過兩個,則ret指令不帶立即數,因為無參數被壓棧

調用約定影響

不同編譯器產生棧幀的方式不盡相同,主調函數不一定能完成清理堆棧的工作,而被調函數一定可以。

同時,為了保證不同平臺堆棧正常,一般使用stdcall調用。(通常用于A語言調用B語言函數

此外,主調函數和被調函數采用相同調用約定,但分別使用C和C++時,會出現鏈接錯誤。

這是因為:兩種語言函數名稱修飾符不一樣。解決方法是使用**extern “C”**修飾被調函數。

同時應該考慮,被調函數也有可能是C++編譯的。通常這樣聲明頭文件:

#ifdef _cplusplus
     extern "C" {
#endif
     type Func(type para);
#ifdef _cplusplus
     }
#endif

x86函數傳遞參數方法

x86處理器的ABI規范中規定,所有參數從右向左壓入棧中

整型和指針參數傳遞

整型參數指針參數傳遞方式相同,在32位的x86處理器上整型與指針大小相同(四個字節)。

下表給出這兩種類型在棧幀中位置關系:

調用語句 參數 棧幀地址
tail(1, 2, 3, (void *)0); 1 8(%ebp)
2 12(%ebp)
3 16(%ebp)
(void *)0 20(%ebp)

浮點參數傳遞

浮點參數的傳遞與整型類似,區別在于參數大小。

x86處理器浮點類型占8個字節,因此在棧中也需要占8個字節。

下表給出浮點參數在棧中位置關系:

調用語句 參數 棧幀地址
tail(1.414, 2, 3.998e10); word 0: 1.414 8(%ebp)
word 1: 1.414 12(%ebp)
2 16(%ebp)
word 0: 3.998e10 20(%ebp)
word 1: 3.998e10 24(%ebp)

結構體和聯合體參數傳遞

結構體和聯合體的傳遞與整型、浮點型類似,只是占用大小不同。

x86處理器棧寬是4字節,故結構體在棧上大小是4的倍數

編譯器會對結構體進行適當的填充使得結構體4字節對齊

對于其它處理器,參數傳遞并不全部通過棧進行。結構體可能通過指針傳遞

x86函數返回值傳遞方法

函數返回值可通過寄存器傳遞:

  1. 若返回值不超過4字節(int、指針),通常保存在EAX中。
  2. 若返回值大于4字節但不超過8字節(long long),通常保存在EAX+EDX,EDX保存高4字節,EAX保存低4字節。
  3. 若返回值為浮點類型(float double),則通過專用的協處理器浮點數寄存器棧的棧頂返回。
  4. 若返回值為結構體或聯合體,主調函數額外傳遞一個參數,該參數是一個保存返回值的空間地址。

注意:函數如何保存結構體或聯合體返回值取決于具體實現。

總結

原文鏈接:https://blog.csdn.net/AtomTeam/article/details/117284278

  • 上一篇:沒有了
  • 下一篇:沒有了
欄目分類
最近更新