x86-64系统架构:栈和栈帧

本文汇编语言均使用AT&T风格。

1. 栈

栈是一种线性数据结构,它遵循后进先出(LIFO,Last In First Out)原则。

栈是处理器执行汇编代码时使用的特殊内存区域,是函数调用的重要组成部分。

栈是内存中的一段连续空间,由RSP/RBP寄存器标记,硬件指令PUSH/POP直接操作栈,增长方向从内存高地址到低地址。栈内存由操作系统为程序分配。

RBP(Base Pointer)栈帧基指针,指向栈帧的“底部”(实际上是栈帧中最高的内存地址)。用作访问函数参数和局部变量的参考点。

RSP(Stack Pointer)栈指针,指向栈的“顶部”,这里是栈的最低的内存地址,随着栈的增长,RSP会被递减。

通过以下代码示例说明栈和栈帧,C代码如下:

int sum(int a1,int a2,int a3, int a4, int a5,int a6,int a7,int a8) {
    return a1+a2+a3+a4+a5+a6+a7+a8;
}

int sumcalc() {
    return sum(1,2,3,4,5,6,7,8);
}

通过gcc命令编译:“`gcc -g -o test test.c“`

再通过GDB进行反汇编函数 sumcalc和sum:

(gdb) disassemble/r sumcalc
Dump of assembler code for function sumcalc:
   0x000000000000116d <+0>:     f3 0f 1e fa         endbr64
   0x0000000000001171 <+4>:     55                  push   %rbp
   0x0000000000001172 <+5>:     48 89 e5            mov    %rsp,%rbp
   0x0000000000001175 <+8>:     6a 08               push   $0x8
   0x0000000000001177 <+10>:    6a 07               push   $0x7
   0x0000000000001179 <+12>:    41 b9 06 00 00 00   mov    $0x6,%r9d
   0x000000000000117f <+18>:    41 b8 05 00 00 00   mov    $0x5,%r8d
   0x0000000000001185 <+24>:    b9 04 00 00 00      mov    $0x4,%ecx
   0x000000000000118a <+29>:    ba 03 00 00 00      mov    $0x3,%edx
   0x000000000000118f <+34>:    be 02 00 00 00      mov    $0x2,%esi
   0x0000000000001194 <+39>:    bf 01 00 00 00      mov    $0x1,%edi
   0x0000000000001199 <+44>:    e8 8b ff ff ff      call   0x1129 <sum>
   0x000000000000119e <+49>:    48 83 c4 10         add    $0x10,%rsp
   0x00000000000011a2 <+53>:    c9                  leave
   0x00000000000011a3 <+54>:    c3                  ret


(gdb) disassemble/r sum
Dump of assembler code for function sum:
   0x0000000000001129 <+0>:     f3 0f 1e fa   endbr64
   0x000000000000112d <+4>:     55            push   %rbp
   0x000000000000112e <+5>:     48 89 e5      mov    %rsp,%rbp
   0x0000000000001131 <+8>:     89 7d fc      mov    %edi,-0x4(%rbp)
   0x0000000000001134 <+11>:    89 75 f8      mov    %esi,-0x8(%rbp)
   0x0000000000001137 <+14>:    89 55 f4      mov    %edx,-0xc(%rbp)
   0x000000000000113a <+17>:    89 4d f0      mov    %ecx,-0x10(%rbp)
   0x000000000000113d <+20>:    44 89 45 ec   mov    %r8d,-0x14(%rbp)
   0x0000000000001141 <+24>:    44 89 4d e8   mov    %r9d,-0x18(%rbp)
   0x0000000000001145 <+28>:    8b 55 fc      mov    -0x4(%rbp),%edx
   0x0000000000001148 <+31>:    8b 45 f8      mov    -0x8(%rbp),%eax
   0x000000000000114b <+34>:    01 c2         add    %eax,%edx
   0x000000000000114d <+36>:    8b 45 f4      mov    -0xc(%rbp),%eax
   0x0000000000001150 <+39>:    01 c2         add    %eax,%edx
   0x0000000000001152 <+41>:    8b 45 f0      mov    -0x10(%rbp),%eax
   0x0000000000001155 <+44>:    01 c2         add    %eax,%edx
   0x0000000000001157 <+46>:    8b 45 ec      mov    -0x14(%rbp),%eax
   0x000000000000115a <+49>:    01 c2         add    %eax,%edx
   0x000000000000115c <+51>:    8b 45 e8      mov    -0x18(%rbp),%eax
   0x000000000000115f <+54>:    01 c2         add    %eax,%edx
   0x0000000000001161 <+56>:    8b 45 10      mov    0x10(%rbp),%eax
   0x0000000000001164 <+59>:    01 c2         add    %eax,%edx
   0x0000000000001166 <+61>:    8b 45 18      mov    0x18(%rbp),%eax
   0x0000000000001169 <+64>:    01 d0         add    %edx,%eax
   0x000000000000116b <+66>:    5d            pop    %rbp
   0x000000000000116c <+67>:    c3            ret

通过上述反汇编代码可以得出如图所示的栈&帧结构如下:

2. 栈帧

当调用函数时会建立一个栈帧以支持该函数。栈帧由函数序言(prologue)建立,通过函数后记(epilogue)进行清理。

函数序言和后记均由编译器生成,固定模板如下:

函数序言&后记:

sum:
    # 函数序言
    push %rbp        # 保存caller函数的rbp
    mov %rsp, %rbp   # 生成当前函数callee的帧基址,callee函数帧的起始位置
    
    
    # 函数后记
    mov %rbp, %rsp   # 释放当前函数callee的栈帧
    pop %rbp         # 恢复caller的栈帧
    ret              # 从栈中弹出返回地址并跳到该地址,控制权返回给caller

如上图所示,栈帧中通常包含以下组成部分:

1)返回地址

2)保存的基指针(RBP)

3)局部变量

4)函数参数(当函数参数的数量大于6个时使用栈传递)

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注