汇编语言函数设置参数(C从汇编的角度理解函数调用与参数传递)
汇编语言函数设置参数(C从汇编的角度理解函数调用与参数传递)调用约定主要定义主调函数和被调函数对于堆栈平衡的分工,参数压栈顺序规定等。1 调用约定主调函数调用被调函数时,对于参数,不管是传值还是传址,都会有一个压栈操作,但压值和压址的操作稍有不同(后续会从汇编的角度分析),参数压栈后,被调函数体对参数的操作就是对压栈空间的引用,当然,对值的引用和址的引用在解析成汇编后也会不同,后者会增加一个解引用(从地址取值)的动作。demo:#include <stdio.h> int __cdecl callee(int *a int &b int d) // __cdecl是函数调用约定,是略写时的默认调用约定 { int t = *a; *a = b*d; b = t*d; return *a*b; } void caller() { int a=3 b=4 d=2; int c = call
主调函数(caller)调用被调函数(callee),编译器要考虑两者的相互独立和相互联系。
一方面通过传递参数(值或址)和函数返回(值或址)来在两段代码之间建立联系。传址还可以形成副作用。
(C 的引用参数也是一种传址,只是编译器做了自动取址和解引用取值的动作。)
另一方面主调函数和被调函数都有各自的函数栈帧(function frame),但两者的地址空间是透明的(主调函数可以通过被调函数栈帧上的地址来间接访问被调函数栈帧上的空间)。
主调函数调用被调函数时,对于参数,不管是传值还是传址,都会有一个压栈操作,但压值和压址的操作稍有不同(后续会从汇编的角度分析),参数压栈后,被调函数体对参数的操作就是对压栈空间的引用,当然,对值的引用和址的引用在解析成汇编后也会不同,后者会增加一个解引用(从地址取值)的动作。
demo:
#include <stdio.h>
int __cdecl callee(int *a int &b int d) // __cdecl是函数调用约定,是略写时的默认调用约定
{
int t = *a;
*a = b*d;
b = t*d;
return *a*b;
}
void caller()
{
int a=3 b=4 d=2;
int c = callee(&a b d); // 函数调用时会传址或传址,参数会通过压栈而形成副本机制
printf("%d\n" c);
}
int main() // main由操作系统调用而被执行,其它函数要由另外的函数去调用才会被执行,
// main函数通常充当被调函数的作用
// main函数内定义的变量也是局部变量
{
caller();
getchar();
return 0;
}
demo中的主调函数是caller,被调函数是callee。
1 调用约定
调用约定主要定义主调函数和被调函数对于堆栈平衡的分工,参数压栈顺序规定等。
2 主调函数caller栈帧空间的建立
9: void caller()
10: {
00401090 push ebp// ebp压栈,届时局部变量会压在ebp之上(低地址方向,栈往低地址方向增长)
00401091 mov ebp esp
00401093 sub esp 50h// 局部变量使用的空间,编译器会计算局部变量的需求(适当增加)而不同
00401096 push ebx// 为保持寄存器状态而额外使用的栈空间(50h以外)
00401097 push esi
00401098 push edi
00401099 lea edi [ebp-50h]
0040109C mov ecx 14h
004010A1 mov eax 0CCCCCCCCh// debug模式时,会将50h的空间全部置0ch
004010A6 rep stos dword ptr [edi]
3 主调函数caller局部变量压栈
11: int a=3 b=4 d=2;
004010A8 mov dword ptr [ebp-4] 3// 局部变量地址以ebp为基准,向低地址方向增长
004010AF mov dword ptr [ebp-8] 4
004010B6 mov dword ptr [ebp-0Ch] 2
4 函数调用(实参压栈)和返回
12: int c = callee(&a b d); // 函数调用时会传址或传址,参数会通过压栈而形成副本机制
004010BD mov eax dword ptr [ebp-0Ch] // d赋值给eax寄存器,注意这里是mov,值赋值(或值传递)
004010C0 push eax // d压栈
004010C1 lea ecx [ebp-8] // b的地址赋值给ecx,注意这里是lea,址赋值(或址传递)
004010C4 push ecx // &b压栈
004010C5 lea edx [ebp-4] // a的地址赋值给edx
004010C8 push edx // &a压栈
004010C9 call @ILT 20(callee) (00401019) // 这里要进入函数调用
004010CE add esp 0Ch // 函数调用完成后返回到这里,按__cdecl约定,由主调函数平衡参数所占空间
004010D1 mov dword ptr [ebp-10h] eax // 返回值存放在寄存器eax中,返回给主调函数的c
注意上述引用传址和指针传址使用了相同的汇编代码。
对于一个寄存器可以存下的返回值,通常通过eax返回,对于浮点数,一般通过浮点栈的寄存器返回,对于复合类型,会在主调函数的局部空间规划出一块空间用来存放返回值,这块空间的首地址会在压完参数后压在栈帧上。
call 函数名
push 返回地址(EIP) jmp 函数地址
(EIP指向下一条指令)
(1) 将程序当前执行的位置IP的下一个地址压入堆栈中;
(2) 转移到调用的子程序。
5 caller call caller
@ILT 5(?callee@@YAHPAHAAHH@Z):
0040100A jmp callee (00401030)
编译器会将返回地址004010CE压栈,此时的栈帧空间是:
6 被调函数栈帧空间建立
2: int __cdecl callee(int *a int &b int d) // __cdecl是函数调用约定,是略写时的默认调用约定
3: {
00401030 push ebp
00401031 mov ebp esp
00401033 sub esp 44h
00401036 push ebx
00401037 push esi
00401038 push edi
00401039 lea edi [ebp-44h]
0040103C mov ecx 11h
00401041 mov eax 0CCCCCCCCh
00401046 rep stos dword ptr [edi]
7 callee函数体对实参的引用
4: int t = *a;
00401048 mov eax dword ptr [ebp 8]
0040104B mov ecx dword ptr [eax] // 对a的解引用并赋值
0040104D mov dword ptr [ebp-4] ecx
5: *a = b*d;
00401050 mov edx dword ptr [ebp 0Ch]
00401053 mov eax dword ptr [edx] // 对b的解引用并赋值
00401055 imul eax dword ptr [ebp 10h] // 对b的值的直接引用
00401059 mov ecx dword ptr [ebp 8]
0040105C mov dword ptr [ecx] eax
6: b = t*d;
0040105E mov edx dword ptr [ebp-4]
00401061 imul edx dword ptr [ebp 10h]
00401065 mov eax dword ptr [ebp 0Ch]
00401068 mov dword ptr [eax] edx
7: return *a*b;
0040106A mov ecx dword ptr [ebp 8]
0040106D mov edx dword ptr [ebp 0Ch]
00401070 mov eax dword ptr [ecx]
00401072 imul eax dword ptr [edx]
8: }
注意以上汇编对值的直接引用,对引用传递和指针传递的变量先是引用地址,然后通过地址来解引用。
8 被调函数负责的自己部分的堆栈平衡
00401075 pop edi
00401076 pop esi
00401077 pop ebx
00401078 mov esp ebp
0040107A pop ebp // 相当于C语言中的ebp = *esp;esp = 4
0040107B ret // 相当于 pop EIP
-End-