真解函數調用約定

前言:本文旨在讓無彙編基礎的人也能夠理解調用約定,而理解函數調用約定最重要的就是理解函數調用過程中系統棧上發生了什麼,本文便是着眼於此。

函數調用約定是什麼

函數調用約定,是指當一個函數被調用時,函數的參數會被傳遞給被調用的函數和返回值會被返回給調用函數。函數的調用約定就是描述參數是怎麼傳遞和由誰平衡堆棧的,當然還有返回值。 —《百度百科》

函數調用約定,從字面上來看,這是一個函數被調用時應該遵循的協議,應該被遵循的規範,那麼誰來遵循呢?編譯器。所謂函數調用約定指的就是當一個函數被調用時的一系列活動,包括參數入棧順序,控制權的轉交等。其中最最重要的就是棧的變化,系統棧中記錄的正是函數的信息。我們甚至可以狹義的理解爲函數調用約定即調用一個函數時,系統棧是如何變化的。


補充知識(有基礎可跳過)

棧是一種線性表的一種。僅能在棧的一端進行插入(入棧push)和刪除(出棧pop)操作,這端被稱爲棧頂;與之相對的另一端,則被稱爲棧底,且不容許進行任何操作。

想象一下,線性表類比數組,元素一個挨一個,而一段操作受限制,另一端不能進行任何操作。你能想到什麼?看到這不妨思考下,主動思考更加受益。

沒錯,我們可以把棧空間畫出如下這樣:開口的一端爲棧頂,封閉一段爲棧底。那麼問題來了,棧底很顯然我們能夠輕易看出,但棧頂呢?我們如何界定棧頂?這個問題前人替我們解決了,他們用top指針標記棧頂的位置,並且top指針會隨着push和pop操作而動態更新。
在這裏插入圖片描述
在這裏插入圖片描述
顯然這2張圖形象的表達了什麼是棧,以及當棧進行push操作時,top是如何更新的,而pop則可類比。

push:
1.調整棧頂:top向上移一個單位,預留一個單位的空間,爲元素入棧準備,同時更新棧頂
2.PUSH:top指針做好預備工作,元素入棧,填充預留空間,入棧完成。

pop:
1.POP:將棧頂指向的元素彈出棧中。
2.調整棧頂:top下移,更新棧頂。

總結:由上面從我們如何確定棧的形狀,到棧空間的動態變化中,可以得到以下結論。

1.由於棧只能在棧頂(top處)進行操作,從而導致先進後出的現象
2.當棧空時棧頂與棧底位於同一處。
3.push和pop操作如何動態更新棧(上面已經給出)。
4.棧底以及棧頂確定一段有效的棧空間。

彙編與系統棧的關係

彙編語言中存在與棧相關的指令以及寄存器。下面以x86彙編例子說明。

指令:

push a 將a放入棧中
pop b 將當前棧頂指向的元素出棧,並放入b中
call 等同2個指令:push EIP --> jmp [目標函數地址]
retn 等同pop EIP

call和retn都對EIP進行操作,這是與程序執行控制權的轉交有關。

寄存器:

EBP:棧底指針
ESP:棧頂指針,相當於top。
EIP:用於標記下一條(是下一條,不是當前)應該被執行彙編指令或者說機器指令。EIP的指向位置,用來告訴CPU下一步應該怎麼辦,控制着程序執行流程,十分重要!通常情況下EIP是順序移動,而上面以及下面所說控制權轉交,就是主動更改EIP中的地址,類似於高級語言中分支和跳轉。

其他相關寄存器:ESI, EDI,EBX

系統底層中用,EBP和ESP可以確定一段棧空間。
前面我們說過,系統棧就是用來記錄函數調用時函數的一系列變化,而每個函數都有一段獨屬於自己的棧空間,這段棧空間是系統棧的子集,我們稱爲棧幀。其實這種思想還更好的體現在其他方面,例如每一個進程被創建的時候都會有獨屬於該進程的虛擬地址空間,這就如同函數調用時與棧幀的確立。
從宏觀上看,系統棧可以認爲以棧幀爲基本單位。微觀上,我需要了解棧幀有什麼。
雖然每個棧幀的內容不盡相同,但幸運的是棧幀的確立有軌跡可循,每個棧幀都有以下內容:

1.保存母函數(也稱爲調用者caller)中重要信息,用於子函數(被調用者callee)調用結束時控制權轉交給母函數和母函數現場恢復。
2.子函數信息。

現在不理解沒關係,下面會纖細的說。


函數調用約定的詳細活動

我將以C語言默認的Cdecl調用約定作爲案例來分析,理解cdecl之後,其他調用約定殊途同歸.

高級語言層面:
我用一個demo從彙編的角度來看,歸納函數調用約定發生了什麼。這裏我以main爲上述母函數,func作爲子函數來講解。最後我們會將這個例子中蘊含的規律推而廣之。
在這裏插入圖片描述
彙編層面:

下面的內容請務必結合我所繪製的系統棧結合觀看。注意自己動態腦補操作。
在這裏插入圖片描述

在這裏插入圖片描述
我們先看哪裏進行了影響系統棧的操作。我們先不去理會第一個紅框,現在關注於第二個紅框,也就是母函數main調用子函數func時棧空間如何變化,棧幀如何確立。

關注call func這條指令。我們發現在這之前進行了一系列push操作,而push的內容正是母函數main傳入給func的三個參數。顯然,不難看出,他們的入棧順序是從右先左。這是我們看到,那麼我們能否就cdecl調用約定推而廣之呢?事實證明這正確的不能再正確了。

函數參數入棧順序是從右先左。

調用call指令,控制權轉交給子函數。這裏暗含push EIP。

下面我們看看母函數main調用子函數func棧空間如何變化:
在這裏插入圖片描述
我們只需要關注紅框即可,我會逐一解釋。
1.創立新棧幀,開闢局部變量空間,保存母函數部分信息。

如何理解保存母函數信息?首先無論是ESP還是EBP這樣有名字的寄存器都只有一個!我知道大家如果沒學過彙編可以疑惑,寄存器入棧是什麼鬼?我當年也是,其實這裏不是寄存器入棧,入棧的是寄存器中的數據!爲什麼要這樣做?沒看過彙編程序會十分苦惱,有必要嗎?確實,有些確實沒必要,但有些必須要保持,比如母函數棧底信息,如果我們不操作,而是直接將EBP中的數據更新爲func的棧底,那func調用結束我們怎麼找回母函數的棧底,沒有棧底母函數在棧幀中的數據就丟失了!大家都寫過交換兩變量數據的代碼,通常我們會用一個臨時變量tmp保持其中一個變量的數據就是這個道理,是爲了防止丟失數據!簡而言之,因爲執行對應功能的寄存器只有一個,因此我們必須得要保存上一個函數使用EBP等寄存器的數據,以便恢復。

push ebp 母函數棧底信息入棧
mov ebp,esp 更新棧底指針,此時相當於我們上面說的空棧,esp = ebp。
sub esp, 0c0h 開闢局部變量空間
push 三個寄存器 保存母函數信息,與保存ebp同理。

2 and 3:函數調用結束階段,恢復母函數數據,銷燬func函數棧幀(esp以下的部分纔是有效棧空間,只要esp下降了那麼esp上面的部分就意味着失效,因此並不是抹除數據而是使其丟出棧空間有效作用域之外,這些非有效空間的數據會隨着棧空間的活動而被覆蓋)。至於2 和3紅框之間的部分,也涉及了esp和ebp,這是一種保護機制,用來Check棧空間是否異常不用理會。

調用ret 這裏等同於 pop eip將棧中保持的在母函數執行時的EIP數據返回給EIP寄存器,控制權轉交給母函數。(什麼控制權轉交,pop push都是在棧頂段操作我都說了,不要看到這又懵逼了,沒懂返回上面重新看,看補充知識。)

那麼是時候解答最後一個疑問了。想必認真閱讀本文的你已經發現,前我說棧幀是由每個函數的ebp和esp之間區域組成,那麼我繪製的系統棧圖中main esp不應該指向func棧底的下面一個單元嗎?爲什麼把參數歸於func棧幀呢?的確如此,按前面所說應該這樣,那麼現在我來解釋爲什麼。(以下爲個人理解和觀察)

我們先看func的棧幀,func是最新的一個棧幀,在這func之上沒有任何棧幀了,我們發現func棧頂的界限和main棧頂的界限完美符合。基於這個觀察得出第一個點。
我們再來看看棧幀底部。func棧幀底部多了些參數,而mian棧幀底部沒有,我想這纔是大家困惑所在。main之所以沒有參數,是因爲我寫的這個main沒有參數!而fun有3個參數!也就是說是子函數是否有參數決定的,那麼顯然把參數劃分到子函數棧幀十分合情合理,儘管子函數參數在子函數棧幀的棧底之下。

在和最後,main函數中緊接着call之後執行了一條add esp,0ch。這條指令的目的是爲了棧平衡,用來銷燬func參數。由於是main對func函數的參數進行銷戶,我們稱之爲調用者進行棧平衡,與之相對的被調用者進行棧平衡就是在子函數執行類似語意的指令。

那麼我已經詳細的說明了demo中func函數的棧幀的創建乃至銷燬的過程,並適當的解釋其中一些需要講解的操作,避開不必要部分的說明。這個過程可以推廣至其他函數,這就是調用約定主要內容,其他不同調用約定只是有席位區別。

我所畫圖中,什麼main esp,main ebp只是爲了便於各位理解才這樣寫的,有些會寫old ebp,意之上一個函數的棧底,也就是母函數棧底信息。

函數調用約定的種類

x86平臺

cdecl

C語言默認調用約定.
參數入棧順序:從右向左
棧平衡:調用者(母函數)

stdcall

win32 API調用約定
參數入棧順序:從右向左
棧平衡:被調用者(子函數)

fastcall

調用效率高效
參數入棧順序:從右向左,其中Windows平臺前2個參數進入寄存器,Linux平臺前4個參數進入寄存器,並且會保留一片shadow space 空間
棧平衡:被調用者(子函數)

x64平臺

fastcall

x64平臺統一使用這個調用約定。
參數入棧順序:從右向左,其中Windows平臺前4個參數進入寄存器,Linux平臺前6個參數進入寄存器,並且會保留一片shadow space 空間
棧平衡:調用者(母函數)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章