首页 > 代码库 > 《coredump问题原理探究》Windows版 笔记

《coredump问题原理探究》Windows版 笔记

《coredump问题原理探究》Windows版 笔记

Debug

    • 一、环境搭建
      • 1、Win7捕获程序dump
      • 2、Windbg符号表设置(Symbols Search Path)
    • 二、WinDbg命令
    • 三、函数栈帧
      • 1、栈内存布局
      • 2、栈溢出
      • 3、栈的规律
      • 4、定位栈溢出问题的经验方法
    • 四、函数逆向
    • 五、C内存布局
      • 1、基本类型
      • 2、数组类型
      • 3、结构体
    • 六、C++内存布局
      • 1、类的内存布局
      • 2、this指针
      • 3、虚函数表及虚表指针
      • 4、单继承
      • 5、多继承(无公共基类)
    • 七、STL容器内存布局
      • 1、vector
      • 2、list
      • 3、map
      • 4、set
      • 5、iterator
      • 6、string
    • 八、堆结构
      • 1、NT内核堆的改造
      • 2、Win7下堆的结构
      • 3、堆块的调试
      • 4、heap corruption问题
    • 九、dll hell问题

原文链接:http://blog.csdn.net/xuzhina/article/details/8247701qinquan
侵删

一、环境搭建

1、Win7捕获程序dump

注册表HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows/Windows Error Reporting/LocalDumps中新建几个key

技术分享

抓dump注册表配置

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、栈的规律

  1. esp的值不会被覆盖,永远指向栈顶
  2. fp/ret对的链表没有被完全破坏,往高地址的一些地方还是保持这种关系
  3. 从栈顶到栈底,内存地址由低到高。如果帧指针fp1的内容是fp2,fp2的内容是fp3,那么存在:esp<fp1<fp2<fp3
  4. 如果两对(fp1,ret2)、(fp2,ret2)符合条件:fp1的内容刚好是fp2,那么它们除了满足第3点外,还满足:ln ret1和ln ret2都能列出函数符号,ret2的上一条指令一定是调用ret1所在的函数

4、定位栈溢出问题的经验方法

  1. dd esp查看由esp开始的内存
  2. 找到某个内容比esp的值稍大、数值相差不是太远的内存单元,称为FP1。下一个单元称为RET1
  3. ln查看RET1的内容。如果没有显示函数符号,跳过FP1,回到第2步
  4. dd *FP1 L 2得到两个内存单元(FP2,RET2)。如果FP2的内容小于FP1的内容,跳过FP1,回到第2步
  5. 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、结构体

  1. 成员全是基本类型的结构体: 先把一个基地址放到某寄存器中,访问成员时在基址上加上前面所有成员的大小,每个成员与基址的偏移量不固定
  2. 复合类型构成的结构体: 同上,没有特别
  3. 结构体数组: 找到数组的首地址;根据索引找到每个元素的地址;以每个元素的地址作为结构体基址,获取成员变量的地址

六、C++内存布局

1、类的内存布局

类的成员变量排列与结构体相同

2、this指针

调用类的成员函数时,this指针放在ecx寄存器中传递,不入栈

  1. 调用函数时的汇编代码: 
  2. lea ecx,[ebp-1
  3. call Test!Test::print (00ec1030) 
  4. 被调函数开始的汇编代码: 
  5. mov dword ptr [ebp-4],ecx 
  6. mov eax,dword ptr [ebp-4
  7. push eax 

3、虚函数表及虚表指针

技术分享

虚函数表及虚表指针

4、单继承

子类先调用基类的构造函数,再初始化自己的成员变量,然后设置虚表指针
子类虚函数表分布规律:

  1. 重载基类的虚函数,按照基类虚函数声明顺序排列,和子类声明顺序无关
  2. 子类独有的虚函数,按照虚函数的声明顺序排列,追加在重载虚函数的后面

5、多继承(无公共基类)

  1. 子类对象的大小等于各个基类的大小(虚表指针+成员变量)加上自身成员变量的大小
  2. 各基类在子类里的“隐含对象”是按照继承顺序来排列的,和基类的声明、定义顺序无关
  3. 每个基类都(尽可能)有自己的虚函数表;子类独有的虚函数追加到第一个虚函数表的后面;子类重载所有虚表中的同名虚函数
  4. 子类对象指针转换成基类指针,实际上是把子类对象包含的对应基类的“隐含对象”的地址赋值给基类指针
  5. 当一个虚函数在多个虚表中都出现时,实际上只会完全重载第一个虚表中的该函数。其余虚表中重载代码是通过调整this指针为子类的地址,然后跳转到子类对应函数来实现
  1. 0:000> u 012e10a8 L 5 
  2. Test![thunk]:Child::print`adjustor{12}‘: 
  3. 012e10a8 83e90c sub ecx,0Ch // ecx是基类“隐含对象”的地址,需调整 
  4. 012e10ab e9e0ffffff jmp Test!Child::print (012e1090) 

七、STL容器内存布局

1、vector

一个vector在栈上占三个单元(指针):
第一个_Myfirst指向vector元素的开始
第二个_Mylast指向vector元素结束的下一个位置
第三个_Myend指向vector空间结束的位置
注意:vector的begin()、end()、push_back()等成员函数的汇编,最后是:ret 4

技术分享

vector

2、list

  1. list有两个成员:第一个_Myhead指向链表的头部节点,第二个_Size表明链表中的节点元素个数
  2. 链表中的每个节点包含三个成员:第一个_Next指向下一个节点,第二个_Prev指向前一个节点,第三个_Myval存储节点的值
  3. list初始化时会生成一个头节点
  4. 头节点的_Next指向链表第一个节点,链表最后一个节点的_Next指向头节点
  5. 头节点的_Prev指向链表最后一个节点,链表第一个节点的_Prev指向头节点
    注意:图中_Prev指针应该都指向节点起始位置,而不是_Prev指针的位置。这里只是为了突出两个链路

技术分享

list

3、map

  1. map有两个成员:_Myhead指向头节点,_Mysize表明map包含的元素个数
  2. 头节点的三个指针分别指向树的最左节点、树的根节点、树的最右节点
  3. 树的根节点的_Parent指向头节点
  4. 树的叶子节点的_Left、_Right指向头节点

技术分享

map

4、set

由于map、set本身的定义都没有声明任何成员变量,所有的成员变量都是从_Tree继承过来的,唯一的区别是traits的定义不一样,因此:set的特征和map类似

  1. template<class _Kty, 
  2. class _Pr = less<_Kty>, 
  3. class _Alloc = allocator<_Kty> > 
  4. class set : public _Tree<_Tset_traits<_Kty, _Pr, _Alloc, false> > 
  5. { ...... } 
  6.  
  7. template<class _Kty, 
  8. class _Ty, 
  9. class _Pr = less<_Kty>, 
  10. class _Alloc = allocator<pair<const _Kty, _Ty> > > 
  11. class map : public _Tree<_Tmap_traits<_Kty, _Ty, _Pr, _Alloc, false> > 
  12. { ...... } 

5、iterator

  1. vector的iterator只有一个成员_Ptr,取值范围:vec._Myfirst <= _Ptr < vec._Mylast
  2. list的iterator也只有一个成员_Ptr,指向list中的每个节点(头节点除外)
  3. map和set的iterator也只有一个成员_Ptr,指向map或set的节点,且iterator的遍历采用中序遍历

实际调试中,set的iterator指向节点的值在for循环中是按照0、1、2、3......、f的顺序遍历

技术分享

set_iterator

6、string

string有三个成员:联合体_Bx,紧接着的是字符串长度_Mysize,预留空间大小_Myres

  1. 当_Mysize < _BUF_SIZE(16)时,字符串存储在_Bx的_Buf里
  2. 当_Mysize >= _BUF_SIZE(16)时,字符串存储在_Bx的_Ptr指向的内存中
  1. // TEMPLATE CLASS _String_val 
  2. template<class _Val_types> 
  3. class _String_val : public _Container_base 
  4. { // base class for basic_string to hold data 
  5. public
  6. ...... 
  7. enum { // length of internal buffer, [1, 16] 
  8. _BUF_SIZE = 16 / sizeof (value_type) < 1  
  9. ? 1 
  10. : 16 / sizeof (value_type) 
  11. }; 
  12. ...... 
  13. union _Bxty { // storage for small buffer or pointer to larger one 
  14. value_type _Buf[_BUF_SIZE]; 
  15. pointer _Ptr; 
  16. char _Alias[_BUF_SIZE]; // to permit aliasing 
  17. } _Bx; 
  18. size_type _Mysize; // current length of string 
  19. size_type _Myres; // current storage reserved for string 
  20. }; 

八、堆结构

1、NT内核堆的改造

文档中介绍的堆结构是XP环境下的。MS从Vista开始对NT内核做了较大改动,其中包括堆的改造。最直观的改造:

  1. _HEAP中采用链表方式管理_HEAP_SEGMENT,解除数组的限制
  2. _HEAP_ENTRY结构进行了编码,引入随机性,增强堆的安全性
  3. 取消空闲堆块链表的头节点数组,直接使用链表管理空闲堆块,即_HEAP中FreeLists从[128]的_LIST_ENTRY数组改为单个元素

2、Win7下堆的结构

  1. struct _HEAP_ENTRY { 
  2. SHORT Size; /* 当前块的大小。 
  3. 直接dt -v打出来的值是经过了编码的, 
  4. 实际值需要和_HEAP中的Encoding做一次异或,取最低的两个字节。 
  5. 真正的块的字节数还需要Size*8。 
  6. 这个值包含了这个堆块头结构_HEAP_ENTRY的8字节 
  7. */ 
  8. // ……省略若干字段 
  9. SHORT PreviousSize; 
  10. // ……省略若干字段 
  11. BYTE UnusedBytes; // 未使用的字节数 
  12. // ……省略若干字段 
  13. }; /* 整个_HEAP_ENTRY结构共8字节, 
  14. 后面紧接着申请出来的内存块,malloc或new返回的也是指向这个内存块的指针, 
  15. 这个指针的值一定是8的倍数, 
  16. 再后面紧接着的就是下一个堆块  
  17. */ 
  18.  
  19. struct _HEAP_SEGMENT { 
  20. _HEAP_ENTRY Entry; 
  21. UINT SegmentSignature; 
  22. UINT SegmentFlags; 
  23. _LIST_ENTRY SegmentListEntry; /* segment list的入口, 
  24. 指示当前_HEAP_SEGMENT节点在segment list中的位置, 
  25. 各_HEAP_SEGMENT通过这个字段连接 
  26. */ 
  27. _PHEAP Heap; /* 指向所属的_HEAP */ 
  28. // ……省略若干字段 
  29. _HEAP_ENTRY* FirstEntry; 
  30. _HEAP_ENTRY* LastValidEntry; 
  31. // ……省略若干字段 
  32. _LIST_ENTRY UCRSegmentList; 
  33. }; 
  34.  
  35. struct _HEAP { 
  36. _HEAP_SEGMENT Segment; /* 第一个段 */ 
  37. // ……省略若干字段 
  38. UINT EncodeFlagMask; /* 是否启用编码功能 */ 
  39. _HEAP_ENTRY Encoding; /* 编码key, 
  40. 用这个结构和每个堆块头结构_HEAP_ENTRY做异或 
  41. */ 
  42. // ……省略若干字段 
  43. _LIST_ENTRY SegmentList; /* segment list的头节点, 
  44. 分别指向第一个、最后一个_HEAP_SEGMENT的SegmentListEntry 
  45. */ 
  46. // ……省略若干字段 
  47. _LIST_ENTRY FreeLists; /* 空闲堆块链表头节点, 
  48. 分别指向第一个、最后一个_HEAP_FREE_ENTRY的FreeList 
  49. XP中这里是[128]的_LIST_ENTRY数组 
  50. */ 
  51. // ……省略若干字段 
  52. _HEAP_TUNING_PARAMETERS TuningParameters; 
  53. }; 
  54.  
  55. /* 空闲堆块结构 */ 
  56. struct _HEAP_FREE_ENTRY { 
  57. _HEAP_ENTRY Entry; 
  58. _LIST_ENTRY FreeList; /* free list的入口, 
  59. 指示当前空闲堆块在链表中的位置 
  60. 各空闲堆块通过这个字段连接 
  61. 如果两个节点在内存中连续则合并 
  62. */ 
  63. }; 

技术分享

heap

3、堆块的调试

获取一个地址所属堆块的信息

  1. 0:000> !heap -x 00730000 
  2. Entry User Heap Segment Size PrevSize Unused Flags 
  3. \----------------------------------------------------------------------------- 
  4. 00730000 00730008 00730000 00730000 588 0 1 busy 
  5. 注意:这里的Size为58816进制 

获取一个堆块的大小

  1. 0:000> dt -v _HEAP_ENTRY 00730000 直接打印_HEAP_ENTRY结构 
  2. ntdll!_HEAP_ENTRY 
  3. struct _HEAP_ENTRY, 19 elements, 0x8 bytes 
  4. +0x000 Size : 0x9496 显然不对 
  5. …… 
  6. +0x007 UnusedBytes : 0x1 ‘‘ 
  7. …… 
  8. 0:000> dd 00730000+0x050 L 4 获取Encoding结构体。Encoding相对_HEAP的偏移是0x050 
  9. 00730050 47329427 000024e0 3b5c5f17 00000000 Encoding低4字节的值为47329427 
  10. 0:000> dd 00730000 L 4 打印_HEAP_ENTRY结构的值 
  11. 00730000 f7339496 010024e0 ffeeffee 00000000 打印出的值f7339496是原始值和Encoding经过异或后得到的 
  12. 0:000> ? f7339496 ^ 47329427 要求原始值只需要当前值和Encoding再异或一遍 
  13. Evaluate expression: -1342111567 = b00100b1 低地址的两字节就是原始的Size 
  14. 0:000> ? 00b1 * 8 实际堆块的字节数还要Size*8 
  15. Evaluate expression: 1416 = 00000588 

4、heap corruption问题

常见原因:

  1. free导致coredump:
    free了野指针:需要检查指针的正确性。例如:是否在!heap -hf所列范围内、是否是8的倍数等
    堆块写越界:需要检查前后堆块的Size和PreSize
  2. malloc导致coredump:
    一般是因为堆块写越界,破坏了空闲堆块的结构。!heap -hf可以找到同一个堆块即是free又是busy的状态

九、dll hell问题

dll hell常导致虚函数的漂移,本质上就是一个dll之间版本不匹配的问题。

《coredump问题原理探究》Windows版 笔记