IL and Verification
IL is stack-based, which means that all its instructions push operands onto an execution stack and pop results off the stack. Because IL offers no instructions to manipulate registers,compiler evelopers have an easy time producing IL code; they don’t have to think about managing registers, and fewer IL instructions are needed (since none exist for manipulating registers).
IL instructions are also typeless. For example, IL offers an add instruction that adds the last in my opinion, the biggest benefit of IL isn’t that it abstracts away the underlying CPU. The biggest benefit is application robustness. While compiling IL into native CPU instructions, the CLR performs a process called verification. Verification examines the high-level IL code and ensures that everything it does is “safe.” For example, verification checks that no memory is read from without having previously been written to, that every method is called with the correct number of parameters and that each parameter is of the correct type, that every two operands pushed on the stack; there are not separate 32-bit and 64-bit add instructions.
When the add instruction executes, it determines the types of the operands on the stack and performs the appropriate operation. method’s return value is used properly, that every method has a return tatement, and so on. The managed module’s metadata includes all the method and type information used by the verification process. If the IL code is determined to be “unsafe,” then a System.Security.VerifierException exception is thrown, preventing the method from executing.
Is Your Code Safe?
By default, the Microsoft C# and Visual Basic compilers produce safe code. Safe code is code that is verifiably safe. However, using the unsafe keyword in C# or other languages (such as C++ with Managed Extensions or IL assembly language), it’s possible to produce code that can’t be verifiably safe. The code might, in fact, be safe, but verification is unable to prove this.
To ensure that all your managed module’s methods contain verifiably safe IL, you can use the PEVerify utility (PEVerify.exe) that ships with the .NET Framework SDK. When Microsoft tests their C# and Visual Basic compilers, they run the resulting module through PEVerify to ensure that the compiler always produces verifiably safe code. If PEVerify detects unsafe code, Microsoft fixes the compiler.
You may want to consider running PEVerify on your own modules before you package and ship them. If PEVerify detects a problem, then there is a bug in the compiler and you should report it to Microsoft (or whatever company produces the compiler you’re using). If PEVerify doesn’t detect any unverifiable code, you know that your code will run without throwing a VerifierException on the end-user’s machine.
You should be aware that verification requires access to the metadata contained in any dependant assemblies. So, when you use PEVerify to check an assembly, it must be able to locate and load all referenced assemblies. Because PEVerify uses the CLR to locate the dependant assemblies, the assemblies are located using the same binding and probing rules that would normally be used when executing the assembly. (I’ll discuss these binding and probing rules in Chapters 2 and 3.)
Note that an administrator can elect to turn off verification (using the Microsoft .NET Framework Configuration administrative tool). With verification off, the JIT compiler will compile unverifiable IL into native CPU instructions; however, the administrator is taking full responsibility for the code’s behavior.
In Windows, each process has its own virtual address space. Separate address spaces are necessary because you can’t trust the application’s code. It is entirely possible (and unfortunately, all too common) that an application will read from or write to an invalid memory address. By placing each Windows process in a separate address space, you gain robustness: one process can’t adversely affect another process. By verifying the managed code, however, you know that the code doesn’t improperly access memory that it shouldn’t and you know that the code can’t adversely affect anotherplication’s code. This means that you can run multiple managed applications in a single
Windows virtual address space.
Because Windows processes require a lot of operating system resources, having many of them can hurt performance and limit available resources. Reducing the number of processes by running multiple applications in a single OS process can improve performance, require fewer resources, and be just as robust. This is another benefit of managed code as compared to unmanaged code.
The CLR does, in fact, offer the ability to execute multiple managed applications in a single OS process. Each managed application is called an AppDomain. By default, every managed EXE will run in its own, separate address space that has just the one AppDomain. However, a process hosting the CLR (such as Internet Information Services [IIS] or a future version of SQL Server) can decide to run AppDomains in a single OS process. I’ll devote part of Chapter 20 to a discussion of AppDomains.