X86如何实现函数调用?

2022-11-30 16:10:32 浏览数 (1)

相关: 《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}

的空间保存数据,用户地址空间3G从0x00000000xC0000000,内核空间1G从0xC00000000xFFFFFFFF

  • (64位系统)程序的虚拟内存空间提供了
2^{64}

的空间保存数据,用户地址空间128T从0x0000 0000 0000 00000x0000 7FFF FFFF F0000,内核空间128T从0xFFFF 8000 0000 00000xFFFF FFFF FFFF FFFF

二、寄存器

  • 寄存器提供了额外的存储空间,每个寄存器可以存一个字(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 �p的含义:

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,�i # 参数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,�x

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    �i,-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.

0 人点赞