b2科目四模拟试题多少题驾考考爆了怎么补救
b2科目四模拟试题多少题 驾考考爆了怎么补救

X86-64函数调用和堆栈框架原理

电脑杂谈  发布时间:2020-04-05 09:11:39  来源:网络整理

24帧和25帧的视觉效果_扩展帧和标准帧_栈帧

一艘船,一厘,一艘扁舟,一英寸的丝绸和一英寸的钩子.

一首歌唱了一瓶酒,一个人抓到一条河,一条河落下.

-jiang江孤钓图片

在C / C ++程序中,函数调用是非常常见的操作. 那么,此操作的基本原理是什么?编译器为我们做了什么?在函数调用中如何使用CPU中的寄存器和存储器堆栈?堆栈框架的创建和恢复如何完成?针对上述问题,本文进行了探索和研究.

在调用函数时,我们通常需要在硬件级别上注意CPU的通用寄存器. 在所有CPU架构中,每个寄存器通常都有推荐的使用方法,并且编译器通常根据CPU架构的建议使用这些寄存器,因此我们可以认为这些建议是强制性的.

对于x86-64体系结构,总共有16个64位通用寄存器. 寄存器和用途如下图所示:

从上图可以得出以下结论:

在这里,我们需要区分“ Caller Save”和“ Callee Save”寄存器,即寄存器的值是由“ caller”还是“ callee”保存的. 进行函数调用时,通常在子函数中使用通用寄存器,并且先前保存在这些寄存器中的调用者(父函数)的值将被覆盖. 为了避免数据重写并从子功能返回时导致寄存器中的数据无法恢复,CPU体系结构规定了如何保存通用寄存器.

如果一个寄存器被标记为“ Caller Save”,则在调用该子功能之前,调用者需要预先保存这些寄存器的值. 保存方法通常是将寄存器的值压入堆栈,调用者保存完成后,这些寄存器的值可以在被调用者中被覆盖(子功能). 如果一个寄存器被标记为“ Callee Save”,则调用者无需保存这些寄存器的值,并在调用函数时直接调用该子函数. 进入子功能后,子功能需要先保存这些内容,然后再覆盖这些寄存器. 这些寄存器的值,即这些寄存器的值由被调用者保存和恢复.

栈帧_24帧和25帧的视觉效果_扩展帧和标准帧

调用子功能时,调用者和被调用者的堆栈框架结构如下图所示:

当调用子函数时,执行的操作是: 父函数将调用参数从后往前推->将返回地址推入堆栈并保存->跳转到子函数的起始地址以执行->子功能将调用父功能堆栈帧起始地址(%rpb)推动堆栈->将%rbp的值设置为%rsp的当前值,即,将%rbp指向子功能的起始地址堆栈框架.

在上述过程中,通过调用指令执行保存返回地址并跳转到子功能的操作. 调用指令完成后,它已进入子例程,因此最后一个堆栈帧%rbp被压入堆栈. ,需要由子例程完成. 调用该函数时在汇编级别的指令顺序如下:

...   # 参数压栈
call FUNC  # 将返回地址压栈,并跳转到子函数 FUNC 处执行
...  # 函数调用的返回位置
FUNC:  # 子函数入口
pushq %rbp  # 保存旧的帧指针,相当于创建新的栈帧
movq  %rsp, %rbp  # 让 %rbp 指向新栈帧的起始位置
subq  $N, %rsp  # 在新栈帧中预留一些空位,供子程序使用,用 (%rsp+K) 或 (%rbp-K) 的形式引用空位

保存返回地址并保存前一个堆栈帧的%rbp是为了在函数返回时恢复父函数的堆栈帧结构. 当使用高级语言进行函数调用时,编译器会自动完成上述整个过程. 编译器还会自动完成“调用者保存”和“调用者保存”寄存器的保存和恢复.

应注意,当在父函数中将参数压入堆栈时,顺序是从后到前. 但是,此行为不是固定的,并且取决于编译器的特定实现. 在gcc中,使用从后到前的堆叠方法. 此方法很容易支持类似于printf(“%d,%d``,i,j)这样的函数,这些函数使用可变长度参数进行调用.

当函数返回时,我们只需要获取函数的返回值(存储在%rax中),那么我们需要将堆栈结构恢复到函数调用之间的差异状态,并跳转到返回值父函数的地址继续执行. 由于调用该函数时已保存了父函数堆栈框架的返回地址和起始地址,因此要在调用子函数之前恢复父堆栈框架,我们只需执行以下两条指令即可:

movq %rbp, %rsp    # 使 %rsp 和 %rbp 指向同一位置,即子栈帧的起始处
popq %rbp # 将栈中保存的父栈帧的 %rbp 的值赋值给 %rbp,并且 %rsp 上移一个位置指向父栈帧的结尾处

为了促进堆栈帧恢复,x86-64体系结构提供了Leave指令以实现上述两个命令的功能. 执行离开后,上图中函数调用的堆栈框架结构如下:

扩展帧和标准帧_24帧和25帧的视觉效果_栈帧

可以看出,在调用Leave之后,%rsp指向返回地址,并且x86-64提供的ret指令具有从当前%rsp所指向的位置弹出数据的功能(即栈顶)并跳转到此数据表示的地址,执行离开后,%rsp指向返回地址,因此ret的作用是将%rsp向上移动一个位置并跳转到返回地址以执行. 可以看出,leave指令用于恢复父函数的堆栈帧,ret用于跳转到返回地址栈帧,leave和ret配合完成子函数的返回. 完成ret的执行后,%rsp指向父堆栈帧末尾,并且编译器会自动释放存储在父堆栈帧末尾的调用参数.

为了更好地理解函数调用原理,我们可以使用一个程序示例来观察函数调用和返回. 步骤如下:

int add(int a, int b, int c, int d, int e, int f, int g, int h) { // 8 个参数相加
  int sum = a + b + c + d + e + f + g + h;
  return sum;
}
int main(void) {
  int i = 10;
  int j = 20;
  int k = i + j;
  int sum = add(11, 22,33, 44, 55, 66, 77, 88);
  int m = k; // 为了观察 %rax Caller Save 寄存器的恢复
  return 0;
}

在主功能中,首先执行k = i + j加法,以观察Caller Save的效果. 因为加法将使用%rax,下面的add函数的返回值也将使用%rax. 由于%rax是调用者保存寄存器,因此程序应在调用add子功能之前保存%rax的值.

add函数使用8个参数. 这是在功能参数超过6个时观察程序的行为. 前6个参数保存到寄存器,多于6个参数保存到堆栈. 但是,由于该参数的地址可能在子例程中,并且存储在寄存器中的前6个参数没有内存地址,因此我们可以猜测存储在寄存器中的前6个参数也在子例程中. 堆栈,以获取这6个参数的内存地址. 由上述程序生成并与子功能调用有关的汇编器如下:

add:
.LFB2:
    pushq    %rbp
.LCFI0:
    movq    %rsp, %rbp
.LCFI1:
    movl    %edi, -20(%rbp)
    movl    %esi, -24(%rbp)
    movl    %edx, -28(%rbp)
    movl    %ecx, -32(%rbp)
    movl    %r8d, -36(%rbp)
    movl    %r9d, -40(%rbp)
    movl    -24(%rbp), %eax
    addl    -20(%rbp), %eax
    addl    -28(%rbp), %eax
    addl    -32(%rbp), %eax
    addl    -36(%rbp), %eax
    addl    -40(%rbp), %eax
    addl    16(%rbp), %eax
    addl    24(%rbp), %eax
    movl    %eax, -4(%rbp)
    movl    -4(%rbp), %eax
    leave
    ret
main:
.LFB3:
    pushq    %rbp
.LCFI2:
    movq    %rsp, %rbp
.LCFI3:
    subq    $48, %rsp
.LCFI4:
    movl    $10, -20(%rbp)
    movl    $20, -16(%rbp)
    movl    -16(%rbp), %eax
    addl    -20(%rbp), %eax
    movl    %eax, -12(%rbp)
    movl    $88, 8(%rsp)
    movl    $77, (%rsp)
    movl    $66, %r9d
    movl    $55, %r8d
    movl    $44, %ecx
    movl    $33, %edx
    movl    $22, %esi
    movl    $11, %edi
    call    add
    movl    %eax, -8(%rbp)
    movl    -12(%rbp), %eax
    movl    %eax, -4(%rbp)
    movl    $0, %eax
    leave
    ret

在汇编器中,如果使用64位通用寄存器的低32位,则该寄存器以“ e”开头,例如%eax,%ebx等. 对于%r8-%r15,低32位位在64位注册后,添加“ d”以表示例如%r8d,%r15d. 如果操作数是32位,则指令以“ l”结尾,例如movl $ 11,%esi. 指令和寄存器均为32位格式. 如果操作数是64位,则指令以q结尾,例如“ movq%rsp,%rbp”. 因为示例程序中的操作数都在32位表示范围内,所以上述加法和移动指令都是32位指令和操作数. 创建堆栈帧和操作数时,只有64位指令用于地址对齐.

首先查看主要功能的前三个汇编语句:

.LFB3:
    pushq    %rbp
.LCFI2:
    movq    %rsp, %rbp
.LCFI3:
    subq    $48, %rsp

扩展帧和标准帧_24帧和25帧的视觉效果_栈帧

这三个语句保存父函数的堆栈框架栈帧,然后创建主函数的堆栈框架,并在堆栈框架中分配48个字节的空间. 完成这三个语句的执行后,主函数的堆栈框架如下所示:

然后,在主函数中执行k = i + j的加法和加法参数的处理:

    movl    $10, -20(%rbp)
    movl    $20, -16(%rbp)
    movl    -16(%rbp), %eax
    addl    -20(%rbp), %eax
    movl    %eax, -12(%rbp)  # 调用子函数前保存 %eax 的值到栈中,caller save
    movl    $88, 8(%rsp)
    movl    $77, (%rsp)
    movl    $66, %r9d
    movl    $55, %r8d
    movl    $44, %ecx
    movl    $33, %edx
    movl    $22, %esi
    movl    $11, %edi
    call    add

当添加k = i + j时,将以特殊方式使用主堆栈空间. 不按照我们通常认为的那样,每次使用堆栈空间时都会执行一次推入操作,但是会使用先前分配的48个空位,并使用-N(%rbp)从%指向的位置开始倒数rbp间隔的使用与每个推操作基本相同. 最后,由i + j计算的结果k以%eax保存. 然后,您需要准备调用add函数.

我们知道add函数的返回值将存储在%eax中,也就是说,%eax必须由子函数add覆盖,现在k的值将保存在%eax中. 如您在C程序中所看到的,调用add之后,我们再次使用了k的值,因此我们需要在覆盖add的调用中覆盖%eax之前保存%eax的值,并且添加后需要恢复%eax %eax. 该值(即k的值),因为%eax是“调用者保存”,所以父函数main应该保存%eax的值,因此在语句中有一个句子“ movl%eax,-12(%rbp)”. 上面的程序集是调用add函数来保存%eax的值.


本文来自电脑杂谈,转载请注明本文网址:
http://www.pc-fly.com/a/jisuanjixue/article-164560-1.html

相关阅读
    发表评论  请自觉遵守互联网相关的政策法规,严禁发布、暴力、反动的言论

    热点图片
    拼命载入中...