前言
本文介绍一种使用IL的方式直接跟踪exception到行的方法,让大家对exception不再感到恶心!特别是
System.NullReferenceException: 未将对象引用设置到对象的实例。
问题的导火线
今天在debug的时候,又出现了空指针,我这次真的火了!每次遇到空指针,.net给出的信息总是非常的少,我根本不知道是哪里Throw出来的,只能反复检查代码。
我火了!我要起义!于是,开始寻求一种能够出现exception后知道什么代码报出来的。例如以下代码:
代码
class
Program
{
static
void
Main(
string
[] args)
{
try
{
string
hello3
=
null
;
hello3
=
hello3.ToUpper();
}
catch
(Exception ex)
{
Console.Write(ex.ToString());
}
}
}
在release模式下,没有pdb的时候,微软给出的答案是:
System.NullReferenceException: 未将对象引用设置到对象的实例。
在 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,把调用源找出来。
代码如下:
代码
class
Program
{
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
[email protected]