浅谈函数调用的汇编实现细节(用栈来传递参数)


前言

      要想理解函数调用的汇编实现,需要清楚几个基本概念。在此针对的是用栈来传递参数。
      1,调用现场的保护:假设函数A调用函数B,一旦程序执行进入函数B中,当函数B执行结束后,我们肯定需要执行流继续从函数A调用现场(callsite)的下一条语句继续执行函数A。当然,各种应用程序在操作系统上也是这样运行,否则的话,程序不就飞了?这不符合操作系统有始有终的性格。这一点特别像递归函数,虽然一层层嵌套,但是最终还是需要一层层返回的,即从哪个函数开始,就从哪个函数结束。
      所以,当函数A准备调用函数B时,需要先把A中调用语句的下一条语句保存起来,通常都是保存到栈里面,这样当函数B返回后,将之前压入栈中的待执行语句从栈中弹出,然后执行流从这条语句接着执行,即函数A继续执行。
      2,关于栈的增长方向:栈是从高地址向低地址方向增长的。即栈底处于高地址,栈顶处于低地址,每次入栈时需要将栈指针减小,每次出栈时需要将栈指针增加。这点可以查看Ubuntu系统中的程序运行时空间布局得知。在Windows系统中也是一样,通过相关的程序分析工具可以直观的看到。
      3,参数和返回值:函数调用在通常情况下都是需要传递参数和返回值的。系统API大都是采用_stdcall调用约定,函数入口参数按从右到左的顺序入栈,由被调用者清理栈中的参数,返回值放在eax寄存器中。而C代码中的子程序采用的是C调用约定,函数入口参数按从右到左的顺序入栈,由调用者清理栈中的参数。
      4,ebp和esp:在x86指令集中,ebp寄存器为栈帧寄存器,用来保存每一个函数的栈底位置(内存地址);esp寄存器为栈顶寄存器,用来保存每一个函数当前的栈顶位置。eip表示当前程序执行的指令地址。

程序分析

 void f(int x,int y)
 {
 	int a,b;
 	a=7,b=9;
 	x=x+y;
 }



 int g=3;
 int main()
 {
 	f(g,5);
 	return 0;
 }

      上面是一个简单的c程序函数调用。在main函数调用了函数 f。从程序语义上来分析,虽然这个 f 函数啥也没干,不过不影响我们分析它的函数调用结构。
      main函数对应的汇编代码如下:

push 5
mov eax,[00422ab6]               ;假设全局变量g存放的地址为0x00422ab6
push eax
call f
add esp,8

      注意函数调用语句 f(g,5) 是从右往左将参数压栈的。
      其中第一条语句将参数 5 压入栈中;
      第二条语句取得 g 的值并存放到 eax 中,第三条语句将 eax 压入栈中,整体实现功能将参数 g 压入栈中;
      然后是 call f 语句,可能有不明白的朋友会好奇为什么没有将下一条语句 add esp,8 的指令地址保存呢?这个牵扯到 call 指令的功能了,它会先保存下一条指令的地址,然后再调用函数 f ;(具体的可以参考博客https://blog.csdn.net/Little_ant_/article/details/108115387讲得比较细致一些)
       最后一条语句 add esp,8 的作用是清除main函数栈中的参数,(注意:参数不等同于局部变量) 按照前面提到的,c程序通常由调用者进行清理。 这里清理掉参数 g 和 5,因为都是 int 类型,所以一共占据8字节空间。


      f 函数对应的汇编如下:

  f:    push ebp
  		mov ebp,esp
  		sub esp,8
  		mov [ebp-4h],7
  		mov [ebp-8h],9
  		mov eax,[ebp+8h]
  		add eax,[ebp+0ch]
  		mov [ebp+8h],eax
  		mov esp,ebp
  		pop ebp
  		ret

      第一句:保存main函数的栈帧
      第二句:设置当前的栈顶为 f 函数的栈帧地址(也就是栈底)
      第三句:esp减8,可以理解为从栈中拿出8字节空间准备进行存储局部变量。
      第四、五句:存储局部变量 a 和 b 的值,局部变量的存储按照其赋值语句的先后顺序依次将它们入栈。 而参数入栈的顺序是从右到左的,从某种角度上来看,局部变量和参数入栈的顺序是相反的。 一般情况下:ebp减去任何数值后得到的内存地址都是位于当前函数的栈空间里面的。
      第六、七句:ebp+8h 表示的是参数 g 所在的栈地址。ebp+0ch 表示的是参数 5 所在的栈地址。这两句的执行结果是将 g+5 的结果存储到 eax寄存器中。一般情况下:ebp加上任何数值后得到的内存地址都是位于调用者函数的栈空间里面的。
      第八句:将eax中 g+5 的最终结果存储到参数 g 所在位置。 故实现了函数调用功能: g=g+5;
      第九句:将当前 f 函数的栈底地址赋给栈顶指针esp,即清空 f 函数的函数栈(即清理栈中保存的两个局部变量)。
      第十句:将原来main函数的栈帧地址传递到ebp寄存器中,此时进入到了main函数原来的栈空间里了。
      最后一句:用ret指令返回,这和call指令是相对应的。将原来call指令保存的返回地址传递给eip寄存器,程序执行流此时回到main函数了。

总结

      本文简单的梳理了关于函数调用的细节,所有相关的东西都有提到,只要好好理解,这其实并不困难。虽然这里我默认大家都有一些汇编的基础,但是如果有哪一点不明白的话欢迎留言~
ps:后面有机会会把栈空间示意图贴出来~

  • 4
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值