《coredump问题原理探究》Windows版 笔记
原文链接:http://blog.csdn.net/xuzhina/article/details/8247701qinquan
侵删
一、环境搭建
1、Win7捕获程序dump
注册表HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows/Windows Error Reporting/LocalDumps
中新建几个key
2、Windbg符号表设置(Symbols Search Path)
自动下载
D:\Debug\Symbols;SRV\*D:\Debug\Symbols\*http://msdl.microsoft.com/download/symbols;D:\Debug\Dump
手动下载
https://developer.microsoft.com/en-us/windows/hardware/download-symbols
二、WinDbg命令
命令 |
含义 |
实例 |
x |
显示所有上下文中符合某种模式的符号 |
x Test!m* |
bp |
设置一个或多个软件断点;可通过 组合、地址、条件、选项来设置多种类型的断点 |
bp Test!main |
u |
显示出内存里某段程序的汇编 |
u Test!main |
g |
开始执行指令的进程或线程;当出现下列情况,执行停止: 1、进程结束;2、执行到断点;3、某个时间导致调试器终止 |
|
dd |
显示指定范围内存单位的内容(双字dword) |
dd esp L 8 |
da |
显示指定内存开始的字符串 |
da 004020dc |
db |
单字节显示内存 |
db esp |
t |
执行一条指令或一行代码,并显示出所有寄存器和状态的值 |
Trace |
p |
执行一条指令或一行代码,并显示出所有寄存器和状态的值 当函数调用或中断发生时,也只是作为一条指令执行 |
Step |
kbn |
显示指定线程的栈帧,并显示相关信息 |
|
.frame n |
切换到栈帧n |
|
ln |
查找就近的符号 |
ln 004010e0 |
dt -v 结构体名 地址 |
打印结构体 |
dt -v _HEAP_ENTRY 00730000 |
!heap -a |
查看堆信息 |
|
!heap -x 地址 |
查看地址所属的堆块信息 |
!heap -x 00730000 |
!heap -hf 地址 |
查看堆上所有的堆块 |
!heap -hf 00730000 |
三、函数栈帧
1、栈内存布局
返回地址ret
上一栈帧地址fp
局部变量
从右往左压入参数
返回地址ret
......
2、栈溢出
往局部变量中写数据越界,导致淹没了fp和ret,函数返回时ebp、eip就会被写入非法值
此时fp/ret对的链表关系被破坏,调试器无法显示正确的函数栈
3、栈的规律
- esp的值不会被覆盖,永远指向栈顶
- fp/ret对的链表没有被完全破坏,往高地址的一些地方还是保持这种关系
- 从栈顶到栈底,内存地址由低到高。如果帧指针fp1的内容是fp2,fp2的内容是fp3,那么存在:esp<fp1<fp2<fp3
- 如果两对(fp1,ret2)、(fp2,ret2)符合条件:fp1的内容刚好是fp2,那么它们除了满足第3点外,还满足:ln ret1和ln ret2都能列出函数符号,ret2的上一条指令一定是调用ret1所在的函数
4、定位栈溢出问题的经验方法
- dd esp查看由esp开始的内存
- 找到某个内容比esp的值稍大、数值相差不是太远的内存单元,称为FP1。下一个单元称为RET1
- ln查看RET1的内容。如果没有显示函数符号,跳过FP1,回到第2步
- dd *FP1 L 2得到两个内存单元(FP2,RET2)。如果FP2的内容小于FP1的内容,跳过FP1,回到第2步
- ln查看RET2的内容。如果没有显示函数符号,跳过FP1,回到第2步;如果OK,跳到FP2,回到第4步
四、函数逆向
汇编代码中跳转和循环为函数的骨架,要优先寻找
五、C内存布局
1、基本类型
类型 |
特征 |
char |
byte ptr 挤在一起 |
short |
word ptr 占用四字节空间 |
int |
dword ptr |
long |
dword ptr win64采用LLP64标准,Windows系统中long和int是相同的 |
float |
dword ptr 单精度占四字节,要配合浮点计算指令确认 |
double |
qword ptr 双精度占八字节,要配合浮点计算指令确认 |
指针 |
lea |
2、数组类型
类型 |
特征 |
char |
基地址 + 索引值 * 1 |
short |
基地址 + 索引值 * 2 数组时会挤在一起,注意与基本类型的区别 |
int |
基地址 + 索引值 * 4 |
long |
基地址 + 索引值 * 4 |
float |
基地址 + 索引值 * 4 单精度占四字节,要配合浮点计算指令确认 |
double |
基地址 + 索引值 * 4 双精度占八字节,要配合浮点计算指令确认 |
指针 |
基地址 + 索引值 * 4 |
3、结构体
- 成员全是基本类型的结构体: 先把一个基地址放到某寄存器中,访问成员时在基址上加上前面所有成员的大小,每个成员与基址的偏移量不固定
- 复合类型构成的结构体: 同上,没有特别
- 结构体数组: 找到数组的首地址;根据索引找到每个元素的地址;以每个元素的地址作为结构体基址,获取成员变量的地址
六、C++内存布局
1、类的内存布局
类的成员变量排列与结构体相同
2、this指针
调用类的成员函数时,this指针放在ecx寄存器中传递,不入栈
- 调用函数时的汇编代码:
- lea ecx,[ebp-1]
- call Test!Test::print (00ec1030)
- 被调函数开始的汇编代码:
- mov dword ptr [ebp-4],ecx
- mov eax,dword ptr [ebp-4]
- push eax
3、虚函数表及虚表指针
4、单继承
子类先调用基类的构造函数,再初始化自己的成员变量,然后设置虚表指针
子类虚函数表分布规律:
- 重载基类的虚函数,按照基类虚函数声明顺序排列,和子类声明顺序无关
- 子类独有的虚函数,按照虚函数的声明顺序排列,追加在重载虚函数的后面
5、多继承(无公共基类)
- 子类对象的大小等于各个基类的大小(虚表指针+成员变量)加上自身成员变量的大小
- 各基类在子类里的“隐含对象”是按照继承顺序来排列的,和基类的声明、定义顺序无关
- 每个基类都(尽可能)有自己的虚函数表;子类独有的虚函数追加到第一个虚函数表的后面;子类重载所有虚表中的同名虚函数
- 子类对象指针转换成基类指针,实际上是把子类对象包含的对应基类的“隐含对象”的地址赋值给基类指针
- 当一个虚函数在多个虚表中都出现时,实际上只会完全重载第一个虚表中的该函数。其余虚表中重载代码是通过调整this指针为子类的地址,然后跳转到子类对应函数来实现
- 0:000> u 012e10a8 L 5
- Test![thunk]:Child::print`adjustor{12}‘:
- 012e10a8 83e90c sub ecx,0Ch // ecx是基类“隐含对象”的地址,需调整
- 012e10ab e9e0ffffff jmp Test!Child::print (012e1090)
七、STL容器内存布局
1、vector
一个vector在栈上占三个单元(指针):
第一个_Myfirst指向vector元素的开始
第二个_Mylast指向vector元素结束的下一个位置
第三个_Myend指向vector空间结束的位置
注意:vector的begin()、end()、push_back()等成员函数的汇编,最后是:ret 4
2、list
- list有两个成员:第一个_Myhead指向链表的头部节点,第二个_Size表明链表中的节点元素个数
- 链表中的每个节点包含三个成员:第一个_Next指向下一个节点,第二个_Prev指向前一个节点,第三个_Myval存储节点的值
- list初始化时会生成一个头节点
- 头节点的_Next指向链表第一个节点,链表最后一个节点的_Next指向头节点
- 头节点的_Prev指向链表最后一个节点,链表第一个节点的_Prev指向头节点
注意:图中_Prev指针应该都指向节点起始位置,而不是_Prev指针的位置。这里只是为了突出两个链路
3、map
- map有两个成员:_Myhead指向头节点,_Mysize表明map包含的元素个数
- 头节点的三个指针分别指向树的最左节点、树的根节点、树的最右节点
- 树的根节点的_Parent指向头节点
- 树的叶子节点的_Left、_Right指向头节点
4、set
由于map、set本身的定义都没有声明任何成员变量,所有的成员变量都是从_Tree继承过来的,唯一的区别是traits的定义不一样,因此:set的特征和map类似
- template<class _Kty,
- class _Pr = less<_Kty>,
- class _Alloc = allocator<_Kty> >
- class set : public _Tree<_Tset_traits<_Kty, _Pr, _Alloc, false> >
- { ...... }
-
- template<class _Kty,
- class _Ty,
- class _Pr = less<_Kty>,
- class _Alloc = allocator<pair<const _Kty, _Ty> > >
- class map : public _Tree<_Tmap_traits<_Kty, _Ty, _Pr, _Alloc, false> >
- { ...... }
5、iterator
- vector的iterator只有一个成员_Ptr,取值范围:vec._Myfirst <= _Ptr < vec._Mylast
- list的iterator也只有一个成员_Ptr,指向list中的每个节点(头节点除外)
- map和set的iterator也只有一个成员_Ptr,指向map或set的节点,且iterator的遍历采用中序遍历
实际调试中,set的iterator指向节点的值在for循环中是按照0、1、2、3......、f的顺序遍历
6、string
string有三个成员:联合体_Bx,紧接着的是字符串长度_Mysize,预留空间大小_Myres
- 当_Mysize < _BUF_SIZE(16)时,字符串存储在_Bx的_Buf里
- 当_Mysize >= _BUF_SIZE(16)时,字符串存储在_Bx的_Ptr指向的内存中
-
- template<class _Val_types>
- class _String_val : public _Container_base
- {
- public:
- ......
- enum {
- _BUF_SIZE = 16 / sizeof (value_type) < 1
- ? 1
- : 16 / sizeof (value_type)
- };
- ......
- union _Bxty {
- value_type _Buf[_BUF_SIZE];
- pointer _Ptr;
- char _Alias[_BUF_SIZE];
- } _Bx;
- size_type _Mysize;
- size_type _Myres;
- };
八、堆结构
1、NT内核堆的改造
文档中介绍的堆结构是XP环境下的。MS从Vista开始对NT内核做了较大改动,其中包括堆的改造。最直观的改造:
- _HEAP中采用链表方式管理_HEAP_SEGMENT,解除数组的限制
- _HEAP_ENTRY结构进行了编码,引入随机性,增强堆的安全性
- 取消空闲堆块链表的头节点数组,直接使用链表管理空闲堆块,即_HEAP中FreeLists从[128]的_LIST_ENTRY数组改为单个元素
2、Win7下堆的结构
- struct _HEAP_ENTRY {
- SHORT Size;
-
-
-
-
-
-
- SHORT PreviousSize;
-
- BYTE UnusedBytes;
-
- };
-
-
-
-
-
- struct _HEAP_SEGMENT {
- _HEAP_ENTRY Entry;
- UINT SegmentSignature;
- UINT SegmentFlags;
- _LIST_ENTRY SegmentListEntry;
-
-
-
- _PHEAP Heap;
-
- _HEAP_ENTRY* FirstEntry;
- _HEAP_ENTRY* LastValidEntry;
-
- _LIST_ENTRY UCRSegmentList;
- };
-
- struct _HEAP {
- _HEAP_SEGMENT Segment;
-
- UINT EncodeFlagMask;
- _HEAP_ENTRY Encoding;
-
-
-
- _LIST_ENTRY SegmentList;
-
-
-
- _LIST_ENTRY FreeLists;
-
-
-
-
- _HEAP_TUNING_PARAMETERS TuningParameters;
- };
-
-
- struct _HEAP_FREE_ENTRY {
- _HEAP_ENTRY Entry;
- _LIST_ENTRY FreeList;
-
-
-
-
- };
3、堆块的调试
获取一个地址所属堆块的信息
- 0:000> !heap -x 00730000
- Entry User Heap Segment Size PrevSize Unused Flags
- \-----------------------------------------------------------------------------
- 00730000 00730008 00730000 00730000 588 0 1 busy
- 注意:这里的Size为588是16进制
获取一个堆块的大小
- 0:000> dt -v _HEAP_ENTRY 00730000 直接打印_HEAP_ENTRY结构
- ntdll!_HEAP_ENTRY
- struct _HEAP_ENTRY, 19 elements, 0x8 bytes
- +0x000 Size : 0x9496 显然不对
- ……
- +0x007 UnusedBytes : 0x1 ‘‘
- ……
- 0:000> dd 00730000+0x050 L 4 获取Encoding结构体。Encoding相对_HEAP的偏移是0x050
- 00730050 47329427 000024e0 3b5c5f17 00000000 Encoding低4字节的值为47329427
- 0:000> dd 00730000 L 4 打印_HEAP_ENTRY结构的值
- 00730000 f7339496 010024e0 ffeeffee 00000000 打印出的值f7339496是原始值和Encoding经过异或后得到的
- 0:000> ? f7339496 ^ 47329427 要求原始值只需要当前值和Encoding再异或一遍
- Evaluate expression: -1342111567 = b00100b1 低地址的两字节就是原始的Size
- 0:000> ? 00b1 * 8 实际堆块的字节数还要Size*8
- Evaluate expression: 1416 = 00000588
4、heap corruption问题
常见原因:
- free导致coredump:
free了野指针:需要检查指针的正确性。例如:是否在!heap -hf所列范围内、是否是8的倍数等
堆块写越界:需要检查前后堆块的Size和PreSize
- malloc导致coredump:
一般是因为堆块写越界,破坏了空闲堆块的结构。!heap -hf可以找到同一个堆块即是free又是busy的状态
九、dll hell问题
dll hell常导致虚函数的漂移,本质上就是一个dll之间版本不匹配的问题。