• X86函数调用模型分析


    相关:
    《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
      在这里插入图片描述
    • (32位系统)程序的虚拟内存空间提供了 2 32 2^{32} 232的空间保存数据,用户地址空间3G从0x00000000xC0000000,内核空间1G从0xC00000000xFFFFFFFF
    • (64位系统)程序的虚拟内存空间提供了 2 64 2^{64} 264的空间保存数据,用户地址空间128T从0x0000 0000 0000 00000x0000 7FFF FFFF F0000,内核空间128T从0xFFFF 8000 0000 00000xFFFF FFFF FFFF FFFF

    二、寄存器

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

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

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

    下图便于理解:

    |----------------------|  high address
    |        ...           |
    |-------frame----------|
    |        ...           |
    |        ...           |
    |        ...           |
    |-------frame----------|   # current frame     <----- ebp
    |        ...           |
    |        ...           |
    |        ...           |                       <----- esp
    |----------------------|  low address
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    三、x86函数调用

    • 当需要调用另一个函数时,栈空间需要生长,用来保存一些局部变量 或者 寄存器信息。

    • 当调用函数发生时,caller执行逻辑会跳转到callee,拿到结果后,在跳转会caller。这就需要改变下面几个寄存器的值:

      • 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栈顶以下的元素。栈顶以下的元素已经不在栈中,没有意义。
    在这里插入图片描述

    四、实例分析

    int main(void) {
        foo(1, 2);
    }
    
    void foo(int a, int b) {
        int bar[4];
    }
    
    gcc -O0 t.c -o t -g
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    main执行过程

    (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    # 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.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    foo函数

    (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.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
  • 相关阅读:
    5、docker mysql安装
    高精度室内定位技术,在智慧工厂安全管理的应用
    c语言中磁盘文件的分类
    AcWing第78场周赛
    【计算机网络】 基于TCP的简单通讯(服务端)
    C#特性(Attribute)
    爬虫学习(15):selenium自动化测试(四):截屏、弹出框和下拉框
    五、Kafka日志存储
    strcmp函数详解:字符串比较的利器
    【云计算】三种云服务
  • 原文地址:https://blog.csdn.net/jackgo73/article/details/126101203