前言
本文介绍一种使用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]