函数调用背后那点事

来源:互联网 发布:手机蓝牙控制app 源码 编辑:程序博客网 时间:2024/05/22 08:00

当你写下一个简单的C语言程序(比如我们都会写的hello world),你可曾知道这个简单的程序背后的那些事情………今天我们从汇编的角度来浅谈一下一个函数在被调用的前前后后。

我们知道栈保存了一个函数调用所需要的维护信息,而这些维护信息通常被称为堆栈帧或活动记录。堆栈帧一般包括如下几个方面:

1>函数的返回地址和参数。
2>临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量。
3>保存的上下文:包括在函数调用前后需要保持不变的寄存器。
废话少说来看代码:
`
#include<stdio.h>
int sum(int a, int b)
{
int temp = 0;
temp = a + b;
return temp;
}
void main()
{
int a = 10;
int b = 20;
int res = 0;
res = sum(a, b);
printf("res = %d\n", res);
}

下面通过反汇编代码来分析下这些简单的代码:
int a = 10;
009A13DE mov dword ptr [a],0Ah
//将10赋值给局部变量a,以下两行代码一样的效果
int b = 20;
009A13E5 mov dword ptr [b],14h
int res = 0;
009A13EC mov dword ptr [res],0
定义三个局部变量
定义三个局部变量后main函数压栈过程

        res = sum(a, b);

009A13F3 mov eax,dword ptr [b]
009A13F6 push eax
//把局部变量b赋给寄存器eax,再把eax压入main函数的栈中

009A13F7 mov ecx,dword ptr [a]
009A13FA push ecx
//把局部变量a赋给寄存器ecx,再把ecx压入main函数的栈中

009A13FB call _sum (09A10FAh)
//调用sum函数,并把call指令下一行指令的地址记录到sum的栈中

009A1400 add esp,8
// 回退形参变量的内存

009A1403 mov dword ptr [res],eax
//把函数的返回值赋给局部变量res


且看sum函数的反汇编代码:
int sum(int a, int b)
{
009A1440 push ebp
//把main函数的栈底指针压进自己的栈中

009A1441 mov ebp,esp
//让当前的栈顶指针赋给sum函数的栈底指针

009A1443 sub esp,0CCh
//开辟一定量的sum函数栈内存

009A1449 push ebx
009A144A push esi
009A144B push edi
//把以上三个寄存器压入sum函数的栈中

009A144C lea edi,[ebp-0CCh]
//把当前的栈顶指针赋值给edi
009A1452 mov ecx,33h
009A1457 mov eax,0CCCCCCCCh
009A145C rep stos dword ptr es:[edi]

//相当于一个循环拷贝动作,把以上sum开辟的内存初始化为0CCCCCCCCh

int temp = 0;

009A145E mov dword ptr [temp],0

temp = a + b;

009A1465 mov eax,dword ptr [a]
009A1468 add eax,dword ptr [b]
009A146B mov dword ptr [temp],eax
//以上三句先将a的值放到eax中,再将b的值和a的值相加和放到eax中

return temp;

009A146E mov eax,dword ptr [temp]
//把temp的值赋值给eax寄存器
}
009A1471 pop edi
009A1472 pop esi
009A1473 pop ebx
//三个寄存器出栈

009A1474 mov esp,ebp
//回退sum函数的内存栈

009A1476 pop ebp
//出栈,回退到main函数中

009A1477 ret
//让esp出栈,并把下一行指令的地址赋值给cpu的pc寄存器
整个内存栈布局如下:
这里写图片描述

至此,整个sum函数的调用过程结束!!
以上sum函数返回值为4字节(32位机器)大小,我们发现是通过eax寄存器带回的,对于c语言的的内置类型、结构体、联合体(union)、枚举类型(enum)函数的返回值如果小于4字节,其由寄存器eax带回。

如果是8个字节则是由eax和edx共同带回。

如果它大于8个字节则在调用函数的时候会先压入(先压参数,后压临时量)main函数栈上的一块临时内存的地址,然后在调用函数return时会把要返回的值拷贝到main函数那块临时内存上,并通过eax寄存器返回临时内存的地址。

再看看最后的函数调用约定吧

调用约定主要有以下:

  • _cdecl c调用约定
  • _stdcall windows标准的调用约定
  • _fastcall 快速调用约定
  • _thiscall C++成员函数的调用约定
  • //_pascal (调用顺序从左向右,其他都是从右向左)

函数的调用约定会影响:

  • 函数产生的符号名字不同
  • 函数参数入栈顺序不同
  • 谁来清理新参的内存
    第一个影响很好理解,如果采取的调用约定不同则,在编译时产生的函数符号不同,则会影响程序的链接,可能会导致链接的失败。
    第二个影响是参数入栈顺序,除了以上的_pascal 调用外其他入栈顺序都是从右向左。
    第三个影响归结如下:
    _cdecl:调用方开辟形参内存,调用方释放新参内存
    _stdcall:调用方开辟形参内存,被调用方自己释放新参内存
    _fastcall :调用方开辟形参内存,但是把最左边(也就是最后8字节的实参通过寄存器带到被调用函数当中),被调用方自己释放新参内存

好累,睡觉。。

原创粉丝点击