前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >X86函数调用模型分析

X86函数调用模型分析

作者头像
mingjie
发布2022-08-03 18:30:24
1.1K0
发布2022-08-03 18:30:24
举报

相关: 《Postgresql中的pg_memory_barrier_impl和C的volatile》 《X86函数调用模型分析》

函数A调用函数B,B执行完毕后继续执行函数A,如何实现这样的调用?

直接思考可能会存在以下几步:

  • A的局部变量如果在寄存器,需要保存起来。
  • 这些变量保存在栈中,栈中的位置需要记录。
  • 多层调用的话记录堆栈位置的信息会有多组,也都需要记录。
  • A调用完B后还需要继续执行,继续执行的位置需要保存起来。

下面分析x86的具体实现。

(资料汇编)

速查:

  1. 对于栈帧来说:栈帧顶部用bp指针(高地址),栈帧底部(低地址)用sp指针。
  2. 对于堆栈来说:整体堆栈的顶部为sp指针(堆栈生长到的最低地址)。

一、内存结构

二进制程序执行时的内存结构:

  • code section:保存程序执行指令的机器码。
  • static section:在程序执行期间不改变的常量和静态变量。
  • heap:使用malloc申请的堆内存,向内存地址升序的方向生长:grows up
  • stack:保存函数局部变量和函数调用的控制信息,向内存地址降序的方向生长:grows down

二、寄存器

  • 程序的虚拟内存空间提供了
2^{32}

的空间保存数据,地址从0x00000000到0xFFFFFFFF(一个十六进制为对应4个二进制位,所以是2的32次方)。

  • 寄存器提供了额外的存储空间,每个寄存器可以存一个字(4字节)。

和函数调用相关的寄存器(e表示扩展的意思):

  • eip:指令指针,存储当前正在执行的机器指令的地址。也叫PC(程序计数器)。
  • ebp:帧指针,保存当前栈帧顶部地址(高地址)。
  • esp:堆栈指针,保存当前堆栈底部地址(低地址)。

下图便于理解:

代码语言:javascript
复制
|----------------------|  high address
|        ...           |
|-------frame----------|
|        ...           |
|        ...           |
|        ...           |
|-------frame----------|   # current frame     <----- ebp
|        ...           |
|        ...           |
|        ...           |                       <----- esp
|----------------------|  low address

三、x86函数调用

  • 当需要调用另一个函数时,栈空间需要生长,用来保存一些局部变量 或者 寄存器信息。
  • 当调用函数发生时,caller执行逻辑会跳转到callee,拿到结果后,在跳转会caller。这就需要改变下面几个寄存器的值:
代码语言:txt
复制
- eip指令指针,需要改成指向callee的指令。
- ebp 和 esp 当前分别指向caller栈帧的顶部和底部。两个寄存器都需要更新为 指向callee的新栈帧的顶部和底部。当函数返回时,需要恢复寄存器中的旧值,才可以返回caller。所以更新寄存器的值,需要将它的旧值保存在堆栈中,以便在函数返回后恢复旧值。

下面是main调用foo的执行过程:

step0

step1:参数入栈

将参数压入堆栈。 x86将参数压入堆栈来传递参数。请注意,当我们将参数压入堆栈时,esp 会递减。参数以相反的顺序压入堆栈。(上面是高地址)

step2:旧的eip入栈

旧的eip(rip)压入堆栈。跳转到子函数执行eip需要指向子函数,所以这里先保存下。

step3:修改eip指向

已经保存了 eip 的旧值,可以安全地将 eip 更改为指向被callee的指令。

step4:将旧的ebp入栈

step5:ebp向下移动指向新栈帧顶部

这就是mov %esp %ebp的含义:

step6:esp向下移动

通过sub esp(esp地址–) 来为新栈帧分配新空间。编译器会根据函数的复杂度确定 esp 应该减少多少。

  • 例如,只有几个局部变量的函数不需要太多的堆栈空间,因此 esp 只会减少几个字节。
  • 例如,如果一个函数将一个大数组声明为一个局部变量,那么 esp 会减少很多来适应堆栈中的数组。

step7:执行callee

现在堆栈中已经保存了函数的局部变量和跳转控制信息;由于ebp指向栈帧的顶部,所以可以用ebp+8找到第一个参数的保存位置。

step8:返回esp回到堆栈顶部

step9:恢复旧的ebp

使用esp从堆栈中pop出一个值(old ebp),把old ebp的值赋给ebp。

step10:弹出eip

继续使用esp弹出old eip的值赋给eip。

step11:从堆栈中删除参数

继续讲堆栈上的参数弹出到寄存器,然后删除esp栈顶以下的元素。栈顶以下的元素已经不在栈中,没有意义。

四、实例分析

代码语言:javascript
复制
int main(void) {
    foo(1, 2);
}

void foo(int a, int b) {
    int bar[4];
}

gcc -O0 t.c -o t -g

main执行过程

代码语言:javascript
复制
(gdb) disassemble /rm
Dump of assembler code for function main:
3       int main(void) {
                                                                 # 由_start调入main函数
   0x0000000000401122 <+0>:     55              push   %rbp      # 栈帧顶部入栈
   0x0000000000401123 <+1>:     48 89 e5        mov    %rsp,%rbp # 栈帧顶部指针rbp指向新栈帧顶部

4           foo(1, 2);
=> 0x0000000000401126 <+4>:     be 02 00 00 00  mov    $0x2,%esi # 参数1入寄存器传递
   0x000000000040112b <+9>:     bf 01 00 00 00  mov    $0x1,%edi # 参数2入寄存器传递
   0x0000000000401130 <+14>:    e8 07 00 00 00  callq  0x40113c <foo>   # push %rip 然后 jmpq
                                                                        # push %rip 等价与 sub $0x8, %rsp 
                                                                        #                 mov $rip, %rsp

   0x0000000000401135 <+19>:    b8 00 00 00 00  mov    $0x0,%eax

5       }
   0x000000000040113a <+24>:    5d              pop    %rbp             # 先恢复rbp的值
   0x000000000040113b <+25>:    c3              retq                    # 在恢复rip的值 popq %rip

End of assembler dump.

foo函数

代码语言:javascript
复制
(gdb) disassemble /rm
Dump of assembler code for function foo:
7       void foo(int a, int b) {
   0x000000000040113c <+0>:     55              push   %rbp              # 帧顶位置 入栈
   0x000000000040113d <+1>:     48 89 e5        mov    %rsp,%rbp         # rbp帧顶指针,指向新帧顶
   0x0000000000401140 <+4>:     89 7d ec        mov    %edi,-0x14(%rbp)  # 参数2入栈(先压最后一个参数入栈)
   0x0000000000401143 <+7>:     89 75 e8        mov    %esi,-0x18(%rbp)  # 参数1入栈

8           int bar[4];
9       }
=> 0x0000000000401146 <+10>:    90              nop
   0x0000000000401147 <+11>:    5d              pop    %rbp  # 先恢复rbp的值
   0x0000000000401148 <+12>:    c3              retq         # 在恢复rip的值 popq %rip

End of assembler dump.
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-08-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、内存结构
  • 二、寄存器
  • 三、x86函数调用
    • step0
      • step1:参数入栈
        • step2:旧的eip入栈
          • step3:修改eip指向
            • step4:将旧的ebp入栈
              • step5:ebp向下移动指向新栈帧顶部
                • step6:esp向下移动
                  • step7:执行callee
                    • step8:返回esp回到堆栈顶部
                      • step9:恢复旧的ebp
                        • step10:弹出eip
                          • step11:从堆栈中删除参数
                          • 四、实例分析
                            • main执行过程
                              • foo函数
                              领券
                              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档