海外优质VPS
高性价比免备案主机推荐

x86_64架构下的函数调用及栈帧原理


x86_64架构下的函数调用及栈帧原理
本文为看雪论坛优秀文章
看雪论坛作者ID:有毒



一、x86_64寄存器


在分析函数调用时,必须要对CPU的寄存器熟悉。在所有的体系架构中,每个寄存器都有建议的使用方法,编译器在对代码进行编译时,也通常按照体系架构建议的寄存器的使用方法进行编译。

在x86_64体系架构中,总共有16个64位通用寄存器,各寄存器及用途如下所示:
 
x86_64架构下的函数调用及栈帧原理
 
对上图中的寄存器做简单说明:
  • %rax :通常存储函数调用的返回结果,也被用在idiv (除法)和imul(乘法)命令中。
  • %rsp :堆栈指针寄存器,指向栈顶位置。pop操作通过增大rsp的值实现出栈,push操作通过减小rsp的值实现入栈。
  • %rbp :栈帧指针,标识当前栈帧的起始位置。
  • %rdi, %rsi, %rdx, %rcx,%r8, %r9 :六个寄存器,当参数少于7个时, 参数从左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9;当参数为7个以上时,前 6 个与前面一样, 但后面的依次从 “右向左” 放入栈中,即和32位汇编一样。
; 参数>=7Func(a, b, c, d, e, f, g, h);;参数的具体存放方式a->%rdi, b->%rsi, c->%rdx, d->%rcx, e->%r8, f->%r9h->8(%esp)g->(%esp)call Func
(备注:用栈在进行参数传递时,即便参数<8字节,也要对齐放在8字节的空间中)
  • 函数执行前后必须保持原始的寄存器有3个:是rbx、rbp、rsp。rx寄存器中,最后4个必须保持原值:r12、r13、r14、r15。保持原值的意义是为了让当前函数有可信任的寄存器,减小在函数调用过程中的保存&恢复操作。除了rbp、rsp用于特定用途外,其余5个寄存器可随意使用。
  • Caller Save 和 Callee Save寄存器 : 寄存器的值是由”调用者保存“ 还是由 ”被调用者保存“。当进行函数调用时,子函数通常也会使用通用寄存器,但这些寄存器中可能保存着父函数(调用者)的值。如果是Caller Save 寄存器,在进行子函数调用之前,需要由调用者提前保存寄存器中的值(入栈),然后在子函数中可以向这些寄存器中写入任何数据;在完成调用后,恢复寄存器原来的值(出栈)。如果是Callee Save寄存器,父函数在进行子函数调用前不会保存寄存器中的值,在调用子函数后,子函数会首先保存寄存器中的值(入栈);子函数完成功能后,恢复寄存器中的值,然后再返回到父函数,结束调用。



    二、函数调用时的栈帧


    >>>>

    1. 函数调用


    在对子函数进行调用时,栈帧情况如下:
     
    x86_64架构下的函数调用及栈帧原理
     
    (注意此处栈帧增长方向从上到下)
    • 调用者栈帧中,保存了被调用函数的参数以及调用者的返回地址,其流程大致如下:
      父函数将调用参数从右到左依次压栈->返回地址入栈->跳转到子函数起始地址->子函数将父函数栈帧起始地址(%rbp)压栈->将%rbp 的值设置为当前 %rsp 的值,开辟栈帧空间
    • 函数调用时的汇编指令如下:
    ...   ;参数压栈call Func  ;将返回地址压栈,并跳转到子函数处执行...  ;函数调用的返回位置 Func:  ;子函数入口pushq %rbp  ;保存父函数的帧指针,创建新栈帧movq  %rsp, %rbp  ;让 %rbp 指向新栈帧的起始位置subq  $N, %rsp  ;开辟栈帧空间供子程序使用
    以上过程由编译器自动完成。需要注意的是,父函数中进行参数压栈时,顺序为从右到左,但并不是固定,要看编译器的具体实现(gcc使用的是从右到左)。

    >>>>

    2. 函数返回


    函数返回时,我们需要的数据是函数的返回值(%rax),然后将栈结构恢复到函数调用之前的状态,最后跳转到父函数的返回地址继续执行。需要执行以下两条指令:

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

    为了便于栈帧恢复,x86_64 架构中提供了 leave 指令来实现上述两条命令的功能。执行 leave 后,前面图中函数调用的栈帧结构如下:
     
    x86_64架构下的函数调用及栈帧原理
     
    调用 leave 后,%rsp 指向返回地址;ret 指令,从栈顶弹出数据,并跳转到此数据指向的地址处。在leave 执行后,%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;}

    上面程序生成的和子函数调用相关的汇编程序如下:

    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

    首先看 main 函数的前三条汇编语句:

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

    保存父函数栈帧,之后创建main 函数的栈帧并且分配了48 Byte 的空间。执行完成后,main 函数的栈帧如下图所示:

    x86_64架构下的函数调用及栈帧原理
     
    继续往后走,可以看到对k=i+j的处理过程:

    movl    $10, -20(%rbp)movl    $20, -16(%rbp)movl    -16(%rbp), %eaxaddl    -20(%rbp), %eaxmovl    %eax, -12(%rbp) movl    $88, 8(%rsp)movl    $77, (%rsp)movl    $66, %r9dmovl    $55, %r8dmovl    $44, %ecxmovl    $33, %edxmovl    $22, %esimovl    $11, %edicall    add

    需要注意的是,传统的栈空间的利用操作是使用一个栈空间,进行一次push操作。

    但是我们的代码里没有这样进行,而是利用之前分配的48字节的空间,以空间的缩减进行分配,本质上是和push操作是一样的。最终的计算结果保存在%eax中。
     
    后续进行add函数调用,add的返回值将会放在%eax中,当前%eax中保存的是k值。

    所以需要首先保存%eax中的值,然后在add函数中进行调用,最后再恢复%eax的值。%eax 是 Caller Save的,所以由父函数main函数来进行保存(movl %eax,-12(%rbp))。
     
    再往后,开始参数入栈,前6个参数依次保存到对应的寄存器中,最后两个参数从右到左压入栈中。
     
    进入 add 函数后之后的操作如下:

    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

    首先创建新栈帧,然后进行参数入栈。
     
    在参数入栈时,我们看到并未使用 push 之类的指令,也没有调整 %esp 指针的值,而是使用了 -N(%rbp) 这样的指令来使用新的栈空间。这种使用”基地址+偏移量“ 来使用栈的方式和直接使用 %esp 指向栈顶的方式其实是一样的。
     
    当add函数返回后,返回结果存储在%eax 中,%rbp 和 %rsp 调整为指向 main 的栈帧,之后执行main 函数中的如下指令:

    movl    %eax, -8(%rbp)  ; 保存 add 函数返回值到栈中,对应 C 语句 int sum = add(...)movl   -12(%rbp), %eax  ; 恢复寄存器 %eax 的值,与调用add前保存 %eax 相对应movl    %eax, -4(%rbp) ; 对应 C 语句 m = k,%eax 中的值就是 k。movl    $0, %eax  ; main 函数返回值leave   ; main 函数返回ret

    add 函数返回时,把返回值保存到了 %eax 中,使用完返回值后,会恢复 caller save 寄存器 %eax的值,这时main 栈帧恢复到调用add函数之前的状态。
     
    需要注意的是,在调用 add 之前,main 中执行了一条 subq 48, %rsp 的指令,这主要是因为main函数并未再调用其他函数,结尾处的leave、ret两条指令直接覆盖了%rsp的值从而回到了父栈帧中。

    如果先调整 main 栈帧的 %rsp 值,之后 leave 再覆盖 %rsp 的值,相当于调整是多余的。因而省略main 中 add返回之后的 %rsp 的调整,而使用 leave 直接覆盖%rsp更为合理。
     
    之前对这一块不是很懂,看了大佬的文章后就明白了~


    参考链接


    最后附上大佬的知乎链接,这篇文章解决了我很多疑惑。
    https://zhuanlan.zhihu.com/p/27339191

    x86_64架构下的函数调用及栈帧原理

    x86_64架构下的函数调用及栈帧原理
    – End –

    x86_64架构下的函数调用及栈帧原理



    看雪ID:有毒

    https://bbs.pediy.com/user-779730.htm 

    *本文由看雪论坛 有毒 原创,转载请注明来自看雪社区。



    推荐文章++++

    x86_64架构下的函数调用及栈帧原理

    * CVE-2017-11882理论以及实战样本分析

    * 恶意代码分析之 RC4 算法学习

    * CVE-2017-0101-Win32k提权分析笔记

    * ROPEmporium全解

    * 实战栈溢出漏洞


    好书推荐x86_64架构下的函数调用及栈帧原理



    x86_64架构下的函数调用及栈帧原理
    公众号ID:ikanxue
    官方微博:看雪安全
    商务合作:wsc@kanxue.com

    x86_64架构下的函数调用及栈帧原理
    戳“阅读原文”一起来充电吧!

    《x86_64架构下的函数调用及栈帧原理》原文地址:http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458303473&idx=1&sn=fe368c6bec6190ef59be725b163663d8&chksm=b1818b7b86f6026d1d45679948ffc52642a4fdb7fc83f9f3c54258efb29483602ec667c86ea7

    赞(0)
    本站所刊载内容均为网络上收集整理,本站不保证其真实性和准确性,所有内容仅供大家娱乐参考。如有异议,请与本站联系,会尽快处理争议内容。VPS主机推荐 » x86_64架构下的函数调用及栈帧原理

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址