-10-
Exception Handling
Exception handling in IL is a
big let down. We expected a significant amount of complexity, but were proved
wrong, right from the beginning. IL cannot be termed as a machine level
assembler. It actually has a number of directives like try and catch, that work
like their higher level counterparts.
a.cs
class zzz
{
public static void Main()
{
try
{
abc();
System.Console.WriteLine("Bye");
}
catch (System.Exception e)
{
System.Console.WriteLine("In Exception");
}
System.Console.WriteLine("After Exception");
}
public static void abc()
{
throw new System.Exception();
System.Console.WriteLine("abc");
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class [mscorlib]System.Exception V_0)
.try
{
call void
zzz::abc()
ldstr
"Bye"
call void [mscorlib]System.Console::WriteLine(class
System.String)
leave.s IL_001e
}
catch [mscorlib]System.Exception
{
stloc.0
ldstr "In
Exception"
call void
[mscorlib]System.Console::WriteLine(class System.String)
leave.s IL_001e
}
IL_001e: ldstr "After Exception"
call void
[mscorlib]System.Console::WriteLine(class System.String)
ret
}
.method public hidebysig static void abc() il managed
{
newobj instance void [mscorlib]System.Exception::.ctor()
throw
}
}
Output
In Exception
After Exception
In the above example, the
function abc first creates an object that looks like Exception using newobj and
places it on the stack. Thereafter the throw instruction throws an exception.
This Exception is placed on the stack, hence the catch instruction is called.
In the catch instruction, e, a local varaible, is an instance of Exception. The
next instruction, leave.s jumps to label IL_001e, the label is beyond the
catch.
To exit off from a try or a catch block, instead of the
branch instruction br, leave is used. The reason is that we are dealing
with exceptions, which are to be
handled in a special way in IL. Exception handling in IL is done using higher
level instructions.
a.cs
class zzz
{
public static void Main()
{
yyy a;
a=new yyy();
try
{
a.abc();
System.Console.WriteLine("Bye");
}
catch
{
System.Console.WriteLine("In Exception");
}
finally
{
System.Console.WriteLine("In finally");
}
}
}
class yyy
{
public void abc()
{
throw new System.Exception();
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class yyy V_0)
newobj instance void
yyy::.ctor()
stloc.0
.try
{
.try
{
ldloc.0
call instance void
yyy::abc()
ldstr
"Bye"
call void
[mscorlib]System.Console::WriteLine(class System.String)
leave.s IL_0025
}
catch [mscorlib]System.Object
{
pop
ldstr "In
Exception"
call void
[mscorlib]System.Console::WriteLine(class System.String)
leave.s IL_0025
}
IL_0025: leave.s IL_0032
}
finally
{
ldstr "In
finally"
call void
[mscorlib]System.Console::WriteLine(class System.String)
endfinally
}
IL_0032: ret
}
}
.class private auto ansi yyy extends [mscorlib]System.Object
{
.method public hidebysig instance void abc() il managed
{
newobj instance void
[mscorlib]System.Exception::.ctor()
throw
}
}
Output
In Exception
In finally
The above program has utilised a
try catch without a parameter and a finally clause. Adding a finally clause
associates the same try with a catch, and a finally. In a sense, two copies of
try are created, one for catch and the other for finally.
If the catch directive is not
supplied with an Exception object, it will take an object that looks like
System.Object. In the catch, the item is popped of the stack, as its value
holds no significance. The string is printed before the leave. Also, along with
try-catch is the finally clause which is the next to be executed. A finally is
executed as a separate try finally directive and it can only be exited using
the endfinally instruction.
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.try
{
leave.s IL_0032
}
finally
{
ldstr "In
finally"
call void
[mscorlib]System.Console::WriteLine(class System.String)
endfinally
}
IL_0032: ret
}
}
Output
In finally
Nowhere is it specified that a
try must have a catch. A finally will ultimately be called at the end of the
try.
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
call void
zzz::abc()
ldstr
"Bye"
call void [mscorlib]System.Console::WriteLine(class
System.String)
ret
}
.method public hidebysig static void abc() il managed
{
newobj instance void [mscorlib]System.Exception::.ctor()
throw
}
}
Output
Exception occurred: System.Exception: An exception of type
System.Exception was thrown.
at zzz.vijay()
In the absence of a try catch
block, if function abc throws an exception, it will not get caught. Instead, a
runtime error is generated. A try catch
clause is recommended to proactively catch the exception, otherwise when an
exception is thrown and the program will come to a grinding halt.
a.cs
public class zzz
{
public static void Main()
{
int i = 1;
for ( i = 1; i<= 10 ; i++)
{
try
{
System.Console.WriteLine("1 try");
try
{
System.Console.WriteLine("2 try");
break;
}
finally
{
System.Console.WriteLine("2 finally");
}
}
finally
{
System.Console.WriteLine("1 finally");
}
}
System.Console.WriteLine(i);
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (int32 V_0)
ldc.i4.1
stloc.0
ldc.i4.1
stloc.0
br.s IL_0032
.try
{
IL_0006: ldstr "1 try"
call void
[mscorlib]System.Console::WriteLine(class System.String)
.try
{
ldstr "2
try"
call void
[mscorlib]System.Console::WriteLine(class System.String)
leave.s IL_0037
}
finally
{
ldstr "2
finally"
call void
[mscorlib]System.Console::WriteLine(class System.String)
endfinally
}
}
finally
{
ldstr "1
finally"
call void
[mscorlib]System.Console::WriteLine(class System.String)
endfinally
}
IL_0032: ldloc.0
ldc.i4.s 10
ble.s IL_0006
IL_0037: ldloc.0
call void
[mscorlib]System.Console::WriteLine(int32)
ret
}
}
Output
1 try
2 try
2 finally
1 finally
1
The above program is quite
lengthy, but very simple. It proves the fact that code placed in a finally
block is always executed. Like death and taxes, a finally cannot be avoided.
The for statement branches to
label IL_0032 where we first check for the value to be less than or equalto10.
If it results in TRUE, the code at
label IL_0006 is executed. A we learnt in one of the earlier chapters, the
condition check for the for statement is always placed at the bottom in IL.
In the first attempt, string
"1 try" is printed . Thereafter the code within the second try is
executed, where "2 try" is
printed. The break statement in C# gets converted to a leave to label IL_0037
in IL. This label signifies the end of the for statement. The leave instruction
is smart enough to realize that it is located within two trys with a finally
clause, hence it calls the code with the finally instruction.
Under normal circumstances,
break becomes a branch instruction if not placed within try-catch.
a.cs
public class zzz
{
public void abc()
{
lock(this)
{
}
}
public static void Main()
{
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed {
.entrypoint
ret
}
.method public hidebysig instance void abc() il managed
{
.locals (class zzz V_0)
ldarg.0
dup
stloc.0
call void [mscorlib]System.Threading.Monitor::Enter(class
System.Object)
.try
{
leave.s IL_0011
}
finally
{
ldloc.0
call void
[mscorlib]System.Threading.Monitor::Exit(class System.Object)
endfinally
}
IL_0011: ret
}
}
The lock keyword ensures that
while one thread executes a function, the other threads remain suspended. This
keyword gets translated into a large amount of IL code. In fact, it generates
the maximum amount of code amongst all the keywords.
The C# compiler first calls the
static function Enter from the Monitor class. Then, it executes the code
located in a try. The try block here contains no code. On encountering the
leave instruction, the program enters the finally which calls the Exit function
from the Monitor class. This initiates
another thread that is waiting at the Enter function.
Whenever an exception occurs, an
object representing the exception must be created. This exception object has to
be a class derived from Exception and cannot be a value type or a pointer.
A Structured Exception Handling
(SEH) block is made up of a try and one or more handlers. A try directive is
used to declare a protected block.
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class [mscorlib]System.Exception V_0)
.try
{
ret
}
catch [mscorlib]System.Exception
{
stloc.0
ldstr "In
Exception"
call void
[mscorlib]System.Console::WriteLine(class System.String)
leave.s IL_001e
}
IL_001e: ldstr "After Exception"
call void
[mscorlib]System.Console::WriteLine(class System.String)
ret
}
}
We cannot exit from a protected
block with a ret but, on using it, no errors are generated at assemble time or
run time. As a rule, only a leave or a throw to another exception is acceptable
to exit from a protected block. A leave statement is permitted in the try and
not in the catch.
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed {
.entrypoint
.locals (class [mscorlib]System.Exception V_0)
.try
{
call void
zzz::abc()
ldstr
"Bye"
call void [mscorlib]System.Console::WriteLine(class
System.String)
.try
{
call void
zzz::pqr()
ldstr
"Bye1"
call void [mscorlib]System.Console::WriteLine(class
System.String)
leave aa1
}
catch [mscorlib]System.Exception
{
stloc.0
ldstr "In
Exception1"
call void
[mscorlib]System.Console::WriteLine(class System.String)
leave.s IL_001e
}
aa1: leave.s IL_001e
}
catch [mscorlib]System.Exception
{
stloc.0
ldstr "In
Exception"
call void
[mscorlib]System.Console::WriteLine(class System.String)
leave.s IL_001e
}
IL_001e: ldstr "After Exception"
call void
[mscorlib]System.Console::WriteLine(class System.String)
ret
}
.method public hidebysig static void abc() il managed
{
ret
}
.method public hidebysig static void pqr() il managed
{
newobj instance void [mscorlib]System.Exception::.ctor()
throw
}
}
Output
Bye
In Exception1
After Exception
We can nest as many trys or protected
blocks within each other. A leave is required at the end of every try to avoid
all errors.
We can have four types of
handlers for a try or a protected block. They are:
• finally
• catch
• fault
• filter
Only one of it can be used at a
time.
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed {
.entrypoint
.locals (class [mscorlib]System.Exception V_0)
.try
{
call void
zzz::abc()
ldstr
"Bye"
call void [mscorlib]System.Console::WriteLine(class
System.String)
leave.s IL_001e
}
catch [mscorlib]System.Exception
{
stloc.0
ldstr "In
Exception"
call void
[mscorlib]System.Console::WriteLine(class System.String)
leave.s IL_001e
}
finally
{
ldstr "In
finally"
call void
[mscorlib]System.Console::WriteLine(class System.String)
endfinally
}
IL_001e: ldstr "After Exception"
call void
[mscorlib]System.Console::WriteLine(class System.String)
ret
}
.method public hidebysig static void abc() il managed
{
newobj instance void [mscorlib]System.Exception::.ctor()
throw
}
}
Output
In Exception
After Exception
If we comment out the code where
the function abc has been called, we get the following output:
Output
Bye
In finally
After Exception
As mentioned earlier, the
protected block can only be handled by a single handler.
In the above example, when an
exception is thrown, the catch is called and not finally as it does not have
its own try directive. The runtime does not give us any error, but it ignores
the finally handler.
If, however, no exception is
thrown, as is the case when we comment out the call of the function abc, then
the finally gets called.
It is quaint that the try is a
directive, but the handler is not. We have two classes of handlers:
• exception resolving handlers
• exception observing handlers.
The finally and fault handlers
are exception observing handlers as they do not resolve the exception.
In an exception resolving
handler, we try and resolve the exception so that normal program control
continues. The catch and filter handlers are examples of such handlers.
The deepest handler will be
visited first, followed by the next enclosing one and so on, until we find an
appropriate handler. A handler has its own instructions, using which, the
program can exit a handler. It is illegal to use any other instruction for this
purpose, but at times the assembler is unable to detect this misfit.
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class [mscorlib]System.Exception V_0)
.try
{
}
catch [mscorlib]System.Exception
{
br a1
}
ret
}
.method public hidebysig static void pqr() il managed
{
a1:
ret
}
}
Error
***** FAILURE *****
We are not allowed to jump off a catch handler. It is essential to leave
the handler in an orderly manner only.
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class zzz V_0)
newobj instance void
zzz::.ctor()
stloc.0
startTry:
ldstr "in try"
call void System.Console::WriteLine(class System.String)
//call instance void zzz::abc()
ldstr "after function call"
call void System.Console::WriteLine(class System.String)
leave exitSEH
endTry:
startFault:
ldstr "in fault"
call void System.Console::WriteLine(class System.String)
endfault
endFault:
.try startTry to endTry fault handler startFault to endFault
exitSEH:
ldstr "over"
call void System.Console::WriteLine(class System.String)
ret
}
.method public hidebysig instance void abc() il managed
{
newobj instance void [mscorlib]System.Exception::.ctor()
throw
}
}
Output
in try
after function call
over
If we comment out the call of
the function abc, we get an error, a Windows error, which is incomprehensible.
The purpose of the above program is to demonstrate that we can use labels to
delimit code in a protected block.
Thereafter, we can use the try directive, indicating to it the start and end
label of our code and also the start and end of the fault handler. This is
another way of utilising the try directive.
The catch keyword creates a type
filtered exception. Whenever an exception occurs in a protected block, the EE
checks whether the exception that occurred earlier, is equal to or a sub-type
of the error that the catch expects. If the type matches, the code of the catch
is called. If the type does not match, the EE will continue to search for
another handler.
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class [mscorlib]System.Exception V_0)
.try
{
call void
zzz::abc()
a1:
ldstr
"Bye"
call void [mscorlib]System.Console::WriteLine(class
System.String)
leave.s IL_001e
}
catch [mscorlib]System.Exception
{
stloc.0
ldstr "In
Exception"
call void
[mscorlib]System.Console::WriteLine(class System.String)
leave.s a1
}
IL_001e: ldstr "After Exception"
call void
[mscorlib]System.Console::WriteLine(class System.String)
ret
}
.method public hidebysig static void abc() il managed
{
newobj instance void [mscorlib]System.Exception::.ctor()
throw
}
}
Output
In Exception
Bye
After Exception
The program is not allowed to
resume execution after an exception occurs. This means that, we cannot go back
to the protected block where the exception took place. In this case, we are
allowed to do so, but keep in mind that we were using a beta copy of the
assembler.
Whenever the EE sees a leave in
a catch block, it knows that the exception is done with, and the system returns
to a normal state.
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class [mscorlib]System.Exception V_0)
.try
{
call void
zzz::abc()
ldstr
"Bye"
call void [mscorlib]System.Console::WriteLine(class
System.String)
leave.s IL_001e
}
catch [mscorlib]System.Exception
{
stloc.0
ldstr "In
Exception"
call void
[mscorlib]System.Console::WriteLine(class System.String)
rethrow
}
IL_001e: ldstr "After Exception"
call void
[mscorlib]System.Console::WriteLine(class System.String)
ret
}
.method public hidebysig static void abc() il managed
{
newobj instance void [mscorlib]System.Exception::.ctor()
throw
}
}
Output
In Exception
Exception occurred: System.Exception: An exception of type
System.Exception was thrown.
at zzz.vijay()
Here, at the end of the catch is
the rethrow instruction, which rethrows
the same exception again. As there is no other catch block to catch the
exception, the exception is thrown at run time.
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class [mscorlib]System.Exception V_0)
.try
{
.try
{
call void
zzz::abc()
ldstr
"Bye"
call void [mscorlib]System.Console::WriteLine(class
System.String)
leave.s IL_001e
}
catch [mscorlib]System.Exception
{
stloc.0
ldstr "In
Exception"
call void
[mscorlib]System.Console::WriteLine(class System.String)
rethrow
}
}
catch [mscorlib]System.Exception
{
stloc.0
ldstr "In
Exception1"
call void
[mscorlib]System.Console::WriteLine(class System.String)
}
IL_001e: ldstr "After Exception"
call void
[mscorlib]System.Console::WriteLine(class System.String)
ret
}
.method public hidebysig static void abc() il managed
{
newobj instance void [mscorlib]System.Exception::.ctor()
throw
}
}
Output
In Exception
In Exception1
After Exception
Here, we placed another try
directive with a separate catch. The exception that is thrown by the inner
catch, cannot be caught by another catch at the same level. It needs to be
caught by the catch at the higher level. Thus one more catch is needed.
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class [mscorlib]System.Exception V_0)
.try
{
call void
zzz::abc()
ldstr
"Bye"
call void [mscorlib]System.Console::WriteLine(class
System.String)
leave.s IL_001e
}
catch [mscorlib]System.Exception
{
stloc.0
ldstr "In
Exception"
call void
[mscorlib]System.Console::WriteLine(class System.String)
leave.s IL_001e
}
catch [mscorlib]System.IOException
{
stloc.0
ldstr "In
Exception1"
call void [mscorlib]System.Console::WriteLine(class
System.String)
leave.s IL_001e
}
IL_001e: ldstr "After Exception"
call void
[mscorlib]System.Console::WriteLine(class System.String)
ret
}
.method public hidebysig static void abc() il managed
{
newobj instance void [mscorlib]System.IOException::.ctor()
throw
}
}
Output
In Exception
After Exception
The C# compiler watches our code
like a hawk. On the other hand, the assembler is a blind bat.
There are two exception
handlers:
• IOException: This handles Input/Output
Exceptions.
• Exception: This is a generic handler
that handles generic exceptions since all exceptions are derived from
exception.
Consciously, we have placed the
generic Exception handler first. Therefore, irrespective of the exception
thrown, the second handler will never get called. The function abc now throws a
IOException. The generic Exception handler, which is placed earlier in the
code, is encountered first, and therefore, it deals with the exception. The C#
compiler would have generated an error in this situation, but the assembler
assumes what you are conscious of your deeds.
Output
Exception occurred: System.ExecutionEngineException: An
exception of type System.ExecutionEngineException was thrown.
at zzz.vijay()
The above exception is thrown
when there is no leave in a catch and there is another catch following this
one. In one of our earlier programs above, we have used only a single catch.
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public static void vijay()
{
.entrypoint
ldstr "start"
call void System.Console::WriteLine(class System.String)
br start
a2:
ldstr "in a2"
call void System.Console::WriteLine(class System.String)
pop
ldc.i4.0
endfilter
start:
.try
{
ldstr "in try"
call void System.Console::WriteLine(class System.String)
call void zzz::abc()
ldstr "after function call"
call void System.Console::WriteLine(class System.String)
leave a1
}
filter a2
{
ldstr "filter"
call void System.Console::WriteLine(class System.String)
pop
leave a1
}
a1:
ret
}
.method public hidebysig static void abc() il managed
{
newobj instance void [mscorlib]System.Exception::.ctor()
throw
}
}
Output
start
in try
Exception occurred: System.Exception: An exception of type
System.Exception was thrown.
at zzz.vijay()
The last type of fault handler,
which is the most generic, either does not seem to work or it could also mean
that we have done something wrong.
We use the keyword filter with a
label, which denotes the start point of some code. This code checks whether the
exception must be handled or not. This is triggered off by placing either the number 0 or 1 on the
stack. In our case, none of the code dealing with the filter gets called.