前言
本文介绍一种使用IL的方式直接跟踪exception到行的方法,让大家对exception不再感到恶心!特别是
System.NullReferenceException: 未将对象引用设置到对象的实例。
问题的导火线
今天在debug的时候,又出现了空指针,我这次真的火了!每次遇到空指针,.net给出的信息总是非常的少,我根本不知道是哪里Throw出来的,只能反复检查代码。
我火了!我要起义!于是,开始寻求一种能够出现exception后知道什么代码报出来的。例如以下代码:
{
static void Main( string [] args)
{
try
{
string hello3 = null ;
hello3 = hello3.ToUpper();
}
catch (Exception ex)
{
Console.Write(ex.ToString());
}
}
}
在release模式下,没有pdb的时候,微软给出的答案是:
在 Pixysoft.Testdriven.Consoles.Program.Main(String[] args)
我靠!鬼才知道哪里出现了空指针?我们的程序比这个复杂多了,在层层代码中、上百行的方法体内,神才知道空指针是什么地方报的。
于是我开始思考如何能够知道程序运行到什么地方出现异常。。
1. 首先是想到了aop,对方法体拦截。但是还是不能知道方法内部。
2. 然后想到了反射,问题还是同上。
3. 然后想到了StackTrace trace = new StackTrace(exception, true); 直接获取调用堆栈Frame。可是在没有pdb的时候,frame.GetFileLineNumber() 返回的是0. 傻逼了。
4. 然后想到了一个软件NCover。他就能够知道代码运行到什么阶段。于是开始查Ncover的源码。。不查不知道,一查吓一跳,原来NCover是对我们原方法进行重构,入侵了自己的计数器(c# code),然后动态编译出来的。郁闷。看来NCover要放弃了。
4. 最后,我会想起了曾经做过的IL。终于。。。看到了一丝希望:
int offset = frame.GetILOffset();
在没有pdb的时候,IL的偏移量仍然正常输出。
正文
思路大概是:
1. 获取exception的调用堆栈。
2. 获取exception相关的这个方法的方法的IL代码
3. 结合excpetion的IL偏移量和方法的IL,把调用源找出来。
代码如下:
{
static void Main( string [] args)
{
try
{
string hello3 = null ;
hello3 = hello3.ToUpper();
}
catch (Exception ex)
{
// 获取调用堆栈
StackTrace trace = new StackTrace(ex, true );
StackFrame frame = trace.GetFrame( 0 );
int offset = frame.GetILOffset();
byte [] il = frame.GetMethod().GetMethodBody().GetILAsByteArray();
// 获取调用指令
offset ++ ;
ushort instruction = il[offset ++ ];
// 打开潘多拉魔盒
ILGlobals global = new ILGlobals();
global .LoadOpCodes();
// 翻译
OpCode code = OpCodes.Nop;
if (instruction != 0xfe )
{
code = global .SingleByteOpCodes[( int )instruction];
}
else
{
instruction = il[offset ++ ];
code = global .MultiByteOpCodes[( int )instruction];
instruction = ( ushort )(instruction | 0xfe00 );
}
// 获取方法信息
int metadataToken = ReadInt32(il, ref offset);
MethodBase callmethod = frame.GetMethod().Module.ResolveMethod(metadataToken,
frame.GetMethod().DeclaringType.GetGenericArguments(),
frame.GetMethod().GetGenericArguments());
// 完成
Console.WriteLine(callmethod.DeclaringType + " . " + callmethod.Name);
Console.Read();
}
}
private static int ReadInt32( byte [] il, ref int position)
{
return (((il[position ++ ] | (il[position ++ ] << 8 )) | (il[position ++ ] << 0x10 )) | (il[position ++ ] << 0x18 ));
}
}
public class ILGlobals
{
private OpCode[] multiByteOpCodes;
private OpCode[] singleByteOpCodes;
/// <summary>
/// Loads the OpCodes for later use.
/// </summary>
public void LoadOpCodes()
{
singleByteOpCodes = new OpCode[ 0x100 ];
multiByteOpCodes = new OpCode[ 0x100 ];
FieldInfo[] infoArray1 = typeof (OpCodes).GetFields();
for ( int num1 = 0 ; num1 < infoArray1.Length; num1 ++ )
{
FieldInfo info1 = infoArray1[num1];
if (info1.FieldType == typeof (OpCode))
{
OpCode code1 = (OpCode)info1.GetValue( null );
ushort num2 = ( ushort )code1.Value;
if (num2 < 0x100 )
{
singleByteOpCodes[( int )num2] = code1;
}
else
{
if ((num2 & 0xff00 ) != 0xfe00 )
{
throw new Exception( " Invalid OpCode. " );
}
multiByteOpCodes[num2 & 0xff ] = code1;
}
}
}
}
/// <summary>
/// Retrieve the friendly name of a type
/// </summary>
/// <param name="typeName">
/// The complete name to the type
/// </param>
/// <returns>
/// The simplified name of the type (i.e. "int" instead f System.Int32)
/// </returns>
public static string ProcessSpecialTypes( string typeName)
{
string result = typeName;
switch (typeName)
{
case " System.string " :
case " System.String " :
case " String " :
result = " string " ; break ;
case " System.Int32 " :
case " Int " :
case " Int32 " :
result = " int " ; break ;
}
return result;
}
public OpCode[] MultiByteOpCodes
{
get { return multiByteOpCodes; }
}
public OpCode[] SingleByteOpCodes
{
get { return singleByteOpCodes; }
}
}
这样,输出的结果是:
在这里出现了空指针。
后续
看到这里,大家应该明白我为啥大骂微软了。
明明所有的信息都能够提供,都在IL里面,但是这个该死的微软就是不提供。给个exception还这么暧昧,让我们不断的浪费时间去debug。
实际上,微软的.net framework完全掌握了我们每一行代码的运行情况,内存情况。怪不得现在出了个VS2010,搞了个什么Intellitrace,所谓历史调试什么的。
希望通过这篇文章,唤醒大家,其实我们可以走的更远!
技术支持
zc22.cnblogs.com
补充 2009-12-25 有位明白真相的群众。哈哈!
楼主的语言是稍微激了一点,不过,该文的应用场景估计是:( 1 )程序在一台机器上已经开发调试OK了;( 2 )Release编译并部署到另一台机器。在这里出现了NRE的话,.NET FW是不能给你这么多的信息的,只能重新到开发机器进行调试。所以,LZ这种方法还是不错的。
但是,更明智的方法是进行精密的异常管理。 [ /quote ]