dmp文件分析(三)- 栈帧结构和原理


| 阅读 |,阅读约 5 分钟
| 复制链接:

Overview

dmp文件分析(三)- 寄存器与栈帧结构

上一篇文件讲解了一个崩溃调用栈中第一个栈帧的分析过程:

  • 获取指令寄存器eip的地址,通过该地址去模块列表中匹配,找到符合的模块,确定崩溃在哪个模块了
  • 通过模块信息,根据一定路径拼接规则去查找给定模块的符号文件是否存在
  • 解析符号文件,并保存
  • 将符号文件的内存地址和模块的内存地址做匹配,得到崩溃模块的源码信息

分析完一个栈帧后,会根据栈帧结构,查找上层调用函数的eip,继续分析下一个栈帧,再继续分析源码之前,先介绍一下栈帧结构和寄存器相关知识,breakpad中主要用到eip、ebp、esp

寄存器

寄存器是CPU内部用来存放数据的一些小型存储区域,包括通用寄存器、专用寄存器和控制寄存器。寄存器拥有非常高的读写速度,所以在寄存器之间的数据传送非常快。不同架构cpu的寄存器不一样,本文介绍32位cpu经典寄存器。包括:

  • 数据寄存器:4个:EAX,EBX,ECX,EDX
  • 指令寄存器:1个:EIP
  • 指针寄存器:2个:ESP、EBP
  • 变址寄存器:2个:ESI、EDI
  • 标志寄存器:1个:EFlags

寄存器

数据寄存器

  • 数据寄存器用于保存操作数和计算结果

  • EAX、EBX、ECX、EDX为32位寄存器,对低16位数据的读取,不会影响高16位数据的读取

  • 低16位寄存器命名为:AX、BX、CX、DX

  • 低16位寄存器又可分割为8个独立的位寄存器,每个寄存器有自己独立的名字,可以独立存取

    • AX:AH-AL
    • BX:BH-BL
    • CX:CH-CL
    • DX:DH-DL
  • EAX:累加器,用于乘、除、输入、输出(赋值)等操作,使用频率很高

  • EBX:基地址寄存器,经常当做返回值使用

  • ECX:计数寄存器,用于控制循环次数

  • EDX:数据寄存器,在乘、除运算时的作为默认操作数,也可用于io操作存储端口

指针寄存器

  • 主要用于访问堆栈内的存储单元
  • EBP:基指针寄存器,用它可直接存取堆栈中的数据
  • ESP:堆栈指针寄存器,只可以访问栈顶
  • breakpad中用到ebp和esp,根据栈帧结构做堆栈调用分析

指令寄存器

  • EIP,ip(Instruction Pointer):存放下次将要执行的指令在代码段的偏移量
  • breakpad中,前面我们多次提到,会以eip的地址做为分析的起始地址

变址寄存器

  • 主要用于存放存储单元在段内的偏移量,目的:存储寻址
  • 用它们可实现多种存储器操作数的寻址方式,为以不同的地址形式访问存储单元提供方便

标志寄存器

标志寄存器包括多个标志位:

  • 进位标志:CF
  • 奇偶标志:PF
  • 辅助进位标志:ACF
  • 零标志:ZF
  • 溢出标志:OF

地址空间

在真正了解栈帧结构前,需要对虚拟地址空间和进程地址空间有一定了解。

虚拟地址空间

  • 操作系统提供了内存的一种抽象概念:虚拟地址空间,使得应用程序不用关心物理内存就可以执行操作
  • 虚拟地址空间分为两类:
    • 内核空间:32位系统,0xC000000000~0xFFFFFFFF共1G的大小
    • 用户空间:32位系统,0x0000000000~0xBFFFFFFF共3G的大小
  • 内核空间是系统预留的,用户进程只能使用用户空间

进程地址空间

  • 每个进程都有一个连续完整的地址空间。
  • 进程的地址空间是分段的,每个段都有特定的作用:
    • Code VMA:代码段,CPU执行的机器指令部分。通常,这一段是可以共享的,即多线程共享进程的代码段。并且,此段是只读的,不能修改。
    • Data VMA: 即程序的数据段
    • 堆:new或者malloc分配的空间在堆上,需要程序猿维护,若没有主动释放堆上的空间,进程运行结束后会被释放。由低向高增长
    • 栈:栈上的是函数栈临时的变量,还有程序的局部变量,自动释放。由高向低下降
    • 共享库和mmap内容映射区:位于栈和堆之间,例如程序使用的printf,函数共享库printf.o固定在某个物理内存位置上,让许多进程映射共享。mmap是一个系统函数,可以把磁盘文件的一部分直接映射到内存,这样文件中的位置直接就有对应的内存地址

地址空间

栈帧结构

一个函数在运行时,会在栈上申明局部变量,为单个过程分配的那部分栈就叫做栈帧。当有子函数调用时,会继续在栈上形成新的栈帧。调用者与被调用者形成的栈帧结构为:

栈帧结构

栈帧有如下特点:

  • 每次函数调用都会形成新的栈帧,函数退出时栈被回收,栈帧消失
  • 栈帧的两端,由基指针寄存器EBP和栈指针寄存器ESP界定

栈帧原理

函数调用可能是有很多层的,但是EBP和ESP只有两个,它是如何实现函数调用的呢?— 跟保存局部变量一样,将上一个栈帧的EBP入栈保存,函数调用结束时,将EBP出栈,就能快速恢复上一次的栈帧。

函数调用的栈帧变化情况

  • 父函数将调用参数从后向前压栈
  • 将返回地址压栈保存
  • 跳转到子函数地址执行
  • 子函数将父函数栈帧起始地址EBP压栈
  • 将EBP的值设置为当前ESP的值(EBP指向子函数栈帧起始地址)

函数返回的栈帧变化情况

函数返回时,从EAX获取返回值,之后需要将栈结构恢复到调用开始的状态,并跳转到父函数的返回地址继续执行。由于函数调用时,已经保存了返回地址和父函数栈帧起始地址,只需执行两步即可恢复栈帧:

  • 将EBP的值传给ESP,即让当前指针指向栈的起始位置
  • 将当前栈顶函数退栈,被传给EBP,即让EBP指向上次保存的父函数的栈底

使用汇编语言描述,可以表示为:

1mov %ebp %esp
2pop %ebp

由上面的分析,可以发现,只需要知道当前栈帧的寄存器情况,就可以通过一定规律,还原整个堆栈调用过程,表示的公式如下,这个公式将在breakpad中的栈帧查找中用得到:

caller: 调用函数(父函数) callee: 被调用函数(子函数)

1%caller_esp = *(%callee_ebp + 16)
2%caller_eip = *(%callee_ebp + 8)
3%caller_ebp = *(%callee_ebp)