gcc x86 calling conventions

11 篇文章 2 订阅

别人的图看着太累了,自己整理一下,简单干脆。

 

calling conventions,翻译过来可以叫“调用规则”,“调用约定”,“调用协议”等。

究其根底,不过是规定了多层函数嵌套时,caller与callee的有关参数的入栈方式。

因为是一个convention,所以你不必纠结为什么要这样,大家都这样了,世界就和平了。

 

我们约定

若一个函数A调用一个函数B,则A被称为调用者(Caller),B被称为被调用者(Callee)。

 

根据习惯,我们需要了解一个事实

在X86中,栈增长方向与内存编号增长方向相反。

 

你还需要知道2个指针的含义

ebp,当前函数栈区的基指针 (base pointer)

esp,当前函数栈区的栈顶指针(stack pointer)

 

下面正式介绍游戏规则。

 

Caller Rules

调用者规则包括一系列操作,描述如下:

1)在调用子程序之前,调用者应该保存一系列被设计为调用者保存的寄存器的值。调用者保存寄存器有eax,ecx,edx。由于被调用的子程序会修改这些寄存器,所以为了在调用子程序完成之后能正确执行,调用者必须在调用子程序之前将这些寄存器的值入栈。

2)在调用子程序之前,将参数入栈。参数入栈的顺序应该是从最后一个参数开始,如上图中parameter3先入栈。

3)利用call指令调用子程序。这条指令将返回地址放置在参数的上面,并进入子程序的指令执行。(子程序的执行将按照被调用者的规则执行)

当子程序返回时,调用者期望找到子程序保存在eax中的返回地址。为了恢复调用子程序执行之前的状态,调用者应该执行以下操作:

1)清除栈中的参数;

2)将栈中保存的eax值、ecx值以及edx值出栈,恢复eax、ecx、edx的值(当然,如果其它寄存器在调用之前需要保存,也需要完成类似入栈和出栈操作)

 

Callee Rules

被调用者应该遵循如下规则:

1)将ebp入栈,并将esp中的值拷贝到ebp中,其汇编代码如下:

    push ebp
    mov  ebp, esp

上述代码的目的是保存调用子程序之前的基址指针,基址指针用于寻找栈上的参数和局部变量。当一个子程序开始执行时,基址指针保存栈指针指示子程序的执行。为了在子程序完成之后调用者能正确定位调用者的参数和局部变量,ebp的值需要返回。

2)在栈上为局部变量分配空间。

3)保存callee-saved寄存器的值,callee-saved寄存器包括ebx,edi和esi,将ebx,edi和esi压栈。

4)在上述三个步骤完成之后,子程序开始执行,当子程序返回时,必须完成如下工作:

  4.1)将返回的执行结果保存在eax中

  4.2)弹出栈中保存的callee-saved寄存器值,恢复callee-saved寄存器的值(ESI和EDI)

  4.3)收回局部变量的内存空间。实际处理时,通过改变EBP的值即可:mov esp, ebp。 

  4.4)通过弹出栈中保存的ebp值恢复调用者的基址寄存器值。

  4.5)执行ret指令返回到调用者程序。

//来源:http://www.cnblogs.com/YukiJohnson/archive/2012/10/27/2741836.html

 

看不懂没关系,看完下面的内容等下再回来看规则就更好理解了。

 

网上有2种图,一种是把高地址放在上方的,比如头顶x1010,脚部0x1000,这样一来,以高地址为栈底,Stack向下扩张。

另一种是高地址在下方的。

所以你看不同的文章,不同的配图,会很头晕。

我总结下来,为了保持与MIT课程的统一,建议采用高地址在上方的版本。

 

MIT的图长这样:

		       +------------+   |
		       | arg 2      |   \
		       +------------+    >- previous function's stack frame
		       | arg 1      |   /
		       +------------+   |
		       | ret %eip   |   /
		       +============+   
		       | saved %ebp |   \
		%ebp-> +------------+   |
		       |            |   |
		       |   local    |   \
		       | variables, |    >- current function's stack frame
		       |    etc.    |   /
		       |            |   |
		       |            |   |
		%esp-> +------------+   /

 

第二类图长这样:


      ESP==>|           :             |
            |           .             |
            +-------------------------+
            | 被调用者保存的寄存器现场   |
            | EBX,ESI和EDI(根据需要) |
            +-------------------------+
            |  临时空间                |
            +-------------------------+
            |  局部变量#2              | [EBP - 8]
            +-------------------------+
            |  局部变量#1              | [EBP - 4]
            +-------------------------+
      EBP==>|  调用者的EBP             |
            +-------------------------+
            |  返回地址                |
            +-------------------------+
            |  实际参数#1              | [EBP + 8]
            +-------------------------+
            |  实际参数#2              | [EBP + 12]
            +-------------------------+
            |  实际参数#3              | [EBP + 16]
            +-------------------------+
            |  调用者保存的寄存器现场    |
            |  EAX,ECX和EDX(根据需要)|
            +-------------------------+
            |            :            |
            |            .            |
      

 

本文采用高地址在上方的版本。

 

依据caller规则,在函数调用时。

1. 参数从后向前入栈。

例如 myfunc(a,b,c)

入栈顺序为c -> b -> a 

入栈完成后的结构是

 

2. caller将返回地址入栈保存

3.跳转到子函数起始地址

上述2和3两步,由call指令完成。

完成后,就已经来到了子函数的起始地址。

 

4.子函数将父函数栈帧起始地址(%epb) 入栈。

子函数第一条指令就是push %ebp,你可以认为这是固定的,这会有助于理解。

 

注意,栈区跟代码区是两个东西。我只画了栈区发生了什么。

比如上图中,push %ebp这条指令可能保存在0xf0ff这个位置。

但是执行完成之后,栈区0x1030这个位置就多出来一个父函数的%ebp里的值。

 

5.将 %ebp 的值设置为当前 %esp 的值,即将 %ebp 指向子函数栈帧的起始地址。

mov  ebp, esp

这条指令只修改寄存器的值,不影响栈区。

注意,这一步之后,ebp里面就是当前esp的值。而当前esp指向的是父函数%ebp保存的地址

所以你可以理解为,【在子程序运行期间,%ebp指向的都是父函数%ebp保存的地址】。 (有点绕口)

 

6.现在开始申请临时空间。

假设我们的myfunc里面,定义了2个int变量,一共8字节。

那么这步结束后,栈顶指针esp来到0x1024这个位置。【局部变量2占用从0x1028到0x1025共4个字节的内容】

 

现在我们可以正式运行子函数了。

 

7.运行结束之后,以防万一先把返回值存进%eax里。

现在该如何返回?

很简单,还记得上文说的,子程序运行期间,%ebp指向的是父函数%ebp保存的地址。

所以我们先用

mov esp,ebp

这样,esp就回到了保存父函数%ebp的地方。 

 

pop ebp

 执行指令后,会把%esp指向的地址里面的值弹出,赋值给ebp。

于是%ebp恢复到了父函数执行时的样子。

而栈顶指针esp再度退回一格。

 

8.现在我们可以用ret指令,或者leave指令。

效果都是一样的,弹出栈顶保存的返回地址(return address),并跳转到它(即赋值给EIP寄存器)。

 

现在,%ebp和%eip都回到了父函数,父函数取得控制权。

 

9.父函数在返回后有义务进行扫尾工作。

之前传入的3个参数ABC已经不需要了,需要退格。

esp += 12

这样esp就重新指向栈底,此时对计算机来说,为栈空。

 

 

 

上述过程我们可以看到,

如果所程序都遵守calling convention,那么就很容易回溯所有函数的栈区基指针ebp。

因为根据规则,在一个子函数中,ebp必然指向保存着父函数ebp的位置。所以可以不断的把当前ebp位置的值取出来赋值给ebp来回到上一级函数的栈区。

 

以上为个人理解版本。

若有误请指教。

 

附录:

袁春风书

 

//参考https://blog.csdn.net/cwcwj3069/article/details/10633493

//参考https://zhuanlan.zhihu.com/p/27339191

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值