在进行函数逆向分析时,分析其函数调用约定具有非常重要的作用,因为调用约定直接影响了函数的参数传递、返回值、栈管理、寄存器使用等多个方面,不同的编译器和平台可能有不同的默认调用约定,识别调用约定可以帮助判断代码是由哪种编译器生成的。例如:
①Windows API 函数通常使用 stdcall 调用约定。 ②在 x64 平台,Windows 使用的调用约定与 Linux 的 System V 调用约定有所不同。 ③通过调用约定,可以更好地识别代码的上下文,甚至推断出目标程序使用的编译器和编译选项。
调用约定描述了函数调用和参数传递的方式,掌握这些约定是正确分析函数的基础,常见的调用约定如下:
cdecl:参数通过栈从右向左传递,调用者负责清理栈。 stdcall:参数通过栈从右向左传递,函数本身负责清理栈。 fastcall:部分参数通过寄存器传递,通常第一个和第二个参数通过 ecx 和 edx 寄存器传递(在windows x86 平台上),剩余参数从右到左压入栈中。 (在 Windows x64 平台上,前四个参数使用 rcx, rdx, r8, r9 寄存器传递。)函数可能负责栈的清理,具体情况与实现相关。
下面是一个 C 代码的示例,通过使用 cdecl
、stdcall
和 fastcall
三种调用约定来演示它们的区别。在代码中,我们使用 Microsoft 编译器提供的关键字来指定不同的调用约定:
#include <stdio.h>
// 使用 cdecl 调用约定
int __cdecl add_cdecl(int a, int b) {
return a + b;
}
// 使用 stdcall 调用约定
int __stdcall add_stdcall(int a, int b) {
return a + b;
}
// 使用 fastcall 调用约定
int __fastcall add_fastcall(int a, int b) {
return a + b;
}
int main() {
int x = 3;
int y = 4;
// 调用 cdecl 函数
int result_cdecl = add_cdecl(x, y);
printf("cdecl result: %d\n", result_cdecl);
// 调用 stdcall 函数
int result_stdcall = add_stdcall(x, y);
printf("stdcall result: %d\n", result_stdcall);
// 调用 fastcall 函数
int result_fastcall = add_fastcall(x, y);
printf("fastcall result: %d\n", result_fastcall);
return 0;
}
接下去我们将代码进行编译,生成exe
文件后放入x32dbg
中进行动态调试,去具体分析这三种调用约定的区别。
在文件载入完毕后,我们需要先进行main
函数的定位(定位main
函数的方法请看笔者前面C/C++逆向:定位main函数
文章);此时我们需要重点关注的是红线以下代码。
mov dword ptr ss:[ebp-8],3 mov dword ptr ss:[ebp-14],4
代码中首先初始化了两个四字节(dword ptr
)的局部变量ebp-8
和ebp-14
;这两个局部变量就对应上述C代码中的两个int
型变量x(ebp-8)
、y(ebp-14)
。
①cdecl调用约定
相关代码如下:
mov eax,dword ptr ss:[ebp-14]
push eax
mov ecx,dword ptr ss:[ebp-8]
push ecx
call function_x86.281050
add esp,8
这串代码首先将局部变量ebp-14(y)
的值加载到eax
中,接着将 eax
寄存器中的值压入栈中,这是即将调用函数的第一个参数。然后将另一个局部变量ebp-8(x)
的值加载到 ecx
寄存器中;将 ecx
寄存器中的值压入栈中,作为调用函数的第二个参数,接着通过call
指令调用名为 function_x86.281050
的函数。通过对比上述的函数调用C代码;
int result_cdecl = add_cdecl(x, y);
cdecl调用约定在进行参数传递时,调用函数所使用的参数是从右到左推入栈中,正如这个例子所展现的,我们调用函数add_cdecl(x, y)
时,反汇编代码中先压入栈中的参数是ebp-14(y)
,接着才是x
。并且在调用函数完毕后需要由调用者来恢复栈指针;add esp,8
调用函数后,恢复栈指针。因为调用之前压入了两个参数,每个 4
字节,共计 8
字节,所以通过 add esp, 8
来调整栈指针,清理掉这些参数。
②stdcall调用约定
相关代码:
mov eax,dword ptr ss:[ebp-14]
push eax
mov ecx,dword ptr ss:[ebp-8]
push ecx
call function_x86.281235
可以看到stdcall
与cdecl
调用约定一样,都是从右往左依次将参数压入栈中,但是与cdecl
不一样的是,stdcall
的平栈操作是在函数内完成的。这个时候我们进入函数function_x86.281235
进行查看:
我们可以看到在函数结尾有一个ret 8
指令;该指令在 x86
汇编中表示返回到调用函数的地址,并在返回之前从栈中移除 8 个字节,这是一种同时进行函数返回和栈清理的指令。
③fastcall调用约定
相关代码:
mov edx,dword ptr ss:[ebp-14]
mov ecx,dword ptr ss:[ebp-8]
call function_x86.2810F5
在 fastcall
调用约定中,部分参数通过寄存器传递,而不是全部通过栈来传递。在该程序的函数调用中,第一个ebp-8(x)
和第二个参数ebp-14(y)
分别通过 ecx
和 edx
寄存器传递,接着使用call
指令调用函数即可。若有更多参数,超出寄存器能够处理的范围,这些额外的参数会被压入栈中;对于通过栈传递的额外参数,栈的清理方式可以与 stdcall
类似,即由被调用者来负责清理栈上的参数。