`
txqc4
  • 浏览: 3709 次
  • 性别: Icon_minigender_1
  • 来自: 上海
最近访客 更多访客>>
文章分类
社区版块
存档分类
最新评论

对“仅通过崩溃地址找出源代码的出错行”一文的补充与改进

阅读更多
下载源代码
读了老罗的“仅通过崩溃地址找出源代码的出错行”(下称"罗文")一文后,感觉该文还是可以学到不少东西的。不过文中尚存在有些说法不妥,以及有些操作太繁琐的地方 。为此,本人在学习了此文后,在多次实验实践基础上,把该文中的一些内容进行补充与改进,希望对大家调试程序,尤其是release版本的程序有帮助 。欢迎各位朋友批评指正。

一、该方法适用的范围
在windows程序中造成程序崩溃的原因很多,而文中所述的方法仅适用与:由一条语句当即引起的程序崩溃。如原文中举的除数为零的崩溃例子。而笔者在实际工作中碰到更多的情况是:指针指向一非法地址 ,然后对指针的内容进行了,读或写的操作。例如:
void Crash1()
{
 char * p =(char*)100;
 *p=100;
}

这些原因造成的崩溃,无论是debug版本,还是release版本的程序,使用该方法都可找到造成崩溃的函数或子程序中的语句行,具体方法的下面还会补充说明。 另外,实践中另一种常见的造成程序崩溃的原因:函数或子程序中局部变量数组越界付值,造成函数或子程序返回地址遭覆盖,从而造成函数或子程序返回时崩溃。例如:
void Crash2();

int main(int argc,char* argv[])
{
	Crash2();
	return 0;
}

void Crash2()
{
	char p[1];
	strcpy(p,"0123456789");
}


在vc中编译运行此程序的release版本,会跳出如下的出错提示框。



图一 上面例子运行结果

这里显示的崩溃地址为:0x34333231。这种由前面语句造成的崩溃根源,在后续程序中方才显现出来的情况,显然用该文所述的方法就无能为力了。不过在此例中多少还有些蛛丝马迹可寻找到崩溃的原因:函数Crash2中的局部数组p只有一个字节大小,显然拷贝"0123456789"这个字符串会把超出长度的字符串拷贝到数组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型数时就是0x34333231。显然拷贝"0123456789"这个字符串时,"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信息。


图二 设置产生MAP文件

三、定位崩溃语句位置的方法
"罗文"所述的定位方法中,找到产生崩溃的函数位置的方法是正确的,即在map文件列出的每个函数的起始地址中,最近的且不大于崩溃地址的地址即为包含崩溃语句的函数的地址 。但之后的再进一步的定位出错语句行的方法不是最妥当,因为那种方法前提是,假设基地址的值是 0x00400000 ,以及一般的 PE 文件的代码段都是从 0x1000偏移开始的 。虽然这种情况很普遍,但在vc中还是可以基地址设置为其他数,比如设置为0x00500000,这时仍旧套用

崩溃行偏移 = 崩溃地址 - 0x00400000 - 0x1000

的公式显然无法找到崩溃行偏移。 其实上述公式若改为

崩溃行偏移 = 崩溃地址 - 崩溃函数绝对地址 + 函数相对偏移

即可通用了。仍以"罗文"中的例子为例:"罗文"中提到的在其崩溃程序的对应map文件中,崩溃函数的编译结果为

0001:00000020 ?Crash@@YAXXZ 00401020 f CrashDemo。obj 


对与上述结果,在使用我的公式时 ,"崩溃函数绝对地址"指00401020, 函数相对偏移指 00000020, 当崩溃地址= 0x0040104a时, 则 崩溃行偏移 = 崩溃地址 - 崩溃函数起始地址+ 函数相对偏移 = 0x0040104a - 0x00401020 + 0x00000020= 0x4a,结果与"罗文"计算结果相同 。但这个公式更通用。

四、更好的定位崩溃语句位置的方法。
其实除了依靠map文件中的Line numbers信息最终定位出错语句行外,在vc6中我们还可以通过编译程序产生的对应的汇编语句,二进制码,以及对应c/c++语句为一体的"cod"文件来定位出错语句行 。先介绍一下产生这种包含了三种信息的"cod"文件的设置方法:先点击菜单"Project"->"Settings。。。",弹出的属性页中选中"C/C++"页 ,然后在"Category"中选则"Listing Files",再在"Listing file type"的组合框中选择"Assembly,Machine code, and source"。接下去再通过一个具体的例子来说明这种方法的具体操作。


图三 设置产生"cod"文件

准备步骤1)产生崩溃的程序如下:

//****************************************************************
//文件名称:crash。cpp
//作用:    演示通过崩溃地址找出源代码的出错行新方法
//作者:   伟功通信 roc
//日期:   2005-5-16
//****************************************************************
void Crash1();
int main(int argc,char* argv[])
{
	Crash1();
	return 0;
}

void Crash1()
{
  char * p =(char*)100;
  *p=100;
}


准备步骤2)按本文所述设置产生map文件(不需要产生Line numbers信息)。
准备步骤3)按本文所述设置产生cod文件。
准备步骤4)编译。这里以debug版本为例(若是release版本需要将编译选项改为不进行任何优化的选项,否则上述代码会因为优化时看作废代码而不被编译,从而看不到崩溃的结果),编译后产生一个"exe"文件 ,一个"map"文件,一个"cod"文件。
运行此程序,产生如下如下崩溃提示:

图四 上面例子运行结果

排错步骤1)定位崩溃函数。可以查询map文件获得。我的机器编译产生的map文件的部分如下:

 Crash

 Timestamp is 42881a01 (Mon May 16 11:56:49 2005)

 Preferred load address is 00400000

 Start Length Name Class
0001:00000000 0000ddf1H .text CODE
0001:0000ddf1 0001000fH .textbss CODE
0002:00000000 00001346H .rdata DATA
0002:00001346 00000000H .edata DATA
0003:00000000 00000104H .CRT$XCA DATA
0003:00000104 00000104H .CRT$XCZ DATA
0003:00000208 00000104H .CRT$XIA DATA
0003:0000030c 00000109H .CRT$XIC DATA
0003:00000418 00000104H .CRT$XIZ DATA
0003:0000051c 00000104H .CRT$XPA DATA
0003:00000620 00000104H .CRT$XPX DATA
0003:00000724 00000104H .CRT$XPZ DATA
0003:00000828 00000104H .CRT$XTA DATA
0003:0000092c 00000104H .CRT$XTZ DATA
0003:00000a30 00000b93H .data DATA
0003:000015c4 00001974H .bss DATA
0004:00000000 00000014H .idata$2 DATA
0004:00000014 00000014H .idata$3 DATA
0004:00000028 00000110H .idata$4 DATA
0004:00000138 00000110H .idata$5 DATA
0004:00000248 000004afH .idata$6 DATA

Address Publics by Value Rva+Base Lib:Object

0001:00000020 _main 00401020 f Crash.obj
0001:00000060 ?Crash1@@YAXXZ 00401060 f Crash.obj
0001:000000a0 __chkesp 004010a0 f LIBCD:chkesp.obj
0001:000000e0 _mainCRTStartup 004010e0 f LIBCD:crt0.obj
0001:00000210 __amsg_exit 00401210 f LIBCD:crt0.obj
0001:00000270 __CrtDbgBreak 00401270 f LIBCD:dbgrpt.obj
...


对于崩溃地址0x00401082而言,小于此地址中最接近的地址(Rva+Base中的地址)为00401060,其对应的函数名为?Crash1@@YAXXZ,由于所有以问号开头的函数名称都是 C++ 修饰的名称 ,"@@YAXXZ"则为区别重载函数而加的后缀,所以?Crash1@@YAXXZ就是我们的源程序中,Crash1() 这个函数。
排错步骤2)定位出错行。打开编译生成的"cod"文件,我机器上生成的文件内容如下:

TITLE	E:\Crash\Crash。cpp
.386P
include listing.inc
if @Version gt 510
.model FLAT
else
_TEXT	SEGMENT PARA USE32 PUBLIC ''CODE''
_TEXT	ENDS
_DATA	SEGMENT DWORD USE32 PUBLIC ''DATA''
_DATA	ENDS
CONST	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	ENDS
FLAT	GROUP _DATA, CONST, _BSS
	ASSUME	CS: FLAT, DS: FLAT, SS: FLAT
endif
PUBLIC	?Crash1@@YAXXZ					; Crash1
PUBLIC	_main
EXTRN	__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			; 00000040H
  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			; 00000010H
  00011	b8 cc cc cc cc	 mov	 eax, -858993460		; 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			; 00000040H
  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			; 00000044H
  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			; 00000011H
  00011	b8 cc cc cc cc	 mov	 eax, -858993460		; 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	; 00000064H

; 17   :  *p=100;

  0001f	8b 45 fc	 mov	 eax, DWORD PTR _p$[ebp]
  00022	c6 00 64	 mov	 BYTE PTR [eax], 100	; 00000064H

; 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	ENDS
END


其中
?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	; 00000064H


其中"0018"表示相对偏移,在debug版本中,这个数据为相对于函数起始地址的偏移(此时每个函数第一条语句相对偏移为0000);release版本中为相对于代码段第一条语句的偏移(即代码段第一条语句相对偏移为0000,而以后的每个函数第一条语句相对偏移就不为0000了)。"c7 45 fc 64 00 00 00 "为编译后的机器代码 ,"mov DWORD PTR _p$[ebp], 100"为汇编代码,汇编语言中";"后的内容为注释,所以";00000064H",是个注释这里用来说明100转换成16进制时为"00000064H"。
接下去,我们开始来定位产生崩溃的语句。
第一步,计算崩溃地址相对于崩溃函数的偏移,在本例中已经知道了崩溃语句的地址(0x00401082),和对应函数的起始地址(0x00401060),所以崩溃地址相对函数起始地址的偏移就很容易计算了:

  崩溃偏移地址 = 崩溃语句地址 - 崩溃函数的起始地址 = 0x00401082 - 0x00401060 = 0x22。

第二步,计算出错的汇编语句在cod文件中的相对偏移。我们可以看到函数Crash1()在cod文件中的相对偏移地址为0000,则

崩溃语句在cod文件中的相对偏移 =  崩溃函数在cod文件中相对偏移 + 崩溃偏移地址 = 0x0000 + 0x22 = 0x22

第三步,我们看Crash1函数偏移0x22除的代码是什么?结果如下

00022	c6 00 64	 mov	 BYTE PTR [eax], 100	; 00000064H

这句汇编语句表示将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版本要看到崩溃的效果,需要将编译选项改为为不优化的配置。


  • 大小: 13.2 KB
  • 大小: 13.3 KB
  • 大小: 57.2 KB
  • 大小: 56.3 KB
分享到:
评论

相关推荐

    VC查内存泄露地址代码_map_cod_ok

    对“仅通过崩溃地址找出源代码的出错行”一文的补充与改进  读了老罗的“仅通过崩溃地址找出源代码的出错行”(下称"罗文")一文后,感觉该文还是可以学到不少东西的。不过文中尚存在有些说法不妥,以及有些操作太...

    源代码就是设计,真的

    ### 源代码就是设计 —— 关键知识点解析 #### 一、源代码作为工程文档的重要性 ...通过深入理解这一理念,开发者可以更好地进行设计思考,编写出既高效又优雅的代码,从而推动软件行业的持续发展。

    自己开发在线编辑器一文的源代码

    在线编辑器是一种网页应用,允许用户在浏览器中直接编辑文本,通常用于创建和修改HTML、Markdown或其他格式的文档。在本文中,我们将深入探讨...此外,分析和改进现有代码可以帮助提升编程技巧,加深对Web开发的理解。

    商业编程-源码-本代码是《自绘按钮补遗》一文的配套源代码.zip

    本案例提供的压缩包"商业编程-源码-本代码是《自绘按钮补遗》一文的配套源代码.zip",其内容是与一篇名为《自绘按钮补遗》的技术文章紧密相关的,这篇文章可能是对如何自定义绘制按钮及其补充细节的深入探讨。源代码...

    osip源代码框架详解.pdf

    《osip源代码框架详解》一文由毛明华撰写于2009年9月25日,深入探讨了osip库的核心架构与功能实现,对于理解SIP(会话初始化协议)通信中的关键组件及其交互过程具有重要的参考价值。以下是对该文档中提及的关键知识...

    【JavaScript源代码】JavaScript一文带你玩转web表单网页.docx

    随着对JavaScript的进一步学习,你还能实现更多复杂的验证、动态效果和服务器通信功能,提升用户的使用体验。在Web开发的世界里,JavaScript是构建动态网页的关键工具,掌握它将助你在网页开发中游刃有余。

    一个streamlit实用例源代码一文学会Stramlit

    @Info 一文学会Stramlit #安装 Python 3.6.5 pip install streamlit==0.62.0 pip install pandas==1.1.5 启动 E:\500w\streamlit\>C:\Python\Python36-32\Scripts\streamlit run app-streamlit.py 访问 ...

    通用权限管理程序源代码

    《通用权限管理程序源代码解析》 在信息技术领域,权限管理是系统安全的重要组成部分,它...通过对源代码的学习,开发者可以深化对权限管理的理解,提升在实际项目中的应用能力,为构建更安全、更健壮的系统奠定基础。

    一个 JS 写的 Table 自增/减行例子,和一个模态对话框传值例子的源代码

    博客《一个 JS 写的 Table 自增/减行例子,和一个模态对话框传值例子的源代码》一文的示例完整源代码。博客地址:http://blog.csdn.net/defonds/archive/2010/04/21/5512015.aspx。

    Linux源代码分析——存储管理.pdf

    《Linux源代码分析——存储管理》一文深入探讨了Linux操作系统中的存储管理机制,特别是虚拟内存的实现。Linux操作系统以其开源、免费、跨平台及强大的功能特性,深受全球政府、企业和学术界的关注。作者们自2000年...

    Netty 粘包/半包原理与拆包实战 【源代码 新】

    - 本实例是《Netty 粘包/半包原理与拆包实战》 一文的源代码工程。 大家好,我是作者尼恩。 在前面的文章中,完成了一个高性能的 Java 聊天程序,尼恩已经再一次的进行了通讯协议的选择。放弃了大家非常熟悉的json...

    【JavaScript源代码】一文搞懂TypeScript的安装、使用、自动编译的教程.docx

    TypeScript的主要特点是提供了类型系统和对ES6及更高版本的支持,旨在帮助开发者在大型应用开发中提高代码质量和可维护性。 TypeScript的三大特点包括: 1. **始于JavaScript,归于JavaScript**:TypeScript编译成...

    C#,哈夫曼编码(Huffman Code)压缩(Compress )与解压缩(Decompress)算法与源代码

    C#,哈夫曼编码(Huffman Code)压缩(Compress )与解压缩(Decompress)算法与源代码 1951年,哈夫曼和他在MIT信息论的同学需要选择是完成学期报告还是期末考试。导师Robert M. Fano给他们的学期报告的题目是,...

    对《一个完善的ODBC数据库程序》一文的补充VC源代码

    ODBC(Open Database Connectivity)是微软提供的一种标准接口,它允许应用程序访问各种不同...通过对这些源代码的分析和理解,我们可以学习到如何在VC++的MFC环境中集成ODBC技术,创建灵活且功能强大的数据库应用。

    本人博客<通过R语言且只用基础package来制作一个小游戏>一文中的源代码

    原文地址https://editor.csdn.net/md/?articleId=124978265

    手机病毒列表及源代码和防治

    根据《手机病毒列表及源代码和防治》一文介绍,常见的手机病毒主要包括: - **Auto.exe/Autorun.inf 类型**:这类病毒通常通过可移动存储介质(如U盘、SD卡等)进行传播,当用户插入被感染的存储介质时,病毒会自动...

    轨迹优化matlab源代码-TrajectoryOptimization:研究一些机器人系统及其轨迹生成策略

    轨迹优化matlab源代码-TrajectoryOptimization:研究一些机器人系统及其轨迹生成策略 轨迹优化matlab源代码轨迹优化此仓库将托管我的研究代码,用于研究不同机器人系统的轨迹优化。我的最初计划是实施论文“使用隐式...

    关于如何写好代码的文章-代码之美

    通过改进代码结构而不改变其外部行为,可以使代码更易于理解和扩展。 8. **持续集成与持续部署(CI/CD)**:自动化构建、测试和部署流程可以确保代码的稳定性和一致性,减少人为错误。 9. **代码审查**:团队成员...

Global site tag (gtag.js) - Google Analytics