ie11调试工具出错出错,求助:EXPRESSION

DELPHI程序的调试与异常处理
DELPHI程序的调试与异常处理
任何一种流行的高级语言编译器都提供了较易使用的调试与异常处理的方法。当然不同的编译器因为其编译的原理不同导致其调试的具体方法和异常处理的具体技巧也有所不同。比如说VB,因为它是解释型的语言,所以象单步跟踪这样的方式可以一边编译一边查看执行结果,因为它的每一条语句本来就是一边运行一边解释再返回结果的;而象VC,相对起来则要等待“很长”的编译过程,这时候写几行代码就调试结果的习惯就会变得非常痛苦。
正因为这些原因,所以我觉得调试技巧和异常处理的具体写法跟一个程序员的编码风格有关,也与开发工具的实现方式有关,大家再随着编码经验的深入,将会形成自己的习惯,如同算法形形色色,不必拘泥于某种成法。但是有三点我认为是比较基本的,特别对于初级的程序员来说,养成良好的习惯往往比追求哪种语言/开发工具好用更有意义。
一、必须认识到一个软件是不可能100%完全可正确的、按常规执行的。不要人为的在浅意识里存在这样的思想:“(假设)理想的情况下,应该得到什么结果,所以我接下来的代码是…”“这里感觉不好…算了无所谓,反正一般情况下不会出错”。为了保证一个健壮的软件,不能无视任何错误出现的可能性!根据经验要尽可能的推断将会产生的问题,尤其是运行期的问题,哪怕只有1%的可能,也要考虑异常情况;
二、不要为了解决错误而写简单添加解决该错误的代码,这样代码会越来越多,错误本身并没有消失,还可能产生更多的错误。最好的方法是考虑某个错误为什么会出现,是否是自己考虑不周,假如是这样原因,与其添加更多的判断,不如重新思考算法;
形成对象(无论是小至一个变量,还是大至整个程序)“有始有终”的习惯。即保证初始化对象时有值,避免引用不可靠或错误的产生。同时要保证无论发生什么情况(运行时错误),都能正确的执行下去,并且回收资源。这个步骤在开始建立一个模块的时候就应该做,以避免因为自己的忘记而导致不稳定的因素。
接下来我将谈谈Delphi调试代码的一般方法。同时结合上面提到的几点,讲述Delphi中异常处理的常规写法。
Delphi没有提供专门的调试工具软件,而是在IDE环境中集成了一个调试器,对那些习惯跟踪地址的人来说可能显得简单了些。但是就我的使用感受来说,其提供的多种调试手段还是非常有用的,普通的软件开发完全可以胜任。Delphi是编译后执行的(每次只编译改动后的单元),所以感觉(编译)速度上会比VB慢一些,但比VC就显得快多了。利用Delphi调试是一件比较轻松的事情,大家可以选择适合自己习惯的调试方法。
文法检查、编译、运行
“Project”菜单上的“Syntax Check
Project”命令用于检查程序的语法错误,不产生目标文件,不链接可执行程序,所以速度很快,一般用于代码的早期检查;
“Project”菜单上的“Compile”命令将编译上次编译后改动的文件,并为每个单元生成DCU文件。假如单元的Interface部分改变,则引用该单元的单元也会被编译;
“Project”菜单上的“Build”命令除了编译外,还将链接生成目标文件;
执行“Run” 菜单或按钮除了生成目标文件外,还将直接执行该文件(dll不会执行)。
错误类型及正确判断
Delphi在设计期间可以简单的判断错误。比如忘记引用某个对象、函数的单元等,在编码的时候不会自动弹出声明菜单,而且会在下部消息区显示“Unable
to invoke Code Parameters due to errors in source
code”错误。早期版本的Delphi该方法并不是非常稳定,在6、7这些较新的版本里面判断准确率得到提高,不失为一个快速检查错误的方法;
编译期间错误也叫语法错误。Delphi的语法检查是非常严格和准确的,一般通过编译错误信息可以准确判断错误的原因。例如以下是常见的两个
';' not allowed before 'ELSE' ElSE前不允许有“;”
Variable '' might not have been initialized 变量名可能没有初始化
但是象Statement expected, but expression of type '' found
(要求语句,但出现类型的表达式)这类编译错误信息,常常是由于begin …
end不配对造成的,这样判断出错的地点和原因只能根据经验了。
跟踪调试、断点
Delphi的跟踪调试方法有如下几种
逐行单步跟踪(Step Over):一次运行一行代码,如果程序行含多条语句、函数或过程也一次执行;
逐条单步跟踪(Trace Into):一次运行一条代码,如果包含函数或过程,则跟进函数或过程;
跟踪至光标位置(Run To Cursor):程序自动运行停留在光标所在行,就象光标处设置了断点一样;
暂停执行(Program Pause):即使未设置断点,也可以在运行时中断程序,激活调试器;
程序复位(Program Reset):强制关闭运行中的程序,回到编辑状态。假如遇到死循环就非得靠它了J
在某行有效语句上可以设置断点(最简单的是在程序行最左边点击,该行会改变颜色)。程序运行到该行时将自动暂停,这时将鼠标移动到变量名称上可以显示变量此时的值。也可以打开Watch
List、Evaluate/Modify、View CPU等窗口,下面将简单介绍它们的作用。
运行时状态:值、堆栈、线程、CPU视图
在程序中断状态下,通过菜单可以打开几种状态查看器(图以Delphi7为例,其他版本大同小异)。
虽然中断时用鼠标指向当前过程内的变量可以显示值,但是对于数组中的某个具体变量,或者对象的某个属性就不容易看到值了。这时候可以使用Watch
List可以添加一批当前过程内变量/对象并查看其值。
List只能查看变量,而Evaluate/Modify还允许快速查看并修改某个变量的值,这对激活某些代码很有用,因为通过修改运行中变量的值,能够轻易模仿特殊状态(例如测试某个临界值)。
Call Stack窗口可以查看堆栈状态,一般用于有效的检查递归过程;
Threads窗口可以获得线程信息,并改变线程等级,查看当前线程代码;
刚开始使用View CPU的人可能会觉得难以明白,其实View
CPU显示的是当前存储状态,同时用汇编语言表达当前语句的执行情况。假如你有汇编的知识,可以很容易的看出地址转移、赋值的情况,这对于猜测指针、对象引用错误的具体原因相当有效。经常会遇到这样的情况,一个对象偶尔会在free的时候出错,你不清楚这是为何导致的,通过View
CPU发现,原来是其中某个成员类的临时父类的指针不正确了,原来是被意外释放了。对于复杂的程序,常常遇到这种“奇怪、不应该”的错误,这时候就可能需要详细的了解代码执行过程;
WinSight32是一个调试工具,用于查看跟踪对象的消息。
外部调试DLL
调试器允许使用任何应用程序作为宿主来调试DLL。打开Dll项目,从主菜单中使用Run|Parameters命令,在Run
Parameters对话框中指定一个宿主程序。宿主程序是个可执行文件,它加载并调用该DLL。然后就可以在DLL工程中设置断点、跟踪等等。
提醒大家Delphi中用上述方法调试DLL(特别是服务器COM、DCOM等用接口调用的DLL)并不是一定有效。由于操作系统或其他软件的影响,可能会无法正常跟踪、中断。对于这种情况,一般是建立一个调用DLL单元的执行程序直接调试,通过后最终再编译成DLL文件。
异常处理篇
异常处理是编程人员都应该熟悉的一章。程序执行时可能会因为各种情况产生完全不可预测的问题。或许你可以说“我的程序没有BUG”,但是你能保证硬件、操作系统完全稳定吗?没有异常保护,比如多层体系的数据库程序,小小的网络错误或者数据库错误就能导致崩溃;再比如一个3D游戏,显卡、CPU、内存任何一处稍微不稳定,小则退出程序,大则死机…类似的情况非常的普遍,千万不能坚信“理想的运行”状态!
除了考虑上述运行错误,还有两种情况也得进行异常处理。一种是你自己无法100%保证代码没有错误,或者说特殊情况下没有错误:内存是否完全回收干净?指针是否绝对指向正确?小数据量、正常操作下可能从未出现的问题,一旦放在一个频繁使用的陌生环境中,可能来出错在哪里你都无法猜测,这时你只能尽可能的考虑异常,并进行相应的“善后”工作。
还有一种异常是完全在你掌握中的情况,甚至有可能是“故意”抛出的异常。比如数据库连接的意外失败,用户经常出现的某个误操作,或者某种极少见的可能导致错误的执行顺序。解决这类情况的方法中,往往最简单的一种就是
“故意”的让它进入异常,处理甚至忽略掉。比如检查一个字符串是否可以转化成整数的简单做法就是:
function CheckStrIsInt(out I : const s :
& I := strtoint(s); //I是整型,s是字符串
Delhpi异常处理机制及异常类
Delphi的异常处理机制采用的是Protected
Blocks(保护块)的概念。保护块是用Try…end封装的一段代码,其作用是当该部分的代码发生错误是自动创建一个异常类Exception,它是允许嵌套的。程序可以捕获并处理该异常类,以确保数据不受破坏。Delphi中所有异常类都是Exception的子类,所以也可以建立自己的异常类。
Delphi将所有异常基本分为运行时间库(RTL)异常、对象异常、部件异常三大部分。大部分异常被定义在SysUtils单元中。其中运行时间库异常是最常见的异常,它分为七类:
I/O异常、堆异常、整数异常、浮点异常、类型匹配异常、类型转换异常、硬件异常。比如来自文件或外设的操作错误将引发I/O异常;除零(c
:= 0;a := b /
c;),值溢出将导致整数异常;类型转换错误(strtoint(‘123a’))引发类型转换异常;未初始化指针或对象的引用会导致硬件异常。
对象异常类有四种:流异常、打印异常、图形异常、字符串链表异常。
部件异常则分为通用及专用部件异常。
资源保护(try … finally)
这里我先例举两个错误的例子。第一个例子显示了为什么内存会丢失:
procedure TForm1.Button1Click(Sender: TObject);
& GetMem(p, 1024);
& a := SizeOf(p);
&&& a := a
showmessage(floattostr(a));
FreeMem(p, 1024);
showmessage('先点击Button2');
procedure TForm1.Button2Click(Sender: TObject);
strtoint(edit1.Text);
这里i是一个全局的变量,作者的思路是先点击了Button2,将i赋值,注意这里他已经考虑到类型转换可能会失败而进行了异常处理。但是他疏忽的考虑了另一种情况:用户没有点击Button2而先点击Button1,这时因为未进行初始化,i为0,结果导致除零的错误。他仅仅又用了一个异常保护。注意,这时候程序可以正常的执行下去,但是因为直接跳转到except部分,而没有执行FreeMem,结果将导致1K的内存被丢失了。
第二个例子是显示为什么引用一个对象出错
& tab : TT
& tab.Create(nil);
& tab.DatabaseName := '.\';
注意,这里要执行到第二句时才会产生错误。初学者常犯的毛病,一是忘记创建对象的实例就使用它,二是直接用实例名Create(应该用类创建实例,返回的指针赋与该实例名,实际是个指向对象地址的指针),三是没有释放或不可靠的释放对象。
第一个例子的正确写法应该改成(请原谅我仅仅为了举例,判断是否除零直接用if I
&& 0 then a := a div I else当然更好):
showmessage(floattostr(a));
showmessage('先点击Button2');
FreeMem(p, 1024);
而第二个例子则是:
& tab := TTable.Create(nil);
//正确代码
try … finally …
end就形成一个资源保护块,通常在一个对象创建或内存分配之后,然后在Finally部分释放内存。无论正确执行还是出现异常情况,Finally都保证绝对可被执行。对于对象的创建一定要习惯这样的写法,以保证无论程序执行中是否产生错误,都可以回收分配的内存。
未知异常(try … except)
try … finally … end与try … except …
end的区别是,无论正常与否,都会执行finally的部分,而正常情况下则不会执行except的部分。所以finally用于必须回收资源的情况,而except用于处理错误的情况。
通常除了前面提到的“故意”利用异常来判断某种“不规范”情况外,有几种错误往往是有经验的程序员也难以避免的:1、大量使用指针、动态数组、对象关联的复杂情况,很难保证不会指针引用错误、数组越界、对象关联失效;2、网络类程序,无法考察千变万化的网络状况;3、多层、远程的系统,无法保证某个层次运行在绝对正常的状态;4、多线程的程序,尤其是公用数据的时候,难以保证线程安全;5、来自操作系统、硬件、其他软件的影响。
这时候只有尽可能的多考虑什么时候“有可能”出错,然后try掉。很多情况下不用考虑太多的解决方式,仅仅提示用户操作失败,重新执行或者重新配置系统就可以了。比如进一步完善上面数据库表格操作的例子:
& tab : TT
& tab := TTable.Create(nil);
tab.DatabaseName := '.\';
tab.TableName := 'tmp.db';
tab.Active :=
//添加表格操作代码
showmessage('表格打开失败,请检查表格文件是否存在');
判断响应异常(on … do)
异常的响应并非只能有一种执行顺序。保护块可能产生好几种异常的情况,这时可以通过on..do语句来判断并分别处理。On …
do是与try … except连用的,举例如下:
& a, b, c, d :
& a := 99;
& b := a *
& c := b *
&&& a := a/b
c/d;&&&&&&
//无法判断是否会除0
round(a * b * c * d);&&
//无法判断i是否越界
ERangeError do showmessage('越界');
EIntError do showmessage('除0');
showmessage('未知错误');
实际上我在执行它的时候,出现的错误是浮点溢出J。
注意,由于异常再处理后既被清除,所以on …
do是依赖于执行顺序的。假如希望对异常多次处理,则需要利用raise在异常响应结束时重新引发一个当前异常。
异常的自定义及自引发 (raise)
定义异常类与定义普通类一致,比如我们来看哑异常的定义:EAbort =
class(Exception);具体实现可参考SysUtils单元中的异常定义。
前面已经提到,raise可以重新引发一个异常。除了实现多次异常处理的作用外,还可以根据任何条件来创造一个“异常”情况。比如:
EpasswordError = class(Exception);
if Password && str then
raise EpasswordError.Create(‘Passwrod is error’);
通过异常自定义及控制引发,可以拥有自己软件的一套错误信息库。
哑异常及异常处理事件
哑异常是通过Abort产生的。调用Abort将产生Eabort异常,然后退出当前例程。由于Eabort没有任何异常消息,所以常把Abort当作例程终止过程来用(这又是个个人习惯问题,比如我喜欢人为考虑不正常的情况,然后exit例程,但有些人喜欢直接Abort)。
对于一个普通的Application应用程序,可以为Application对象的OnException事件编码,进行自己的异常处理,这和接管对象的其他事件是一样的。也可以接收一个未处理的异常事件,通过消息处理机制,再转到Application.
OnException。假如未编写该事件的处理代码,则调用标准的HandleException例程,此例程会调用ShowException显示异常提示信息。
开篇已经说过,随着经验的增加,减少错误的产生是完全有可能的。比如记住赋初值并绝对保证资源的回收。再很多情况下变量的初值是不可靠的,常见的错误是简单的认为整/实型的初值为0,实际上局部的变量是没有初值的,它的值根据当时内存分配情况来定;也不要认为从数据库或文件中读出来的NULL就等于0,NULL是绝对不等于0的!所以养成除法一定要判断除零错误之类的习惯是相当必要的。
引用对象时记得检查是否为nil,动态数组先判断边界,检查指针正确性,转换对象用as、is判断等等…初学者往往学会try后就喜欢用try,记住,try不是万能的解决方法,可预知的错误尽量的解决,而无法预知的错误才考虑异常。异常处理只是解决方法中的一种,虽然它是必须的,但不能滥用。
已投稿到:
以上网友发言只代表其个人观点,不代表新浪网的观点或立场。C语言常见错误分析和程序调试_图文_百度文库
两大类热门资源免费畅读
续费一年阅读会员,立省24元!
评价文档:
C语言常见错误分析和程序调试
上传于||文档简介
&&C​+​+
大小:223.00KB
登录百度文库,专享文档复制特权,财富值每天免费拿!
你可能喜欢异常发生如何跟踪调试及map文件生成
/KB/debug/
&http://blog.chinaunix.net/uid--id-3240007.html
一个更完整的解决方法,比较复杂
首先我们用VC IDE装载这个工程,按F11执行它,Alt+8 切换至反汇编窗口(Disassembly)。
Ctrl+G调出“Go To”的窗口。默认选择“Address”。
在“Enter address expression”编辑框中输入崩溃发生地0x005B7697。
然后点击“Go To”按钮。跳转到 0x005B7697
005B768C&&
sub&&&&&&&&
005B7692&&
sub&&&&&&&&
005B7697&&
test&&&&&&&
dword ptr [ecx],eax& &--- stack
005B7699&&
cmp&&&&&&&&
005B769E&&
jae&&&&&&&&
005B76A0&&
sub&&&&&&&&
005B76A2&&
mov&&&&&&&&
eax,esp&mov&&&&&&&&
dword ptr [eax],0Ah
看到崩溃时执行的反汇编代码,设置一个断点,按F5来到这里。
在Watch窗口中键入”察看EAX寄存器,得到的数值是“0xcccccccc”。显然这是因为向一个空指针指向的地址复制一个数据,从而造成了崩溃。
方法2. 生成映射文件 xx.map:
a.在VC Project Setting对话框中打开Generate mapfile,
b.在Project Options对话框中键入“/mapinfo:lines /mapinfo:exports”。
寻找Release版程发生异常退出的地方比Debug版麻烦得多。发生异常的时候windows通常会弹出一个错误对话框,点击详细信息,我们能获得出错的地址和大概的出错信息,然后可以用以下办法分析我们的程序。
一.用MAP文件定位异常代码位置。
1.如何生成map文件
打开“Project&→Project
Settings”,选择&C/C++&选项卡,在“Debug
Info”栏选择“Line
Numbers Only”(或者在最下面的&Project
Options&里面输入:/Zd),然后要选择&Link&选项卡,选中“Generate
mapfile”复选框,并再次编辑&Project
Options,输入:/mapinfo:lines,以便在&MAP&文件中加入行信息。然后编译工程则可以在输出目录得到同名的.map文件。
2.使用map文件定位发生异常的代码行
编译得到的map文件可以用文本方式打开,大致是这样的格式:(括号内是PomeloWu填加的注释)
0729&&&&&&&&&&&&&&&&&(←工程名)
&Timestamp
is 42e9bc51 (Fri Jul 29 14:19:13 2005)&&&&(←时间戳)
&Preferred
load address is &&&&&&&&&(←基址)
……(Data段描述,省略)
&Address&&&&&&&&
Publics by
Value&&&&&&&&&&&&&
Rva+Base&&&&
Lib:Object
<font COLOR="#01:&&&&&&
?_GetBaseMessageMap@C0729App@@KGPBUAFX_MSGMAP@@XZ
f&& 0729.obj
……(↑这一行开始是函数信息,下面省略)
Line numbers for
./Release/ShowDlg.obj(C:/0729/ShowDlg.cpp) segment
……(行号信息,前面的数字是行号,后一个数字是偏移量,下面省略)
&&在获得程序异常的地址以后,首先通过函数信息部分定位出错的OBJ和函数。做法是用获得的异常地址与Rva+Base栏地址进行比较(Rva,偏移地址;Base,基址)。找到最后一个比获得的异常地址小的那个函数,那就是出错的函数。
之后,用获得的异常地址减去该函数的Rva+Base,就得到了异常行代码相对于函数起始地址的偏移。在“Line number for”部分找到相对应的模块,并把其后的行号信息与上面减得的偏移量对比,找到最接近的一个,前面的行号大致就是目标行了。
二.获得错误的详细信息。
实际上,光靠Windows的错误消息对话框提供的信息量是很有限的,用自己写的exception filter可以获得更多的错误信息。用SetUnhandledExceptionFilter设定自定义错误处理回调函数替换Win32默认的top-level&exception filter:
2SetUnhandledExceptionFilter的函数原型:
LPTOP_LEVEL_EXCEPTION_FILTER
SetUnhandledExceptionFilter(&LPTOP_LEVEL_EXCEPTION_FILTER
lpTopLevelExceptionFilter
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&//
exception filter
function&);
2SetUnhandledExceptionFilter返回当前的exception
filter。应当保存这个函数指针并在不再需要使用自定义错误处理函数的时候当作参数再次调用SetUnhandledExceptionFilter。
2lpTopLevelExceptionFilter&是自定义的exception
filter函数指针,如果传入NULL值则指定UnhandledExceptionFilter来负责异常处理。lpTopLevelExceptionFilter其函数原型应该是与UnhandledExceptionFilter同型:
UnhandledExceptionFilter(&STRUCT
_EXCEPTION_POINTERS *ExceptionInfo&& // address
of &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&//
exception info);
2lpTopLevelExceptionFilter的返回值应该是下面3种之一:
EXCEPTION_EXECUTE_HANDLER&= 1
EXCEPTION_CONTINUE_EXECUTION&= -1
这两个返回值都应该由调用UnhandledExceptionFilter后返回。
EXCEPTION_EXECUTE_HANDLER表示进程结束
EXCEPTION_CONTINUE_EXECUTION表示处理异常之后继续执行
EXCEPTION_CONTINUE_SEARCH&= 0
进行系统通常的异常处理(错误消息对话框)
2lpTopLevelExceptionFilter的唯一的参数是_EXCEPTION_POINTERS结构指针。
typedef struct _EXCEPTION_POINTERS {
// exp &&&&PEXCEPTION_RECORD
ExceptionR &&&&PCONTEXT
ContextR }
EXCEPTION_POINTERS;
其中PCONTEXT是一个指向进程上下文结构的指针,保存了各个寄存器在异常发生的时候的值,详细信息参考《Windows核心编程》。
ExceptionRecord则指向另一个结构体EXCEPTION_RECORD:
typedef struct _EXCEPTION_RECORD { //
exr &&&&DWORD
ExceptionC &&&&DWORD
ExceptionF &&&&struct
_EXCEPTION_RECORD *ExceptionR &&&&PVOID
ExceptionA &&&&DWORD
NumberP &&&&DWORD
ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
EXCEPTION_RECORD;
ExceptionC异常代码,指出异常原因。常见异常代码有:
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
EXCEPTION_SINGLE_STEP = h单步调试状态(INT
ExceptionF异常标志0,表示可修复异常EXCEPTION_NONCONTINUABLE = 1,表示不可修复异常。在不可修复异常后尝试继续执行会导致EXCEPTION_NONCONTINUABLE_EXCEPTION =
C0000025H异常。
_EXCEPTION_RECORD *ExceptionR当异常处理程序中发生异常时,此字段被填充,否则为NULL
ExceptionA发生异常的地址(EIP)
NumberP规定与异常相关的参数数量(0-15),是ExceptionInformation数组中元素个数。
ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];&
异常描述信息,大多数异常都未定义此数组,仅有EXCEPTION_ACCESS_VIOLATION异常的描述信息:
ExceptionInformation[0],描述导致异常的操作类型
= 0&读异常
= 1&写异常
ExceptionInformation[1],发生读写异常的内存地址&
也就是说,只要注册了自己写的这个exception filter,一旦发生异常,进入这个exception filter,从参数我们就能获得各种需要的信息了。而这个exception filter需要做的就是保存这些信息,然后将异常处理的事情交还给系统就行了:
// in the beginning
// Install the unhandled exception filter
g_previousFilter =
SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);
//&exception
LONG WINAPI MyUnhandledExceptionFilter(PEXCEPTION_POINTERS
pExceptionInfo)
WriteLogFile(pExceptionInfo);&//&写入文件
g_previousFilter )
&return g_previousFilter( pExceptionInfo
&&&&&&&&&&&
return EXCEPTION_CONTINUE_SEARCH;
三.使用COD文件精确分析异常原因
说精确分析多少有点言过其实,发生异常的情况各不相同,分析真正原因很可能是一件极其复杂的事情。不过用COD文件能比MAP文件更精确地定位产生异常的位置。结合汇编代码和自定义的exception
filter获得的错误情报寄存器状态等各种信息,找到异常发生的直接原因是很容易的。
1.如何生成cod文件
仍然是打开“Project&→Project
Settings”,选择&C/C++&选项卡,在“Category”栏选择“Listing
Files”然后在Listing
file type栏选择“Assembly with Machine
Code”。重新编译工程后则可以在输出目录看到与每一个.cpp文件同名的.cod文件。
2.Cod文件的使用
首先还是利用map文件用获得的程序异常地址通过函数信息部分定位出错的OBJ和函数,并同样记录偏移地址(用获得的异常地址减去该函数的Rva+Base的差值)。然后,在相应的cod文件中(而不是在map文件后面的行号信息部分)来查找出错的函数,找到如下的格式:
?OnPaint@CShowDlg @@IAEXXZ
NEAR&&&&&&&&&&&&&&&&&&&&&&&&
; CShowDlg::OnPaint, COMDAT
&&&&&&&&&&&&&&&&&&&&(←格式为:行号&:&源代码)
&&<font COLOR="#000&&
83 ec 64&&&
sub&&&&&&&&&
100&&&&&&&&&&&&&&&&&&&
(↑偏移地址)&&(&#8598;机器码)&&(↑汇编码)
56&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
push&&&&&&&
(IsIconic())
(下面省略)
&&&找到出错的函数以后,再用偏移地址就能找到准确的异常发生的地方。然后通过源程序、汇编码即可进行更详尽的分析了。
CodeProject文章:
如何去做崩溃后的定位是一个有效的方法。
1. 可以先利用连接器的配置,产生 map
在 VC Project Setting 对话框中打开 Generate
mapfile,然后在Project Options对话框中键入“/mapinfo:lines
/mapinfo:exports”,这样在 ./release 目录下就有 .pdb 文件了。
然后在配合 这个方法 就可以获取源码行数了。
2. 可以产生一下 mfc42.dll 的 pdb
1). cd C:\Program Files\Microsoft Visual
Studio\VC98\MFC\SRC
2). ..\..\bin\vcvars32.bat
设置一下 vc 的编译环境
3). nmake /f mfcdll.mak libname=mfc42 debug=0
编译的中间文件在 $DLL.W。
4). 将产生的 mfc42.dll
放到待调试程序的相同路径下,开始调试。
后者在调试 mfc
程序的时候非常有效,通过调用堆栈即可获悉一些很重要的信息。
我把这个试验的源代码列出来:
const int x =10000;
int main(int argc, char* argv[])
y=(int*)&x;
我们用Microsoft Visual C++
6.0(SP5)编译出一个Debug版本的EXE。双击运行它。在Windows 2000 Server下,你将会得到这样一个对话框:
标题:“Pointer.exe ?
应用程序错误”;
正文:“”0x”指令引用的”0x0043101c”内存。该内存不能为”written”。
要终止程序,请单击”确定”。
要调试程序,请单击”取消”。”
知道了这些信息后,如何找到错误发生策源地呢?
请记住这个地址“0x”,它是崩溃发生地。
如何找到崩溃的源头:
有两种情况:
一是我们拥有源代码,可以现场调试;
二是现场绝对不可以安装VC,无法调试,但是我们有它的MAP文件。
第一种情况,有源代码,这被叫做“事后调试”:
首先我们用VC IDE装载这个工程,按F11执行它,切换至反汇编窗口(Disassembly)。
按下Ctrl+G热键。
你就会得到一个“Go To”的窗口。默认选择是“Address”。在“Enter address
expression”编辑框中输入崩溃发生地0x。然后点击“Go To”按钮。你就来到了这个地方:
mov&&&&&&&&
dword ptr [eax],0Ah
好了,我们看到了发生崩溃时执行的是这行反汇编代码,但是为什么会崩溃呢?
我们在这里设置一个断点,按F5来到这里。
在Watch窗口中键入“@EAX”察看EAX寄存器,得到的数值是“0xcccccccc”。显然这是因为向一个空指针指向的地址复制一个数据,从而造成了崩溃。
好了,针对这个问题,你已经调试成功了。
还有一个问题,对于Release版本的EXE,也可以这么调试吗?
当然可以。同样是这个例子,运行它的Release版本,得到的崩溃地址是0x0040108a。
我们在VC中装载这个工程的Release版本,按F11运行它。
来到它的反汇编代码的0x004018a处,我们看到:
0040108A&&
mov&&&&&&&&
dword ptr ds:[40B0D0h],0Ah
第二种情况,有映射文件Pointer.map:
值得注意的是,如果你只在VC Project Setting对话框中打开Generate mapfile,还是不够的。因为你一定还要输出程序代码地址和源代码行号!!这非常的重要!
要得到这些信息,请在Project Options对话框中键入“/mapinfo:lines
/mapinfo:exports”。请你一定要养成这种习惯!因为这不是默认设置。
我们得到的map文件大致如下,我删节了大多数输出:
(应用程序名)
Timestamp is 3d4407a7 (Sun Jul 28 23:03:03
Preferred load address is
(最佳装载基地址。非常重要的一个数据。不过一般都是这个数。)
Address&&&&&&&&
Publics by
Value&&&&&&&&&&&&&
Rva+Base&&&&
Lib:Object
_main&&&&&&&&&&&&&&&&&&&&&
Pointer.obj
(_main的虚地址)
Line numbers for .\Debug\Pointer.obj(E:\
Pointer\Pointer.cpp) segment .text
(这就是我们的Pointer.cpp所对应的程序代码行号和相对虚拟地址的对应表)
我们可以从中看到,最佳装载基地址是0x,_main的虚地址是0x,而0又是什么意思呢?
0x就是_main的相对虚拟地址(RVA)。
0x就是PE头文件的大小,一般都是这个数。
所以虚地址就是这么算出来的:
虚地址&&&&&
最佳装载基地址 + PE头文件的大小
相对虚拟地址(RVA)
通过_main的RVA的计算,我们也就知道了怎么计算崩溃地址0x的RVA,是0x,对吧?
然后,在这个MAP映射文件的“Line numbers for .\Debug\Pointer.obj(E:\
Pointer\Pointer.cpp) segment .text”这个行号段中查找这个地址。如你所看到的,只有16行对应的和18行对应的0000027F,没有呀?
没有17行的对应关系,说明17行是空行。
那么就一定是16行的了!这样你不用看那个程序员的代码,就可以通知他:崩溃发生在你的Pointer.cpp的第16行了!很酷吧!
(function(w, d, g, J) { var e = J.stringify || J. d[g] =
d[g] || {}; d[g]['showValidImages'] = d[g]['showValidImages'] ||
function() { w.postMessage(e({'msg': {'g': g, 'm':'s'}}),
location.href); } })(window, document, '__huaban', JSON);
(function(w, d, g, J) { var e = J.stringify || J. d[g] =
d[g] || {}; d[g]['showValidImages'] = d[g]['showValidImages'] ||
function() { w.postMessage(e({'msg': {'g': g, 'm':'s'}}),
location.href); } })(window, document, '__huaban', JSON);
已投稿到:
以上网友发言只代表其个人观点,不代表新浪网的观点或立场。

我要回帖

更多关于 程序调试没错运行出错 的文章

 

随机推荐