beta 的第六篇心得:如何判断程序刚才出现了异常(100分)

建立一套Error Code和Log Code, 对系统维护很有帮助.
 
呵呵,明白你的意思了:)
 
我觉得好像用{$DEFINE DEBUG},然后再在下面写上{$IFDEF Debug}这样的标签,并结合
Assert方法,可以更好的了解程序在哪里出错了!
我记得看过一本C++的书,里面提到,让异常及时出现在程序员面前,比忽略异常更加有
帮助,这样一些隐藏的,不可见的,不是经常发生的错误将会在可见的时间内,被程序员
捕获,从而提高了解决错误,消除bug的机会,比起让程序在用户手中莫名其妙的崩溃,而
负责技术支持的程序员得到用户反馈后仍然无法再现错误,要好很多!
似乎看了beta大虾的代码,我觉得是不是可以把这两种技巧合用呢?就是在编译的时候,采用
Debug版本编译,并在这里加入beta大虾的设计技巧,等所有错误都查出来后(呵呵,当然不可能
是所有错误,只是相对的说),再去掉Debug的编译指令,这样最终的成品和Debug可以分开
,同时也可以让最终的成品中减少一些只有程序员才需要的代码。不知beta大虾觉得如何?
 
很想知道,顶一下。
 
to beta:
对不起,没看到你的第一个(常犯这种错误[:D]),我只是看到了你和delp的讨论。
不过,我觉得你后面的一种,虽然技术的含量要高些,但效率与前面的一种差不多,
但写起来麻烦的多了,而且不易懂。[:)]
 
assert是用来debug的. 可以在release时屏蔽它.
$ASSERTIONS ON/OFF (long form)
$C +/- (short form)
这和异常控制是不一样的, 异常控制是用来处理可能发生的异常, 如硬盘满了, 不是用来
查错的
 
to zqw0117:
使用 assert 的确是好的习惯,我在写比较正式的东西时就会用到,我认为这能
使你避免一些“莫名其妙”的问题。不过这和本讨论主题联系不大:)
to wfzha:
呵呵,和我以前的心得一样,仅仅是提供一种比较新颖的实现方式而已,并不是
说非要这样用啊:) 看个人喜好罢了,如 delp 恐怕就不会选前一个方法:)
to yyanghhong:
我一般是直接在 Project Options -> Compiler -> Assertions 这里设置的
其实我的do
Log 里面基本上就相当于你的 HandleMyException 干的事情:)
 
我所知exception处理一般上有两种.
一种是java采用的, 叫chained exception Facility, 是把异常一层层的处理, 可在处理过程中转换异常
try {
...
} catch(YourException e) {
throw new MyException();
}
因为java有Throwable接口, 这样编译器会检查带Throwable接口的类方法是否被异常处理了, 当异常发生后, vm会用Throwable.printStackTrace打印出所有的exception Stack.很利于调试.
另一种是delphi和windows API等采用的Global Exception Handling
就是有一个全局的exception handler过程去处理程序中所有异常, 比如
procedure TForm1.FormCreate(Sender: TObject);
begin
Application.OnException := AppException;
end;
procedure TForm1.AppException(Sender: TObject;
E: Exception);
begin
Application.ShowException(E);
end;
除了EAbort以外, 程序中异常发生后都会执行AppException里的代码, 这种方法写起来很省事, 但不方便调试, 不过可以用JCLDebug或ExceptionalMagic去打印exception Stack.
在windows API中, 程序员可用GetLastError去得到最后的exception信息.
这两种方法好坏, 还希望大家讨论一下.
 
要知道,调试占了我们日常编程的大部分时间,所以,如果说让我选“写起来方便”和
“调试方便”的话,我肯定选后者:)
Java 的异常处理方式我不是很清楚,不过真要像你说的那样,比 Delphi 的更方便调试
的话,咱们什么时候模拟它一个:)
希望熟悉 Java 的朋友介绍一下。
 
delphi里也是可以用chained exception的.

try
..
except
on e:YourExceptiondo
raise Myexception.create(e);
end
我们可用JCLDebug或ExceptionalMagic这些工具去打印execution stack.
他们的工作原理是借助delphi ide的mapfile, mapfile包含了所有unit和method的地址,JCLDebug把mapfile编译入exe文, 在exception发生后, 根据错误点的地址去查找
mapfile, 得到unit和procedure 名称.
 
原来还可以这样用的啊,长见识了,我原来顶多就用了个 reraise 而已:)

 
to yyanghhong: 你那里还是得不到正确结果吗?
to all: 该问题还有什么需要讨论的吗?没有的话就结贴啰。
 
我又试了一下, 可以的
 
hehe, 那就好
 
to beta:
--------
如果在 try 中出现了异常,那么程序会从出错的地点直接跳到内部的出错处理部分,
然后再跳到我们的 finally 部分(严格来说,过程不是这样,不过你可以认为是这样,
对于处理本问题是适用的,要是有朋友想打破沙锅问到底,我再罗嗦也不迟),
-------
的确不是这样,如果是try-finally-end块,则一旦发生异常,最先进入的并不是finally-end块,而是外层的第一个要处理该异常的except-end块。
在外层处理异常时,一个RTLUnwind()的调用将总是发生,这是异常展开。一个finally-end块总是处理被展开的异常(而不是被触发的异常)。在一个finally-end块处理完RTLUnwind()展开的异常后,才会回到except-end块,来处理用户的异常处理代码。
在.exe和.dll的初始化代码中,SetExceptionHandler()用于设置一个顶层异常处理句柄。这使得一个异常无论如何,总会被展开。也就是说,至少会被_ExceptionHandler()例程展开。finally-end总会得到响应异常的机会,而except-end却不一定(需要加上SysUtils.pas单元)。
--------
这就是说,我们完全可以在 finally 的第一行判断 eax 的值,就可以知道刚才
是否发生异常了。
-------
这绝对是不安全的,eax的值并不能保证。不过有一点可以肯定,如果代码正常执行到finally-end,eax的确为0。
-------
那何不干脆让do
Log 没有参数,直接在do
Log 里面判断 eax 不也可以吗?还是不
行,DoLog 在执行第一行用户代码前,还有动作,而如果你的过程没有参数的话,这些
动作很有可能修改 eax。
-------
其实这是个好办法。要让“DoLog 在执行第一行用户代码前,没有动作”,其实并不难。──如果一个过程没有入口参数,也没有局部变量,并且是完全汇编例程,那么,它在第一行代码前就什么也没有。例如这样:
procedure _DoLog(EAX:DWORD);
begin
// if you want, you cando
any... :)
end;

proceduredo
Log;
asm
call _DoLog
end;

procedure Work;
begin
try
finally
do
Log;
// OK. ^.^
end;
end;

-------
//DoLog 参数看起来及其不爽,加把油搞定他......
没有参数怎么判断?除非你想麻烦点,DoLog 无参数版本:
-------
哈哈,这个版本也不好。还是变态。^.^

to zqw0117,
----------------
哎,你的建议其实有非常简单的解决方法,也是比较常用的。
{$IFOPT D+}
log_Exception....
{$else
}
do
your code....
{$END}
这就是编译调试版本和发布版本的不同之处。

to yyanghhong,
---------------------
我不了解java的异常机制。但是Delphi的异常处理机制的确就是分层的。Global Exception Handling只是留给开发者的一个接口而已,Delphi内核是不用它来处理异常的。
正是因为delphi是分层处理异常,才使得JCLDebug或ExceptionalMagic等可用。要不,内核不支持的话,外部应用又如何打印exception Stack呢?
此外,Delphi分层处理异常,是基于OS的SEH机制的。并不是Delphi自身的机制,准确地说,Delphi只是实现了它。

其它,
--------
我想想代码,或者有别的方法来实现一个HaveException()函数。:)
 
to aimingoo,
JCLDebug打印的不是exception stack, 而是execution stack,
eg
procedure first_caller;
begin
second_caller;
end;
procedure second_caller;
begin
third_caller;
end;
procedure third_caller;
begin
try
raise exception.create('error here');
except
..JclPrintTraceStack;
// Syntax is similar but not sure;
end;

jcldebug打印出的象这样
[00421B6C] uTest.first_caller(Line 456, "uTest.pas" + 265) + $15
[00421AE8] uTest.second_caller(Line 500, "uTest.pas" + 265) + $E
[007FC63F] uTest.third_caller(Line 568, "uTest.pas" + 265) + $C
 
1. 用寄存器做判定条件的问题
从流程的角度上来讲,可以判定EAX、ECX和EBP。
1). ECX=EBP:如果为真,则是正常进入,否则ECX指向一个异常帧描述TExcDesc.instructions
的地址。
2). EAX:如果为0,则是正常进入,否则EAX指向一个异常记录:TExceptionRecord。
但是,我个人并不倾向于判定寄存器。尽管从system.pas中的代码的分析来看,这些寄
存器总存在上述关系。
2. 用栈做判定条件
前面说到过finally和except异常的机制。由于在正常进入finally-end时,需要用
push $00458d32 // finally-end后面的代码
来使得栈顶保存后面的ret要用的值。而异常发生时,是从_HandleFinally()进入
finally-end代码块,这时使用的是call调用,这使得栈顶将保存_HandleFinally()
中下一行代码的地址。──只有这样,流程才能在调用RTLUnwind()之后,返回到外
层的异常处理代码中。
OK. 你可能已经想到了:因为异常进入finally-end块时,栈顶是恒值,而正常进入
时,会是一个未知的值。──除非Delphi改变它实现异常的机制,否则这是不可能
改变的。
后面的代码来实现这个思路:
function HaveException : boolean;
const
{$WRITEABLECONST ON}
IsFinally : DWORD = 0;
procedure GetFinallyReturnAddr;
begin
try
try
asm xor eax, eax;
idiv eax end;
finally
asm pop [IsFinally];
push [IsFinally] end;
end;
except
end;
end;

asm
CMP IsFinally, 0
JNE @@CHECK
CALL GetFinallyReturnAddr
@@CHECK:
MOV EAX, [ESP + 4]
CMP IsFinally, EAX
JE @@DONE
MOV EAX, 0
@@DONE:
end;

原理:
-----
GetFinallyReturnAddr()中制造了一个异常(div 0),用于取栈顶返回地址。
定义类型化常量IsFinally和使用子例程的原因,都是使整个操作在一个函数内
完成,这样使用起来方便一些。哈哈。
 
to yyanghhong,
---
对,我想了一下,JCLDebugger等打印的的确是execution stack。哈哈。但是Delphi的异常处理机制的确就是分层的,exception stack也存在。^.^
 
其实delphi和java在exception处理上没大区别,
两者都是基于chained exception Facility的, java本身提供了打印stack的接口函数.
delphi的global exception handling只是用来处理没被程序catch的exception.
在application 和tthread有这个事件. 可在最外层保护程序,
java 有throwable接口, 可强制catch exception.
 

try
......
except
......
end;
 
顶部