dmp文件分析(三)- 栈帧结构和原理
| 阅读 | 共 2221 字,阅读约
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)