linux调试之栈帧原理与应用

linux调试之栈帧原理与应用

Posted by Albert on August 26, 2020

Linux调试之栈帧原理与应用

​ 上一篇文章讲解了如何在Linux应用程序中进行栈回溯,本篇我们以x86_64位为例,说明栈回溯的原理与过程,其中可能会涉及到少量汇编,Google查一下就可以了。rbp一般代表栈基址寄存器, rsp一般是栈顶寄存器:

一、 函数调用过程

首先我们先写一段测试代码,然后通过汇编来看下函数的调用流程。 需要注意的是,每种处理器的汇编代码不尽相同,所以分析都要建立在特定处理器上。

否则你可能去看其他人写的文章,可能又会得出其他结论。

代码如下:


/*************************************************************************
	> File Name: test_stack.c
	> Author: Albert Jie
	> Mail: huangjieajy@163.com 
	> Created Time: 2020年11月19日 星期四 14时48分23秒
 ************************************************************************/

#include <stdio.h>

int sub(int a, int b) {
    int result = 0;

    result = a - b;

    return result;
}

int main(int argc, char *argv[]) {
    int result = 0;

    result = sub(5, 3);

    printf("my debug result = %d \r\n", result);

    return 0;
}

编译该文件:

   gcc -o test_stack test_stack.c

使用gdb调试查看汇编代码:

   gdb  ./test_stack

查看main函数汇编代码
(gdb) disassemble main
Dump of assembler code for function main:
   0x0000000000000669 <+0>:	push   %rbp
   0x000000000000066a <+1>:	mov    %rsp,%rbp
   0x000000000000066d <+4>:	sub    $0x20,%rsp
   0x0000000000000671 <+8>:	mov    %edi,-0x14(%rbp)
   0x0000000000000674 <+11>:	mov    %rsi,-0x20(%rbp)
   0x0000000000000678 <+15>:	movl   $0x0,-0x4(%rbp)    # 栈基址向下偏移4个字节的地方存放result的值,也就是给result赋值为0
   0x000000000000067f <+22>:	mov    $0x3,%esi          # 将3赋值给esi寄存器
   0x0000000000000684 <+27>:	mov    $0x5,%edi          # 将5赋值给edi寄存器
   0x0000000000000689 <+32>:	callq  0x64a <sub>        # 调用sub函数
   0x000000000000068e <+37>:	mov    %eax,-0x4(%rbp)    # 将eax的值赋值给result, eax存放的是sub函数的返回值,看下面sub函数的汇编可以看到
   0x0000000000000691 <+40>:	mov    -0x4(%rbp),%eax    
   0x0000000000000694 <+43>:	mov    %eax,%esi
   0x0000000000000696 <+45>:	lea    0x97(%rip),%rdi        # 0x734
   0x000000000000069d <+52>:	mov    $0x0,%eax
   0x00000000000006a2 <+57>:	callq  0x520 <printf@plt>
   0x00000000000006a7 <+62>:	mov    $0x0,%eax
   0x00000000000006ac <+67>:	leaveq 
   0x00000000000006ad <+68>:	retq   
End of assembler dump.

查看sub函数的汇编代码:
(gdb) disassemble sub
Dump of assembler code for function sub:
   0x000000000000064a <+0>:	push   %rbp                 #保存栈基址寄存器,这里存放的是main函数的基地址
   0x000000000000064b <+1>:	mov    %rsp,%rbp            # 将rsp的地址赋值给当前的rbp.也就是rbp寄存器里面存放的就是sub函数的地址了
   0x000000000000064e <+4>:	mov    %edi,-0x14(%rbp)     # 将edi的值放到rbp基地址下偏0X14的位置,也就是形式参数5
   0x0000000000000651 <+7>:	mov    %esi,-0x18(%rbp)     # 将esi的值放到rbp基地址下偏0X18的位置,也就是形式参数3
   0x0000000000000654 <+10>:	movl   $0x0,-0x4(%rbp)  # 给result赋值为0
   0x000000000000065b <+17>:	mov    -0x14(%rbp),%eax # 将eax的值赋值为5
   0x000000000000065e <+20>:	sub    -0x18(%rbp),%eax # eax的值为eax - 3
   0x0000000000000661 <+23>:	mov    %eax,-0x4(%rbp)  # 将eax的值赋值给result
   0x0000000000000664 <+26>:	mov    -0x4(%rbp),%eax  # 将返回值放到eax中, 调用者就到这个寄存器里面拿值
   0x0000000000000667 <+29>:	pop    %rbp             # 恢复main函数的栈基址寄存器
   0x0000000000000668 <+30>:	retq                    # 返回原来的执行点
End of assembler dump.

可以看到,参数是在sub函数调用前传到相关的寄存器,后续在被调用函数中直接去取这些寄存器的值,就可以得到函数传过来的参数,由于处理器的寄存器数量有限,所以
如果函数参数过多,这些值可能会存放调用者的栈中。这个不同的处理器,可以用该demo 程序 实际测试看一下。

二、 栈帧原理

基于上面的函数,我们绘制出栈帧的图形:

image-20201119161715746

​ 可以发现,每调用一次函数,都会对调用者的栈基址(ebp)进行压栈操作,并且由于栈基址是由当时栈顶指针(esp)而来,我们会发现,各层函数的栈基址很巧妙的构成了一个链,即当前的栈基址指向下一层函数栈基址所在的位置, 所以我们可以依次根据ebp寄存器,来一步一步返回打印出每个函数调用地址,从而就打印出了栈回溯。

三、栈回溯的实现

原则上,我们可以通过获取当前函数的ebp栈基址寄存器,一步一步取上一个栈基址寄存器,从而找到整个调用栈的返回地址,最终打印出函数调用栈,这里不再多说。