返回目录
代码保护
迄今,我还是非常确信,在我从托管代码调用非托管函数sscanf时,并没有什么糟糕的事情发生,因此我简单的调用了它。但是谁知道在非托管代码中深深阴影中隐藏着什么可怕的危险?我不知道。因此我喜欢逐步保证应用程序行为表现为一种有序的方式。出于这个意图,我可以使用异常处理的机制,这是C++和C#程序员都知道的。
检查对这个示例(源文件Simple2.il)的轻量修改如清单2-2所示。正如之前,我在修改的地方做了CHANGE!的注释标记。
清单2-2. 对Simple2.il的再次修改
//
----------- Program header
.assembly
extern
mscorlib {
auto
}
.assembly
OddOrEven { }
.module
OddOrEven.exe
//
----------- Class Declaration
.namespace
Odd.or {
.class
public
auto
ansi
Even
extends
[mscorlib]System.Object {
//
------------ Field declaration
.field
public
static
int32
val
//
------------ Method declaration
.method
public
static
void
check( )
cil
managed
{
.entrypoint
.locals
init
(
int32
Retval)
AskForNumber:
ldstr
"
Enter a number
"
call
void
[mscorlib]System.Console::WriteLine(
string
)
.try{
//
CHANGE!
//
Guarded block begins
call
string
[mscorlib]System.Console::ReadLine()
//
pop // CHANGE!
//
ldnull // CHANGE!
ldstr
"
%d
"
ldsflda
int32
Odd.or.Even::val
call
vararg
int32
sscanf(
string
,
string
,,
int32
*)
stloc.0
leave
.s DidntBlowUp
//
CHANGE!
//
Guarded block ends
}
//
CHANGE!
//
CHANGE block begins! --->
catch [mscorlib]System.Exception
{
//
Exception handler begins
pop
ldstr
"
KABOOM!
"
call
void
[mscorlib]System.Console::WriteLine(
string
)
leave
.s Return
}
//
Exception handler ends
DidntBlowUp:
//
<--- CHANGE block ends!
ldloc.0
brfalse.s
Error
ldsfld
int32
Odd.or.Even::val
ldc.i4.1
and
brfalse.s
ItsEven
ldstr
"
odd!
"
br.s
PrintAndReturn
ItsEven:
ldstr
"
even!
"
br.s
PrintAndReturn
Error:
ldstr
"
How rude!
"
PrintAndReturn:
call
void
[mscorlib]System.Console::WriteLine(
string
)
ldloc.0
brtrue.s
AskForNumber
Return:
//
CHANGE!
ret
}
//
End of method
}
//
End of class
}
//
End of namespace
//
----------- Calling unmanaged code
.method
public
static pinvokeimpl(
"
msvcrt.dll
"
cdecl)
vararg
int32
sscanf(
string
,
string
)
cil
managed
{ }
这些改动是什么呢?在代码“有危险的”部分的范围嵌套在被称为try块(或保护块)之中,这就促使了运行时在执行这段代码块的时候监视着异常的抛出。如果有什么次序颠倒的事情发生,就会抛出异常——例如,一个内存访问错误或一个指向未定义的类或方法的引用。
.try {
//
Guarded block begins
call
string
[mscorlib]System.Console::ReadLine()
ldstr
"
%d
"
ldsflda
int32
Odd.or.Even::val
call
vararg
int32
sscanf(
string
,
string
,,
int32
*)
stloc.0
leave
.s DidntBlowUp
//
Guarded block ends
}
注意到,这个try块以leave.s DidntBlowUp指令结束。这个leave.s指令是leave的简写形式,跳转当前计算流程到标记了DidntBlowUp标签的地方。你不能在这里使用分支指令,因为根据由JIT编译器严格执行的CLR异常处理机制的规则,在try块外唯一合理的方式是使用leave指令。
这种限制是由一个重要的函数引起的,该函数由leave指令执行:在跳转计算流程之前,会进行unwinding stack(剥去当前栈上的所有项),同时如果这些项被指向到对象实例,会销毁它们。这是我为什么需要在使用leave指令之前,将这些sscanf函数的返回值存储在本地变量Retval中:如果我试图晚一些做这件事情,这个值就会丢失。
包包译注:每次函数调用发生的时候,都会执行保护现场寄存器、参数压栈、为被调用的函数创建堆栈这几个对堆栈的操作,它们都使堆栈增长。每次函数返回则是恢复现场,使堆栈减小。我们把函数返回过程中恢复现场的过程称为unwinding stack。
Catch [mscorlib]System.Exception指出我打算在保护块中截取任何抛出的异常并处理这个异常。
{
leave
.s DidntBlowUp
//
Guarded block ends
}
catch [mscorlib]System.Exception
{
//
Exception handler begins
pop
}
因为我要截取任意异常,所以我指定一个基本的托管异常类型[mscorlib]System.Exception,所有托管的异常类型都派生于这个基类。技术上,我称 [mscorlib]System.Exception为“所有异常的父亲”,但是恰当的术语多少不是很通俗:“所有异常的继承根”。
提及另一个,更明确的,catch子句中的异常类型——在这种情形中,[mscorlib]System.NullReferenceException会指出我准备只处理这个特定类型的异常,而其他类型的异常将会在其他地方处理。这个方法是便利的——如果你想不同类型的异常具有不同的处理方式(这会倾向于产生更少的错误,同时被认为是一种比较好的编程风格),而这也是这种机制被称为结构化异常处理的原因。
下面这个catch子句,是这个异常处理的直接范围(处理块):
catch [mscorlib]System.Exception
{
//
Exception handler begins
pop
ldstr
"
KABOOM!
"
call
void
[mscorlib]System.Console::WriteLine(
string
)
leave
.s Return
}
//
Exception handler ends
当一个异常被捕获并进入这个处理块的时候,当前栈上唯一存在的是一个指向被捕获异常的引用——一个异常类型的实例。为了实现这个处理机制,我不想花时间分析捕获到的异常,因此我简单的使用pop指令将其除去。在这个简单的应用程序里,了解到一个异常就已经足够了,而不用回顾其中的细节。
接下来我加载字符串常量”KABOOM”到栈上,使用控制台输出方法[mscorlib]System.Console::WriteLine(string)打印这个字符串,并使用leave.s指令跳转到Return标签。这个“只通过leave离开”的规则应用到处理块和应用到try块一样。我不能简单的加载字符串“KABOOM”到栈上并交给PrintAndReturn处理;leave.s指令会从栈上移除这个字符串并只会调用WriteLine方法。
你可能想知道为什么,如果我试图保护对非托管函数sscanf的调用,我要包含三个前面的指令在try块中?为什么不在.try范围内只包含对sscanf的调用呢?
ldstr
"
Enter a number
"
call
void
[mscorlib]System.Console::WriteLine(
string
)
.try {
//
Guarded block begins
call
string
[mscorlib]System.Console::ReadLine()
ldstr
"
%d
"
ldsflda
int32
Odd.or.Even::val
call
vararg
int32
sscanf(
string
,
string
,,
int32
*)
stloc.0
leave
.s DidntBlowUp
//
Guarded block ends
}
根据异常处理规则,一个受保护的块(try块)开始于当方法栈为空的时候。这个在调用sscanf之前最接近的时刻,是立即在调用[mscorlib]System.Console::WriteLine(string)之后的。 这将从栈上得到“Enter a number”字符串而不放回到栈上什么。因为这三条位于调用sscanf之前的指令加载了调用参数到栈上,你必须打开被保护块在任何被执行的指令之前。
可能你会感到困惑——为什么看起来会有这么严格的限制。为什么你不能开始和结束一个try块在你想的其他什么地方,正如你能在C++中看到的?事实上是,你不但不能用和C++同样的方式来处理,而且也不会更好。
高级语言编译器以这样一种方式工作——每个完整的语句都被编译到一系列的开始和结束于一个空栈的指令。在C++中,try块可能如下所示:
try {
Retval = sscanf(System.Console::ReadLine(),
"
%d
"
, &val);
}
高级语言编译器的这种特性是通用的以至于所有的高级语言编译器在这个指令序列中使用这些空栈的指针,以识别这条完整语句的开始和结束。
最后一个遗留下来的任务是测试这个保护机制。从Apress网站复制源文件Simple2.il到你的工作目录,并使用控制台命令ilasm simple2将它编译到可执行体Simple2.exe中。测试它以保证其可以像前一个实例一样运行。
现在我模拟一个非托管代码中的严重崩溃。加载这个源文件Simple2.il到任何文本编辑器,并在try块中去除对指令pop和ldnull的注释:
.try {
//
Guarded block begins
call
string
[mscorlib]System.Console::ReadLine()
pop
ldnull
ldstr
"
%d
"
ldsflda
int32
Odd.or.Even::val
call
vararg
int32
sscanf(
string
,
string
,,
int32
*)
stloc.0
leave
.s DidntBlowUp
//
Guarded block ends
}
pop指令从栈上移除了由ReadLine返回的字符串,同时ldnull替代的加载了一个空引用。这个空引用被封装成非托管的sscanf方法——作为一个空指针。sscanf并没有准备获取这个空指针并试图废弃对这个空指针。这个操作系统平台将会抛出一个非托管异常“Memory Access Violation”,这将被CLR捕获并转换为一个托管的System.NullReferenceException类型异常,这将依次被.try-catch保护块捕获到。接着这个应用程序将很自然地终止。
重编译Simple.il并运行编译产生的可执行体。你将得到没有比“KABOOM”更好的结果显示在控制台上。
接着你可以在Simple.il或Simple1.il中修改源代码,添加相同的两条指令,pop和ldnull,在调用System.Console::ReadLine之后。重编译这个源文件来看看在没有结构化异常处理保护下它是怎么运行的。