栈帧结构简析

1. 前言

函数的栈帧可以显示栈当前的使用情况,对于理解栈的使用以及定位bug比较有帮助。本文就使用一个简单的函数调用示例来学习栈帧的分布。主要使用32位和64位 gcc编译器分别进行编译,并查看相应的栈帧结构。

2. aach32栈帧分析

//ARCH: armv7
//GCC版本:arm-linux-gnueabi-gcc (Linaro GCC 5.4-2017.01) 5.4.1 20161213
int fun(int a,int b)
{
    
    
   int c = 1;
   int d = 2;
   
   return 0;
}

int main(int argc,char **argv)
{
    
    
   int a = 0;
   int b = 1;
   fun(a,b);
}

arm-linux-gnueabihf-gc -d a.out
反汇编后的结果:

......
00010398 <fun>:
   //将fun函数的栈底指针r7入栈
   10398:       b480            push    {
    
    r7}
   //分配20个字节的栈空间
   1039a:       b085            sub     sp, #20
   //fun函数的帧指针指向栈底
   1039c:       af00            add     r7, sp, #0
   //形参0入栈
   1039e:       6078            str     r0, [r7, #4]
   //形参1入栈
   103a0:       6039            str     r1, [r7, #0]
   //局部变量c入栈
   103a2:       2301            movs    r3, #1
   103a4:       60fb            str     r3, [r7, #12]
   //局部变量d入栈
   103a6:       2302            movs    r3, #2
   103a8:       60bb            str     r3, [r7, #8]
   //保存返回值为0
   103aa:       2300            movs    r3, #0
   103ac:       4618            mov     r0, r3
   //恢复sp为main函数的栈指针
   103ae:       3714            adds    r7, #20
   103b0:       46bd            mov     sp, r7
   //恢复fp(r7)为main函数的帧指针
   103b2:       bc80            pop     {
    
    r7}
   //返回到main函数
   103b4:       4770            bx      lr
   103b6:       bf00            nop

000103b8 <main>:
   //将main函数的栈底和lr入栈
   103b8:       b580            push    {
    
    r7, lr}
   //分配16字节的栈空间
   103ba:       b084            sub     sp, #16
   103bc:       af00            add     r7, sp, #0
   //形参argv入栈
   103be:       6078            str     r0, [r7, #4]
   //形参argc入栈
   103c0:       6039            str     r1, [r7, #0]
   //局部变量a入栈
   103c2:       2300            movs    r3, #0
   103c4:       60fb            str     r3, [r7, #12]
   //局部变量b入栈
   103c6:       2301            movs    r3, #1
   103c8:       60bb            str     r3, [r7, #8]
   //局部变量b存放到r1寄存器
   103ca:       68b9            ldr     r1, [r7, #8]
   //局部变量a存放到r0寄存器
   103cc:       68f8            ldr     r0, [r7, #12]
   //跳转到fun函数
   103ce:       f7ff ffe3       bl      10398 <fun>
   103d2:       2300            movs    r3, #0
   103d4:       4618            mov     r0, r3
   103d6:       3710            adds    r7, #16
   103d8:       46bd            mov     sp, r7
   103da:       bd80            pop     {
    
    r7, pc}
......

栈帧结构:
在这里插入图片描述

需要注意的是从上面的示例可以看到栈是如何被使用的,通过反汇编也可以看到是如何恢复到调用函数的,但是通过如上分析并不能看出栈帧是如何回溯的,因此栈帧的回溯需要依赖其他的部分。

3. aach64栈帧分析

//ARCH: armv8
//GCC版本:aarch64-linux-gnu-gcc (Linaro GCC 5.4-2017.01) 5.4.1 20161213
int fun2(int c,int d)
{
    
    
   return 0;
}

int fun1(int a,int b)
{
    
    
   int c = 1;
   int d = 2;
   
   fun2(c, d);
   return 0;
}

int main(int argc,char **argv)
{
    
    
   int a = 0;
   int b = 1;
   fun1(a,b);
}

aarch64-linux-gnu-objdump -d a.out
反汇编后的结果为:

0000000000400530 <fun2>:
  //更新sp到fun2的栈底
  400530:       d10043ff        sub     sp, sp, #0x10
  400534:       b9000fe0        str     w0, [sp,#12]
  400538:       b9000be1        str     w1, [sp,#8]
  40053c:       52800000        mov     w0, #0x0                        // #0
  400540:       910043ff        add     sp, sp, #0x10
  400544:       d65f03c0        ret

0000000000400548 <fun1>:
  //分配48字节栈空间,先更新sp=sp-48, 再入栈x29, x30, 此时sp指向栈顶
  400548:       a9bd7bfd        stp     x29, x30, [sp,#-48]!
  40054c:       910003fd        mov     x29, sp
  //入栈fun1参数0
  400550:       b9001fa0        str     w0, [x29,#28]
  //入栈fun1参数1
  400554:       b9001ba1        str     w1, [x29,#24]
  //入栈fun1局部变量c
  400558:       52800020        mov     w0, #0x1                        // #1
  40055c:       b9002fa0        str     w0, [x29,#44]
  //入栈fun1局部变量d
  400560:       52800040        mov     w0, #0x2                        // #2
  400564:       b9002ba0        str     w0, [x29,#40]
  400568:       b9402ba1        ldr     w1, [x29,#40]
  40056c:       b9402fa0        ldr     w0, [x29,#44]
  //跳转到fun2
  400570:       97fffff0        bl      400530 <fun2>
  400574:       52800000        mov     w0, #0x0                        // #0
  400578:       a8c37bfd        ldp     x29, x30, [sp],#48
  40057c:       d65f03c0        ret

0000000000400580 <main>:
  //分配48字节栈空间,先更新sp=sp-48, 再入栈x29, x30, 此时sp指向栈顶
  400580:       a9bd7bfd        stp     x29, x30, [sp,#-48]!
  //x29、sp指向栈顶
  400584:       910003fd        mov     x29, sp
  //入栈main参数0
  400588:       b9001fa0        str     w0, [x29,#28]
  //入栈main参数1
  40058c:       f9000ba1        str     x1, [x29,#16]
  //入栈变量a
  400590:       b9002fbf        str     wzr, [x29,#44]
  400594:       52800020        mov     w0, #0x1                        // #1
  //入栈变量b
  400598:       b9002ba0        str     w0, [x29,#40]
  40059c:       b9402ba1        ldr     w1, [x29,#40]
  4005a0:       b9402fa0        ldr     w0, [x29,#44]
  //跳转到fun1
  4005a4:       97ffffe9        bl      400548 <fun1>
  4005a8:       52800000        mov     w0, #0x0                        // #0
  4005ac:       a8c37bfd        ldp     x29, x30, [sp],#48
  4005b0:       d65f03c0        ret
  4005b4:       00000000        .inst   0x00000000 ; undefined

对应栈帧结构为:
在这里插入图片描述

总结一下:
通过对aarch64代码反汇编的分析,可以得出:

  1. 每个函数在入口处首先会分配栈空间,且一次分配,确定栈顶,之后sp将不再变化;
  2. 每个函数的栈顶部存放的是caller的栈顶指针,即fun1的栈顶存放的是main栈顶指针;
  3. 对于最后一级callee函数,由于x29保存了上一级caller的栈顶sp指针,因此不在需要入栈保存,如示例中fun2执行时,此时x29指向fun1的栈顶sp

4. x86_64栈帧分析

X86-64有16个64位寄存器,分别是:
%rax,%rbx,%rcx,%rdx,%esi,%edi,%rbp,%rsp,%r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15。
其中:
%rax 作为函数返回值使用。
%rsp 栈指针寄存器,指向栈顶
%rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数…
%rbx,%rbp,%r12,%r13,%14,%15 用作数据存储,遵循被调用者使用规则,简单说就是随便用,调用子函数之前要备份它,以防他被修改
%r10,%r11 用作数据存储,遵循调用者使用规则,简单说就是使用之前要先保存原值
%rip:相当pc,指向下一条将要运行的指令的地址
在这里插入图片描述
注:rip相当于arm中的pc
ref: :https://blog.csdn.net/zhbt1234/article/details/54019620

//ARCH: x86_64
//GCC版本:gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
int fun2(int c,int d)
{
    
    
   return 0;
}

int fun1(int a,int b)
{
    
    
   int c = 1;
   int d = 2;
   
   func2(c, d);
   return 0;
}

int main(int argc,char **argv)
{
    
    
   int a = 0;
   int b = 1;
   fun1(a,b);
}

objdump -d a.out
反编译后的结果如下:

00000000000005fa <fun2>:
//rbp保存fun1函数的栈底,入栈
 5fa:   55                      push   %rbp
 //初始化fun2函数的帧指针为栈底
 5fb:   48 89 e5                mov    %rsp,%rbp
 //参数c入栈
 5fe:   89 7d fc                mov    %edi,-0x4(%rbp)
 //参数d入栈
 601:   89 75 f8                mov    %esi,-0x8(%rbp)
 //返回值0
 604:   b8 00 00 00 00          mov    $0x0,%eax
 //恢复fun1的栈底指针rbp
 609:   5d                      pop    %rbp
 60a:   c3                      retq   

000000000000060b <fun1>:
 //rbp保存main函数的栈底,入栈
 60b:   55                      push   %rbp
 //初始化fun1函数的帧指针为栈底
 60c:   48 89 e5                mov    %rsp,%rbp
 //分配栈空间
 60f:   48 83 ec 18             sub    $0x18,%rsp
 //参数a入栈
 613:   89 7d ec                mov    %edi,-0x14(%rbp)
 //参数b入栈
 616:   89 75 e8                mov    %esi,-0x18(%rbp)
 //局部变量c入栈
 619:   c7 45 f8 01 00 00 00    movl   $0x1,-0x8(%rbp)
 //局部变量d入栈
 620:   c7 45 fc 02 00 00 00    movl   $0x2,-0x4(%rbp)
 //取出参数c,d分别存放到edi, esi寄存器准备调用fun1函数
 627:   8b 55 fc                mov    -0x4(%rbp),%edx
 62a:   8b 45 f8                mov    -0x8(%rbp),%eax
 62d:   89 d6                   mov    %edx,%esi
 62f:   89 c7                   mov    %eax,%edi
 //调用fun2
 631:   e8 c4 ff ff ff          callq  5fa <fun2>
 636:   b8 00 00 00 00          mov    $0x0,%eax
 63b:   c9                      leaveq 
 63c:   c3                      retq   

000000000000063d <main>:
 //rbp保存上一个函数的栈底,入栈
 63d:   55                      push   %rbp
 //初始化main函数的帧指针为栈底
 63e:   48 89 e5                mov    %rsp,%rbp
 //分配0x20的栈空间
 641:   48 83 ec 20             sub    $0x20,%rsp
 //入栈参数0
 645:   89 7d ec                mov    %edi,-0x14(%rbp)
 //入栈参数1
 648:   48 89 75 e0             mov    %rsi,-0x20(%rbp)
 //入栈局部变量a
 64c:   c7 45 f8 00 00 00 00    movl   $0x0,-0x8(%rbp)
 //入栈局部变量b
 653:   c7 45 fc 01 00 00 00    movl   $0x1,-0x4(%rbp)
 //取出参数a,b分别存放到edi, esi寄存器准备调用fun1函数
 65a:   8b 55 fc                mov    -0x4(%rbp),%edx
 65d:   8b 45 f8                mov    -0x8(%rbp),%eax
 660:   89 d6                   mov    %edx,%esi
 662:   89 c7                   mov    %eax,%edi
 //调用fun1函数
 664:   e8 a2 ff ff ff          callq  60b <fun1>
 669:   b8 00 00 00 00          mov    $0x0,%eax
 66e:   c9                      leaveq 
 66f:   c3                      retq 

根据如上反汇编代码,通过gdb单步实时查看当前栈的情况:

main:
-----------------------
rsp:0x7fffffffe9c0 入栈main函数的父函数的rip 
    0x7fffffffe9b8 入栈main函数的父函数的rbp
          

rbp: 0x7fffffffe9b8

0x7fffffffe998: 0xffffeac8      0x00007fff      0x00000001      0x00000001
                |     main参数1          |                      |main参数0|
0x7fffffffe9a8: 0x555546cd      0x00005555      0x00000000      0x00000001
                                                |局部变量a|      |局部变量b|
0x7fffffffe9b8: 0xffffe9e0      0x00007fff      0xffffeac8      0x00007fff
                |    main函数父函数的rbp  |      |     main函数父函数的rip  |
                
     

fun1:
----------------------
rsp:0x7fffffffe990  入栈main函数的rip
    0x7fffffffe988  入栈main函数的rbp
    0x7fffffffe970  分配栈空间

rbp:0x7fffffffe988  


0x7fffffffe968: 0x00000000      0x00000000      0x00000001      0x00000000
                                                |fun1参数b|     |fun1参数a|
0x7fffffffe978: 0x756e6547      0x00000001      0x00000002      0x00000000
                                                |局部变量c|      |局部变量d|
0x7fffffffe988: 0xffffe9b8      0x00007fff      0x55554672      0x00005555
                |    main函数的rbp        |      |     main函数的rip      |                                   


func2:
-----------------------
rsp: 0x7fffffffe968 入栈fun1函数的rip
     0x7fffffffe960 入栈fun1函数的rbp
     
     

rbp: 0x7fffffffe960


0x7fffffffe960: 0xffffe988      0x00007fff      0x55554636      0x00005555
                |    fun2函数的rbp       |      |     fun2函数的rip       | 


fun2返回时:
--------------------------
rsp:0x7fffffffe968 出栈fun1函数的rbp
    0x7fffffffe970 出栈fun1函数的rip

rbp:0x7fffffffe988

fun1返回时:
-------------------------
rsp:0x7fffffffe990 出栈main函数的rbp
    0x7fffffffe998 出栈main函数的rip

rbp:0x7fffffffe9b8

栈帧结构为:
在这里插入图片描述

总结一下:
通过对x86 64代码反汇编的分析,与aarch64还是有区别的,可以得出:

  1. 每个函数在入口处首先会分配栈空间,且一次分配,确定栈顶,之后rsp将不再变化;
  2. 每个函数的栈底部存放的是caller的栈底指针,即fun1的栈底存放的是main栈底指针;
  3. 对于最后一级callee函数,rbp指向自身栈底,如示例中fun2执行时,此时rbp指向fun2的栈底

总结一句话:
aarch64当前栈顶保存上一级函数的栈顶;x86_64当前栈底保存上一级函数的栈底

参考文档

  1. https://gcc.gnu.org/gcc-5/changes.html#arm
  2. https://f5.pm/go-30007.html
  3. https://developer.arm.com/documentation/ihi0038/b/
  4. 内核中dump_stack的实现原理(1) —— 栈回溯

Guess you like

Origin blog.csdn.net/jasonactions/article/details/117749169