前言
在编程中,理解函数调用约定和栈的机制对于编写高效代码、调试程序以及进行逆向工程至关重要。本文将深入探讨 C 和 C++ 的调用约定,以及栈与平栈的相关知识。
C 调用约定
在 C 语言中,默认的调用约定是 cdecl
。cdecl
调用约定的特点如下:
- 参数传递:参数从右向左依次压入栈中。
- 栈清理:调用者负责清理栈(即调用者在函数返回后负责平栈)。
- 返回值:返回值通常存放在
EAX
寄存器中。
示例:
int add(int a, int b) {
return a + b;
}
int main() {
int result = add(3, 4); // 调用add函数
return 0;
}
在汇编层面,调用 add(3, 4)
的代码可能如下:
push 4 ; 第二个参数压栈
push 3 ; 第一个参数压栈
call add ; 调用add函数
add esp, 8 ; 调用者平栈,清理8字节的栈空间
C++ 调用约定
C++ 调用约定与 C 调用约定有所不同,主要体现在以下几点:
名称修饰(Name Mangling)
- C++ 编译器会对函数名进行修饰(Name Mangling),以支持函数重载、命名空间等特性。例如,函数
int add(int a, int b)
可能会被修饰为_Z3addii
。 - C 语言没有名称修饰,函数名在编译后保持不变。
thiscall
调用约定
在 C++ 中,非静态成员函数的调用约定通常是 thiscall
。thiscall
调用约定的特点:
this
指针:this
指针通常通过ECX
寄存器传递。- 参数传递:其他参数从右向左压入栈中。
- 栈清理:被调用函数负责清理栈。
示例:
class MyClass {
public:
int add(int a, int b) {
return a + b;
}
};
int main() {
MyClass obj;
int result = obj.add(3, 4); // 调用成员函数add
return 0;
}
在汇编层面,调用 obj.add(3, 4)
的代码可能如下:
lea ecx, [obj] ; 将this指针(即obj的地址)放入ECX寄存器
push 4 ; 第二个参数压栈
push 3 ; 第一个参数压栈
call ?add@MyClass@@QAEHHH@Z ; 调用成员函数add
栈与平栈
栈的基本概念
- 栈(Stack):栈是一种后进先出(LIFO)的数据结构,用于存储函数调用时的局部变量、参数、返回地址等信息。
- 栈帧(Stack Frame):每个函数调用都会在栈上创建一个栈帧,用于存储该函数的局部变量、参数等信息。
- 栈指针(ESP):
ESP
寄存器指向当前栈顶的位置。
平栈(Stack Cleanup)
平栈是指在函数调用结束后,清理栈上的参数,使栈恢复到函数调用前的状态。不同的调用约定决定了由谁负责平栈:
-
cdecl
调用约定:- 调用者负责平栈:调用者在函数返回后使用
add esp, n
指令清理栈。 - 示例:
push 4 push 3 call add add esp, 8 ; 调用者平栈,清理8字节的栈空间
- 调用者负责平栈:调用者在函数返回后使用
-
stdcall
调用约定:- 被调用函数负责平栈:被调用函数在返回前使用
ret n
指令自动清理栈。 - 示例:
push 4 push 3 call add ; 被调用函数内部: ret 8 ; 被调用函数平栈,清理8字节的栈空间
- 被调用函数负责平栈:被调用函数在返回前使用
-
fastcall
调用约定:- 被调用函数负责平栈:被调用函数在返回前使用
ret n
指令自动清理栈。 - 示例:
mov ecx, 3 ; 第一个参数通过ECX寄存器传递 mov edx, 4 ; 第二个参数通过EDX寄存器传递 call add ; 被调用函数内部: ret 0 ; 没有参数通过栈传递,无需清理栈
- 被调用函数负责平栈:被调用函数在返回前使用
-
thiscall
调用约定:- 被调用函数负责平栈:被调用函数在返回前使用
ret n
指令自动清理栈。 - 示例:
lea ecx, [obj] ; this指针通过ECX寄存器传递 push 4 ; 第二个参数压栈 push 3 ; 第一个参数压栈 call ?add@MyClass@@QAEHHH@Z ; 被调用函数内部: ret 8 ; 被调用函数平栈,清理8字节的栈空间
- 被调用函数负责平栈:被调用函数在返回前使用
总结
- C 调用约定:默认使用
cdecl
,调用者负责平栈。 - C++ 调用约定:默认使用
thiscall
,被调用函数负责平栈。 - 栈与平栈:栈用于存储函数调用的局部变量和参数,平栈是清理栈的过程,不同的调用约定决定了由谁负责平栈。
理解这些调用约定和栈的机制对于编写高效的代码、调试程序以及进行逆向工程都非常重要。希望本文能帮助你更好地掌握这些知识,提升编程技能!
如果你觉得这篇文章对你有帮助,请点赞、收藏并分享给你的朋友们!