使用GDB探索进程的栈以及栈帧的内容

 

进程与栈的关系、栈与栈帧的关系

        一个进程里面包含了很多内容,既有代码段、静态变量以及全局变量、无效区域、堆、栈,也还有最上面的内核态的东西。

         这里只讨论用户态的栈。

         用户态的栈可以被分为多个栈帧,多个栈帧自上而下组成了栈,栈帧是描述了一个函数的执行过程。

         ebp寄存器保存了一个栈帧的底部地址,esp寄存器保存了一个栈帧的顶部地址。

 

栈帧的组成

栈帧包括了如下内容:

  • 该函数在上层函数中,下一个语句的地址(如test函数栈帧中保存了int b = 2这条指令的地址),也可以认为这个算上个栈帧的内容。
  • ebp的值(上层函数的栈帧底部地址,在test的栈帧中,ebp为main的变量开始保存的地址。)
  • 调试模式产生的一些标记
  • 函数的参数(前6个在本栈栈帧中,其他的在上层函数的栈帧里面)
  • 函数内声明的变量
  • 调用的函数的大于6个参数的那些参数

 

栈帧的形成过程

        当fun1调用一个函数fun2时,函数fun2的参数先被保存在寄存器(rdi, rsi, rdx, rcx, r8d, r9d)中,如果参数大于6个,这些多出来的参数会被push到当前栈中,也就是fun1的栈帧。函数的地址不会被保存在栈中。

        rbp表示栈帧的开始,rsp表示栈帧的结束。在fun1中call fun2时,会执行push fun2的下一个地址,所以rsp会减少8字节,进入fun2的第一步是push rbp,所以rsp又会减小8字节。这时再保存rbp, 然后把rsp的值给rbp。于是就会发现这次的栈帧的开始相较于上次的栈帧顶部差了16字节。

GDB的操作

        使用GDB操作可以分为:1.  进入调试  2. 显示汇编代码  3. 显示寄存器内容 4. 单步调试  5. 查看栈内容

1. 在terminal输入

gdb -q -tui a.out     

   # -q是不要显示那么多版本信息,quiet,-tui是打开ui窗口,a.out是程序  

2. 在进入的gdb界面输入以下命令

layout asm
layout regs

3. 运行到main处

start

 4. 单步调试,因为是在汇编基础上调试,所以使用  ni 和si 命令, ni代表不进入函数内部, si代表进入函数内部

 5. 查看栈内容,其实也就是查看指定地址的内存存放的内容, 这个地址后面演示的时候再填写,这样就会显示出一些地址的内容

x /40xw  0x???????

实战 

    源代码 

   main函数调用test,test有9个参数,从右到左,将大于6个参数的压栈,然后将最左6个存入寄存器。

int test(int a, int b, int c, int d, int e, int f, int g, int h, int i)
{
    int res = a+b+c+d+e+f+g+h+i;
    int aa = 10;
    int bb = 11;
    int cc = 12;
    return res;
}

int main()
{
    int aaa = 1;
    test(1,2,3,4,5,6,7,8,9);
    int bbb = 2;
}

 main函数相应的汇编, 可以验证之前所说的形成栈帧的过程。 

       如push 9  push 8  push 7,   mov 6.... mov 5.....

       需要说明的是callq , callq将0x11cb也就是当前callq所在行的下一行放入了栈里面。之后retq会将其pop出,用于跳转指令

    0x118d <main>           endbr64                                                                                                                                                 │
│   0x1191 <main+4>         push   %rbp                                                                                                                                             │
│   0x1192 <main+5>         mov    %rsp,%rbp                                                                                                                                        │
│   0x1195 <main+8>         sub    $0x10,%rsp                                                                                                                                       │
│   0x1199 <main+12>        movl   $0x1,-0x8(%rbp)                                                                                                                                  │
│   0x11a0 <main+19>        pushq  $0x9                                                                                                                                             │
│   0x11a2 <main+21>        pushq  $0x8                                                                                                                                             │
│   0x11a4 <main+23>        pushq  $0x7                                                                                                                                             │
│   0x11a6 <main+25>        mov    $0x6,%r9d                                                                                                                                        │
│   0x11ac <main+31>        mov    $0x5,%r8d                                                                                                                                        │
│   0x11b2 <main+37>        mov    $0x4,%ecx                                                                                                                                        │
│   0x11b7 <main+42>        mov    $0x3,%edx                                                                                                                                        │
│   0x11bc <main+47>        mov    $0x2,%esi                                                                                                                                        │
│   0x11c1 <main+52>        mov    $0x1,%edi
 
    0x11c6 <main+57>                callq  0x1129 <test>                                                                                                                            │
│   0x11cb <main+62>                add    $0x18,%rsp                                                                                                                               │
│   0x11cf <main+66>                movl   $0x2,-0x4(%rbp)                                                                                                                          │
│   0x11d6 <main+73>                mov    $0x0,%eax                                                                                                                                │
│   0x11db <main+78>                leaveq                                                                                                                                          │
│   0x11dc <main+79>                retq                                                                                                                                            │
│   0x11dd                              nopl   (%rax)                                                                                                                               │
│   0x11e0 <__libc_csu_init>        endbr64                                                                                                                                         │
│   0x11e4 <__libc_csu_init+4>      push   %r15 

再看 test的汇编 , 

1. push rbp,   mov  rsp, rbp, 即保存rbp的值,这可以帮助知道函数调用关系。在每一个push的时候,rsp的值都会变小,pop则会使rsp变大

2. 把之前存入寄存器的内容重新压入栈里面,  mov edi, -0x14(%rbp), 也就是比rbp地址小0x14字节的位置放了1, 其他依次往后, 占据了 -0x14(%rbp) 到 -0x28(%rbp)。 这里面存了 1,2,3,4,5,6 ,而7 8 9 则存在比rbp大的地方,可以从上面的汇编代码看出

3.  mov $0xa, -0xc(%rbp)  和 mov $0xb, -0x8(%rbp) 和mov $0xc, -0x4(%rbp) 这是函数里面的三个临时变量,可以看出临时变量更加靠近栈帧的底部。

4. mov -0x10(%rbp), %eax则是将某个值给了eax,eax寄存器通常用于保存一个函数的返回值。

5. retq 则表示pop pc, 也就是将之前压入栈的call 下面的指令的地址拿出来,换给pc, 而pc代表了 程序计数器,也就是下次执行指令要去的地方。

 

 

 如何验证

使用 gdb的x命令,当我们打开寄存器显示时,里面有rbp,我们将rbp附近的内容打印出来就好了。

在main函数内部时, rbp的内容为 0x7fffffffde60 , rsp会随着程序运行逐渐变小。

 进入test时,根据最先的两个汇编指令,可以知道某个内存就要保存0x7fffffffde60。

    查看某个地址的内容,我选了0x7fffffffde00,因为此时的rbp为0x7fffffffde28,而之前的汇编显示了有个值会存在 rbp下偏移0x28的位置。 可以看到图片中的6 5 4 3 2 1这是前6个参数。 也可以看到再往下的 9 8 7. 还能看到0x7fffffffde60 。而这个位置再往前就是0x555551cb,这是test函数的下一行指令的地址。 而我们接着往下走,还可以知道main执行完了之后会跳到哪里。按下面的图,那个地址应该是 0xde80b3,这是main执行后应该做的事情。如下图,我们也确实看到这个地址被保存在内存里了。

 

总结

    通过这一系列的过程,我不仅明白了栈帧的结构,同时还理解了函数调用堆栈是怎么成功找到上层函数的。但这还只是基础中的基础,未来还很漫长啊 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值