Intermediate Language


Compilers that target the CLR translate their source code into CIL and metadata. CIL is often described as an object-oriented assembly language for an abstract stack-based machine. What exactly does this description mean? CIL instructions resemble an assembly language; they include instructions to load values onto an execution stack, sometimes referred to as push instructions; instructions to store values from the stack into memory locations, sometimes referred to as pop instructions; and instructions to invoke method calls. This assembly language is not specific to any hardware architecture. For example, it makes no assumptions about the number of registers or their size in its instruction set. Rather, the instructions execute on a pseudo-machine architecture ”hence the term abstract machine. Because IL is machine independent, it can be moved from machine to machine; translation from IL to native code can be accomplished at any stage up to execution of the code.

IL is said to be object-oriented because it includes a number of instructions specifically designed to support object-oriented concepts. These instructions include the following:

  • box and unbox move value types from the stack and into corresponding reference types allocated on the managed heap.

  • callvirt provides a dynamically dispatched method call where the runtime type of the object on which the call is invoked determines the actual method called. This is a form of polymorphism, one of the cornerstones of object-oriented programming.

  • newobj allocates a new object on the managed heap.

Example: Generating Intermediate Language

This section uses a simple C# program to describe the IL produced by compiling a source file. The program consists of two functions, which add the values of two integers and write the result to the console window. Although this example is not the definitive guide to IL , it does demonstrate most of the basic concepts.

The C# program in Listing 4.1 provides two static methods :

  • Main , the program's entry point, calls the Add method and passes two integer values to it.

  • Add takes two integer arguments and returns the sum of its two arguments.

Listing 4.1 C# program used for generating IL
 using System; namespace SampleIL {   class EntryPoint   {     private static int Add(int a, int b)     {       return a + b;     }     static void Main(string[] args)     {       int a = 40;       int b = 2;       int c = Add(a, b);       Console.WriteLine("Total is: {0}", c);     }   } } 

This trivial program is next compiled to produce an executable file containing IL. If you used a tool such as ILDASM to examine the generated executable, the IL for the Main method would look something like the following:

 .method private hidebysig static void  Main(string[] args)  cil managed {   .entrypoint   // Code size       30 (0x1e)   .maxstack  2   .locals init ([0] int32 a,            [1] int32 b,            [2] int32 c)   IL_0000:  ldc.i4.s   40   IL_0002:  stloc.0   IL_0003:  ldc.i4.2   IL_0004:  stloc.1   IL_0005:  ldloc.0   IL_0006:  ldloc.1   IL_0007:  call       int32 SampleIL.EntryPoint::Add(int32,                                                       int32)   IL_000c:  stloc.2   IL_000d:  ldstr      "Total is: {0}"   IL_0012:  ldloc.2   IL_0013:  box        [mscorlib]System.Int32   IL_0018:  call       void               [mscorlib]System.Console::WriteLine(string,                                                                 object)   IL_001d:  ret } // end of method EntryPoint::Main 

The method header, which consists of the first line of the output, specifies the contract of the method. A simple explanation of the keywords in the header follows :

  • .method specifies that this is a method.

  • private specifies that this method has private accessibility.

  • hidebysig specifies that this method hides other methods with the same name and signature, as is characteristic of the semantics of a language such as C#. Methods can hide methods by name only ( hidebyname ), the semantics for programming languages such as C.

  • static specifies that this is a static (type) method.

  • void specifies that this method does not return a value.

  • Main is the name of the method. The name Main is not special; that is, any method can serve as the entry point for a program. The .entrypoint annotation within the method body identifies the method as the program's entry point.

  • (String[] args) specifies the parameter list that is passed to this function. In this case, the parameter list consists of an array of strings that represent the command-line arguments given to the program.

  • cil states that this method's body is written in CIL.

  • managed states that this method is managed ”that is, executed under the control of the execution engine.

Next comes the method's body, designated by the matching curly braces ( {} ). The .maxstack value indicates the maximum depth of the stack at any point during program execution. This value is used by the verification algorithm when it allocates data structures to ensure that methods do not underflow/overflow the stack. This technique is often used to circumvent security requirements.

The .locals directive defines storage locations for the three integer variables : a , b , and c . All local variables for a method are declared at the beginning of a method even if their declaration in the source file occurs at some point during the method. Local variables can be accessed by their locations within the local variable array. Thus, the number in square brackets, such as [0] , provides the index into the local variable array containing the variable's storage location. This technique of using an index also allows two variables to share the same index and, therefore, the same location, similar to a union in some programming languages.

In the IL generated from Listing 4.1, the first instruction loads the constant 40 onto the stack ( ldc.i4.s 40 ); the second IL instruction then stores this value into the location of variable a . Here you could read the store instruction as "store into local variable array at index 0 the value on the top of the stack" ( stloc.0 ). A similar instruction sequence is then executed for the value 2 , which is stored into variable b . Next, the values of a and b are loaded onto the stack. Once again, they are referred to by their indexes ”0 and 1, respectively ( ldloc.0 and ldloc.1 ).

A simple method call comes next. Because the method being called is located within the same class, the method signature is not decorated with information needed by the runtime environment to locate the method if it was located in another assembly. (We will see an example of a more decorated call shortly.) The call instruction includes more information than you might at first expect. The call keyword is followed by the return type of the method. Next comes the scoped name of the method, which specifies the Add method found in the SampleIL.EntryPoint class. Finally, the signature describes the types of the arguments passed to the method.

Executing this method call has a number of effects. The values stored on the stack, which are copies of the values of the local variables a and b , are popped during the method call. In this way, the size of the stack is reduced by 2 when the method returns. Pushed onto the stack by the method is its return value, which is of type int . This move increases the stack size by 1. Thus, the net effect of the call on the stack is that the stack size is decreased by 1, with the return value appearing on the stack immediately after the call instruction is executed. Executing the store instruction after the method call, therefore, stores the value on the top of the stack into the local variable c . The store instruction also pops the top of the stack, reducing its size by 1.

The ldstr instruction is an example of an IL-supplied instruction intended to support one of the built-in reference types, System.String . It allocates a string object in the managed heap and returns a reference to it on the top of the stack. Next, the value in c is loaded onto the stack. Recall that c is of type int32 , a value type. Here it demonstrates the use of a box instruction, which takes the value on the stack and stores it into an object on the managed heap. In effect, this instruction reduces the stack size by 1 when it pops the actual argument off but then adds a value back onto the stack ”namely, the return value of the method call. The net result is that stack effectively remains constant, but swaps a value type for a reference type.

The following call instruction uses a more decorated format than the previous example. The specified signature informs us that the call goes to the System.Console::WriteLine method found in the assembly mscorlib . This method takes two references from the stack: a reference to a string and a reference to an object. The method does not return anything, but the program produces the following output:

 Total is: 42 

At this point, you may be wondering why the stack size is so important. In general, developers do not need to worry about controlling the stack, because the compilers and execution system will regulate its growth correctly. However, to prevent errors, a number of rules govern the use of the stack. For example, when a method returns, the stack size delta (the change in the stack size) must be equal to the number of values returned from the function, which may be zero if the function is void, minus the number of arguments passed to the function. Most of these stack- related rules are designed to prevent malicious code from exploiting erroneous conditions that can arise if stack underflow or overflow occurs. These requirements often become important when verifying CIL to ensure that it is type-safe. (The topic of verification is covered shortly.)

While also not complicated, a quick look at the Add method highlights some other informative points:

 .method private hidebysig static int32  Add(int32 a,                                              int32 b) cil managed {   // Code size       8 (0x8)   .maxstack  2   .locals ([0] int32 CS 
 .method private hidebysig static int32 Add(int32 a, int32 b) cil managed { // Code size 8 (0x8) .maxstack 2 .locals ([0] int32 CS$00000003$00000000) IL_0000: ldarg.0 IL_0001: ldarg.1 IL_0002: add IL_0003: stloc.0 IL_0004: br.s IL_0006 IL_0006: ldloc.0 IL_0007: ret } // end of method EntryPoint::Add 
000003
 .method private hidebysig static int32 Add(int32 a, int32 b) cil managed { // Code size 8 (0x8) .maxstack 2 .locals ([0] int32 CS$00000003$00000000) IL_0000: ldarg.0 IL_0001: ldarg.1 IL_0002: add IL_0003: stloc.0 IL_0004: br.s IL_0006 IL_0006: ldloc.0 IL_0007: ret } // end of method EntryPoint::Add 
000000) IL_0000: ldarg.0 IL_0001: ldarg.1 IL_0002: add IL_0003: stloc.0 IL_0004: br.s IL_0006 IL_0006: ldloc.0 IL_0007: ret } // end of method EntryPoint::Add

Arguments, like local variables, are accessed as elements in a local variable array, so the instruction to load arguments resembles the instruction to load a local variable. The Add method uses the "load value from argument array index 0" ( ldarg.0 ) instruction to load the value of a . It works similarly for b . Note that the add instruction does not specify the types of the values to be added, unlike some other execution environments. Instead, the stack is strongly typed. The execution system knows what types have been loaded onto the stack and, therefore, what type of addition operation is required ”in this case, the addition of two integers. The only local variable created in the method, which was not defined by the source code, holds the return value.

At first glance, CIL may not appear to be extremely well optimized. For example, the unconditional branch from IL 0004 to the next instruction is nonsense . This lack of optimization occurs for two reasons in most of the sample code given in this book. First, the code shown is often compiled to be unoptimized but simple to debug, as this approach provides a closer match between the source code and the IL. Second, and most importantly, most of the optimization happens at JIT compilation. This delay offers a significant advantage: An increase in the performance of the JIT complier benefits all languages targeting the CLR.

Note that the example class also contains a constructor for an instance of type SampleIL.EntryPoint . The IL for this method follows:

 .method public hidebysig specialname rtspecialname          instance void  .ctor() cil managed {   // Code size       7 (0x7)   .maxstack  8   IL_0000:  ldarg.0   IL_0001:  call instance void                  [mscorlib]System.Object::.ctor()   IL_0006:  ret } // end of method EntryPoint::.ctor 

As far as IL is concerned , the interesting part is the way that the this pointer (argument 0) is stored on the stack and then the base class constructor is invoked. When an optimizing JIT compiler compiles this method, it will probably be eliminated as both this method and the only method it calls do no work.

Verification of Intermediate Language

As programs have become more dynamic and distributed, hosts, such as operating systems and browsers, have been asked more often to execute remote, downloaded, or mobile code. This execution can be quite problematic . Some programs, known as viruses, are intended to cause mischief when executed within unsuspecting hosts . Traditionally, execution environments have offered only a simplistic approach to dealing with this scenario ”specifically, either the code is allowed to executed or it is not. An improvement over this scenario limits the execution environment provided to download code. For example, the "sandbox" provided by the Java Virtual Machine allows code to be downloaded and executed but restricts the interaction that the code can have with local resources and the distributed systems with which the code can communicate. Central to the notion of safe execution of mobile code within the .NET Framework is the concept of type safety .

Executing un-type-safe CIL, whether the code was accidentally or intentionally created, can produce erroneous or destructive behavior within the execution system. This behavior may manifest itself as errors in the operation of the system or even as permanent loss of sensitive data. To prevent this kind of situation, an assembly undergoes a pre-execution inspection to see whether it will act in a type-safe manner. Just because IL can be proved to be type-safe, however, does not mean that executing the code may not have destructive effects. For example, a method called FormatDisk should, as its name suggests, format a disk, thereby removing all data from it. While an administrator might legitimately call this method, ordinary users should not have access to it. The security system, which is a component of the execution system, is responsible for preventing CIL from calling methods that it does not have permission to call.

Verifying an assembly for type safety involves two tasks :

  • Checking the assembly itself ”that is, checking the assembly manifest

  • Checking the types the assembly contains

Checking an Assembly

Verifying assembly metadata is vital . In fact, the entire verification process is based on the integrity of an assembly's metadata. Assembly metadata is verified either when an assembly is load into a cache ”for example, the global assembly cache (GAC) ”or when it is read from disk if it has not been inserted into a cache. The GAC is a central storage location for assemblies that are used by a number of programs; the download cache holds assemblies downloaded from other locations, such as the Internet.

Metadata verification involves such requirements as examining metadata tokens to confirm that they index correctly into the tables they access and that indexes into string tables do not point at strings that exceed the size of the buffers that should hold them, thus eliminating buffer overflow.

Once the assembly manifest has been verified, the types with the assembly can be checked.

Checking Types

All types in the CLR specify the contracts that they will implement, and this information is persisted as metadata along with the CIL. For example, when a type specifies that it inherits from another class or interface, thereby indicating that it will implement a number of methods, this specification creates a contract. A contract may also be related to visibility. For example, types may be declared as public (exported from their assembly) or private.

CIL can be described as type-safe or not type-safe. Type safety is a property of code that ensures that the code accesses types only in accordance with their contracts. Type safety also includes requirements other than just accessing types correctly, such as prohibition of stack underflow/overflow, correct use of the exception-handling facilities, and object initialization.

The CLR's type-safety verification algorithm can be described as conservative. That is, CIL that passes verification is deemed type-safe but some type-safe CIL may not pass verification. Verification represents a fundamental building block in the security system, although it is currently performed only on managed code. Unmanaged code executed by the CLR must be fully trusted, as the CLR cannot verify it.

Classification of CIL Verification Status

To understand verification, it is important to understand how CIL can be classified . CIL can be categorized in one of four ways: as illegal, legal, type-safe, or verifiable : [1]

[1] The following definitions are more informative than normative. More precise versions can be found in other documentation, such as the ECMA specification.

  • Illegal CIL is CIL for which the JIT compiler cannot produce a native representation. For example, CIL containing an invalid operation code cannot be translated into native code. Likewise, a jump instruction whose target is the address of an operand rather than an opcode constitutes illegal CIL.

  • Legal CIL is all CIL that satisfies the CIL grammar and, therefore, can be represented in native code. Note that this classification includes CIL that uses non-type-safe forms of pointer arithmetic to gain access to members of a type, for example.

  • Type-safe CIL interacts with types only through their publicly exposed contracts. CIL that attempts to access a private member of a type from another type would not be considered type-safe, for example.

  • Verifiable CIL is type-safe CIL that can be proved to be type-safe by a verification algorithm. As noted earlier, the conservative nature of the verification algorithm means that some type-safe CIL may not pass verification. Of course, verifiable CIL is also type-safe and legal but not illegal.

The verification process occurs during the JIT compilation and proceeds intermittently within this phase; thus verification and JIT compilation do not execute as two separate processes. If a sequence of unverifiable CIL is found within an assembly, the security system checks whether the assembly can skip verification. If the assembly was loaded from a local disk, for example, under the default settings of the security model avoiding verification is possible. If the assembly can skip verification, then the CIL is translated into native code. If the assembly cannot skip verification, then the offending CIL is replaced with a stub that throws an exception if the method is called. At runtime, any call to this code will produce an exception. A commonly asked question is, "Why not check whether an assembly needs verification before verification begins?" In general, verification, as it is performed during the JIT compilation, is often faster than checking whether the assembly can skip verification. [2] Verification and the elimination of un-type-safe code is the first part of security on the CLR.

[2] The decision to skip verification is actually smarter than the process described here. For example, results of previous verification attempts can be cached to provide a quick lookup scenario.



Programming in the .NET Environment
Programming in the .NET Environment
ISBN: 0201770180
EAN: 2147483647
Year: 2002
Pages: 146

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net