2.7 CLR Execution
Now that you understand the elements of a .NET executable,
let's talk about the services that the CLR provides
to support management and execution of .NET assemblies. There are
many fascinating components in the CLR, but for brevity, we will
limit our discussions to just the major components, as shown in Figure 2-4.
Figure 2-4. Major CLR components: the Virtual Execution System (VES)

The major components of the CLR include the class loader, verifier,
JIT compilers, and other execution support, such as code management,
security management, garbage collection, exception management, debug
management, marshaling management, thread management, and so on. As
you can see from Figure 2-4, your .NET PE files lay
on top of the CLR and execute within the
CLR's Virtual Execution System (VES), which
hosts the major components of the runtime. Your .NET PE files will
have to go through the class loader, the type verifier, the JIT
compilers, and other execution support components before they will
execute.
2.7.1 Class Loader
When
you run a standard Windows
application, the OS loader loads it before it can execute. At the
time of this writing, the default loaders in the existing Windows
operating systems, such as Windows 98, Windows Me, Windows 2000, and
so forth, recognize only the standard Windows PE files. As a result,
Microsoft has provided an updated OS loader for each of these
operating systems that support the .NET runtime. The updated OS
loaders know the .NET PE file format and can handle the file
appropriately.When you run a .NET application on one of these systems that have an
updated OS loader, the OS loader recognizes the .NET application and
thus passes control to the CLR. The CLR then finds the entry point,
which is typically Main( ), and executes it to
jump-start the application. But before Main( ) can
execute, the class loader must find the class that exposes
Main( ) and load the class. In addition, when
Main( ) instantiates an object of a specific
class, the class loader also kicks in. In short, the class loader
performs its magic the first time a type is referenced.The class loader loads .NET classes into memory
and prepares them for execution. Before it can successfully do this,
it must locate the target class. To find the target class, the class
loader looks in several different places, including the
application configuration file
(.config) in the current directory, the GAC, and
the metadata that is part of the PE file, specifically the manifest.
The information that is provided by one or more of these items is
crucial to locating the correct target class. Recall that a class can
be scoped to a particular namespace, a namespace can be scoped to a
particular assembly, and an assembly can be scoped to a specific
version. Given this, two classes, both named Car, are treated as
different types even if the version information of their assemblies
are the same.Once the class loader has found and loaded the target class, it
caches the type information for the class so that it
doesn't have to load the class again for the
duration of this process. By caching this information, it will later
determine how much memory is needed to allocate for the newly created
instance of this class. Once the target class is loaded, the class
loader injects a small stub, like a function prolog, into every
single method of the loaded class. This stub is used for two
purposes: to denote the status of JIT compilation and to transition
between managed and unmanaged code. At this point, if the loaded
class references other classes, the class loader will also try to
load the referenced types. However, if the referenced types have
already been loaded, the class loader has to do nothing. Finally, the
class loader uses the appropriate metadata to initialize the static
variables and instantiate an object of the loaded class for you.
2.7.2 Verifier
Scripting
and
interpreted languages are very lenient on type usages, allowing you
to write code without explicit variable declarations. This
flexibility can introduce code that is extremely error-prone and hard
to maintain, and that is often a culprit for mysterious program
crashes. Unlike scripting and interpreted languages, compiled
languages require types to be explicitly defined prior to their use,
permitting the compiler to ensure that types are used correctly and
the code will execute peacefully at runtime.The key here is type safety, and it is a fundamental concept
for code
verification in .NET. Within the VES, the verifier is the component
that executes at runtime to verify that the code is type safe. Note
that this type verification is done at runtime and that this is a
fundamental difference between .NET and other environments. By
verifying type safety at runtime, the CLR can prevent the execution
of code that is not type safe and ensure that the code is used as
intended. In short, type safety means more reliability.Let's talk about where the verifier fits within the
CLR. After the class loader has loaded a class and before a piece of
IL code can execute, the verifier kicks in for code that must be
verified. The verifier is responsible for verifying that:The metadata is well formed, meaning the metadata must be valid.The IL code is type safe, meaning type signatures are used correctly.
Both of these criteria must be met before the code can be executed
because JIT compilation will take place only when code and metadata
have been successfully verified. In addition to checking for type
safety, the verifier also performs rudimentary control-flow analysis
of the code to ensure that the code is using types correctly. You
should note that since the verifier is a part of the JIT compilers,
it kicks in only when a method is being invoked, not when a class or
assembly is loaded. You should also note that verification is an
optional step because trusted code will never be verified but will be
immediately directed to the JIT compiler for compilation.
2.7.3 JIT Compilers
JIT
compilers play a major role in the
.NET platform because all .NET PE files contain
IL and metadata, not native code. The JIT
compilers convert IL to native code so that it can execute on the
target operating system. For each method that has been successfully
verified for type safety, a JIT compiler in the CLR will compile the
method and convert it into native code.One advantage of a JIT compiler is that it can dynamically compile
code that is optimized for the target machine. If you take the same
.NET PE file from a one-CPU machine to a
two-CPU machine, the JIT compiler on the two-CPU machine knows about
the second CPU and may be able to spit out the native code that takes
advantage of the second CPU. Another obvious advantage is that you
can take the same .NET PE file and run it on a totally different
platform, whether it be Windows, Unix, or whatever, as long as that
platform has a CLR.For optimization reasons, JIT compilation occurs only the first time
a method is invoked. Recall that the class loader adds a stub to each
method during class loading. At the first method invocation, the VES
reads the information in this stub, which tells it that the code for
the method has not been JIT-compiled. At this indication, the JIT
compiler compiles the method and injects the address of the native
method into this stub. During subsequent invocations to the same
method, no JIT compilation is needed because each time the VES goes
to read information in the stub, it sees the address of the native
method. Because the JIT compiler only performs its magic the first
time a method is invoked, the methods you don't need
at runtime will never be JIT-compiled.The compiled, native code lies in memory until the process shuts down
and until the garbage collector clears off all references and memory
associated with the process. This means that the next time you
execute the process or component, the JIT compiler will again perform
its magic.If you want to avoid the cost of JIT compilation at runtime, you can
use a special tool called ngen.exe, which
compiles your IL during installation and setup time. Using ngen, you
can JIT-compile the code once and cache it on the machine so that you
can avoid JIT compilation at runtime (this process is referred to as
pre-JITting). In the event that the PE file has been updated, you
must PreJIT
the PE file again. Otherwise, the CLR can detect the update and
dynamically command the appropriate JIT compiler to compile the
assembly.
2.7.4 Execution Support and Management
By now, you should see that
every component in the CLR that we've covered so far
uses metadata and IL in some way to successfully carry out the
services that it supports. In addition to the provided metadata and
generated managed code, the JIT compiler must generate managed data
that the code manager needs to locate and unwind stack
frames.[12]
The
code manager uses managed data to control the
execution of code, including performing stack walks that are required
for exception handling, security checks, and garbage collection.
Besides the code manager, the CLR also provides a number of important
execution-support and management services. A detailed discussion of
these services is beyond the scope of this book, so we will briefly
enumerate a few of them here:[12] By the way, you can write a custom JIT
compiler or a custom code manager for the CLR because the CLR
supports the plug-and-play of these components.
Garbage collection
Unlike
C++, where you must delete all heap-based objects manually, the CLR
supports automatic lifetime management for all .NET objects. The
garbage collector can detect when your objects are no longer being
referenced and perform garbage collection to reclaim the unused
memory.
Exception handling
Prior to
.NET, there was no consistent method for error or exception handling,
causing lots of pain in error handling and reporting. In .NET, the
CLR supports a standard exception-handling mechanism that works
across all languages, allowing every program to use a common
error-handling mechanism. The CLR exception-handling mechanism is
integrated with Windows Structured Exception Handling (SEH).
Security support
The CLR performs
various security checks at runtime to make sure that the code is safe
to execute and that the code is not breaching any security
requirements. In addition to supporting code access security, the
security engine also supports declarative and imperative security
checks. Declarative security requires no special security code, but
you have to specify the security requirements through attributes or
administrative configuration. Imperative security requires that you
write the code in your method to specifically cause security checks.
Debugging support
The CLR
provides rich support for debugging and profiling. There is an API
that compiler vendors can use to develop a debugger. This API
contains support for controlling program execution, breakpoints,
exceptions, control flow, and so forth. There is also an API for
tools to support the profiling of running programs.
Interoperation support
The CLR
supports interoperation between the managed (CLR) and unmanaged (no
CLR) worlds. The COM Interop facility serves as
a bridge between COM and the CLR, allowing a COM object to use a .NET
object, and vice versa. The Platform Invoke
(P/Invoke) facility allows you to call Windows API functions.
This is by no means an exhaustive list. The one thing that we want to
reiterate is that like the class loader, verifier, JIT compiler, and
just about everything else that deals with .NET, these
execution-support and management facilities all use metadata, managed
code, and managed data in some way to carry out their services.