1. 程式人生 > >論函式呼叫約定

論函式呼叫約定

在C語言中,假設我們有這樣的一個函式:

int function(int a,int b)

呼叫時只要用result = function(1,2)這樣的方式就可以使用這個函式。但是,當高階
語言被編譯成計算機可以識別的機器碼時,有一個問題就凸現出來:在CPU中,計算機沒
有辦法知道一個函式呼叫需要多少個、什麼樣的引數,也沒有硬體可以儲存這些引數。
也就是說,計算機不知道怎麼給這個函式傳遞引數,傳遞引數的工作必須由函式呼叫者
和函式本身來協調。為此,計算機提供了一種被稱為棧的資料結構來支援引數傳遞。

棧是一種先進後出的資料結構,棧有一個儲存區、一個棧頂指標。棧頂指標指向堆疊中
第一個可用的資料項(被稱為棧頂)。使用者可以在棧頂上方向棧中加入資料,這個操作
被稱為壓棧(Push),壓棧以後,棧頂自動變成新加入資料項的位置,棧頂指標也隨之修
改。使用者也可以從堆疊中取走棧頂,稱為彈出棧(pop),彈出棧後,棧頂下的一個元素變
成棧頂,棧頂指標隨之修改。

函式呼叫時,呼叫者依次把引數壓棧,然後呼叫函式,函式被呼叫以後,在堆疊中取得
資料,並進行計算。函式計算結束以後,或者呼叫者、或者函式本身修改堆疊,使堆疊
恢復原裝。

在引數傳遞中,有兩個很重要的問題必須得到明確說明:

當引數個數多於一個時,按照什麼順序把引數壓入堆疊
函式呼叫後,由誰來把堆疊恢復原裝
在高階語言中,通過函式呼叫約定來說明這兩個問題。常見的呼叫約定有:

stdcall
cdecl
fastcall
thiscall
naked call
stdcall呼叫約定
stdcall很多時候被稱為pascal呼叫約定,因為pascal是早期很常見的一種教學用計算機
程式設計語言,其語法嚴謹,使用的函式呼叫約定就是stdcall。在Microsoft C++系列
的C/C++編譯器中,常常用PASCAL巨集來宣告這個呼叫約定,類似的巨集還有WINAPI和CALLB
ACK。

stdcall呼叫約定宣告的語法為(以前文的那個函式為例):

int __stdcall function(int a,int b)

stdcall的呼叫約定意味著:1)引數從右向左壓入堆疊,2)函式自身修改堆疊 3)函式
名自動加前導的下劃線,後面緊跟一個@符號,其後緊跟著引數的尺寸

以上述這個函式為例,引數b首先被壓棧,然後是引數a,函式呼叫function(1,2)呼叫處
翻譯成組合語言將變成:


push 2 第二個引數入棧
push 1 第一個引數入棧
call function 呼叫引數,注意此時自動把cs:eip入棧


而對於函式自身,則可以翻譯為:


push ebp 儲存ebp暫存器,該暫存器將用來儲存堆疊的棧頂指標,可以在函式退出
時恢復
mov ebp,esp 儲存堆疊指標
mov eax,[ebp + 8H] 堆疊中ebp指向位置之前依次儲存有ebp,cs:eip,a,b,ebp +8指向
a
add eax,[ebp + 0CH] 堆疊中ebp + 12處儲存了b
mov esp,ebp 恢復esp
pop ebp
ret 8

而在編譯時,這個函式的名字被翻譯成[email protected]

注意不同編譯器會插入自己的彙編程式碼以提供編譯的通用性,但是大體程式碼如此。其中
在函式開始處保留esp到ebp中,在函式結束恢復是編譯器常用的方法。

從函式呼叫看,2和1依次被push進堆疊,而在函式中又通過相對於ebp(即剛進函式時的
堆疊指標)的偏移量存取引數。函式結束後,ret 8表示清理8個位元組的堆疊,函式自己
恢復了堆疊。

cdecl呼叫約定
cdecl呼叫約定又稱為C呼叫約定,是C語言預設的呼叫約定,它的定義語法是:


int function (int a ,int b) //不加修飾就是C呼叫約定
int __cdecl function(int a,int b)//明確指出C呼叫約定


在寫本文時,出乎我的意料,發現cdecl呼叫約定的引數壓棧順序是和stdcall是一樣的
,引數首先由有向左壓入堆疊。所不同的是,函式本身不清理堆疊,呼叫者負責清理堆
棧。由於這種變化,C呼叫約定允許函式的引數的個數是不固定的,這也是C語言的一大
特色。對於前面的function函式,使用cdecl後的彙編碼變成:


呼叫處
push 1
push 2
call function
add esp,8 注意:這裡呼叫者在恢復堆疊
被呼叫函式_function處
push ebp 儲存ebp暫存器,該暫存器將用來儲存堆疊的棧頂指標,可以在函式退出
時恢復
mov ebp,esp 儲存堆疊指標
mov eax,[ebp + 8H] 堆疊中ebp指向位置之前依次儲存有ebp,cs:eip,a,b,ebp +8指向
a
add eax,[ebp + 0CH] 堆疊中ebp + 12處儲存了b
mov esp,ebp 恢復esp
pop ebp
ret 注意,這裡沒有修改堆疊


MSDN中說,該修飾自動在函式名前加前導的下劃線,因此函式名在符號表中被記錄為_f
unction,但是我在編譯時似乎沒有看到這種變化。

由於引數按照從右向左順序壓棧,因此最開始的引數在最接近棧頂的位置,因此當採用
不定個數引數時,第一個引數在棧中的位置肯定能知道,只要不定的引數個數能夠根據
第一個後者後續的明確的引數確定下來,就可以使用不定引數,例如對於CRT中的sprin
tf函式,定義為:

int sprintf(char* buffer,const char* format,...)

由於所有的不定引數都可以通過format確定,因此使用不定個數的引數是沒有問題的。

fastcall
fastcall呼叫約定和stdcall類似,它意味著:

函式的第一個和第二個DWORD引數(或者尺寸更小的)通過ecx和edx傳遞,其他引數通過
從右向左的順序壓棧
被呼叫函式清理堆疊
函式名修改規則同stdcall
其宣告語法為:int fastcall function(int a,int b)

thiscall
thiscall是唯一一個不能明確指明的函式修飾,因為thiscall不是關鍵字。它是C++類成
員函式預設的呼叫約定。由於成員函式呼叫還有一個this指標,因此必須特殊處理,th
iscall意味著:

引數從右向左入棧
如果引數個數確定,this指標通過ecx傳遞給被呼叫者;如果引數個數不確定,this指標
在所有引數壓棧後被壓入堆疊。
對引數個數不定的,呼叫者清理堆疊,否則函式自己清理堆疊
為了說明這個呼叫約定,定義如下類和使用程式碼:

class A
{
public:
int function1(int a,int b);
int function2(int a,...);
};
int A::function1 (int a,int b)
{
return a+b;
}
#include
int A::function2(int a,...)
{
va_list ap;
va_start(ap,a);
int i;
int result = 0;
for(i = 0 i < a i ++)
{
result += va_arg(ap,int);
}
return result;
}
void callee()
{
A a;
a.function1 (1,2);
a.function2(3,1,2,3);
}

callee函式被翻譯成彙編後就變成:


//函式function1呼叫
0401C1D push 2
00401C1F push 1
00401C21 lea ecx,[ebp-8]
00401C24 call function1 注意,這裡this沒有被入棧
//函式function2呼叫
00401C29 push 3
00401C2B push 2
00401C2D push 1
00401C2F push 3
00401C31 lea eax,[ebp-8] 這裡引入this指標
00401C34 push eax
00401C35 call function2
00401C3A add esp,14h

可見,對於引數個數固定情況下,它類似於stdcall,不定時則類似cdecl

naked call
這是一個很少見的呼叫約定,一般程式設計者建議不要使用。編譯器不會給這種函式增
加初始化和清理程式碼,更特殊的是,你不能用return返回返回值,只能用插入彙編返回
結果。這一般用於真實模式驅動程式設計,假設定義一個求和的加法程式,可以定義為:


__declspec(naked) int add(int a,int b)
{
__asm mov eax,a
__asm add eax,b
__asm ret
}

注意,這個函式沒有顯式的return返回值,返回通過修改eax暫存器實現,而且連退出函
數的ret指令都必須顯式插入。上面程式碼被翻譯成彙編以後變成:


mov eax,[ebp+8]
add eax,[ebp+12]
ret 8


注意這個修飾是和__stdcall及cdecl結合使用的,前面是它和cdecl結合使用的程式碼,對
於和stdcall結合的程式碼,則變成:

__declspec(naked) int __stdcall function(int a,int b)
{
__asm mov eax,a
__asm add eax,b
__asm ret 8 //注意後面的8
}

至於這種函式被呼叫,則和普通的cdecl及stdcall呼叫函式一致。

函式呼叫約定導致的常見問題
如果定義的約定和使用的約定不一致,則將導致堆疊被破壞,導致嚴重問題,下面是兩
種常見的問題:

函式原型宣告和函式體定義不一致
DLL匯入函式時聲明瞭不同的函式約定
以後者為例,假設我們在dll種聲明瞭一種函式為:

__declspec(dllexport) int func(int a,int b);//注意,這裡沒有stdcall,使用的是
cdecl
使用時程式碼為:

typedef int (*WINAPI DLLFUNC)func(int a,int b);
hLib = LoadLibrary(...);
DLLFUNC func = (DLLFUNC)GetProcAddress(...)//這裡修改了呼叫約定
result = func(1,2);//導致錯誤

由於呼叫者沒有理解WINAPI的含義錯誤的增加了這個修飾,上述程式碼必然導致堆疊被破
壞,MFC在編譯時插入的checkesp函式將告訴你,堆疊被破壞了。 

相關推薦

函式呼叫約定

在C語言中,假設我們有這樣的一個函式: int function(int a,int b) 呼叫時只要用result = function(1,2)這樣的方式就可以使用這個函式。但是,當高階語言被編譯成計算機可以識別的機器碼時,有一個問題就凸現出來:在CPU中,計算機沒有辦法

2018/10/03-函式呼叫約定、cdecl、stdcall、fastcall- 《惡意程式碼分析實戰》

  cdecl是最常用的約定之一,引數是從右到左按序被壓入棧,當函式完成時由呼叫者清理棧,並且將返回值儲存在EAX中。   stdcall約定是被呼叫函式負責清理棧,其他和cdecl非常類似。   fastcall呼叫約定跨編譯器時變化最多,但是它總體上在所有情況下的工作方式都是相似的。在fastcall

x64函式呼叫約定——MSVC & GCC

傳參 MSVC 前4個引數使用rcx、rdx、r8、r9,剩下的引數用棧 GCC 前6個引數使用rdi、rsi、rdx、rcx、r8、r9,剩下的引數用棧。注意rdx、rcx的順序和MSVC上不一樣 caller saved registers 這類暫存器可由子函式自由使用,

從彙編角度檢視C語言函式呼叫約定【非常有用】

轉自:https://blog.csdn.net/Holmofy/article/details/76094986   為了防止出現不必要的程式碼影響組合語言的檢視,所以程式中不使用任何庫函式,以保持彙編程式碼的簡潔。 這裡所使用的彙編是VC的MASM。 預設函式呼叫方式_

cdecl、stdcall、fastcall、thiscall函式呼叫約定區別 (轉)

 在C語言中,假設我們有這樣的一個函式:    int function(int a,int b)    呼叫時只要用result = function(1,2)這樣的方式就可以使用這個函式。但是,當高階語言被編譯成計算機可以識別的機器碼時,有一個問題就凸現出來:在CPU中,計算機沒有辦法知道一個函式呼叫需要

cdecl、stdcall、fastcall函式呼叫約定區別

論函式呼叫約定   在C語言中,假設我們有這樣的一個函式:      int function(int a,int b)      呼叫時只要用result = function(1,2)這樣的方式就可以使用這個函式。但是,當高階語言被編譯成計算機可以識別的機器碼時,有一個問題就凸現出來:在CPU中

【軟體開發底層知識修煉】二十五 ABI之函式呼叫約定二之函式返回值為結構體時的約定

上一篇文章學習了幾種函式呼叫約定的區別,點選連結檢視上一篇文章:【軟體開發底層知識修煉】二十四 ABI之函式呼叫約定 本篇文章繼續學習函式呼叫約定中,關於函式返回值的問題。當函式返回值為結構體時,函式返回值是如何來傳給呼叫者的。

【軟體開發底層知識修煉】二十四 ABI之函式呼叫約定

上一篇文章學習了Linux環境下的函式棧幀的形成與摧毀。點選連結檢視相關文章:軟體開發底層知識修煉】二十三 ABI-應用程式二進位制介面三之深入理解函式棧幀的形成與摧毀 本篇文章繼續學習ABI介面相關的內容。函式呼叫約定

名字修飾約定函式呼叫約定

所謂名字修飾約定,就是指變數名、函式名等經過編譯後重新輸出名稱的規則。   比如原始碼中函式名稱為int Func(int a,int b),經過編譯後名稱可能為[email protected]@[email protected]、[email&#

函式呼叫約定函式名稱修飾規則(一)

    作者:星軌(oRbIt)    E_Mail:[email protected]    轉載請註明原作者,否則請勿轉載       使用C/C++語言開發軟體的程式設計師經常碰到這樣的問題:有時候是程式編譯沒有問題,但是連結的時候總是報告函式不存在(經典的L

常見函式呼叫約定(x86、x64、arm、arm64)

我學習逆向,整理的一些常見的函式呼叫約定反彙編筆記。由於我是新手,肯定有一些疏漏不完善的,我遇到了會實時更新的。 X86 函式呼叫約定 X86 有三種常用呼叫約定,cdecl(C規範)/stdcall(WinAPI預設)/fastcall

函式呼叫約定函式名修飾規則

函式呼叫約定:是指當一個函式被呼叫時,函式的引數會被傳遞給被呼叫的函式和返回值會被返回給呼叫函式。函式的呼叫約定就是描述引數是怎麼傳遞和由誰平衡堆疊的,當然還有返回值。 幾種型別:__stdcall,__cdecl,__fastcall,__thiscall,__n

ARM函式呼叫約定

1、函式呼叫約定主要涉及引數如何傳遞、返回值如何傳遞、返回地址如何儲存以及不要破壞呼叫函式的上下文。那麼在ARM中,這些約定規則是什麼樣呢? 2、測試程式如下: static int fun_a(uint32_t a, uint32_t b, uint32_t c) {

Windows x64彙編函式呼叫約定

最近在寫一些字串函式的優化,用到x64彙編,我也是第一次接觸,故跟大家分享一下。 x86:又名 x32 ,表示 Intel x86 架構,即 Intel 的32位 80386 彙編指令集。 x64:表示 AMD64 和 Intel 的 EM64T ,而不包括 IA6

push、pop及函式呼叫約定

push: 把一個32位的運算元壓入堆疊中。這個操作導致esp被減4。esp被形象地稱為棧頂。我們認為頂部是地址小的區域,那麼,壓入堆疊中資料越多,這個堆疊也就越堆越高,esp也就越來越小。在32位平臺,esp每次減少4位元組。 pop: 相反,esp被加4,一個數據

函式呼叫約定

函式的呼叫約定 1、_cdecl  C標準呼叫約定 2、_stdcall  windows標準呼叫約定 3、_fastcall  快讀呼叫約定 4、_thiscall  成員方法呼叫約定(本次不做詳解)   那麼函式的呼叫約定

c理解提高(3)程式的記憶體四區模型和函式呼叫模型

程式的記憶體四區模型 記憶體四區的建立流程 流程說明 1、作業系統把物理硬碟程式碼load到記憶體 2、作業系統把c程式碼分成四個區 3、作業系統找到main函式入口執行   各區元素分析 函式呼叫模型 基本原理

JNI函式呼叫流程,基本資料型別

文章目錄 JNI 開發 例子01 靜態方法 jni基本使用 例子02 非靜態方法. 例子03 訪問非靜態域 例子04 訪問靜態域 JNI java native interface 什麼時候使

iOS 函式呼叫的流程

OC是一門動態語言,一個函式是由一個selector(SEL),和一個implement(IML)組成的。selector相當於地址,而implement才是真正的房間。和我們網購一樣,地址可以隨意寫。但不一定都能找到收件人。如果找不到系統會給程式幾次機會來使程式正常執行,之後依然不行才會丟擲異常。

函式呼叫與棧幀詳解

函式呼叫與棧幀詳解 2017年04月19日 22:07:34 T_tangc 閱讀數:826 版權宣告:本文為博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/qq_25424545/article/details/70232141 &