请您给这个C语言链栈的实现代码纠错,非常感谢。现在它运行到Push就崩溃了

求高手看看,C语言,判断式子里的圆括号是否配对,利用链栈方法,遇到‘(’进栈,遇到‘)’出栈,最后判断‘(’栈中是否是空,空则对称,但一直运行不出,快疯了!!
[问题点数:40分]
求高手看看,C语言,判断式子里的圆括号是否配对,利用链栈方法,遇到‘(’进栈,遇到‘)’出栈,最后判断‘(’栈中是否是空,空则对称,但一直运行不出,快疯了!!
[问题点数:40分]
不显示删除回复
显示所有回复
显示星级回复
显示得分回复
只显示楼主
2018年1月 总版技术专家分月排行榜第一2016年12月 总版技术专家分月排行榜第一2016年11月 总版技术专家分月排行榜第一2016年10月 总版技术专家分月排行榜第一
2016年10月优秀大版主2016年8月优秀大版主
2016年5月 总版技术专家分月排行榜第二
2016年10月优秀大版主2016年8月论坛优秀大版主
2014年 总版技术专家分年内排行榜第二
2013年 总版技术专家分年内排行榜第三
匿名用户不能发表回复!|写在前面首先还是广播一下2017 Pwn2Own 大赛的最终赛果,本次比赛共发现51个漏洞,长亭安全实验室贡献11个,以积26分的总成绩,在11支参赛团队中名列第三!同时,也祝贺国内的安全团队包揽本次大赛的前三名!0x10
上期回顾上篇文章介绍了栈溢出的原理和两种执行方法,两种方法都是通过覆盖返回地址来执行输入的指令片段(shellcode)或者动态库中的函数(return2libc)。本篇会继续介绍另外两种实现方法。一种是覆盖返回地址来执行内存内已有的代码片段(ROP),另一种是将某个函数的地址替换成另一个函数的地址(hijack GOT)。0x20
相关知识0x21
寄存器 在上篇的背景知识中,我们提到了函数状态相关的三个寄存器--esp,ebp,eip。下面的内容会涉及更多的寄存器,所以我们大致介绍下寄存器在执行程序指令中的不同用途。32位x86架构下的寄存器可以被简单分为通用寄存器和特殊寄存器两类,通用寄存器在大部分汇编指令下是可以任意使用的(虽然有些指令规定了某些寄存器的特定用途),而特殊寄存器只能被特定的汇编指令使用,不能用来任意存储数据。32位x86架构下的通用寄存器包括一般寄存器(eax、ebx、ecx、edx),索引寄存器(esi、edi),以及堆栈指针寄存器(esp、ebp)。 一般寄存器用来存储运行时数据,是指令最常用到的寄存器,除了存放一般性的数据,每个一般寄存器都有自己较为固定的独特用途。eax 被称为累加寄存器(Accumulator),用以进行算数运算和返回函数结果等。ebx 被称为基址寄存器(Base),在内存寻址时(比如数组运算)用以存放基地址。ecx 被称为记数寄存器(Counter),用以在循环过程中记数。edx 被称为数据寄存器(Data),常配合 eax 一起存放运算结果等数据。 索引寄存器通常用于字符串操作中,esi 指向要处理的数据地址(Source Index),edi 指向存放处理结果的数据地址(Destination Index)。 堆栈指针寄存器(esp、ebp)用于保存函数在调用栈中的状态,上篇已有详细的介绍。 32位x86架构下的特殊寄存器包括段地址寄存器(ss、cs、ds、es、fs、gs),标志位寄存器(EFLAGS),以及指令指针寄存器(eip)。 现代操作系统内存通常是以分段的形式存放不同类型的信息的。我们在上篇谈及的函数调用栈就是分段的一个部分(Stack Segment)。内存分段还包括堆(Heap Segment)、数据段(Data Segment),BSS段,以及代码段(Code Segment)。代码段存储可执行代码和只读常量(如常量字符串),属性可读可执行,但通常不可写。数据段存储已经初始化且初值不为0的全局变量和静态局部变量,BSS段存储未初始化或初值为0的全局变量和静态局部变量,这两段数据都有可写的属性。堆用于存放程序运行中动态分配的内存,例如C语言中的 malloc() 和 free() 函数就是在堆上分配和释放内存。各段在内存的排列如下图所示。Fig1. 内存分段的典型布局段地址寄存器就是用来存储内存分段地址的,其中寄存器 ss 存储函数调用栈(Stack Segment)的地址,寄存器 cs 存储代码段(Code Segment)的地址,寄存器 ds 存储数据段(Data Segment)的地址,es、fs、gs 是附加的存储数据段地址的寄存器。标志位寄存器(EFLAGS)32位中的大部分被用于标志数据或程序的状态,例如 OF(Overflow Flag)对应数值溢出、IF(Interrupt Flag)对应中断、ZF(Zero Flag)对应运算结果为0、CF(Carry Flag)对应运算产生进位等等。 指令指针寄存器(eip)存储下一条运行指令的地址,上篇已有详细的介绍。0x22
汇编指令为了更好地理解后面的内容,一点点汇编语言的知识也是必要的,本节会介绍一些基础指令的含义。32位x86架构下的汇编语言有 Intel 和 AT&T 两种格式,本文所用汇编指令都是 Intel 格式。两者最主要的差别如下。Intel 格式,寄存器名称和数值前无符号: “指令名称
目标操作数 DST,源操作数 SRC”AT&T 格式,寄存器名称前加“%”,数值前加“$”:“指令名称
源操作数 SRC,目标操作数 DST”一些最常用的汇编指令如下:MOV:数据传输指令,将 SRC 传至 DST,格式为MOV DST, SRC;
PUSH:压入堆栈指令,将 SRC 压入栈内,格式为PUSH SRC;
POP:弹出堆栈指令,将栈顶的数据弹出并存至 DST,格式为POP DST;
LEA:取地址指令,将 MEM 的地址存至 REG ,格式为LEA REG, MEM;
ADD/SUB:加/减法指令,将运算结果存至 DST,格式为ADD/SUB DST, SRC;
AND/OR/XOR:按位与/或/异或,将运算结果存至 DST ,格式为AND/OR/XOR DST,SRC;
CALL:调用指令,将当前的 eip 压入栈顶,并将 PTR 存入 eip,格式为CALL PTR;
RET:返回指令,操作为将栈顶数据弹出至 eip,格式为RET;
介绍完以上背景知识,就可以继续栈溢出技术之路了。0x30
ROP ( Return Oriented Programming ) --修改返回地址,让其指向内存中已有的一段指令 根据上面副标题的说明,要完成的任务包括:在内存中确定某段指令的地址,并用其覆盖返回地址。可是既然可以覆盖返回地址并定位到内存地址,为什么不直接用上篇提到的 return2libc 呢?因为有时目标函数在内存内无法找到,有时目标操作并没有特定的函数可以完美适配。这时就需要在内存中寻找多个指令片段,拼凑出一系列操作来达成目的。假如要执行某段指令(我们将其称为“gadget”,意为小工具),溢出数据应该以下面的方式构造(padding 长度和内容的确定方式参见上篇):payload : padding + address of gadgetFig 2. 包含单个 gadget 的溢出数据 如果想连续执行若干段指令,就需要每个 gadget 执行完毕可以将控制权交给下一个 gadget。所以 gadget 的最后一步应该是 RET 指令,这样程序的控制权(eip)才能得到切换,所以这种技术被称为返回导向编程( Return Oriented Programming )。要执行多个 gadget,溢出数据应该以下面的方式构造:payload : padding + address of gadget 1 + address of gadget 2 + ......
+ address of gadget n 在这样的构造下,被调用函数返回时会跳转执行 gadget 1,执行完毕时 gadget 1 的 RET 指令会将此时的栈顶数据(也就是 gadget 2 的地址)弹出至 eip,程序继续跳转执行 gadget 2,以此类推。Fig 3. 包含多个 gadget 的溢出数据现在任务可以分解为:针对程序栈溢出所要实现的效果,找到若干段以 ret 作为结束的指令片段,按照上述的构造将它们的地址填充到溢出数据中。所以我们要解决以下几个问题。首先,栈溢出之后要实现什么效果?ROP 常见的拼凑效果是实现一次系统调用,Linux系统下对应的汇编指令是 int 0x80。执行这条指令时,被调用函数的编号应存入 eax,调用参数应按顺序存入 ebx,ecx,edx,esi,edi 中。例如,编号125对应函数 mprotect (void *addr, size_t len, int prot)
,可用该函数将栈的属性改为可执行,这样就可以使用 shellcode 了。假如我们想利用系统调用执行这个函数,eax、ebx、ecx、edx 应该分别为“125”、内存栈的分段地址(可以通过调试工具确定)、“0x10000”(需要修改的空间长度,也许需要更长)、“7”(RWX 权限)。其次,如何寻找对应的指令片段?有若干开源工具可以实现搜索以 ret 结尾的指令片段,著名的包括 ROPgadget、rp++、ropeme 等,甚至也可以用 grep 等文本匹配工具在汇编指令中搜索 ret 再进一步筛选。搜索的详细过程在这里就不再赘述,有兴趣的同学可以参考上述工具的说明文档。最后,如何传入系统调用的参数?对于上面提到的 mprotect 函数,我们需要将参数传输至寄存器,所以可以用 pop 指令将栈顶数据弹入寄存器。如果在内存中能找到直接可用的数据,也可以用 mov 指令来进行传输,不过写入数据再 pop 要比先搜索再 mov 来的简单,对吧?如果要用 pop 指令来传输调用参数,就需要在溢出数据内包含这些参数,所以上面的溢出数据格式需要一点修改。对于单个 gadget,pop 所传输的数据应该在 gadget 地址之后,如下图所示。Fig 4. gadget “”在调用 mprotect() 为栈开启可执行权限之后,我们希望执行一段 shellcode,所以要将 shellcode 也加入溢出数据,并将 shellcode 的开始地址加到 int 0x80 的 gadget之后。但确定 shellcode 在内存的确切地址是很困难的事(想起上篇里面艰难试探的过程了吗?),我们可以使用 push esp 这个 gadget(加入可以找到的话)。Fig 5. gadget “”我们假设现在内存中可以找到如下几条指令:
# pop stack top into eax
# pop stack top into ebx
# pop stack top into ecx
# pop stack top into edx
# system call
# push address of shellcode
对于所有包含 pop 指令的 gadget,在其地址之后都要添加 pop 的传输数据,同时在所有 gadget 最后包含一段 shellcode,最终溢出数据结构应该变为如下格式。payload : padding + address of gadget 1 + param for gadget 1 + address of gadget 2
param for gadget 2 + ...... + address of gadget n + shellcodeFig 6. 包含多个 gadget 的溢出数据(修改后)此处为了简单,先假定输入溢出数据不受“\x00"字符的影响,所以 payload 可以直接包含 “\x7d\x00\x00\x00”(传给 eax 的参数125)。如果希望实现更为真实的操作,可以用多个 gadget 通过运算得到上述参数。比如可以通过下面三条 gadget 来给 eax 传递参数。
# pop stack top 0x1111118e into eax
# pop stack top 0x into ebx
# eax -= ebx
解决完上述问题,我们就可以拼接出溢出数据,输入至程序来为程序调用栈开启可执行权限并执行 shellcode。同时,由于 ROP 方法带来的灵活性,现在不再需要痛苦地试探 shellcode 起始地址了。回顾整个输入数据,只有栈的分段地址需要获取确定地址。如果利用 gadget 读取 ebp 的值再加上某个合适的数值,就可以保证溢出数据都具有可执行权限,这样就不再需要获取确切地址,也就具有了绕过内存随机化的可能。出于演示的目的,我们假设(简直是钦点)了所有需要的 gadget 的存在。在实际搜索及拼接 gadget 时,并不会像上面一样顺利,有两个方面需要注意。第一,很多时候并不能一次凑齐全部的理想指令片段,这时就要通过数据地址的偏移、寄存器之间的数据传输等方法来“曲线救国”。举个例子,假设找不到下面这条 gadget
但假如可以找到下面的 gadgetmov ebx,
我们就可以将它和
组合起来实现将数据传输给 ebx 的功能。上面提到的用多个 gadget 避免输入“\x00”也是一个实例应用。第二,要小心 gadget 是否会破坏前面各个 gadget 已经实现的部分,比如可能修改某个已经写入数值的寄存器。另外,要特别小心 gadget 对 ebp 和 esp 的操作,因为它们的变化会改变返回地址的位置,进而使后续的 gadget 无法执行。0x40
Hijack GOT --修改某个被调用函数的地址,让其指向另一个函数 根据上面副标题的说明,要完成的任务包括:在内存中修改某个函数的地址,使其指向另一个函数。为了便于理解,不妨假设修改 printf() 函数的地址使其指向 system(),这样修改之后程序内对 printf() 的调用就执行 system() 函数。要实现这个过程,我们就要弄清楚发生函数调用时程序是如何“找到”被调用函数的。 程序对外部函数的调用需要在生成可执行文件时将外部函数链接到程序中,链接的方式分为静态链接和动态链接。静态链接得到的可执行文件包含外部函数的全部代码,动态链接得到的可执行文件并不包含外部函数的代码,而是在运行时将动态链接库(若干外部函数的集合)加载到内存的某个位置,再在发生调用时去链接库定位所需的函数。可程序是如何在链接库内定位到所需的函数呢?这个过程用到了两张表--GOT 和 PLT。GOT 全称是全局偏移量表(Global Offset Table),用来存储外部函数在内存的确切地址。GOT 存储在数据段(Data Segment)内,可以在程序运行中被修改。PLT 全称是程序链接表(Procedure Linkage Table),用来存储外部函数的入口点(entry),换言之程序总会到 PLT 这里寻找外部函数的地址。PLT 存储在代码段(Code Segment)内,在运行之前就已经确定并且不会被修改,所以 PLT 并不会知道程序运行时动态链接库被加载的确切位置。那么 PLT 表内存储的入口点是什么呢?就是 GOT 表中对应条目的地址。Fig 7. PLT 和 GOT 表等等,我们好像发现了一个不合理的地方,外部函数的内存地址存储在 GOT 而非 PLT 表内,PLT 存储的入口点又指向 GOT 的对应条目,那么程序为什么选择 PLT 而非 GOT 作为调用的入口点呢?在程序启动时确定所有外部函数的内存地址并写入 GOT 表,之后只使用 GOT 表不是更方便吗?这样的设计是为了程序的运行效率。GOT 表的初始值都指向 PLT 表对应条目中的某个片段,这个片段的作用是调用一个函数地址解析函数。当程序需要调用某个外部函数时,首先到 PLT 表内寻找对应的入口点,跳转到 GOT 表中。如果这是第一次调用这个函数,程序会通过 GOT 表再次跳转回 PLT 表,运行地址解析程序来确定函数的确切地址,并用其覆盖掉 GOT 表的初始值,之后再执行函数调用。当再次调用这个函数时,程序仍然首先通过 PLT 表跳转到 GOT 表,此时 GOT 表已经存有获取函数的内存地址,所以会直接跳转到函数所在地址执行函数。整个过程如下面两张图所示。Fig 8. 第一次调用函数时解析函数地址并存入 GOT 表Fig 9. 再次调用函数时直接读取 GOT 内的地址 上述实现遵循的是一种被称为 LAZY 的设计思想,它将需要完成的操作(解析外部函数的内存地址)留到调用实际发生时才进行,而非在程序一开始运行时就解析出全部函数地址。这个过程也启示了我们如何实现函数的伪装,那就是到 GOT 表中将函数 A 的地址修改为函数 B 的地址。这样在后面所有对函数 A 的调用都会执行函数 B。 那么我们的目标可以分解为如下几部分:确定函数 A 在 GOT 表中的条目位置,确定函数 B 在内存中的地址,将函数 B 的地址写入函数 A 在 GOT 表中的条目。 首先,如何确定函数 A 在 GOT 表中的条目位置? 程序调用函数时是通过 PLT 表跳转到 GOT 表的对应条目,所以可以在函数调用的汇编指令中找到 PLT 表中该函数的入口点位置,从而定位到该函数在 GOT 中的条目。例如 call 0x &printf@plt&
就说明 printf 在 PLT 表中的入口点是在 0x,所以 0x 处存储的就是 GOT 表中 printf 的条目地址。 其次,如何确定函数 B 在内存中的地址? 如果系统开启了内存布局随机化,程序每次运行动态链接库的加载位置都是随机的,就很难通过调试工具直接确定函数的地址。假如函数 B 在栈溢出之前已经被调用过,我们当然可以通过前一个问题的答案来获得地址。但我们心仪的攻击函数往往并不满足被调用过的要求,也就是 GOT 表中并没有其真实的内存地址。幸运的是,函数在动态链接库内的相对位置是固定的,在动态库打包生成时就已经确定。所以假如我们知道了函数 A 的运行时地址(读取 GOT 表内容),也知道函数 A 和函数 B 在动态链接库内的相对位置,就可以推算出函数 B 的运行时地址。 最后,如何实现 GOT 表中数据的修改? 很难找到合适的函数来完成这一任务,不过我们还有强大的 ROP(DIY大法好)。假设我们可以找到以下若干条 gadget(继续钦点),就不难改写 GOT 表中数据,从而实现函数的伪装。ROP 的具体实现请回看上一章,这里就不再赘述了。
# printf@plt -& eax
mov ebx [eax]; # printf@got -& ebx
# addr_diff = system - printf -& ecx
# printf@got += addr_diff
从修改 GOT 表的过程可以看出,这种方法也可以在一定程度上绕过内存随机化。0x50
防御措施介绍过几种栈溢出的基础方法,我们再来补充一下操作系统内有哪些常见的措施可以进行防御。首先,通常情况下程序在默认编译设置下都会取消栈上数据的可执行权限,这样简单的 shellcode 溢出攻击就无法实现了。其次,可以在操作系统内开启内存布局随机化(ASLR),这样可以增大确定堆栈内数据和动态库内函数的内存地址的难度。编译程序时还可以设置某些编译选项,使程序在运行时会在函数栈上的 ebp 地址和返回地址之间生成一个特殊的值,这个值被称为“金丝雀”(关于这个典故,请大家自行谷歌)。这样一旦发生了栈溢出并覆盖了返回地址,这个值就会被改写,从而实现函数栈的越界检查。最后值得强调的是,尽可能写出安全可靠的代码,不给栈溢出提供写入越界的可能。0x60
全篇小结 本文简要介绍了栈溢出这种古老而经典的技术领域,并概括描述了四种入门级的实现方法。至此我们专栏的第一讲就全部结束啦,接下来专栏会陆续放出计算机安全相关的更多文章,内容也会涵盖网络安全、Web渗透、密码学、操作系统,还会有ctf 比赛题解等等...... 最后感谢大家的关注,让我们一起学习,共同进步!References:《Hacking: Art of Exploitation》14918 条评论分享收藏文章被以下专栏收录如何用C语言创建一个链栈,并进行操作_百度知道
如何用C语言创建一个链栈,并进行操作
链栈结点类型定义为:
typedef struct node
struct node *
2)编写进栈函数push
3)编写出栈函数pop
4)编写main函数,首先建立一空链栈;
调用进栈函数,将从键盘输入的数据元素逐...
我有更好的答案
1 思路: 主要是链表的插入和删除操作2 代码#include&stdio.h&#include&stdlib.h&typedef&struct&node{ int&& struct&node&*}node_void&push(node_type*&&stack,&int&elem){ node_type*node&=&(node_type*)malloc(sizeof(node_type)); node-&data&=& node-&next&=& stack&=&}int&pop(node_type*&&stack){ int&elem&=&stack-& node_type*node&=& stack&=&stack-& free(node); return&}bool&IsEmpty(node_type*&stack){ return&stack&==&NULL;}void&display(node_type*stack){ while&(stack){
printf(&%d&&,&stack-&data);
stack&=&stack-& } puts(&&);}void&destroy(node_type*stack){ while&(!IsEmpty(stack)){
pop(stack); }}int&main(){ puts(&(1)&建立空链栈&); node_type*stack&=&NULL; puts(&\n(2)&调用进栈函数,将从键盘输入的数据元素逐个进栈,输入0结束;&); int& scanf(&%d&,&&num); while&(num&!=&0){
push(stack,&num);
scanf(&%d&,&&num); } puts(&\n(3)&显示进栈后的数据元素&); display(stack); puts(&\n(4)&调用两次出栈函数,显示出栈后的数据元素&); if&(!IsEmpty(stack))
printf(&%d\n&,&pop(stack)); if&(!IsEmpty(stack))
printf(&%d\n&,&pop(stack)); destroy(stack); getchar(); getchar(); return&0;}3 运行效果
采纳率:85%
来自团队:
为您推荐:
其他类似问题
您可能关注的内容
c语言的相关知识
换一换
回答问题,赢新手礼包
个人、企业类
违法有害信息,请在下方选择后提交
色情、暴力
我们会通过消息、邮箱等方式尽快将举报结果通知您。程序崩溃的诊断,调试,解决办法|开发技术 - 美国主机支持论坛
查看完整版本: [--
程序崩溃的诊断,调试,解决办法
作为程序员,我们平时最担心见到的事情是什么?是内存泄漏?是界面不好看?……错啦!我相信我的看法是不会有人反对的——那就是,程序发生了崩溃!“该程序执行了非法操作,即将关闭。请与你的软件供应商联系。”,呵呵,这句 M$ 的“名言”,恐怕就是程序员最担心见到的东西了。有的时候,自己的程序在自己的机器上运行得好好的,但是到了别人的机器上就崩溃了;有时自己在编写和测试的过程中就莫名其妙地遇到了非法操作,但是却无法确定到底是源代码中的哪行引起的……是不是很痛苦呢?不要紧,本文可以帮助你走出这种困境,甚至你从此之后可以自豪地要求用户把崩溃地址告诉你,然后你就可以精确地定位到源代码中出错的那行了。(很神奇吧?呵呵。)首先我必须强调的是,本方法可以在目前市面上任意一款编译器上面使用。但是我只熟悉 M$ 的 VC 和 MASM ,因此后面的部分只介绍如何在这两个编译器中实现,请读者自行融会贯通,掌握在别的编译器上使用的方法。Well,废话说完了,让我们开始! :)首先必须生成程序的 MAP 文件。什么是 MAP 文件?简单地讲, MAP 文件是程序的全局符号、源文件和代码行号信息的唯一的文本表示方法,它可以在任何地方、任何时候使用,不需要有额外的程序进行支持。而且,这是唯一能找出程序崩溃的地方的救星。好吧,既然 MAP 文件如此神奇,那么我们应该如何生成它呢?在 VC 中,我们可以按下 Alt+F7 ,打开“Project Settings”选项页,选择 C/C++ 选项卡,并在最下面的 Project Options 里面输入:/Zd ,然后要选择 Link 选项卡,在最下面的 Project Options 里面输入: /mapinfo:lines 和 /map:PROJECT_NAME.map 。最后按下 F7 来编译生成 EXE 可执行文件和 MAP 文件。在 MASM 中,我们要设置编译和连接参数,我通常是这样做的:rc %1.rcml /c /coff /Zd %1.asmlink /subsystem:windows /mapinfo:exports /mapinfo:lines /map:%1.map %1.obj %1.res把它保存成 makem.bat ,就可以在命令行输入 makem filename 来编译生成 EXE 可执行文件和 MAP 文件了。在此我先解释一下加入的参数的含义:/Zd 表示在编译的时候生成行信息/map[:filename] 表示生成 MAP 文件的路径和文件名/mapinfo:lines 表示生成 MAP 文件时,加入行信息/mapinfo:exports 表示生成 MAP 文件时,加入 exported functions (如果生成的是 DLL 文件,这个选项就要加上)OK,通过上面的步骤,我们已经得到了 MAP 文件,那么我们该如何利用它呢?让我们从简单的实例入手,请打开你的 VC ,新建这样一个文件:01 //****************************************************************02 //程序名称:演示如何通过崩溃地址找出源代码的出错行03 //作者:罗聪04 //日期:05 //出处:(老罗的缤纷天地)06 //本程序会产生“除0错误”,以至于会弹出“非法操作”对话框。07 //“除0错误”只会在 Debug 版本下产生,本程序为了演示而尽量简化。08 //注意事项:如欲转载,请保持本程序的完整,并注明:09 //转载自“老罗的缤纷天地”()10 //****************************************************************11 12 void Crash(void)13 {14 int i = 1;15 int j = 0;16 i /=17 }18 19 void main(void)20 {21 Crash();22 }很显然本程序有“除0错误”,在 Debug 方式下编译的话,运行时肯定会产生“非法操作”。好,让我们运行它,果然,“非法操作”对话框出现了,这时我们点击“详细信息”按钮,记录下产生崩溃的地址——在我的机器上是 0x0040104a 。再看看它的 MAP 文件:(由于文件内容太长,中间没用的部分我进行了省略)CrashDemoTimestamp is 3e430a76 (Fri Feb 07 09:23:02 2003)Preferred load address is Start Length Name Class0 0000de04H .text CODEde04 0001000cH .textbss CODE0 H .rdata DATA6 H .edata DATA0 H .CRT$XCA DATA4 H .CRT$XCZ DATA8 H .CRT$XIA DATAc H .CRT$XIC DATA8 H .CRT$XIZ DATAc H .CRT$XPA DATA0 H .CRT$XPX DATA4 H .CRT$XPZ DATA8 H .CRT$XTA DATAc H .CRT$XTZ DATAa30 00000b93H .data DATAc4 H .bss DATA0 H .idata$2 DATA4 H .idata$3 DATA8 H .idata$4 DATA8 H .idata$5 DATA8 000004afH .idata$6 DATAAddress Publics by Value Rva+Base Lib:Object0 ?Crash@@YAXXZ
f CrashDemo.obj0 _main
f CrashDemo.obj0 __IMPORT_DESCRIPTOR_KERNEL32
kernel32:KERNEL32.dll4 __NULL_IMPORT_DESCRIPTOR
kernel32:KERNEL32.dll8 __imp__GetCommandLineA@0
kernel32:KERNEL32.dllc __imp__GetVersion@0 0042413c kernel32:KERNEL32.dll0 __imp__ExitProcess@4
kernel32:KERNEL32.dll4 __imp__DebugBreak@0
kernel32:KERNEL32.dll8 __imp__GetStdHandle@4
kernel32:KERNEL32.dllc __imp__WriteFile@20 0042414c kernel32:KERNEL32.dll0 __imp__InterlockedDecrement@4
kernel32:KERNEL32.dll4 __imp__OutputDebugStringA@4
kernel32:KERNEL32.dll8 __imp__GetProcAddress@8
kernel32:KERNEL32.dllc __imp__LoadLibraryA@4 0042415c kernel32:KERNEL32.dll0 __imp__InterlockedIncrement@4
kernel32:KERNEL32.dll4 __imp__GetModuleFileNameA@12
kernel32:KERNEL32.dll8 __imp__TerminateProcess@8
kernel32:KERNEL32.dllc __imp__GetCurrentProcess@0 0042416c kernel32:KERNEL32.dll0 __imp__UnhandledExceptionFilter@4
kernel32:KERNEL32.dll4 __imp__FreeEnvironmentStringsA@4
kernel32:KERNEL32.dll8 __imp__FreeEnvironmentStringsW@4
kernel32:KERNEL32.dllc __imp__WideCharToMultiByte@32 0042417c kernel32:KERNEL32.dll0 __imp__GetEnvironmentStrings@0
kernel32:KERNEL32.dll4 __imp__GetEnvironmentStringsW@0
kernel32:KERNEL32.dll8 __imp__SetHandleCount@4
kernel32:KERNEL32.dllc __imp__GetFileType@4 0042418c kernel32:KERNEL32.dll0 __imp__GetStartupInfoA@4
kernel32:KERNEL32.dll4 __imp__HeapDestroy@4
kernel32:KERNEL32.dll8 __imp__HeapCreate@12
kernel32:KERNEL32.dllc __imp__HeapFree@12 0042419c kernel32:KERNEL32.dlla0 __imp__VirtualFree@12
kernel32:KERNEL32.dlla4 __imp__RtlUnwind@16
kernel32:KERNEL32.dlla8 __imp__GetLastError@0
kernel32:KERNEL32.dllac __imp__SetConsoleCtrlHandler@8 004241ac kernel32:KERNEL32.dllb0 __imp__IsBadWritePtr@8
kernel32:KERNEL32.dllb4 __imp__IsBadReadPtr@8
kernel32:KERNEL32.dllb8 __imp__HeapValidate@12
kernel32:KERNEL32.dllbc __imp__GetCPInfo@8 004241bc kernel32:KERNEL32.dllc0 __imp__GetACP@0
kernel32:KERNEL32.dllc4 __imp__GetOEMCP@0
kernel32:KERNEL32.dllc8 __imp__HeapAlloc@12
kernel32:KERNEL32.dllcc __imp__VirtualAlloc@16 004241cc kernel32:KERNEL32.dlld0 __imp__HeapReAlloc@16
kernel32:KERNEL32.dlld4 __imp__MultiByteToWideChar@24
kernel32:KERNEL32.dlld8 __imp__LCMapStringA@24
kernel32:KERNEL32.dlldc __imp__LCMapStringW@24 004241dc kernel32:KERNEL32.dlle0 __imp__GetStringTypeA@20
kernel32:KERNEL32.dlle4 __imp__GetStringTypeW@16
kernel32:KERNEL32.dlle8 __imp__SetFilePointer@16
kernel32:KERNEL32.dllec __imp__SetStdHandle@8 004241ec kernel32:KERNEL32.dllf0 __imp__FlushFileBuffers@4
kernel32:KERNEL32.dllf4 __imp__CloseHandle@4
kernel32:KERNEL32.dllf8 \177KERNEL32_NULL_THUNK_DATA
kernel32:KERNEL32.dllentry point at f0Line numbers for .\Debug\CrashDemo.obj(d:\msdev\myprojects\crashdemo\crashdemo.cpp) segment .text13 0 14 8 15 f 16 617 0 20 0 21 8 22 d如果仔细浏览 Rva+Base 这栏,你会发现第一个比崩溃地址 0x0040104a 大的函数地址是 0x ,所以在 0x 这个地址之前的那个入口就是产生崩溃的函数,也就是这行:0 ?Crash@@YAXXZ
f CrashDemo.obj因此,发生崩溃的函数就是 ?Crash@@YAXXZ ,所有以问号开头的函数名称都是 C++ 修饰的名称。在我们的源程序中,也就是 Crash() 这个子函数。OK,现在我们轻而易举地便知道了发生崩溃的函数名称,你是不是很兴奋呢?呵呵,先别忙,接下来,更厉害的招数要出场了。请注意 MAP 文件的最后部分——代码行信息(Line numbers information),它是以这样的形式显示的:13 0第一个数字代表在源代码中的代码行号,第二个数是该代码行在所属的代码段中的偏移量。如果要查找代码行号,需要使用下面的公式做一些十六进制的减法运算:崩溃行偏移 = 崩溃地址(Crash Address) - 基地址(ImageBase Address) - 0x1000为什么要这样做呢?细心的朋友可能会留意到 Rva+Base 这栏了,我们得到的崩溃地址都是由 偏移地址(Rva)+ 基地址(Base) 得来的,所以在计算行号的时候要把基地址减去,一般情况下,基地址的值是 0x 。另外,由于一般的 PE 文件的代码段都是从 0x1000 偏移开始的,所以也必须减去 0x1000 。好了,明白了这点,我们就可以来进行小学减法计算了:崩溃行偏移 = 0x0040104a - 0x - 0x1000 = 0x4a如果浏览 MAP 文件的代码行信息,会看到不超过计算结果,但却最接近的数是 CrashDemo.cpp 文件中的:16 6也就是在源代码中的第 16 行,让我们来看看源代码:16 i /=哈!!!果然就是第 16 行啊!兴奋吗?我也一样! :)方法已经介绍完了,从今以后,我们就可以精确地定位到源代码中的崩溃行,而且只要编译器可以生成 MAP 文件(包括 VC、MASM、VB、BCB、Delphi……),本方法都是适用的。我们时常抱怨 M$ 的产品如何如何差,但其实 M$ 还是有意无意间提供了很多有价值的信息给我们的,只是我们往往不懂得怎么利用而已……相信这样一来,你就可以更为从容地面对“非法操作”提示了。你甚至可以要求用户提供崩溃的地址,然后就可以坐在家中舒舒服服地找到出错的那行,并进行修正。
本人在学习了此文后,在多次实验实践基础上,把该文中的一些内容进行补充与改进,希望对大家调试程序,尤其是release版本的程序有帮助 。欢迎各位朋友批评指正。一、该方法适用的范围  在windows程序中造成程序崩溃的原因很多,而文中所述的方法仅适用与:由一条语句当即引起的程序崩溃。如原文中举的除数为零的崩溃例子。而笔者在实际工作中碰到更多的情况是:指针指向一非法地址 ,然后对指针的内容进行了,读或写的操作。例如:[pre]void Crash1(){ char * p =(char*)100; *p=100;}[/pre]   这些原因造成的崩溃,无论是debug版本,还是release版本的程序,使用该方法都可找到造成崩溃的函数或子程序中的语句行,具体方法的下面还会补充说明。 另外,实践中另一种常见的造成程序崩溃的原因:函数或子程序中局部变量数组越界付值,造成函数或子程序返回地址遭覆盖,从而造成函数或子程序返回时崩溃。例如:[pre]#include void Crash2();int main(int argc,char* argv[]){&&&&Crash2();&&&&return 0;}void Crash2(){&&&&char p[1];&&&&strcpy(p,&&);}[/pre]在vc中编译运行此程序的release版本,会跳出如下的出错提示框。 =700) window.open('http://www.vckbase.com/document/journal/vckbase42/images/crashimg1.jpg');" onload="if(this.offsetWidth>'700')this.width='700';if(this.offsetHeight>'700')this.height='700';" >图一 上面例子运行结果   这里显示的崩溃地址为:0x。这种由前面语句造成的崩溃根源,在后续程序中方才显现出来的情况,显然用该文所述的方法就无能为力了。不过在此例中多少还有些蛛丝马迹可寻找到崩溃的原因:函数Crash2中的局部数组p只有一个字节大小 ,显然拷贝&&这个字符串会把超出长度的字符串拷贝到数组p的后面,即*(p+1)=''1'',*(p+2)=''2'',*(p+3)=''3'',*(p+4)=4。。。。。。而字符''1''的ASC码的值为0x31,''2''为0x32,''3''为0x33,''4''为0x34。。。。。,由于intel的cpu中int型数据是低字节保存在低地址中 ,所以保存字符串''1234''的内存,显示为一个4字节的int型数时就是0x。显然拷贝&&这个字符串时,&1234&这几个字符把函数Crash2的返回地址给覆盖 ,从而造成程序崩溃。对于类似的这种造成程序崩溃的错误朋友们还有其他方法排错的话,欢迎一起交流讨论。二、设置编译产生map文件的方法  该文中产生map文件的方法是手工添加编译参数来产生map文件。其实在vc6的IDE中有产生map文件的配置选项的。操作如下:先点击菜单&Project&-&&Settings。。。&,弹出的属性页中选中&Link&页 ,确保在&category&中选中&General&,最后选中&Generate mapfile&的可选项。若要在在map文件中显示Line numbers的信息的话 ,还需在project options 中加入/mapinfo:lines 。Line numbers信息对于&罗文&所用的方法来定位出错源代码行很重要 ,但笔者后面会介绍更加好的方法来定位出错代码行,那种方法不需要Line numbers信息。 =700) window.open('http://www.vckbase.com/document/journal/vckbase42/images/crashimg3.JPG');" onload="if(this.offsetWidth>'700')this.width='700';if(this.offsetHeight>'700')this.height='700';" >图二 设置产生MAP文件 三、定位崩溃语句位置的方法  &罗文&所述的定位方法中,找到产生崩溃的函数位置的方法是正确的,即在map文件列出的每个函数的起始地址中,最近的且不大于崩溃地址的地址即为包含崩溃语句的函数的地址 。但之后的再进一步的定位出错语句行的方法不是最妥当,因为那种方法前提是,假设基地址的值是 0x ,以及一般的 PE 文件的代码段都是从 0x1000偏移开始的 。虽然这种情况很普遍,但在vc中还是可以基地址设置为其他数,比如设置为0x,这时仍旧套用[pre] 崩溃行偏移 = 崩溃地址 - 0x - 0x1000 [/pre]的公式显然无法找到崩溃行偏移。 其实上述公式若改为[pre]崩溃行偏移 = 崩溃地址 - 崩溃函数绝对地址 + 函数相对偏移[/pre]即可通用了。仍以&罗文&中的例子为例:&罗文&中提到的在其崩溃程序的对应map文件中,崩溃函数的编译结果为[pre]0 ?Crash@@YAXXZ
f CrashDemo。obj [/pre]对与上述结果,在使用我的公式时 ,&崩溃函数绝对地址&指, 函数相对偏移指 , 当崩溃地址= 0x0040104a时, 则 崩溃行偏移 = 崩溃地址 - 崩溃函数起始地址+ 函数相对偏移 = 0x0040104a - 0x + 0xx4a,结果与&罗文&计算结果相同 。但这个公式更通用。四、更好的定位崩溃语句位置的方法。  其实除了依靠map文件中的Line numbers信息最终定位出错语句行外,在vc6中我们还可以通过编译程序产生的对应的汇编语句,二进制码,以及对应c/c++语句为一体的&cod&文件来定位出错语句行 。先介绍一下产生这种包含了三种信息的&cod&文件的设置方法:先点击菜单&Project&-&&Settings。。。&,弹出的属性页中选中&C/C++&页 ,然后在&Category&中选则&Listing Files&,再在&Listing file type&的组合框中选择&Assembly,Machine code, and source&。接下去再通过一个具体的例子来说明这种方法的具体操作。 =700) window.open('http://www.vckbase.com/document/journal/vckbase42/images/crashimg4.JPG');" onload="if(this.offsetWidth>'700')this.width='700';if(this.offsetHeight>'700')this.height='700';" >图三 设置产生&cod&文件 排错步骤1)定位崩溃函数。可以查询map文件获得。我的机器编译产生的map文件的部分如下: Crash Timestamp is 42881a01 (Mon May 16 11:56:49 2005) Preferred load address is
Start Length Name Class0 0000ddf1H .text CODEddf1 0001000fH .textbss CODE0 H .rdata DATA6 H .edata DATA0 H .CRT$XCA DATA4 H .CRT$XCZ DATA8 H .CRT$XIA DATAc H .CRT$XIC DATA8 H .CRT$XIZ DATAc H .CRT$XPA DATA0 H .CRT$XPX DATA4 H .CRT$XPZ DATA8 H .CRT$XTA DATAc H .CRT$XTZ DATAa30 00000b93H .data DATAc4 H .bss DATA0 H .idata$2 DATA4 H .idata$3 DATA8 H .idata$4 DATA8 H .idata$5 DATA8 000004afH .idata$6 DATAAddress Publics by Value Rva+Base Lib:Object0 _main
f Crash.obj0 ?Crash1@@YAXXZ
f Crash.obja0 __chkesp
f LIBCD:chkesp.obje0 _mainCRTStartup
f LIBCD:crt0.obj0 __amsg_exit
f LIBCD:crt0.obj0 __CrtDbgBreak
f LIBCD:dbgrpt.obj...对于崩溃地址0x而言,小于此地址中最接近的地址(Rva+Base中的地址)为,其对应的函数名为?Crash1@@YAXXZ,由于所有以问号开头的函数名称都是 C++ 修饰的名称 ,&@@YAXXZ&则为区别重载函数而加的后缀,所以?Crash1@@YAXXZ就是我们的源程序中,Crash1() 这个函数。排错步骤2)定位出错行。打开编译生成的&cod&文件,我机器上生成的文件内容如下:&&&&TITLE&&&&E:\Crash\Crash。cpp&&&&.386Pinclude listing.incif @Version gt 510.model FLATelse_TEXT&&&&SEGMENT PARA USE32 PUBLIC ''CODE''_TEXT&&&&ENDS_DATA&&&&SEGMENT DWORD USE32 PUBLIC ''DATA''_DATA&&&&ENDSCONST&&&&SEGMENT DWORD USE32 PUBLIC ''CONST''CONST&&&&ENDS_BSS&&&&SEGMENT DWORD USE32 PUBLIC ''BSS''_BSS&&&&ENDS$$SYMBOLS&&&&SEGMENT BYTE USE32 ''DEBSYM''$$SYMBOLS&&&&ENDS$$TYPES&&&&SEGMENT BYTE USE32 ''DEBTYP''$$TYPES&&&&ENDS_TLS&&&&SEGMENT DWORD USE32 PUBLIC ''TLS''_TLS&&&&ENDS;&&&&COMDAT _main_TEXT&&&&SEGMENT PARA USE32 PUBLIC ''CODE''_TEXT&&&&ENDS;&&&&COMDAT ?Crash1@@YAXXZ_TEXT&&&&SEGMENT PARA USE32 PUBLIC ''CODE''_TEXT&&&&ENDSFLAT&&&&GROUP _DATA, CONST, _BSS&&&&ASSUME&&&&CS: FLAT, DS: FLAT, SS: FLATendifPUBLIC&&&&?Crash1@@YAXXZ&&&&&&&&&&&&&&&&&&&&; Crash1PUBLIC&&&&_mainEXTRN&&&&__chkesp:NEAR;&&&&COMDAT _main_TEXT&&&&SEGMENT_main&&&&PROC NEAR&&&&&&&&&&&&&&&&&&&&; COMDAT; 9&&&&: {&&00000&&&&55&&&&&&&& push&&&& ebp&&00001&&&&8b ec&&&&&&&& mov&&&& ebp, esp&&00003&&&&83 ec 40&&&& sub&&&& esp, 64&&&&&&&&&&&&; H&&00006&&&&53&&&&&&&& push&&&& ebx&&00007&&&&56&&&&&&&& push&&&& esi&&00008&&&&57&&&&&&&& push&&&& edi&&00009&&&&8d 7d c0&&&& lea&&&& edi, DWORD PTR [ebp-64]&&0000c&&&&b9 10 00 00 00&&&& mov&&&& ecx, 16&&&&&&&&&&&&; H&&00011&&&&b8 cc cc cc cc&&&& mov&&&& eax, -&&&&&&&&; ccccccccH&&00016&&&&f3 ab&&&&&&&& rep stosd; 10&& :&&&& Crash1();&&00018&&&&e8 00 00 00 00&&&& call&&&& ?Crash1@@YAXXZ&&&&&&&&; Crash1; 11&& :&&&& return 0;&&0001d&&&&33 c0&&&&&&&& xor&&&& eax, eax; 12&& : }&&0001f&&&&5f&&&&&&&& pop&&&& edi&&00020&&&&5e&&&&&&&& pop&&&& esi&&00021&&&&5b&&&&&&&& pop&&&& ebx&&00022&&&&83 c4 40&&&& add&&&& esp, 64&&&&&&&&&&&&; H&&00025&&&&3b ec&&&&&&&& cmp&&&& ebp, esp&&00027&&&&e8 00 00 00 00&&&& call&&&& __chkesp&&0002c&&&&8b e5&&&&&&&& mov&&&& esp, ebp&&0002e&&&&5d&&&&&&&& pop&&&& ebp&&0002f&&&&c3&&&&&&&& ret&&&& 0_main&&&&ENDP_TEXT&&&&ENDS;&&&&COMDAT ?Crash1@@YAXXZ_TEXT&&&&SEGMENT_p$ = -4?Crash1@@YAXXZ PROC NEAR&&&&&&&&&&&&&&&&; Crash1, COMDAT; 15&& : {&&00000&&&&55&&&&&&&& push&&&& ebp&&00001&&&&8b ec&&&&&&&& mov&&&& ebp, esp&&00003&&&&83 ec 44&&&& sub&&&& esp, 68&&&&&&&&&&&&; H&&00006&&&&53&&&&&&&& push&&&& ebx&&00007&&&&56&&&&&&&& push&&&& esi&&00008&&&&57&&&&&&&& push&&&& edi&&00009&&&&8d 7d bc&&&& lea&&&& edi, DWORD PTR [ebp-68]&&0000c&&&&b9 11 00 00 00&&&& mov&&&& ecx, 17&&&&&&&&&&&&; H&&00011&&&&b8 cc cc cc cc&&&& mov&&&& eax, -&&&&&&&&; ccccccccH&&00016&&&&f3 ab&&&&&&&& rep stosd; 16&& :&&char * p =(char*)100;&&00018&&&&c7 45 fc 64 00&&&&00 00&&&&&&&& mov&&&& DWORD PTR _p$[ebp], 100&&&&; H; 17&& :&&*p=100;&&0001f&&&&8b 45 fc&&&& mov&&&& eax, DWORD PTR _p$[ebp]&&00022&&&&c6 00 64&&&& mov&&&& BYTE PTR [eax], 100&&&&; H; 18&& : }&&00025&&&&5f&&&&&&&& pop&&&& edi&&00026&&&&5e&&&&&&&& pop&&&& esi&&00027&&&&5b&&&&&&&& pop&&&& ebx&&00028&&&&8b e5&&&&&&&& mov&&&& esp, ebp&&0002a&&&&5d&&&&&&&& pop&&&& ebp&&0002b&&&&c3&&&&&&&& ret&&&& 0?Crash1@@YAXXZ ENDP&&&&&&&&&&&&&&&&&&&&; Crash1_TEXT&&&&ENDSEND其中?Crash1@@YAXXZ PROC NEAR&&&&&&&&&&&&&&&&; Crash1, COMDAT为Crash1汇编代码的起始行。产生崩溃的代码便在其后的某个位置。接下去的一行为: ; 15&& : {冒号后的&{&表示源文件中的语句,冒号前的&15&表示该语句在源文件中的行数。 这之后显示该语句汇编后的偏移地址,二进制码,汇编代码。如 00000&&&&55&&&&&&&& push&&&& ebp其中&0000&表示相对于函数开始地址后的偏移,&55&为编译后的机器代码,& push&&&& ebp&为汇编代码。从&cod&文件中我们可以看出,一条(c/c++)语句通常需要编译成数条汇编语句 。此外有些汇编语句太长则会分两行显示如: 00018&&&&c7 45 fc 64 00&&&&00 00&&&&&&&& mov&&&& DWORD PTR _p$[ebp], 100&&&&; H其中&0018&表示相对偏移,在debug版本中,这个数据为相对于函数起始地址的偏移(此时每个函数第一条语句相对偏移为0000);release版本中为相对于代码段第一条语句的偏移(即代码段第一条语句相对偏移为0000,而以后的每个函数第一条语句相对偏移就不为0000了)。&c7 45 fc 64 00 00 00 &为编译后的机器代码 ,&mov&&&& DWORD PTR _p$[ebp], 100&为汇编代码, 汇编语言中&;&后的内容为注释,所以&;H&,是个注释这里用来说明100转换成16进制时为&H&。接下去,我们开始来定位产生崩溃的语句。第一步,计算崩溃地址相对于崩溃函数的偏移,在本例中已经知道了崩溃语句的地址(0x),和对应函数的起始地址(0x),所以崩溃地址相对函数起始地址的偏移就很容易计算了: &&崩溃偏移地址 = 崩溃语句地址 - 崩溃函数的起始地址 = 0x - 0x = 0x22。第二步,计算出错的汇编语句在cod文件中的相对偏移。我们可以看到函数Crash1()在cod文件中的相对偏移地址为0000,则 崩溃语句在cod文件中的相对偏移 =&&崩溃函数在cod文件中相对偏移 + 崩溃偏移地址 = 0x0000 + 0x22 = 0x22第三步,我们看Crash1函数偏移0x22除的代码是什么?结果如下
00022&&&&c6 00 64&&&& mov&&&& BYTE PTR [eax], 100&&&&; H这句汇编语句表示将100这个数保存到寄存器eax所指的内存单元中去,保存空间大小为1个字节(byte)。程序正是执行这条命令时产生了崩溃,显然这里eax中的为一个非法地址 ,所以程序崩溃了!第四步,再查看该汇编语句在其前面几行的其对应的源代码,结果如下: ; 17&& :&&*p=100;其中17表示该语句位于源文件中第17行,而“*p=100;”这正是源文件中产生崩溃的语句。至此我们仅从崩溃地址就查找出了造成崩溃的源代码语句和该语句所在源文件中的确切位置,甚至查找到了造成崩溃的编译后的确切汇编代码!怎么样,是不是感觉更爽啊?五、小节1、新方法同样要注意可以适用的范围,即程序由一条语句当即引起的崩溃。另外我不知道除了VC6外,是否还有其他的编译器能够产生类似的&cod&文件。2、我们可以通过比较 新方法产生的debug和releae版本的&cod&文件,查找那些仅release版本(或debug版本)有另一个版本没有的bug(或其他性状)。例如&罗文&中所举的那个用例 ,只要打开release版本的&cod&文件,就明白了为啥debug版本会产生崩溃而release版本却没有:原来release版本中产生崩溃的语句其实根本都没有编译 。同样本例中的release版本要看到崩溃的效果,需要将编译选项改为为不优化的配置。
作为Windows程序员,平时最担心见到的事情可能就是程序发生了崩溃(异常),这时Windows会提示该程序执行了非法操作,即将关闭。请与您的供应商联系。呵呵,这句微软的“名言”,恐怕是程序员最怕见也最常见的东西了。在一个大型软件的测试过程中,初期出现程序崩溃似乎成了不可避免的事。其实测试中出现程序崩溃并不可怕,反而是测试的成功。作为开发的我们更需要关心的是程序中的哪个函数或哪一行导致了系统崩溃,这样才能有针对性的进行改正。本文描述了自己总结的几种定位崩溃的办法。以下是几种常见的崩溃现象及对应的处理办法:1.对于Release版本必现的崩溃且在Debug版本上也崩溃的程序。解决思路:去掉所有断点,直接在Debug版本上运行程序,在程序崩溃时,VC会自动跳转定位到崩溃代码行,&这种方法最简单也最常用。2.对于在Debug版本上不崩溃但Release版本崩溃的程序,很有可能是Debug和Release版本的差异。例如Debug版本所有成员在构造时会被清0,而Release版本所有成员在构造时是内存里面的原始值,而且Debug有运行时库做保护,这些都会导致某些程序在Debug正常而Release崩溃。解决思路:1)在程序中加打印,通过程序崩溃之前的打印定位出错位置;&2)逐段注释代码,直到程序不崩溃为止。这种方法耗时较长,对程序员要求较高,而且对于那种不是必现的bug或者很难搭建执行环境的情况就较难处理了。3.对于在客户现场崩溃的情况,显然不适合直接带一台电脑去调试。解决思路:应该有文件记录下崩溃信息,客服人员可以将崩溃信息文件发送给程序员,以便程序员查询崩溃原因,然后利用编译时生成MAP文件(工程信息文件,存放在版本编译机中)的信息来定位问题函数或问题代码行。下面就这种方法展开讨论一下:对于上节第三种情况,也是最难解决的情况,解决过程如下:1.崩溃回调注册,拦截Windows程序崩溃;2.在回调处理中,输出崩溃原因,崩溃内存地址,崩溃堆栈;3.工程输出map文件;4.通过崩溃内存地址以及map文件找出崩溃的函数。5.使用COD文件精确定位崩溃行&实际上,只靠Windows的错误消息对话框提供的信息量是很有限的。用SetUnhandledExceptionFilter注册自定义错误处理回调函数,可以替换Win32默认的异常处理过滤器(top-level exception filter),而且能打印出崩溃堆栈,这对定位崩溃原因非常有用。SetUnhandledExceptionFilter的函数原型:LPTOP_LEVEL_EXCEPTION_FILTER&SetUnhandledExceptionFilter(LPTOP_LEVEL_EXCEPTION_FILTER&lpTopLevelExceptionFilter&);&功&&能:注册和注销异常处理回调;用&&法:第一次调用注册异常处理回调,第二次调用注销;返回值:返回当前的exception filter。需要保存这个函数指针,在注销异常处理回调的时候,以此为参数再次调用SetUnhandledExceptionFilter。打印异常处理也需要此值。参数:&异常处理的回调函数;&崩溃信息在异常回调函数中打印,输出到程序执行目录下的文件:异常处理回调的函数原形:LONG&WINAPI&CallBackDebugInfo&(&EXCEPTION_POINTERS&*pException);&功&&能:异常处理回调处理,打印崩溃信息;用&&法:注册自定义错误处理回调:SetUnhandledExceptionFilter (CallBackDebugInfo);返回值:EXCEPTION_CONTINUE_EXECUTION –&&错误已经被修复,从异常发生处继续执行EXCEPTION_CONTINUE_SEARCH&&&&–&&继续查找异常过滤器EXCEPTION_EXECUTE_HANDLER&&&–&&正常返回参数:&崩溃信息结构,包含崩溃原因、崩溃模块、崩溃地址、崩溃堆栈等;常见崩溃原因有:EXCEPTION_ACCESS_VIOLATION = C0000005h&&&读写内存错误EXCEPTION_INT_DIVIDE_BY_ZERO = C0000094h&&除0错误EXCEPTION_STACK_OVERFLOW = C00000FDh&&堆栈溢出或者越界EXCEPTION_GUARD_PAGE = h&由Virtual Alloc建立起来的属性页冲突EXCEPTION_NONCONTINUABLE_EXCEPTION = C0000025h不可持续异常,程序无法恢复执行,异常处理例程不应处理这个异常EXCEPTION_INVALID_DISPOSITION = C0000026h在异常处理过程中系统使用的代码EXCEPTION_BREAKPOINT = h&&调试时中断(INT 3)EXCEPTION_SINGLE_STEP = h&&单步调试状态(INT 1)&map文件map文件记录程序的全局符号、源文件和代码行号信息,是整个程序工程信息的静态文本。通过文本阅读工具如Ultra Edit或记事本就可以打开Map文件。在&VC&中,打开“Project Settings”选项页,选择&C/C++&选项卡,并在最下面的&Project Options&里面输入:/Zd&,然后选择&Link&选项卡,选中“Generate mapfile”复选框。并在最下面的&Project Options&里面输入:/mapinfo:lines,表示生成&map&文件时,加入行信息。&最后编译就可以生成&MAP&文件,可以在工程的Debug或Release目录下找到刚刚生成的MAP文件,文件名为“工程名.map”。&map文件找出崩溃函数  &&& 通过上面的步骤,已经得到了&MAP&文件,那么我们该如何利用它呢?下面一步步演示使用MAP文件定位程序崩溃行的过程。&&&&&&1.我们先在代码中加入非法内存操作(最常见的异常)的代码:BOOL CMainFrameDlg::OnInitDialog(){&&&&&&&&&::SetProp(m_hWnd,&AfxGetApp()-&m_pszExeName, (HANDLE)1);&&&&&&&&&s32 *p&=&NULL;&&&&&&&&&*p=&123;&2.执行程序,程序在开始就异常,在异常打印文件中打印了如下信息:========================&崩溃信息&==========================崩溃时间:& 16:58:22崩溃原因:&非法内存操作异常代码&= c0000005异常地址&= 0x0045a76f异常模块: E:\ccroot\liuxiaojing_Enterprise\Enterprise_VOB\70-nms1\pcmt2\prj_win32\Release\pcmt2.exeSection name: .text - offset(rva) : 0x0005976f---------------------- Trips of Stack ----------------------E:\ccroot\liuxiaojing_Enterprise\Enterprise_VOB\70-nms1\pcmt2\prj_win32\Release\pcmt2.exename : pcmtver - location: 2bef3.&确定崩溃地址是:0x0005976f,在Map文件中定位函数:0 ?OnCreate@CMainFrameDlg@@IAEHPAUtagCREATESTRUCTA@@@Z&0045a420 f&&MainFrameDlg.obj&0&&&&&&&?SetTooltips@CMainFrameDlg@@AAEXXZ&0045a460 f&&&MainFrameDlg.obj&0&&&&&&&?OnTranslate@CMainFrameDlg@@IAEJIJ@Z&0045a700 f&&&MainFrameDlg.obj&0&&&&&&&?OnInitDialog@CMainFrameDlg@@MAEHXZ&0045a730 f&&&MainFrameDlg.obj&0001:00059a10&&?OnSysCommand@CMainFrameDlg@@IAEXIJ@Z 0045aa10 f&&&MainFrameDlg.obj&0001:00059c20&&&&&&&?OnPaint@CMainFrameDlg@@IAEXXZ&0045ac20 f&&&MainFrameDlg.obj根据&&0005976f&&&00059a10&,确定是在CMainFrameDlg&的OnInitDialog函数中的某一行产生了异常。map代码行定位崩溃行区间&Line numbers for .\Release\MainFrameDlg.obj(E:\ccroot\liuxiaojing_Enterprise\Enterprise_VOB\70-nms1\pcmt2\source\MainFrameDlg.cpp) segment .text&&&498 7&&&499 7&&&501 e&&&502 af&&&503 ed&&&506 0&&&507 3&&&508 8&&&510 0001:0005970f&&&511 0&&&512 3&&&515 0&&&516 e&&&521 d&&&524 e&&&526 b我们在map文件的代码行信息里查找不超过计算结果0x0005976f,但可以找最接近的数。发现是&MainFrameDlg.cpp&文件中的:521 d,而程序实际崩溃行在519(注释行和空行也要计算在内),非常接近实际崩溃行了,考虑到程序实际执行的是汇编指令,我们可以在(516 ~524)行区间内寻找到实际崩溃行。  但是这种输出文件的方法也不能定位所有崩溃问题,俗话说得好:没有万能的救世主。例如我们有时会碰到下层编解码器崩溃,崩溃打印如下表:========================&崩溃信息&==========================崩溃时间:& 09:48:17崩溃原因:&非法内存操作异常代码&= c0000005异常地址&= 0x02163b32异常模块: C:\WINDOWS\system32\kdg7221.acmSection name: .text - offset(rva) : 0x00002b32---------------------- Trips of Stack ----------------------C:\WINDOWS\system32\kdg7221.acm&&&这时可以看出是我们的音频解码器kdg7221.acm崩溃了,此时就要考虑我们的音频编解码参数是否设置错了,如果没有设错,bug可以转到媒体处理层或者软件一部处理。
作为程序员,平时最担心见到的事情就是程序发生了崩溃,无论是指针越界还是非法操作,都将给我们的应用系统造成巨大的损失。但在一个大型系统的测试过程中,初期出现程序崩溃似乎成了不可避免的事。其实测试中出现程序崩溃并不可怕,反而是测试的成功。我们更为关心的是程序中的哪一行导致了系统崩溃,这样我们才能有针对性的进行改正。  在VC中,我们可以利用出现程序崩溃时VC的自动跳转,定位到出错代码行。但在大量的压力测试时,尤其是多线程测试时,同时出现几十个错,这时VC本身的出错跳转往往会失灵。  在这里我们介绍一种辅助查找程序崩溃代码行的好方法,它的核心就是利用编译时生成MAP文件中的信息来定位代码行。下面就开始我们的介绍。  首先我们必须生成程序的MAP文件。那么什么是 MAP 文件呢?简单地讲, MAP 文件是程序的全局符号、源文件和代码行号信息的唯一的文本表示方法,是整个程序工程信息的静态文本。它可以在任何地方、任何时候使用,不需要有额外的程序进行支持,仅仅通过一个文本阅读工具如Ultra Edit就可以打开了。而且,这是唯一能找出程序崩溃代码行的救星。  那么我们应该如何生成MAP文件呢?在 VC 中,我们可以按下 Alt+F7,打开“Project Settings”选项页,选择 C/C++ 选项卡,并在最下面的 Project Options 里面输入:/Zd ,然后要选择 Link 选项卡,选中“Generate mapfile”复选框,并在最下面的 Project Options 里面输入:/mapinfo:lines,表示生成 MAP 文件时,加入行信息。最后按下 F7 来编译生成 EXE 可执行文件和 MAP 文件,此时可以在工程的Debug目录下找到刚刚生成的MAP文件,文件名为“工程名.map”。  通过上面的步骤,已经得到了 MAP 文件,那么我们该如何利用它呢?让我们从一个简单的实例入手,一步一步演示使用MAP文件定位程序崩溃行的过程。首先假设我们的VC工程中有下面这个文件://***************************************************** // 程序名称:演示如何通过崩溃地址找出源代码的出错行 // 作者:刘可 // 日期: // 本程序会产生“除0错误”,所以会导致// 程序崩溃,弹出“非法操作”对话框。 //****************************************************** #includeint crashtest(int a,int b) {c = a/b;}void main(void) { int a = 30;int b = 0;printf(&let's begin crash test...n&);ret = crashtest(a,b);}很显然本程序有“除0错误”,在 Debug 方式下编译,运行时会产生“非法操作”。我们记录下产生崩溃的地址——在我的机器上是 0x0040102f 。这个在不同的机器上可能地址不同,但记下这个地址我们下面将要使用。我们打开它的 MAP 文件:(这里列出我们比较关心的内容,其他的就略过了)abort(工程名)Timestamp is 3ef16533 (Thu Jun 19 15:24:35 2003)Preferred load address is Start    Length    Name        Class0 0001081dH .text        CODE0 000013baH .rdata        DATAba H .edata        DATA0 H .CRT$XCA       DATA4 H .CRT$XCZ       DATA8 H .CRT$XIA       DATAc H .CRT$XIC       DATA8 H .CRT$XIZ       DATAc H .CRT$XPA       DATA0 H .CRT$XPX       DATA4 H .CRT$XPZ       DATA8  H .CRT$XTA       DATAc  H .CRT$XTZ       DATAa30  H .data        DATAc68  H .bss        DATA0  H .idata$2       DATA4  H .idata$3       DATA8  H .idata$       DATA8  H .idata$5        DATA8  H .idata$6       DATAAddress Publics by Value Rva+Base Lib:Object0 ?crashtest@@YAHHH@Z
f main.objc _main 0040103c f main.objb0 _printf
f LIBCD:printf.obj0 __chkesp
f LIBCD:chkesp.obj0 _mainCRTStartup
f LIBCD:crt0.obja0 __amsg_exit
f LIBCD:crt0.obj0 __stbuf
f LIBCD:_sftbuf.obj0 __ftbuf
f LIBCD:_sftbuf.obj0 __output
f LIBCD:output.objc0 ___initstdio
f LIBCD:_file.objf0 ___endstdio
f LIBCD:_file.obj0 __CrtDbgBreak
f LIBCD:dbgrpt.obj0 __CrtSetReportMode
f LIBCD:dbgrpt.obj0 __CrtSetReportFile
f LIBCD:dbgrpt.obj0 __CrtSetReportHook
f LIBCD:dbgrpt.obj0 __CrtDbgReport
f LIBCD:dbgrpt.obj  如果仔细浏览 Rva+Base 这栏,我们可以发现第一个比崩溃地址 0x0040102f 大的函数地址是 0x0040103c ,所以在 0x0040103c 这个地址之前的那个入口就是产生崩溃的函数,也就是这行:0 ?crashtest@@YAHHH@Z
f main.obj  因此,发生崩溃的函数就是 ?crashtest@@YAHHH@Z,所有以问号开头的函数名称都是 C++ 修饰的名称。所以在我们的源程序中,这个发生崩溃的函数就是 crashtest ()!  现在我们便轻而易举地知道了发生崩溃的函数名称。把它记下来,然后我们将要直接定位发生崩溃的代码行了。我们注意 MAP 文件的最后部分——代码行信息(Line numbers information),它是以这样的形式显示的:Line numbers for .Debugmain.obj(D:我的工作技术出异常例子abortmain.cpp) segment .text12 0 14 b 15 5 16 819 c 20 7 21 e 23 524 2 25 5  第一个数字代表在源代码中的代码行号,第二个数是该代码行在所属的代码段中的偏移量。如果要查找代码行号,需要使用下面的公式做一些十六进制的减法运算:崩溃行偏移 = 崩溃地址(Crash Address)- 基地址(ImageBase Address)- 0x1000  为什么要这样做呢?因为我们得到的崩溃地址都是由 偏移地址(Rva)+ 基地址(Base) 得来的,所以在计算行号的时候要把基地址减去。一般情况下,基地址的值是 0x 。另外,由于一般的 PE 文件的代码段都是从 0x1000 偏移开始的,所以也必须减去 0x1000 。   所以我们的:崩溃行偏移 = 0x0040102f - 0x - 0x1000 = 0x2f我们在MAP 文件的中的代码行信息里查找不超过计算结果0x2f,但却最接近的数。发现是 main.cpp 文件中的:14 b  也就意味着在源代码中的第 14 行!让我们来看看源代码,注意注释行和空行也要计算在内,程序的第14行为:c = a/b;果然就是第 14 行啊,它发生了“除0异常”!  方法已经介绍完了,从今以后,我们就可以精确地定位到源代码中的崩溃行,而且只要编译器可以生成 MAP 文件,无论在WIN平台还是UNIX平台,本方法都是适用的。  本文我们只是列举了一个非常简单的“除0异常”例子,使用MAP文件的效力或许还不十分明显。但相信在我们的大型应用系统调试中,使用MAP文件的辅助方法来快速定位发生程序崩溃的函数以及代码行,将会为我们的程序调试工作节省大量时间和精力,提高我们的调试质量。我们甚至可以要求远地用户直接提供程序崩溃的地址,然后就可以在自己机器上利用MAP文件静态地找到出错的那行,并在程序中进行相应修正了。
debugdiag可以用来追踪windows下程序崩溃,卡死(包括死锁),断言等一些疑难问题的原因,对检测程序内存泄漏也有很好的帮助,相对于另外一款windbg调试器来说,是程序员的轻量级武器。本文讲述debug版本(release版本也可以,只不过debug版本能更明显地定位引起出错的代码行)的程序在程序崩溃,卡死,和出现断言时,怎样用debugdiag来帮助定位源代码中引起问题的代码。一 程序崩溃时,debugdiag抓dump文件的方法注意在程序崩溃之前,就要用debugdiag设置好rule,打开Tools/Rule Actions/Add Rule…对话框,选择Crash,单击下一步,选择A specific process,这里以程序test_crash.exe作为例子,输入test_crash.exe,如下图所示:单击下一步,根据需要选择相应的选项,这里选择Log Stack Trace,表示记录崩溃时的堆栈情形。如下图所示。接下来一路单击下一步,然后单击完成激活这个rule,运行test_crash,按回车,程序崩溃,会在Rules选项卡界面显示Userdump计数和Userdump路径,打开dump文件所在文件夹,右击生成的dump文件,选择Analyze Crash/Hang Issue,会生成一个分析报告,这里贴出其中二段。In test_crash__PID__1912__Date__06_07_2011__Time_01_18_08PM__840__Second_Chance_Exception_C;dmp the assembly instruction at&test_crash!main+3f&in&E:\practice\test\Debug\test_crash.exehas caused an&unknown exception (0xc0000094)&on thread&0Thread 0 – System ID 5584Entry pointtest_crash!ILT+280(_mainCRTStartup)Create time 13:18:00Time spent in user mode0 Days 0:0:0.0Time spent in kernel mode0 Days 0:0:0.15FunctionArg 1Arg 2Arg 3Sourcetest_crash!main+3f003f56c0003f3220&test_crash!__tmainCRTStartup+1a60012fff07c8170777c930981&test_crash!mainCRTStartup+d7c930981000a06087ffdc000&kernel32!BaseProcessStart+230041111d&注意到test_crash!main+3f这里,就是造成崩溃的具体代码行地址(基于汇编指令来说),意思就是说, 造成崩溃的具体代码行地址 = main函数地址+偏移0×3f。可以对源程序反汇编验证一下,可以看到是004113FF& idiv&&&&&&& eax,dword ptr 这一行导致的崩溃,而0×加上0×3F正好等于0×004113FF,反应到实际代码上就是语句int k = j /导致的崩溃。注:test_crash的C++源码附于文章末尾。注:只有在满足如下条件时,才能根据dump文件定位是哪个函数中哪一行出了问题。一.&&&&&&&&&&&&& 生成了程序的PDB文件二.&&&&&&&&&&&&& 程序和PDB文件完全匹配三.&&&&&&&&&&&&& 程序的dump文件和PDB文件同时存在,并且PDB文件所在目录为原来生成程序的目录否则只会给出从基址(一般是0×)开始的偏移量(其实这样就已经能定位问题,只不过还需多做几步),不会给出具体的函数信息。二 程序卡死或者断言,debugdiag抓dump文件的方法程序卡死或者出现断言时,靠debugdiag的自动抓dump文件rule还不一定能奏效,但是这种情况下程序没有退出,现场还保留着,这时可以直接导出dump文件来跟踪问题。这里以程序test_assert作为例子,程序虽然简单,但是通过这种方法就能够定位所有断言问题。运行test_assert出现断言,如下图所示:在debugdiag的Processes选项卡中找到test_assert进程,然后右击,选择Create Full Userdump或者Create Mini Userdump,然后会提示dump文件在哪个目录,选择是,到dump文件所在目录,一般在debugdiag安装目录的Logs\Misc目录下,右击刚才生成的dump,选择Analyze Crash/Hang Issue,同样也会生成分析报告,同程序崩溃时的分析报告类似,也会指出具体断言原因,这里就不再赘述了。附test_crash源码:
l 手动抓取IIS dump文件l 查看dump文件的默认存储位置l 修改dump文件的存储位置l 抓取dump文件的时机手动抓取IIS dump文件1、启动DebugDiag 1.1 (x86)后点击“选择规则类型”窗口中的“取消”按钮后,点击Processes分页。2、右键单击w3wp进程,并选择Create Full Userdump等待一段时间后出现成功创建dump文件,以及dump的文件名和存储路径的对话框,点击确定即可。查看dump文件的默认存储位置1、点击DebugDiag 1.1 (x86)工具主窗口工具栏中的“文件夹”图标&&&&&&查看弹出的窗口&&&&&&&&&&&&打开其中的misc文件夹,刚才所抓取的dump文件就存放在其中。&&&&&&修改dump文件的存储位置点击DebugDiag 1.1 (x86)工具菜单栏中的“Tools”-&“Options and Settings”设置Manual Userdump Save Folder中的路径为所要修改的路径即可。 抓取dump文件的时机1、应用程序初始化完毕(执行前)需要抓取一个dump文件2、使用性能计数器观察测试中的内存使用情况,在IIS崩溃前(观察Prvite和#time in GC的变化)抓取dump文件以便于开发分析3、(可选)在性能计数器中观察gen2、large object heap、bytes in all heaps堆的使用和释放情况,在堆的使用即将达到一个高峰值(相对值)前抓取dump,并同时抓取堆释放到一个低峰值(波谷)前抓取一个dump,方便开发对比分析。
今天尝试集成了CrashRpt,感觉还不错,功能很完善,集成也很容易。下载以后解压缩,虽然有现成的Dll,EXE,但是还是建议自己编译,因为这个CrashRpt需要和你程序共享一样的vc runtime dll,否则某些异常截获不到。编译很简单,有现成的VC 工程,支持 ,就是需要WTL支持下载 WTL 80, 但是有一个小问题,如果你用VC Express版本的话,那么是没有ATL的,如果你装的Windows Plarform SDK是2008的话,那么就彻底杯具了,也没有ATL(在这个开源的大时代,微软连ATL这破东西都不舍得开放),解决方法是安装 2003版本的SDK,如果你能找到的话,或者找别人copy一份ATL ,就在SDK\include\atl目录下。&& 我自己打包了一份ATL,希望微软不要告我。。。然后, 到这里抄个例子[pre]info. = _T(&&); //改成你自己的email[/pre]随便写个什么程序,自己弄个crash出来,注意,需要把下列文件复制到你的程序的exe目录CrashRpt.dllCrashSender.exedbghelp.dllcrashrpt_lang.ini好了,测试下吧,应该会提示程序崩溃,是否发送crash log的对话框。如果发送,那么会在email里收到一个zip文件,和一个zip的md5校验码。这个zip文件里包含有crashdump.dmp 和 crashrpt.xml, xml只是些信息,dmp是Crash dump可以用vc直接打开,如果你有源代码和所有符号表,那么就能还原当时崩溃的现场。
crash示例代码#include &string.h&#include &stdio.h&void Crash2(){&&&&char * p =(char*)100;&&&&*p=100;&&&&printf(&%c&, *p);}int main(int argc,char* argv[]){&&&&Crash2();&&&&return 0;}1、编译示例代码。确保工程属性C/C++ \ General \ Debug Information Format = Program Database (/Zi). Linker \ Debugging \ Generate Debug Information = Yes (/DEBUG)2、把exe和pdb文件放在同一个目录下,否则得不到debug信息3、设置ntsd为默认debugger。修改注册表[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug]&Debugger&=ntsd -p %ld -e %ld -g4、运行程序,弹出错误框“ &0x& 指令引用的 &0x& 内存。该内存不能为 &read&”,单击“取消”,自动运行ntsd5、输入命令“.lines”,读入行号信息。输入命令“k”,可以列出调用堆栈,输入命令“r”可以列出寄存器状态。ntsd的输出如下Microsoft (R) Windows User-Mode Debugger Version 5.1.2600.0Copyright (c) Microsoft Corporation. All rights reserved.*** wait with pending attachLoaded dbghelp extension DLLThe call to LoadLibrary(ext) failed with error 2.Please check your debugger configuration and/or network accessLoaded exts extension DLLThe call to LoadLibrary(uext) failed with error 2.Please check your debugger configuration and/or network accessLoaded ntsdexts extension DLLSymbol search path is: *** Invalid *** : Verify _NT_SYMBOL_PATH settingExecutable search path is:ModLoad: 05000&& e:\Projects\test\Release\test.exeModLoad: 7cc9b3000&& C:\WINDOWS\system32\ntdll.dllModLoad: 7cc91e000&& C:\WINDOWS\system32\kernel32.dllModLoad: cb000&& C:\WINDOWS\WinSxS\x86_Microsoft.VC80.CRT_1fc8b3b9a1e18e3b_8.0._x-ww_6b128700\MSVCR80.dllModLoad: 77be00&& C:\WINDOWS\system32\msvcrt.dllModLoad: 77da00&& C:\WINDOWS\system32\ADVAPI32.dllModLoad: 77e00&& C:\WINDOWS\system32\RPCRT4.dllModLoad: 77fc00&& C:\WINDOWS\system32\Secur32.dllModLoad: 77d00&& C:\WINDOWS\system32\USER32.dllModLoad: 77ef00&& C:\WINDOWS\system32\GDI32.dllModLoad: 1d000&& C:\WINDOWS\system32\IMM32.DLLModLoad: 62c00&& C:\WINDOWS\system32\LPK.DLLModLoad: 73fa0&& C:\WINDOWS\system32\USP10.dllModLoad: 76d00&& C:\WINDOWS\system32\Apphelp.dllModLoad: 77bd00&& C:\WINDOWS\system32\VERSION.dllAccess violation - code c0000005 (!!! second chance !!!)eax= ebx= ecx=781c37e4 edx= esi= edi=eip=0040100b esp=0012ff78 ebp=0012ffc0 iopl=0&&&&&&&& nv up ei pl zr na po nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000&&&&&&&&&&&& efl=test!main+b:00&&&&&&&&&&&& mov&&&& [eax],al&&&&&&&&&&&&&&&&ds:4=??0:000& .linesLine number information will be loaded0:000& kChildEBP RetAddr401182 test!main+0xb [e:\projects\test\y4m.cpp @ 13]0012ff8c c2c23d6c test!__tmainCRTStartup+0x10f [f:\sp\vctools\crt_bld\self_x86\crt\src\crtexe.c @ 597]003b0 0xc2c23d6c003b3 0x3b3540554c4c41 x0:000& reax= ebx= ecx=781c37e4 edx= esi= edi=eip=0040100b esp=0012ff78 ebp=0012ffc0 iopl=0&&&&&&&& nv up ei pl zr na po nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000&&&&&&&&&&&& efl=test!main+b:00&&&&&&&&&&&& mov&&&& [eax],al&&&&&&&&&&&&&&&&ds:4=??通过上述的信息,我们可以知道程序崩在了y4m.cpp的13行,也就是调用Crash2的一行。但是调用栈之中没有Crash2函数。这是由于编译器的优化,Crash2函数被内联了。我们大致可以分析出是Crash内部导致了崩溃。6、也可以不通过修改注册表的默认debugger,让ntsd加载到一个已经崩掉的进程。假设程序已经崩掉,弹出了红框。先不要按确定或者取消。利用Windows任务管理器,查到崩溃程序的PID(任务管理器&查看&菜单下&选择列&,选中PID)。利用ntsd -g -p (崩溃程序的PID) 加载到崩掉的进程。这时再按红框的取消,则进程已经被ntsd完全接管。参考网页
可以根据函数名进行排序,分类统计每个函数占用的代码量,把结果导出到Excel/HTML,查询奔溃地址。简陋习作,目前只测试了VC6.0(个人比较守旧), 欢迎用过其他vc版本的同学反馈,以便改进。=700) window.open('http://www.cppblog.com/images/cppblog_com/foxriver/screen.png');" onload="if(this.offsetWidth>'700')this.width='700';if(this.offsetHeight>'700')this.height='700';" > (更新1.1版本,支持文件拖移操作,修复字符过长BUG)文件拖进窗体WIN32API实现笔记:1. 加入头文件#include &shellapi.h&//DragAcceptFiles2. 修改CreateWindowEx里的exsytle, 添加属性值WS_EX_ACCEPTFILES3. 最后消息循环里加入处理函数&&case WM_DROPFILES:&& {&&&&HDROP hDrop = (HDROP)wP&&&&char buf[MAX_PATH];&&&&for(int i=0; DragQueryFile(hDrop, i, buf, MAX_PATH); i++)&&&&{//&&&& MessageBox(0, buf, &Dropped File&, MB_ICONINFORMATION); //此处处理文件,gbk编码文件名&&&&}&&&&DragFinish(hDrop);// 一定别忘了这句,否则会有内存泄露。&& }&& return 0;
VC的MAP文件是一个文本文件,记录了程序的入口地址、基地址、符号及其对应的地址的一个文本文件。下面对其中的内容做一简要的说明。[table=medium][tr][td=1,1,616]DllTwo&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&//―――模块名
Timestamp is
(Sun Dec 28 10:27:49 2008)&&&&&&&&&&&&&&&&&& //―――时间戳
Preferred load address is &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&//―――默认加载基地址
Start&&&&&&&& Length&&&& Name&&&&&&&&&&&&&&&&&& Class&&&&&&&&&&&&&&&&&&&&&&&&&&&&//各节的起始地址、长度、 0 00000da6H .text&&&&&&&&&&&&&&&&&& CODE&&&&&&&& &&&&&&&&&&//节名、类型等信息 db0 H .text$x&&&&&&&&&&&&&&&& CODE 0 H .idata$5&&&&&&&&&&&&&&&&DATA e8 H .CRT$XCA&&&&&&&&&&&&&&&&DATA…… 0 H .rdata&&&&&&&&&&&&&&&&&&DATA…… c H .idata$6&&&&&&&&&&&&&&&&DATA be0 H .edata&&&&&&&&&&&&&&&&&&DATA 0 H .data&&&&&&&&&&&&&&&&&& DATA 8 H .bss&&&&&&&&&&&&&&&&&&&&DATA //各符号在节内的偏移地址、加载地址及符号出处&&Address&&&&&&&& Publics by Value&&&&&&&&&&&&&&Rva+Base&&&&&& Lib:Object
3&&&&&& ___safe_se_handler_count&& &&&& &absolute& 0&&&&&& ___ImageBase&&&&&&&&&&&&&& &&&& &linker-defined& 0&&&&&& ??4CDllTwo@@QAEAAV0@ABV0@@Z
f i DllTwo.obj……4&&&&&& __DllMainCRTStartup@12 &&&& f&& MSVCRT:crtdll.obj…… cd0&&&&&& ?CrashFun@@YAHXZ&&&&&&&&&& 10001cd0 f&& DllTwo.obj cf0&&&&&& _DllMain@12&&&&&&&&&&&&&&&&10001cf0 f&& DllTwo.obj d00&&&&&& ?PrintInDllTwo@@YAHXZ&&&&&&10001d00 f&& DllTwo.obj
entry point at&&&&&&&&4&&&&&&&&&&&&&&&&&&&& //模块的入口地址
Static symbols&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& //静态符号
3&&&&&& __catch$??$?6U?$char_traits@D@std@@@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@0@AAV10@PBD@Z$0
f i&&CIL library: CIL module de&&&&&& _pre_c_init&&&&&&&&&&&&&&&&100014de f&& MSVCRT:crtdll.obj ee&&&&&& ___DllMainCRTStartup&&&&&& 100016ee f&& MSVCRT:crtdll.obj db0&&&&&&……
Exports&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&//导出符号 &&ordinal&&&&name&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& //序号和名称 &&&&&&&&1&&&&??4CDllTwo@@QAEAAV0@ABV0@@Z (public: class CDllTwo & __thiscall CDllTwo::operator=(class CDllTwo const &))&&&&&&&&2&&&&?CrashFun@@YAHXZ (int __cdecl CrashFun(void))&&&&&&&&3&&&&?PrintInDllTwo@@YAHXZ (int __cdecl PrintInDllTwo(void))[/td][/tr][/table] 说明:1.&&&&&& Rva+Base地址,是加载地址,其中Rva是相对虚拟地址(Relative Virtual Address,RVA),是指令在所在节内的偏移量;Base是加载的基地址;关于此地址的更多信息,参看动态库的加载部分。2.&&&&&& VC 8.0生成的Map文件,已经去掉了“导出行”,因此上面的文件,没有源代码行到入口地址之间的映射信息。
CrashRpt.dll用来在应用程序出现异常crash时,捕获到错误,并收集出错信息:MiniDump文件、硬件信息、系统信息、出错信息、进程信息、服务信息、驱动信息、启动信息、软件列表、端口信息、磁盘分区、WinSock LSP、IE插件、网卡信息。 使用方法1、在程序每个线程最初位置调用InitializeCrashRpt函数来初始化CrashRpt.dll,当线程出错后将自动弹出CrashRpt.dll程序崩溃处理报告。 2、截图&&&& =700) window.open('http://www.xuyibo.org/res/img/CrashRpt.gif');" onload="if(this.offsetWidth>'700')this.width='700';if(this.offsetHeight>'700')this.height='700';" > 3、SDK文件说明[pre]CrashRpt.h 头文件,编译时需要include这个文件 CrashRpt.lib 导入库,链接时需要这个文件 CrashRpt.dll 动态库,程序发布时需要这个文件 TstCrashRpt.c 测试文件[/pre]
原文: 在Windows平台下用C++开发应用程序,最不想见到的情况恐怕就是程序崩溃,而要想解决引起问题的bug,最困难的应该就是调试release版本了。因为release版本来就少了很多调试信息,更何况一般都是发布出去由用户使用,crash的现场很难保留和重现。本文将给出几个解决方案,完成对release版应用程序crash错误的调试。(本文只讨论Windows平台MSVC环境下的调试,对于其他平台和开发环境没有关注,请大家自己借鉴和尝试。) &&&&方案一:崩溃地址 + MAP文件&&
这种方案只能对VC7以前的版本开发的程序使用。 &&&&1、崩溃地址&&&& 所谓崩溃地址就是引起程序崩溃的内存地址,在WinXP下应用程序crash的对话框如下图:&&&&上面第2张图中画红线的值为crash的代码偏移地址,第3张图为即crash绝对地址;一般引起crash的原因多为内存操作错误,我们用这两个地址和MAP文件就能定位出错的代码行。&&&&2、MAP文件&&&&MAP文件是记录应用程序信息的文件(文本文件),里面大概包含了程序的全局符号、源码模块名、源码文件和行号等信息,而这些信息能够帮助我们定位出错的代码行。&&
怎样生成MAP文件呢?以VC6为例,在 Project Settings -& C/C++ -& Debug info中,选择 Line Numbers Only ;在 Project Settings -& Link 中,选择 Generate mapfile项,并在Project Options里面输入 /MAPINFO:LINES 和 /MAPINFO:EXPORTS,重新编译程序就会生成.map文件。&&
以上设置对应的编译链接选项分别分:&&
/Zi — 表示生成pdb调试信息;&&
/MAP[:filename] — 表示生成map文件名;&&
/MAPINFO:EXPORTS
— 表示生成的map文件中加入exported functions(生成DLL文件时);&&
/MAPINFO:LINES
— 表示生成的map文件中加入代码行信息。&&
由于/MAPINFO:LINES选项在VC8以后的版本中不再支持,因此通过MAP文件中的信息和crash地址定位出错代码行就比较困难了,所以这种方案只能在VC7及以前的版本中使用。&&&&一个MAP文件片段示例如下: &&&& &&&&&&&&图中Rva+Base列的地址为该行函数对应的函数绝对地址,Address列中冒号后面的地址为函数相对偏移地址。&& &&&&3、定位crash代码&&
有了上面的介绍,定位crash代码就很简单了。用下面的公式来进行定位:&&
崩溃行偏移 = 崩溃地址 - 崩溃函数绝对地址 + 函数相对偏移&&
我们首先根据崩溃地址(绝对地址),按照找到第2张图中Rva+Base列的地址找到发生崩溃的函数(即崩溃地址大于该函数行的Rva+Base地址且小于下个函数的地址),然后找到该行对应的函数相对偏移地址,带入公式中,就得到了崩溃行偏移,该值表示崩溃行的代码相对于代码所在函数的偏移量。用该值去与第3张图中对应函数冒号后面的偏移量去比较,最接近的值前面的那个十进制数即为代码所在函数中的行号。&&&&ok,到此我们已经成功找到了崩溃的代码行,只不过这种方法还是比较费力,并且限制比较多,我们看看下面的方案。
上篇给出的方案一还要补充几句。通过“crash地址 + MAP文件”来定位出错代码位置虽然需要经过比较复杂的地址计算,但却是最简单实现的方式。如果仅仅想通过崩溃地址定位出错的函数,就更加方便了。我在网上找到一个解析MAP文件的小工具,可以非常清晰的列出每个函数的地址,并且可以将分析表格导出为Excel文件。工具下载地址:,工具目录下VCMapper.exe。&&&&另外上篇主要参考两篇文章:&&&&&&&& &&&&方案二:崩溃地址 + MAP文件 + COD文件&&&&由于VC8以后的版本都不再支持MAP文件中产生代码行信息,因此我们寻找另一种定位方式:COD文件。&&&&1、COD文件&&&&COD文件是一个包含了汇编码、二进制机器码和源代码对应信息的文件,每一个cpp都对应一个COD文件。通过这个文件,我们可以非常方便地进行定位。&&&&在VC6中生成COD文件的设置方式为:Project Settings -& C/C++,在 Category 中选 Listing Files,在 Listing file type 组合框中选 Assembly,Machine code,and source。在VC8中生成COD文件的设置方式为:Project Properties -& C/C++ -& Output Files -& Assembler Output 项,选择 Assembly,Machine code,and Source(/Facs)。&& &&&&2、定位崩溃行&&&&下面通过举例进行说明。现在我有一个基于对话框的MFC应用程序CrashTest,在CCrashTestDlg::OnInitDialog函数中写入导致crash的代码语句(第99行),源文件如下:&&&&&&&&根据崩溃地址(0x)以及MAP文件(定位片段图片如下),定位crash函数为OnInitDialog;并且我们可以很容易地计算出崩溃地址相对于崩溃函数的偏移量为 0x - 0x = 0xC3。&&&&&&&&再来看看CrashTestDlg.cod文件,我们根据文件中源码信息找到OnInitDialog函数信息片段:&&&&&&&&可以看到图片中第一行为OnInitDialog函数汇编代码的起始行;找到“int * p = NULL;”这一句源码,其前面的98表示这行代码在源文件中的行号,下面的000c1表示相对于函数开始位置的偏移量,后面的“33 c0”为机器码,“xor eax,eax”为汇编码。那么我们根据前面算出来的偏移量0xC3,找到对应出错的语句为99行:“*p = 5;”。&&&&总结一下定位步骤:&&&&1) 根据公式 崩溃语句在函数中偏移地址 = 崩溃地址 - 崩溃函数地址 计算出偏移量X;&&&&2) 根据公式 崩溃语句在COD文件中地址 = 崩溃函数在COD文件中地址 + X 计算出地址Y。其中崩溃函数在COD文件中地址为COD文件中函数起始括号“{”后面表明的地址,一般情况下为0x0000;&&&&3) 根据Y在COD文件中找到对应代码行。&& &&&&ok,方案二介绍完了。这种方法最大的好处是没有VC开发环境版本限制,而且COD文件里面包含的信息更加丰富,不但可以帮助我们定位crash,还能帮我们分析很多东西。当然,这也导致编译生成了很多信息文件。
根据前面两篇博文,我们要定位崩溃行代码,必须要自己根据相关信息文件进行计算。如果需要处理的量比较大,恐怕会很费力气。有没有更简单快速的办法呢?&&&&最直接的想法就是写一个小工具,根据规则和信息进行自动定位,不过开发起来也是要费一番功夫的。令人开心的是,我们可以找到类似的工具,而且是开源免费的!程序员的世界也许很多时候都是这么单纯而乐于分享!&& &&&&方案三:崩溃地址 + PDB文件 + CrashFinder&&&&CrashFinder是一个开源工具,作者是John Robbin,大家可以去他的blog上去找关于CrashFinder的信息。我们这里以CrashFinder2.5版本为例介绍,相关文章链接为:&&&&1、PDB文件&&&&PDB(Program Database)文件中包含了exe程序所有的调试相关信息,具体可以查阅MSDN。当编译选项设置为/Zi,链接选项设置为/DEBUG,/OPT:REF时,就会生成工程的.pdb文件。具体到VC2005中,就是 Project Propertise -& C/C++ -& General -& Debug Information Format 项设置为 Program Database(/Zi),Linker -& Debugging -& Generate Debug Info 项设置为 Yes(/Debug),Linker -& Optimization -& References 项设置为 Eliminate Unreferenced Data(/OPT:REF)。&&&&只要设置以上选项,release版本也能生成PDB文件。当然,对应的应用程序也会稍大。&&&&2、CrashFinder&&&&CrashFinder能够运行需要两个条件:一是系统必须要有dbghelp.dll文件;二是PDB文件必须与exe文件在一个路径下。对于dbghelp.dll,一般在系统system32路径下都有,如果没有下载一个放到这个目录下就可以了。&&&&先看一下CrashFinder的界面。&& &&&&用起来也非常简单。首先选择File-&New或点击工具栏新建按钮,选择要调试的exe文件打开,会发现exe及所依赖的dll文件信息都已经加载进来。在下半

我要回帖

更多关于 区块链 技术栈 的文章

 

随机推荐