12 Virtual Execution System


The Virtual Execution System (VES) provides an environment for executing managed code. It provides direct support for a set of built-in data types, defines a hypothetical machine with an associated machine model and state, a set of control flow constructs, and an exception handling model. To a large extent, the purpose of the VES is to provide the support required to execute the Common Intermediate Language instruction set (see Partition III).

12.1 Supported Data Types

The CLI directly supports the data types shown in Table 2-6, Data Types Directly Supported by the CLI Instruction Set. That is, these data types can be manipulated using the CIL instruction set (see Partition III).

Table 2-6. Data Types Directly Supported by the CLI Instruction Set

Data Type

Description

int8

8-bit 2's complement signed value

unsigned int8

8-bit unsigned binary value

int16

16-bit 2's complement signed value

unsigned int16

16-bit unsigned binary value

int32

32-bit 2's complement signed value

unsigned int32

32-bit unsigned binary value

int64

64-bit 2's complement signed value

unsigned int64

64-bit unsigned binary value

float32

32-bit IEC 60559:1989 floating point value

float64

64-bit IEC 60559:1989 floating point value

native int

native size 2's complement signed value

native unsigned int

native size unsigned binary value, also unmanaged pointer

F

native size floating point number (internal to VES, not user visible)

O

native size object reference to managed memory

&

native size managed pointer (may point into managed memory)

The CLI model uses an evaluation stack. Instructions that copy values from memory to the evaluation stack are "loads"; instructions that copy values from the stack back to memory are "stores." The full set of data types in Table 2-6, Data Types Directly Supported by the CLI Instruction Set, can be represented in memory. However, the CLI supports only a subset of these types in its operations upon values stored on its evaluation stack int32, int64, native int. In addition, the CLI supports an internal data type to represent floating point values on the internal evaluation stack. The size of the internal data type is implementation-dependent. For further information on the treatment of floating point values on the evaluation stack, see Partition I, section 12.1.3 and Partition III. Short numeric values (int8, int16, unsigned int8, unsigned int16) are widened when loaded (memory-to-stack) and narrowed when stored (stack-to-memory). This reflects a computer model that assumes, for numeric and object references, [that] memory cells are 1, 2, 4, or 8 bytes wide but stack locations are either 4 or 8 bytes wide. User-defined value types may appear in memory locations or on the stack and have no size limitation; the only built-in operations on them are those that compute their address and copy them between the stack and memory.

The only CIL instructions with special support for short numeric values (rather than support for simply the 4- or 8-byte integral values) are:

  • Load and store instructions to/from memory: ldelem, ldind, stind, stelem

  • Data conversion: conv, conv.ovf

  • Array creation: newarr

The signed integer (int8, int16, int32, int64, and native int) and the respective unsigned integer (unsigned int8, unsigned int16, unsigned int32, unsigned int64, and native unsigned int) types differ only in how the bits of the integer are interpreted. For those operations where an unsigned integer is treated differently from a signed integer (e.g., comparisons or arithmetic with overflow) there are separate instructions for treating an integer as unsigned (e.g., cgt.un and add.ovf.u).

This instruction set design simplifies CIL-to-native-code (e.g., JIT) compilers and interpreters of CIL by allowing them to internally track a smaller number of data types. See Partition I, section 12.3.2.1.

As described below, CIL instructions do not specify their operand types. Instead, the CLI keeps track of operand types based on data flow and aided by a stack consistency requirement described below. For example, the single add instruction will add two integers or two floats from the stack.

ANNOTATION

Which data types are supported, and what that means, depends on the point of view. Viewed from programming languages, each language has its own type system. From the point of view of the CLI, there is a full-fledged type system, the CTS, that includes user-defined types, all the built-in types, etc.

Then there is a type system that is used by the internals of the JIT and the one actually implemented by the VES, which is much reduced and really only knows about the size of integers (not whether they are signed or unsigned), a floating point type, objects, and pointers. The verifier sees a more expanded type system than that, but still restricted. Finally there's the one described in Partition I, section 12.1, which is what the CIL instructions can make of the base type system in the VES, which includes both signed and unsigned arithmetic.

If you are implementing a VES, you concentrate on the basics, and that's what the JIT understands. But this section describes the types that can be supported with the instruction set using normal programming techniques. This topic is of particular interest to compiler writers, who have to implement the types of their language on top of what the VES provides.


12.1.1 Native Size: native int, native unsigned int, O, and &

The native-size, or generic, types (native int, native unsigned int, O, and &) are a mechanism in the CLI for deferring the choice of a value's size. These data types exist as CIL types. But the CLI maps each to the native size for a specific processor. (For example, data type I would map to int32 on a Pentium processor, but to int64 on an IA64 processor). So, the choice of size is deferred until JIT compilation or runtime, when the CLI has been initialized and the architecture is known. This implies that field and stack frame offsets are also not known at compile time. For languages like Visual Basic, where field offsets are not computed early anyway, this is not a hardship. In languages like C or C++, where sizes must be known when source code is compiled, a conservative assumption that they occupy 8 bytes is sometimes acceptable (for example, when laying out compile-time storage).

12.1.1.1 Unmanaged Pointers as Type Native Unsigned Int

RATIONALE

For languages like C, when compiling all the way to native code, where the size of a pointer is known at compile time and there are no managed objects, the fixed-size unsigned integer types (unsigned int32 or unsigned int64) may serve as pointers. However, choosing pointer size at compile time has its disadvantages. If pointers were chosen to be 32-bit quantities at compile time, the code would be restricted to 4 gigabytes of address space, even if it were run on a 64-bit machine. Moreover, a 64-bit CLI would need to take special care so those pointers passed back to 32-bit code would always fit in 32 bits. If pointers were chosen at compile time to be 64 bits, the code would run on a 32-bit machine, but pointers in every data structure would be twice as large as necessary on that CLI.

For other languages, where the size of a data type need not be known at compile time, it is desirable to defer the choice of pointer size from compile time to CLI initialization time. In that way, the same CIL code can handle large address spaces for those applications that need them, while also being able to reap the size benefit of 32-bit pointers for those applications that do not need a large address space.


The native unsigned int type is used to represent unmanaged pointers with the VES. The metadata allows unmanaged pointers to be represented in a strongly typed manner, but these types are translated into type native unsigned int for use by the VES.

12.1.1.2 Managed Pointer Types: O and &

The O data type represents an object reference that is managed by the CLI. As such, the number of specified operations is severely limited. In particular, references shall only be used on operations that indicate that they operate on reference types (e.g., ceq and ldind.ref), or on operations whose metadata indicates that references are allowed (e.g., call, ldsfld, and stfld).

The & data type (managed pointer) is similar to the O type but points to the interior of an object. That is, a managed pointer is allowed to point to a field within an object or an element within an array, rather than to point to the "start" of object or array.

ANNOTATION

The previous paragraph is accurate but not complete. A managed pointer can also point directly to a value type. It can point to the entire value type on the stack when it is not associated with an object, or it can point, after an unbox operation, to the value type within the boxed object.


Object references (O) and managed pointers (&) may be changed during garbage collection, since the data to which they refer may be moved.

NOTE

In summary, object references, or O types, refer to the "outside" of an object, or to an object as a whole. But managed pointers, or & types, refer to the interior of an object. The & types are sometimes called "by-ref types" in source languages, since passing a field of an object by reference is represented in the VES by using an & type to represent the type of the parameter.


In order to allow managed pointers to be used more flexibly, they are also permitted to point to areas that aren't under the control of the CLI garbage collector, such as the evaluation stack, static variables, and unmanaged memory. This allows them to be used in many of the same ways that unmanaged pointers (U) are used. Verification restrictions guarantee that, if all code is verifiable, a managed pointer to a value on the evaluation stack doesn't outlast the life of the location to which it points.

12.1.1.3 Portability: Storing Pointers in Memory

Several instructions, including calli, cpblk, initblk, ldind.*, and stind.*, expect an address on the top of the stack. If this address is derived from a pointer stored in memory, there is an important portability consideration.

  1. Code that stores pointers in a native sized integer or pointer location (types native int, O, native unsigned int, or &) is always fully portable.

  2. Code that stores pointers in an 8-byte integer (type int64 or unsigned int64) can be portable. But this requires that a conv.ovf.u instruction be used to convert the pointer from its memory format before its use as a pointer. This may cause a runtime exception if run on a 32-bit machine.

  3. Code that uses any smaller integer type to store a pointer in memory (int8, unsigned int8, int16, unsigned int16, int32, unsigned int32) is never portable, even though the use of an unsigned int32 or int32 will work correctly on a 32-bit machine.

ANNOTATION

This section is directed to compiler writers, or those writing an IL assembly language. If you expect to generate code that works correctly on all machines, it is important to follow these rules.


12.1.2 Handling of Short Integer Data Types

The CLI defines an evaluation stack that contains either 4-byte or 8-byte integers, but a memory model that encompasses in addition 1-byte and 2-byte integers. To be more precise, the following rules are part of the CLI model:

  • Loading from 1-byte or 2-byte locations (arguments, locals, fields, statics, pointers) expands to 4-byte values. For locations with a known type (e.g., local variables) the type being accessed determines whether the load sign-extends (signed locations) or zero-extends (unsigned locations). For pointer dereference (ldind.*), the instruction itself identifies the type of the location (e.g., ldind.u1 indicates an unsigned location, while ldind.i1 indicates a signed location).

  • Storing into a 1-byte or 2-byte location truncates to fit and will not generate an overflow error. Specific instructions (conv.ovf.*) can be used to test for overflow before storing.

  • Calling a method assigns values from the evaluation stack to the arguments for the method, hence it truncates just as any other store would when the actual argument is larger than the formal argument.

  • Returning from a method assigns a value to an invisible return variable, so it also truncates as a store would when the type of the value returned is larger than the return type of the method. Since the value of this return variable is then placed on the evaluation stack, it is then sign-extended or zero-extended as any other load would be. Note that this truncation followed by extending is not identical to simply leaving the computed value unchanged.

It is the responsibility of any translator from CIL to native machine instructions to make sure that these rules are faithfully modeled through the native conventions of the target machine. The CLI does not specify, for example, whether truncation of short integer arguments occurs at the call site or in the target method.

ANNOTATION

This section brings out a subtle and important point. It is important to understand when sign extensions and truncations happen automatically without notification. If you are implementing a VES, you have to ensure that these truncations happen. If you are writing an IL assembler or a compiler, you have to know they are happening. For complete information on this, see the tables in Partition III, section 1.5.


12.1.3 Handling of Floating Point Data Types

Floating point calculations shall be handled as described in IEC 60559:1989. This standard describes encoding of floating point numbers, definitions of the basic operations and conversion, rounding control, and exception handling.

The standard defines special values, NaN (not a number), +infinity, and infinity. These values are returned on overflow conditions. A general principle is that operations that have a value in the limit return an appropriate infinity while those that have no limiting value return NaN, but see the standard for details.

NOTE

The following examples show the most commonly encountered cases.

X rem 0 = NaN

0 * +infinity = 0 * -infinity = NaN

(X / 0) = +infinity, if X>0

NaN, if X=0

-infinity, if X < 0

NaN op X = X op NaN = NaN for all operations

(+infinity) + (+infinity) = (+infinity)

X / (+infinity) = 0

X mod (-infinity) = -X

(+infinity) (+infinity) = NaN


NOTE

This standard does not specify the behavior of arithmetic operations on denormalized floating point numbers, nor does it specify when or whether such representations should be created. This is in keeping with IEC 60559:1989. In addition, this standard does not specify how to access the exact bit pattern of NaNs that are created, nor the behavior when converting a NaN between 32-bit and 64-bit representation. All of this behavior is deliberately left implementation-specific.


For purposes of comparison, infinite values act like a number of the correct sign but with a very large magnitude when compared with finite values. NaN is "unordered" for comparisons (see clt, clt.un).

While the IEC 60559:1989 standard also allows for exceptions to be thrown under unusual conditions (such as overflow and invalid operand), the CLI does not generate these exceptions. Instead, the CLI uses the NaN, +infinity, and infinity return values and provides the instruction ckfinite to allow users to generate an exception if a result is NaN, +infinity, or infinity.

The rounding mode defined in IEC 60559:1989 shall be set by the CLI to "round to the nearest number," and neither the CIL nor the class library provide a mechanism for modifying this setting. Conforming implementations of the CLI need not be resilient to external interference with this setting. That is, they need not restore the mode prior to performing floating point operations, but rather may rely on it having been set as part of their initialization.

ANNOTATION

There is an important point in the preceding paragraph: you must do your computation using the specified rounding mode (round to nearest). This is a requirement, not an option. Some scientific computations that require other rounding modes cannot be accommodated in the VES, and these operations need to be done in native code using PInvoke.


For conversion to integers, the default operation supplied by the CIL is "truncate toward zero." There are class libraries supplied to allow floating point numbers to be converted to integers using any of the other three traditional operations (round to nearest integer, floor (truncate towards infinity), ceiling (truncate toward +infinity)).

Storage locations for floating point numbers (statics, array elements, and fields of classes) are of fixed size. The supported storage sizes are float32 and float64. Everywhere else (on the evaluation stack, as arguments, as return types, and as local variables) floating point numbers are represented using an internal floating point type. In each such instance, the nominal type of the variable or expression is either R4 or R8, but its value may be represented internally with additional range and/or precision. The size of the internal floating point representation is implementation-dependent, may vary, and shall have precision at least as great as that of the variable or expression being represented. An implicit widening conversion to the internal representation from float32 or float64 is performed when those types are loaded from storage. The internal representation is typically the native size for the hardware, or as required for efficient implementation of an operation. The internal representation shall have the following characteristics:

  • The internal representation shall have precision and range greater than or equal to the nominal type.

  • Conversions to and from the internal representation shall preserve value.

NOTE

This implies that an implicit widening conversion from float32 (or float64) to the internal representation, followed by an explicit conversion from the internal representation to float32 (or float64), will result in a value that is identical to the original float32 (or float64) value.


RATIONALE

This design allows the CLI to choose a platform-specific high performance representation for floating point numbers until they are placed in storage locations. For example, it may be able to leave floating point variables in hardware registers that provide more precision than a user has requested. At the same time, CIL generators can force operations to respect language-specific rules for representations through the use of conversion instructions.


When a floating point value whose internal representation has greater range and/or precision than its nominal type is put in a storage location, it is automatically coerced to the type of the storage location. This may involve a loss of precision or the creation of an out-of-range value (NaN, +infinity, or infinity). However, the value may be retained in the internal representation for future use, if it is reloaded from the storage location without having been modified. It is the responsibility of the compiler to ensure that the retained value is still valid at the time of a subsequent load, taking into account the effects of aliasing and other execution threads (see Partition I, section 12.6). This freedom to carry extra precision is not permitted, however, following the execution of an explicit conversion (conv.r4 or conv.r8), at which time the internal representation must be exactly representable in the associated type.

NOTE

To detect values that cannot be converted to a particular storage type, a conversion instruction (conv.r4, or conv.r8) may be used, followed by a check for a non-finite value using ckfinite. To detect underflow when converting to a particular storage type, a comparison to zero is required before and after the conversion.


NOTE

The use of an internal representation that is wider than float32 or float64 may cause differences in computational results when a developer makes seemingly unrelated modifications to their code, the result of which may be that a value is spilled from the internal representation (e.g., in a register) to a location on the stack.


ANNOTATION

The above specification allows a compliant implementation to avoid rounding to the precision of the target type on intermediate computations, and thus permits the use of wider precision hardware registers, as well as the application of optimizing transformations that result in the same or greater precision, such as contractions. Where exactly reproducible precision is required by a language or application (e.g., the Kahan Summation Formula), explicit conversions may be used. Reproducible precision does not guarantee reproducible behavior. Implementations with extra precision may round twice: once for the floating point operation, and once for the explicit conversion. Implementations without extra precision effectively round only once. In rare cases, rounding twice versus rounding once can yield results differing by one unit of least precision.

For example, consider adding two float64 values via the sequence add conv.r8. If the internal floating point type has the same precision as float64, the add instruction rounds to float64, and the conv.r8 has no effect. If the internal floating point type has greater precision, then both instructions might round. Rounding twice can yield a different result from rounding once. Here's a simple example of the principle in decimal: Consider the value $1.49. If rounded to dimes, it's $1.50, and rounding $1.50 to dollars yields $2.00 (by the usual round-to-even rule). But rounding $1.49 directly to dollars yields $1.00.

Arch Robison

It is left to the implementation of the CLI whether to support precise floating point computations.


ANNOTATION

To those not familiar with floating point arithmetic, a word of caution. It is very easy to underestimate the subtlety of small implementation decisions that have profound effects. People writing the standard strongly advise you to hire a professional floating point expert if you are worried about such things, because novices can make mistakes that ultimately crash space shuttles.


12.1.4 CIL Instructions and Numeric Types

This section contains only informative text.


Most CIL instructions that deal with numbers take their operands from the evaluation stack (see Partition I, section 12.3.2.1), and these inputs have an associated type that is known to the VES. As a result, a single operation like add can have inputs of any numeric data type, although not all instructions can deal with all combinations of operand types. Binary operations other than addition and subtraction require that both operands be of the same type. Addition and subtraction allow an integer to be added to or subtracted from a managed pointer (types & and O). Details are specified in Partition II.

Instructions fall into the following categories:

Numeric: These instructions deal with both integers and floating point numbers, and consider integers to be signed. Simple arithmetic, conditional branch, and comparison instructions fit in this category.

Integer: These instructions deal only with integers. Bit operations and unsigned integer division/remainder fit in this category.

Floating point: These instructions deal only with floating point numbers.

Specific: These instructions deal with integer and/or floating point numbers, but have variants that deal specially with different sizes and unsigned integers. Integer operations with overflow detection, data conversion instructions, and operations that transfer data between the evaluation stack and other parts of memory (see Partition I, section 12.3.2) fit into this category.

Unsigned/unordered: There are special comparison and branch instructions that treat integers as unsigned and consider unordered floating point numbers specially (as in "branch if greater than or unordered"):

Load constant: The load constant (ldc.*) instructions are used to load constants of type int32, int64, float32, or float64. Native size constants (type native int) shall be created by conversion from int32 (conversion from int64 would not be portable) using conv.i or conv.u. Table 2-7, CIL Instructions by Numeric Category, shows the CIL instructions that deal with numeric values, along with the category to which they belong. Instructions that end in ".*" indicate all variants of the instruction (based on size of data and whether the data is treated as signed or unsigned).

Table 2-7. CIL Instructions by Numeric Category

add

Numeric

div

Numeric

add.ovf.*

Specific

div.un

Integer

and

Integer

ldc.*

Load constant

beq[.s]

Numeric

ldelem.*

Specific

bge[.s]

Numeric

ldind.*

Specific

bge.un[.s]

Unsigned/unordered

mul

Numeric

bgt[.s]

Numeric

mul.ovf.*

Specific

bgt.un[.s]

Unsigned/unordered

neg

Integer

ble[.s]

Numeric

newarr.*

Specific

ble.un[.s]

Unsigned/unordered

not

Integer

blt[.s]

Numeric

or

Integer

blt.un[.s]

Unsigned/unordered

rem

Numeric

bne.un[.s]

Unsigned/unordered

rem.un

Integer

ceq

Numeric

shl

Integer

cgt

Numeric

shr

Integer

cgt.un

Unsigned/unordered

shr.un

Specific

ckfinite

Floating point

stelem.*

Specific

clt

Numeric

stind.*

Specific

clt.un

Unsigned/unordered

sub

Numeric

conv.*

Specific

sub.ovf.*

Specific

conv.ovf.*

Specific

xor

Integer

End informative text


12.1.5 CIL Instructions and Pointer Types

This section contains only informative text.


RATIONALE

Some implementations of the CLI will require the ability to track pointers to objects and to collect objects that are no longer reachable (thus providing memory management by "garbage collection"). This process moves objects in order to reduce the working set and thus will modify all pointers to those objects as they move. For this to work correctly, pointers to objects may only be used in certain ways. The O (object reference) and & (managed pointer) datatypes are the formalization of these restrictions.


The use of object references is tightly restricted in the CIL. They are used almost exclusively with the "virtual object system" instructions, which are specifically designed to deal with objects. In addition, a few of the base instructions of the CIL handle object references. In particular, object references can be:

  1. Loaded onto the evaluation stack to be passed as arguments to methods (ldloc, ldarg), and stored from the stack to their home locations (stloc, starg)

  2. Duplicated or popped off the evaluation stack (dup, pop)

  3. Tested for equality with one another, but not other data types (beq, beq.s, bne, bne.s, ceq)

  4. Loaded from / stored into unmanaged memory, in type unmanaged code only (ldind.ref, stind.ref)

  5. Created as a null reference (ldnull)

  6. Returned as a value (ret)

Managed pointers have several additional base operations.

  1. Addition and subtraction of integers, in units of bytes, returning a managed pointer (add, add.ovf.u, sub, sub.ovf.u)

  2. Subtraction of two managed pointers to elements of the same array, returning the number of bytes between them (sub, sub.ovf.u)

  3. Unsigned comparison and conditional branches based on two managed pointers (bge.un, bge.un.s, bgt.un, bgt.un.s, ble.un, ble.un.s, blt.un, blt.un.s, cgt.un, clt.un)

Arithmetic operations upon managed pointers are intended only for use on pointers to elements of the same array. Other uses of arithmetic on managed pointers is unspecified.

RATIONALE

Since the memory manager runs asynchronously with respect to programs and updates managed pointers, both the distance between distinct objects and their relative position can change.


End informative text


12.1.6 Aggregate Data

This section contains only informative text.


The CLI supports aggregate data, that is, data items that have sub-components (arrays, structures, or object instances) but are passed by copying the value. The sub-components can include references to managed memory. Aggregate data is represented using a value type, which can be instantiated in two different ways:

  • Boxed: as an Object, carrying full type information at runtime, and typically allocated on the heap by the CLI memory manager.

  • Unboxed: as a "value type instance" that does not carry type information at runtime and that is never allocated directly on the heap. It can be part of a larger structure on the heap a field of a class, a field of a boxed value type, or an element of an array. Or it can be in the local variables or incoming arguments array (see Partition I, section 12.3.2). Or it can be allocated as a static variable or static member of a class or a static member of another value type.

Because value type instances, specified as method arguments, are copied on method call, they do not have "identity" in the sense that Objects (boxed instances of classes) have.

12.1.6.1 Homes for Values

The home of a data value is where it is stored for possible reuse. The CLI directly supports the following home locations:

  • An incoming argument

  • A local variable of a method

  • An instance field of an object or value type

  • A static field of a class, interface, or module

  • An array element

For each home location, there is a means to compute (at runtime) the address of the home location and a means to determine (at JIT compile time) the type of a home location. These are summarized in Table 2-8, Address and Type of Home Locations.

In addition to homes, built-in values can exist in two additional ways (i.e., without homes):

  1. As constant values (typically embedded in the CIL instruction stream using ldc.* instructions)

  2. As an intermediate value on the evaluation stack, when returned by a method or CIL instruction

Table 2-8. Address and Type of Home Locations

Type of Home

Runtime Address Computation

JITtime Type Determination

Argument

ldarga for by-value arguments or ldarg for by-reference arguments

Method signature

Local Variable

ldloca for by-value locals or ldloc for by-reference locals

Locals signature in method header

Field

ldflda

Type of field in the class, interface, or module

Static

ldsflda

Type of field in the class, interface, or module

Array Element

ldelema for single-dimensional zero-based arrays or call the instance method Address

Element type of array

12.1.6.2 Operations on Value Type Instances

Value type instances can be created (see Partition I, section 12.1.6.2.1), passed as arguments (see Partition I, section 12.1.6.2.3), returned as values (see Partition I, section 12.1.6.2.3), and stored into and extracted from locals, fields, and elements of arrays (i.e., copied). Like classes, value types may have both static and non-static members (methods and fields). But, because they carry no type information at runtime, value type instances are not substitutable for items of type Object; in this respect, they act like the built-in types int, long, and so forth. There are two operations, box and unbox (see Partition I, sections 8.2.4 and 12.1.6.2.5), that convert between value type instances and Objects.

12.1.6.2.1 Initializing Instances of Value Types

There are three options for initializing the home of a value type instance. You can zero it by loading the address of the home (see Table 2-8, Address and Type of Home Locations) and using the initobj instruction (for local variables this is also accomplished by setting the zero initialize bit in the method's header). You can call a user-defined constructor by loading the address of the home (see Table 2-8, Address and Type of Home Locations) and then calling the constructor directly. Or you can copy an existing instance into the home, as described in Partition I, section 12.1.6.2.

ANNOTATION

The "zero init flag" syntax is the .local init directive in the assembler syntax, and the flag CorILMethod_InitLocals in the file format (see Partition II, section 24.4.4).


12.1.6.2.2 Loading and Storing Instances of Value Types

There are two ways to load a value type onto the evaluation stack:

  • Directly load the value from a home that has the appropriate type, using an ldarg, ldloc, ldfld, or ldsfld instruction.

  • Compute the address of the value type, then use an ldobj instruction.

Similarly, there are two ways to store a value type from the evaluation stack:

  • Directly store the value into a home of the appropriate type, using a starg, stloc, stfld, or stsfld instruction.

  • Compute the address of the value type, then use a stobj instruction.

12.1.6.2.3 Passing and Returning Value Types

Value types are treated just as any other value would be treated:

  • To pass a value type by value, simply load it onto the stack as you would any other argument: use ldloc, ldarg, etc., or call a method that returns a value type. To access a value type parameter that has been passed by value, use the ldarga instruction to compute its address or the ldarg instruction to load the value onto the evaluation stack.

  • To pass a value type by reference, load the address of the value type as you normally would (see Table 2-8, Address and Type of Home Locations). To access a value type parameter that has been passed by reference, use the ldarg instruction to load the address of the value type and then the ldobj instruction to load the value type onto the evaluation stack.

  • To return a value type, just load the value onto an otherwise empty evaluation stack and then issue a ret instruction.

12.1.6.2.4 Calling Methods

Static methods on value types are handled no differently from static methods on an ordinary class: use a call instruction with a metadata token specifying the value type as the class of the method. Non-static methods (i.e., instance and virtual methods) are supported on value types, but they are given special treatment. A non-static method on a class (rather than a value type) expects a this pointer that is an instance of that class. This makes sense for classes, since they have identity and the this pointer represents that identity. Value types, however, have identity only when boxed. To address this issue, the this pointer on a non-static method of a value type is a by-ref parameter of the value type rather than an ordinary by-value parameter.

A non-static method on a value type may be called in the following ways:

  • Given an unboxed instance of a value type, the compiler will know the exact type of the object statically. The call instruction can be used to invoke the function, passing as the first parameter (the this pointer) the address of the instance. The metadata token used with the call instruction shall specify the value type itself as the class of the method.

  • Given a boxed instance of a value type, there are three cases to consider:

    • Instance or virtual methods introduced on the value type itself: unbox the instance and call the method directly using the value type as the class of the method.

    • Virtual methods inherited from a parent class: use the callvirt instruction and specify the method on the System.Object, System.ValueType, or System.Enum class as appropriate.

    • Virtual methods on interfaces implemented by the value type: use the callvirt instruction and specify the method on the interface type.

12.1.6.2.5 Boxing and Unboxing

Box and unbox are conceptually equivalent to (and may be seen in higher-level languages as) casting between a value type instance and System.Object. Because they change data representations, however, boxing and unboxing are like the widening and narrowing of various sizes of integers (the conv and conv.ovf instructions) rather than the casting of reference types (the isinst and castclass instructions). The box instruction is a widening (always typesafe) operation that converts a value type instance to System.Object by making a copy of the instance and embedding it in a newly allocated object. Unbox is a narrowing (runtime exception may be generated) operation that converts a System.Object (whose runtime type is a value type) to a value type instance. This is done by computing the address of the embedded value type instance without making a copy of the instance.

12.1.6.2.6 Castclass and Isinst on Value Types

Casting to and from value type instances isn't permitted (the equivalent operations are box and unbox). When boxed, however, it is possible to use the isinst instruction to see whether a value of type System.Object is the boxed representation of a particular class.

12.1.6.3 Opaque Classes

Some languages provide multi-byte data structures whose contents are manipulated directly by address arithmetic and indirection operations. To support this feature, the CLI allows value types to be created with a specified size but no information about their data members. Instances of these "opaque classes" are handled in precisely the same way as instances of any other class, but the ldfld, stfld, ldflda, ldsfld, and stsfld instructions shall not be used to access their contents.

ANNOTATION

Opaque classes were included to support languages like C, C++, and COBOL, which have multi-byte data types, where the individual fields are not intended to be seen by other languages. So you may pass a lump of data, or the address of a lump of data, but the actual content is intended only for the language that defined it.


End informative text


12.2 Module Information

Partition II[, section 24 and its subsections] provides details of the CLI PE file format. The CLI relies on the following information about each method defined in a PE file:

  • The instructions composing the method body, including all exception handlers.

  • The signature of the method, which specifies the return type and the number, order, parameter passing convention, and built-in data type of each of the arguments. It also specifies the native calling convention (this does not affect the CIL virtual calling convention, just the native code).

  • The exception handling array. This array holds information delineating the ranges over which exceptions are filtered and caught. See Partition II and Partition I, section 12.4.2.

  • The size of the evaluation stack that the method will require.

  • The size of the locals array that the method will require.

    ANNOTATION

    The size of the evaluation stack is not the number of machine words, or bytes needed, but the number of pushes of objects or data on the stack. The size of the locals array is not in bytes, but the number of locals.

    The JIT compiler is free to discover that not all the locals are actually needed, and not to allocate space for them, or to overlap that space with the evaluation stack where appropriate. The actual size at runtime bears no relation to these numbers, which are for JIT compilers or interpreters, to allow them to size their data structures.


  • A "zero init flag" that indicates whether the local variables and memory pool should be initialized by the CLI (see also localloc).

  • Type of each local variable in the form of a signature of the local variable array (called the "locals signature").

In addition, the file format is capable of indicating the degree of portability of the file. There is one kind of restriction that may be described:

  • Restriction to a specific (32-bit) native size for integers.

By stating which restrictions are placed on executing the code, the CLI class loader can prevent non-portable code from running on an architecture that it cannot support.

ANNOTATION

The "zero init flag" syntax is the .local init directive in the assembler syntax, and the flag CorILMethod_InitLocals in the file format (see Partition II, section 24.4.4).


12.3 Machine State

One of the design goals of the CLI is to hide the details of a method call frame from the CIL code generator. This allows the VES (and not the CIL code generator) to choose the most efficient calling convention and stack layout. To achieve this abstraction, the call frame is integrated into the CLI. The machine state definitions below reflect these design choices, where machine state consists primarily of global state and method state.

12.3.1 The Global State

The CLI manages multiple concurrent threads of control (not necessarily the same as the threads provided by a host operating system), multiple managed heaps, and a shared memory address space.

NOTE

A thread of control can be thought of, somewhat simplistically, as a singly linked list of method states, where a new state is created and linked back to the current state by a method call instruction the traditional model of a stack-based calling sequence. Notice that this model of the thread of control doesn't correctly explain the operation of tail., jmp, or throw instructions.


Figure 2-4, Machine State Model, illustrates the machine state model, which includes threads of control, method states, and multiple heaps in a shared address space. Method state, shown separately in Figure 2-5, Method State, is an abstraction of the stack frame. Arguments and local variables are part of the method state, but they can contain Object References that refer to data stored in any of the managed heaps. In general, arguments and local variables are only visible to the executing thread, while instance and static fields and array elements may be visible to multiple threads, and modification of such values is considered a side-effect.

Figure 2-4. Machine State Model

graphics/02fig04.gif

Figure 2-5. Method State

graphics/02fig05.gif

12.3.2 Method State

Method state describes the environment within which a method executes. (In conventional compiler terminology, it corresponds to a superset of the information captured in the "invocation stack frame.") The CLI method state consists of the following items:

  • An instruction pointer (IP). This points to the next CIL instruction to be executed by the CLI in the present method.

  • An evaluation stack. The stack is empty upon method entry. Its contents are entirely local to the method and are preserved across call instructions (that's to say, if this method calls another, once that other method returns, our evaluation stack contents are "still there.") The evaluation stack is not addressable. At all times it is possible to deduce which one of a reduced set of types is stored in any stack location at a specific point in the CIL instruction stream (see Partition I, section 12.3.2.1).

  • A local variable array (starting at index 0). Values of local variables are preserved across calls (in the same sense as for the evaluation stack). A local variable may hold any data type. However, a particular slot shall be used in a type-consistent way (where the type system is the one described in Partition I, section 12.3.2.1). Local variables are initialized to 0 before entry if the initialize flag for the method is set (see Partition II, section 12.2). The address of an individual local variable may be taken using the ldloca instruction.

    ANNOTATION

    The ilasm syntax for initializing local variables to 0 is the .local init directive (see Partition II, section 12.2). In the file format it is the flag CorILMethod_InitLocals, described in Partition II, section 24.4.4.


  • An argument array. The values of the current method's incoming arguments (starting at index 0). These can be read and written by logical index. The address of an argument can be taken using the ldarga instruction. The address of an argument is also implicitly taken by the arglist instruction for use in conjunction with typesafe iteration through variable-length argument lists.

  • A methodInfo handle. This contains read-only information about the method. In particular it holds the signature of the method, the types of its local variables, and data about its exception handlers.

  • A local memory pool. The CLI includes instructions for dynamic allocation of objects from the local memory pool (localloc). Memory allocated in the local memory pool is addressable. The memory allocated in the local memory pool is reclaimed upon method context termination.

  • A return state handle. This handle is used to restore the method state on return from the current method. Typically, this would be the state of the method's caller. This corresponds to what in conventional compiler terminology would be the dynamic link.

  • A security descriptor. This descriptor is not directly accessible to managed code but is used by the CLI security system to record security overrides (assert, permit-only, and deny).

The four areas of the method state incoming arguments array, local variables array, local memory pool, and evaluation stack are specified as if logically distinct areas. A conforming implementation of the CLI may map these areas into one contiguous array of memory, held as a conventional stack frame on the underlying target architecture, or use any other equivalent representation technique.

12.3.2.1 The Evaluation Stack

Associated with each method state is an evaluation stack. Most CLI instructions retrieve their arguments from the evaluation stack and place their return values on the stack. Arguments to other methods and their return values are also placed on the evaluation stack. When a procedure call is made, the arguments to the called methods become the incoming arguments array (see Partition I, section 12.3.2.2) to the method. This may require a memory copy, or simply a sharing of these two areas by the two methods.

The evaluation stack is made up of slots that can hold any data type, including an unboxed instance of a value type. The type state of the stack (the stack depth and types of each element on the stack) at any given point in a program shall be identical for all possible control flow paths. For example, a program that loops an unknown number of times and pushes a new element on the stack at each iteration would be prohibited.

While the CLI, in general, supports the full set of types described in Partition I, section 12.1, the CLI treats the evaluation stack in a special way. While some JIT compilers may track the types on the stack in more detail, the CLI only requires that values be one of:

  • int64, an 8-byte signed integer

  • int32, a 4-byte signed integer

  • native int, a signed integer of either 4 or 8 bytes, whichever is more convenient for the target architecture

  • F, a floating point value (float32, float64, or other representation supported by the underlying hardware)

  • &, a managed pointer

  • O, an object reference

  • *, a "transient pointer," which may be used only within the body of a single method, that points to a value known to be in unmanaged memory (see Partition III for more details. * types are generated internally within the CLI; they are not created by the user).

ANNOTATION

Transient pointers affect only those writing for CIL or an assembly language. They are not visible in higher-level languages. A transient pointer is a legitimate managed pointer to a managed data type, but the data is known to be stored in a place where it cannot be moved by the garbage collector. This means that transient pointers can be passed to unmanaged code, which needs absolute memory addresses that won't move.


  • A user-defined value type

The other types are synthesized through a combination of techniques:

  • Shorter integer types in other memory locations are zero-extended or sign-extended when loaded onto the evaluation stack; these values are truncated when stored back to their home location.

  • Special instructions perform numeric conversions, with or without overflow detection, between different sizes and between signed and unsigned integers.

  • Special instructions treat an integer on the stack as though it were unsigned.

  • Instructions that create pointers which are guaranteed not to point into the memory manager's heaps (e.g., ldloca, ldarga, and ldsflda) produce transient pointers (type *) that may be used wherever a managed pointer (type &) or unmanaged pointer (type native unsigned int) is expected.

  • When a method is called, an unmanaged pointer (type native unsigned int or *) is permitted to match a parameter that requires a managed pointer (type &). The reverse, however, is not permitted, since it would allow a managed pointer to be "lost" by the memory manager.

  • A managed pointer (type &) may be explicitly converted to an unmanaged pointer (type native unsigned int), although this is not verifiable and may produce a runtime exception.

ANNOTATION

Managed pointers can be converted to unmanaged pointers. If the managed pointer points into the stack or unmanaged memory, it is safe to turn it into an unmanaged pointer. But if it is pointing into the garbage collector heap, it is not legal and will produce a runtime exception.


12.3.2.2 Local Variables and Arguments

Part of each method state is an array that holds local variables and an array that holds arguments. Like the evaluation stack, each element of these arrays can hold any single data type or an instance of a value type. Both arrays start at 0 (that is, the first argument or local variable is numbered 0). The address of a local variable can be computed using the ldloca instruction, and the address of an argument using the ldarga instruction.

Associated with each method is metadata that specifies:

  • Whether the local variables and memory pool memory will be initialized when the method is entered

    ANNOTATION

    The ilasm syntax for initializing local variables to 0 is the .local init directive (see Partition II, section 12.2). In the file format it is the flag CorILMethod_InitLocals, described in Partition II, section 24.4.4.


  • The type of each argument and the length of the argument array (but see below for variable[-length] argument lists)

  • The type of each local variable and the length of the local variable array

The VES inserts padding as appropriate for the target architecture. That is, on some 64-bit architectures all local variables may be 64-bit aligned, while on others they may be 8-, 16-, or 32-bit aligned. The CIL generator shall make no assumptions about the offsets of local variables within the array. In fact, the VES is free to reorder the elements in the local variable array, and different JITters may choose to order them in different ways.

12.3.2.3 Variable[-Length] Argument Lists

The CLI works in conjunction with the class library to implement methods that accept argument lists of unknown length and type ("varargs methods"). Access to these arguments is through a typesafe iterator in the [Base] Class Library, called System.ArgIterator (see the .NET Framework Standard Library Annotated Reference).

The CIL includes one instruction provided specifically to support the argument iterator, arglist. This instruction may be used only within a method that is declared to take a variable number of arguments. It returns a value that is needed by the constructor for a System.ArgIterator object. Basically, the value created by arglist provides access both to the address of the argument list that was passed to the method and a runtime data structure that specifies the number and type of the arguments that were provided. This is sufficient for the class library to implement the user-visible iteration mechanism.

From the CLI point of view, varargs methods have an array of arguments like other methods. But only the initial portion of the array has a fixed set of types, and only these may be accessed directly using the ldarg, starg, and ldarga instructions. The argument iterator allows access to both this initial segment and the remaining entries in the array.

ANNOTATION

Support for variable-length argument lists is not part of the minimum requirement for implementing the CLI, so a given implementation may not support it. The library support for it, System.ArgIterator, is not standardized.


12.3.2.4 Local Memory Pool

Part of each method state is a local memory pool. Memory can be explicitly allocated from the local memory pool using the localloc instruction. All memory in the local memory pool is reclaimed on method exit, and that is the only way local memory pool memory is reclaimed (there is no instruction provided to free local memory that was allocated during this method invocation). The local memory pool is used to allocate objects whose type or size is not known at compile time and which the programmer does not wish to allocate in the managed heap.

Because the local memory pool cannot be shrunk during the lifetime of the method, a language implementation cannot use the local memory pool for general-purpose memory allocation.

ANNOTATION

Support for the local memory pool is not part of the minimum requirement for implementing the CLI. Therefore, the localloc instruction may not be available in your implementation.


12.4 Control Flow

The CIL instruction set provides a rich set of instructions to alter the normal flow of control from one CIL instruction to the next.

  • Conditional and Unconditional Branch instructions for use within a method, provided the transfer doesn't cross a protected region boundary (see Partition I, section 12.4.2).

  • Method call instructions to compute new arguments, [and to] transfer them and control to a known or computed destination method (see Partition I, section 12.4.1).

  • Tail call prefix to indicate that a method should relinquish its stack frame before executing a method call (see Partition I, section 12.4.1).

  • Return from a method, returning a value if necessary.

  • Method jump instructions to transfer the current method's arguments to a known or computed destination method (see Partition I, section 12.4.1).

  • Exception-related instructions (see Partition I, section 12.4.2). These include instructions to initiate an exception, transfer control out of a protected region, and end a filter, catch clause, or finally clause.

While the CLI supports control transfers within a method, there are several restrictions that shall be observed:

  1. Control transfer is never permitted to enter a catch handler or finally clause (see Partition I, section 12.4.2) except through the exception handling mechanism.

  2. Control transfer out of a protected region (see Partition I, section 12.4.2) is only permitted through an exception instruction (leave, endfilter, endcatch, or endfinally).

  3. The evaluation stack shall be empty after the return value is popped by a ret instruction.

  4. Each slot on the stack shall have the same data type at any given point within the method body, regardless of the control flow that allows execution to arrive there.

  5. In order for the JIT compilers to efficiently track the data types stored on the stack, the stack shall normally be empty at the instruction following an unconditional control transfer instruction (br, br.s, ret, jmp, throw, endfilter, endcatch, or endfinally). The stack may be non-empty at such an instruction only if at some earlier location within the method there has been a forward branch to that instruction.

  6. Control is not permitted to simply "fall through" the end of a method. All paths shall terminate with one of these instructions: ret, throw, jmp, or (tail. followed by call, calli, or callvirt).

ANNOTATION

Points 1 and 2 in the preceding list are described more fully in Partition I, section 12.4.2. Points 3 and 4 are common restrictions on virtual machines, although they are unusual in hardware. Point 5, concerning method jump instructions, is fairly tricky from the point of view of a compiler, and too easily overlooked. It says that there are certain rotations of loops that you cannot perform. After an unconditional jump, the VES assumes that the evaluation stack is empty unless there has been a forward branch to that location. For generating portable code this is important because not all JITs enforce this constraint. Although it is possible to write a compiler that does not follow this rule, such a compiler would run on only some implementations of the VES.


ANNOTATION

Support for exception filters is not part of the minimum requirement for implementing the CLI. Therefore, the endfilter instruction may not be available in your implementation.


12.4.1 Method Calls

Instructions emitted by the CIL code generator contain sufficient information for different implementations of the CLI to use different native calling conventions. All method calls initialize the method state areas (see Partition I, section 12.3.2) as follows:

  1. The incoming arguments array is set by the caller to the desired values.

  2. The local variables array always has null for Object types and for fields within value types that hold objects. In addition, if the "zero init flag" is set in the method header, then the local variables array is initialized to 0 for all integer types and 0.0 for all floating point types. Value Types are not initialized by the CLI, but verified code will supply a call to an initializer as part of the method's entry point code.

ANNOTATION

The last sentence of number 2 above is not accurate. To be verifiable, the zero init flag must be set. The ilasm syntax for setting this is the .local init directive (see Partition II, section 12.2). In the file format it is the flag CorILMethod_InitLocals, described in Partition II, section 24.4.4.


  • 3. The evaluation stack is empty.

12.4.1.1 Call Site Descriptors

Call sites specify additional information that enables an interpreter or JIT compiler to synthesize any native calling convention. All CIL calling instructions (call, calli, and callvirt) include a description of the call site. This description can take one of two forms. The simpler form, used with the calli instruction, is a "call site description" (represented as a metadata token for a stand-alone call signature) that provides:

  • The number of arguments being passed

  • The data type of each argument

  • The order in which they have been placed on the call stack

  • The native calling convention to be used

The more complicated form, used for the call and callvirt instructions, is a "method reference" (a metadata methodref token) that augments the call site description with an identifier for the target of the call instruction.

ANNOTATION

Many see the call instruction as simpler than the calli instruction, although the reverse is true from the point of view of the call site descriptor. The calli instruction actually provides less information, which is what is meant by simpler here. In the calli instruction, the destination address is computed at runtime, so that information is not part of the call. It carries all of the other information the number of arguments, the type of each argument, the order of arguments, the calling convention. With call and callvirt you also know the destination of the call.


12.4.1.2 Calling Instructions

The CIL has three call instructions that are used to transfer new argument values to a destination method. Under normal circumstances, the called method will terminate and return control to the calling method.

  • call is designed to be used when the destination address is fixed at the time the CIL is generated. In this case, a method reference is placed directly in the instruction. This is comparable to a direct call to a static function in C. It may be used to call static or instance methods or the (statically known) superclass method within an instance method body.

  • calli is designed for use when the destination address is calculated at runtime. A method pointer is passed on the stack, and the instruction contains only the call site description.

  • callvirt uses the exact type of an object (known only at runtime) to determine the method to be called. The instruction includes a method reference, but the particular method isn't computed until the call actually occurs. This allows an instance of a subclass to be supplied and the method appropriate for that subclass to be invoked. The callvirt instruction is used both for instance methods and methods on interfaces.

In addition, each of these instructions may be immediately preceded by a tail. instruction prefix. This specifies that the calling method terminates with this method call (and returns whatever value is returned by the called method). The tail. prefix instructs the JIT compiler to discard the caller's method state prior to making the call (if the call is from untrusted code to trusted code, the frame cannot be fully discarded for security reasons). When the called method executes a ret instruction, control returns not to the calling method but rather to wherever that method would itself have returned (typically, to the caller's caller). Notice that the tail. instruction shortens the lifetime of the caller's frame so it is unsafe to pass managed pointers (type &) as arguments.

Finally, there is an instruction that indicates an optimization of the tail. case, which is the jmp instruction, followed by a methodref or methoddef token, and indicates that the current method's state should be discarded, its arguments should be transferred intact to the destination method, and control should be transferred to the destination. The signature of the calling method shall exactly match the signature of the destination method.

12.4.1.3 Computed Destinations

The destination of a method call may be either encoded directly in the CIL instruction stream (the call and jmp instructions) or computed (the callvirt and calli instructions). The destination address for a callvirt instruction is automatically computed by the CLI based on the method token and the value of the first argument (the this pointer). The method token shall refer to a virtual method on a class that is a direct ancestor of the class of the first argument. The CLI computes the correct destination by locating the nearest ancestor of the first argument's class that supplies an implementation of the desired method.

NOTE

The implementation can be assumed to be more efficient than the linear search implied here.


For the calli instruction the CIL code is responsible for computing a destination address and pushing it on the stack. This is typically done through the use of a ldftn or ldvirtfn instruction at some earlier time. The ldftn instruction includes a metadata token in the CIL stream that specifies a method, and the instruction pushes the address of that method. The ldvirtfn instruction takes a metadata token for a virtual method in the CIL stream and an object on the stack. It performs the same computation described above for the callvirt instruction but pushes the resulting destination on the stack rather than calling the method.

The calli instruction includes a call site description that includes information about the native calling convention that should be used to invoke the method. Correct CIL code shall specify a calling convention specified in the calli instruction that matches the calling convention for the method that is being called.

12.4.1.4 Virtual Calling Convention

The CIL provides a "virtual calling convention" that is converted by the JIT into a native calling convention. The JIT determines the optimal native calling convention for the target architecture. This allows the native calling convention to differ from machine to machine, including details of register usage, local variable homes, copying conventions for large call-by-value objects (as well as deciding, based on the target machine, what is considered "large"). This also allows the JIT to reorder the values placed on the CIL virtual stack to match the location and order of arguments passed in the native calling convention.

The CLI uses a single uniform calling convention for all method calls. It is the responsibility of the JITters to convert this into the appropriate native calling convention. The contents of the stack at the time of a call instruction (call, calli, or callvirt any of which may be preceded by tail.) are as follows:

  1. If the method being called is an instance method (class or interface) or a virtual method, the this pointer is the first object on the stack at the time of the call instruction. For methods on Objects (including boxed value types), the this pointer is of type O (object reference). For methods on value types, the this pointer is provided as a by-ref parameter; that is, the value is a pointer (managed, &, or unmanaged, *, or native int) to the instance.

  2. The remaining arguments appear on the stack in left-to-right order (that is, the lexically leftmost argument is the first push on the stack, immediately following the this pointer, if any). Partition I, section 12.4.1.5 describes how each of the three parameter passing conventions (by-value, by-reference, and typed reference) should be implemented.

ANNOTATION

The main thrust of this section is that the calling convention of CIL has nothing to do with the calling convention of the underlying native system.

Although JIT (just-in-time) compilation is not required by this standard, it is commonly used, and it relates directly to this discussion of calling convention. Most implementations do use it, however. The assumption is that source language compilers produce CIL (the Common Intermediate Language) only. If the implementation does not have a JIT compiler, it may have an interpreter.


12.4.1.5 Parameter Passing

The CLI supports three kinds of parameter passing, all indicated in metadata as part of the signature of the method. Each parameter to a method has its own passing convention (e.g., the first parameter may be passed by value while all others are passed by ref). Parameters shall be passed in one of the following ways (see detailed descriptions below):

  • By-value parameters, where the value of an object is passed from the caller to the callee.

  • By-ref parameters, where the address of the data is passed from the caller to the callee, and the type of the parameter is therefore a managed or unmanaged pointer.

  • Typed reference parameters, where a runtime representation of the data type is passed along with the address of the data, and the type of the parameter is therefore one specially supplied for this purpose.

It is the responsibility of the CIL generator to follow these conventions. Verification checks that the types of parameters match the types of values passed, but is otherwise unaware of the details of the calling convention.

12.4.1.5.1 By-Value Parameters

For built-in types (integers, floats, etc.) the caller copies the value onto the stack before the call. For objects, the object reference (type O) is pushed on the stack. For managed pointers (type &) or unmanaged pointers (type native unsigned int), the address is passed from the caller to the callee. For value types, see the protocol in Partition I, section 12.1.6.2.

12.4.1.5.2 By-Ref Parameters

By-ref parameters are the equivalent of C++ reference parameters or Pascal var parameters: instead of passing as an argument the value of a variable, field, or array element, its address is passed instead; and any assignment to the corresponding parameter actually modifies the corresponding caller's variable, field, or array element. Much of this work is done by the higher-level language, which hides from the user the need to compute addresses to pass a value and the use of indirection to reference or update values.

Passing a value by reference requires that the value have a home (see Partition I, section 12.1.6.1), and it is the address of this home that is passed. Constants, and intermediate values on the evaluation stack, cannot be passed as by-ref parameters because they have no home.

The CLI provides instructions to support by-ref parameters:

  • Calculate addresses of home locations (see Table 2-8, Address and Type of Home Locations).

  • Load and store built-in data types through these address pointers (ldind.*, stind.*, ldfld, etc.).

  • Ccopy value types (ldobj and cpobj).

Some addresses (e.g., local variables and arguments) have lifetimes tied to that method invocation. These shall not be referenced outside their lifetimes, and so they should not be stored in locations that last beyond their lifetime. The CIL does not (and cannot) enforce this restriction, so the CIL generator shall enforce this restriction or the resulting CIL will not work correctly. For code to be verifiable (see Partition I, section 8.8), by-ref parameters may only be passed to other methods or referenced via the appropriate stind or ldind instructions.

12.4.1.5.3 Typed Reference Parameters

By-ref parameters and value types are sufficient to support statically typed languages (C++, Pascal, etc.). They also support dynamically typed languages that pay a performance penalty to box value types before passing them to polymorphic methods (Lisp, Scheme, Smalltalk, etc.). Unfortunately, they are not sufficient to support languages like Visual Basic that require by-reference passing of unboxed data to methods that are not statically restricted as to the type of data they accept. These languages require a way of passing both the address of the home of the data and the static type of the home. This is exactly the information that would be provided if the data were boxed, but without the heap allocation required of a box operation.

Typed reference parameters address this requirement. A typed reference parameter is very similar to a standard by-ref parameter, but the static data type is passed as well as the address of the data. Like by-ref parameters, the argument corresponding to a typed reference parameter will have a home.

NOTE

If it were not for the fact that verification and the memory manager need to be aware of the data type and the corresponding address, a by-ref parameter could be implemented as a standard value type with two fields: the address of the data and the type of the data.


Like a regular by-ref parameter, a typed reference parameter can refer to a home that is on the stack, and that home will have a lifetime limited by the call stack. Thus, the CIL generator shall apply appropriate checks on the lifetime of by-ref parameters; and verification imposes the same restrictions on the use of typed reference parameters as it does on by-ref parameters (see Partition I, section 12.4.1.5.2).

A typed reference is passed by either creating a new typed reference (using the mkrefany instruction) or by copying an existing typed reference. Given a typed reference argument, the address to which it refers can be extracted using the refanyval instruction; the type to which it refers can be extracted using the refanytype instruction.

ANNOTATION

The typed reference is not required in all CLI implementations, so a given implementation may not support it. It resides in the Vararg Library, which is excluded from the Kernel Profile.


12.4.1.5.4 Parameter Interactions

A given parameter may be passed using any one of the parameter passing conventions: by-value, by-ref, or typed reference. No combination of these is allowed for a single parameter, although a method may have different parameters with different calling mechanisms.

A parameter that has been passed in as typed reference shall not be passed on as by-ref or by-value without a runtime type check and (in the case of by-value) a copy.

A by-ref parameter may be passed on as a typed reference by attaching the static type. Table 2-9, Parameter Passing Conventions, illustrates the parameter passing convention used for each data type.

Table 2-9. Parameter Passing Conventions

Type of Data

Pass By

How Data Is Sent

Built-in value type (int, float, etc.)

Value

Copied to called method, type statically known at both sides

 

Reference

Address sent to called method, type statically known at both sides

 

Typed reference

Address sent along with type information to called method

User-defined value type

Value

Called method receives a copy; type statically known at both sides

 

Reference

Address sent to called method, type statically known at both sides

 

Typed reference

Address sent along with type information to called method

Object

Value

Reference to data sent to called method, type statically known and class available from reference

 

Reference

Address of reference sent to called method, type statically known and class available from reference

 

Typed reference

Address of reference sent to called method along with static type information, class (i.e., dynamic type) available from reference

12.4.2 Exception Handling

Exception handling is supported in the CLI through exception objects and protected blocks of code. When an exception occurs, an object is created to represent the exception. All exception objects are instances of some class (i.e., they can be boxed value types, but not pointers, unboxed value types, etc.). Users may create their own exception classes, typically by subclassing System.Exception (see the .NET Framework Standard Library Annotated Reference).

There are four kinds of handlers for protected blocks. A single protected block shall have exactly one handler associated with it:

  • A finally handler that shall be executed whenever the block exits, regardless of whether that occurs by normal control flow or by an unhandled exception.

  • A fault handler that shall be executed if an exception occurs, but not on completion of normal control flow.

  • A type-filtered handler that handles any exception of a specified class or any of its sub-classes.

  • A user-filtered handler that runs a user-specified set of CIL instructions to determine whether the exception should be ignored (i.e., execution should resume), handled by the associated handler, or passed on to the next protected block.

Protected regions, the type of the associated handler, and the location of the associated handler and (if needed) user-supplied filter code are described through an Exception Handler Table associated with each method. The exact format of the Exception Handler Table is specified in detail in Partition II. Details of the exception handling mechanism are also specified in Partition II, section 18.

ANNOTATION

Filtered exception handling is not required in the Kernel Profile, so a given implementation of the VES may not support it and its associated instructions.


12.4.2.1 Exceptions Thrown by the CLI

CLI instructions can throw the following exceptions as part of executing individual instructions. The documentation for each instruction lists all the exceptions the instruction can throw (except for the general-purpose ExecutionEngineException described below that may be generated by all instructions).

Base Instructions (see Partition III[, section 3])

  • ArithmeticException

  • DivideByZeroException

  • ExecutionEngineException

  • InvalidAddressException

  • OverflowException

  • SecurityException

  • StackOverflowException

Object Model Instructions (see Partition III[, section 4])

  • TypeLoadException

  • IndexOutOfRangeException

  • InvalidAddressException

  • InvalidCastException

  • MissingFieldException

  • MissingMethodException

  • NullReferenceException

  • OutOfMemoryException

  • SecurityException

  • StackOverflowException

The ExecutionEngineException is special. It can be thrown by any instruction and indicates an unexpected inconsistency in the CLI. Running exclusively verified code can never cause this exception to be thrown by a conforming implementation of the CLI. However, unverified code (even though that code is conforming CIL) can cause this exception to be thrown if it corrupts memory. Any attempt to execute non-conforming CIL or non-conforming file formats can cause completely unspecified behavior: a conforming implementation of the CLI need not make any provision for these cases.

There are no exceptions for things like "MetaDataTokenNotFound." CIL verification (see Partition V) will detect this inconsistency before the instruction is executed, leading to a verification violation. If the CIL is not verified, this type of inconsistency shall raise the generic ExecutionEngineException.

Exceptions can also be thrown by the CLI, as well as by user code, using the throw instruction. The handing of an exception is identical, regardless of the source.

12.4.2.2 Subclassing of Exceptions

Certain types of exceptions thrown by the CLI may be subclassed to provide more information to the user. The specification of CIL instructions in Partition III describes what types of exceptions should be thrown by the runtime environment when an abnormal situation occurs. Each of these descriptions allows a conforming implementation to throw an object of the type described or an object of a subclass of that type.

NOTE

For instance, the specification of the ckfinite instruction requires that an exception of type ArithmeticException or a subclass of ArithmeticException be thrown by the CLI. A conforming implementation may simply throw an exception of type ArithmeticException, but it may also choose to provide more information to the programmer by throwing an exception of type NotFiniteNumberException with the offending number.


12.4.2.3 Resolution Exceptions

CIL allows types to reference, among other things, interfaces, classes, methods, and fields. Resolution errors occur when references are not found or are mismatched. Resolution exceptions can be generated by references from CIL instructions, references to base classes, to implemented interfaces, and by references from signatures of fields, methods, and other class members.

To allow scalability with respect to optimization, detection of resolution exceptions is given latitude such that it may occur as early as install time and as late as execution time.

The latest opportunity to check for resolution exceptions from all references except CIL instructions is as part of initialization of the type that is doing the referencing (see Partition II). If such a resolution exception is detected, the static initializer for that type, if present, shall not be executed.

The latest opportunity to check for resolution exceptions in CIL instructions is as part of the first execution of the associated CIL instruction. When an implementation chooses to perform resolution exception checking in CIL instructions as late as possible, these exceptions, if they occur, shall be thrown prior to any other non-resolution exception that the VES may throw for that CIL instruction. Once a CIL instruction has passed the point of throwing resolution errors (it has completed without exception, or has completed by throwing a non-resolution exception), subsequent executions of that instruction shall no longer throw resolution exceptions.

If an implementation chooses to detect some resolution errors, from any references, earlier than the latest opportunity for that kind of reference, it is not required to detect all resolution exceptions early.

An implementation that detects resolution errors early is allowed to prevent a class from being installed, loaded, or initialized as a result of resolution exceptions detected in the class itself or in the transitive closure of types from following references of any kind.

For example, each of the following represents a permitted scenario. An installation program can throw resolution exceptions (thus failing the installation) as a result of checking CIL instructions for resolution errors in the set of items being installed. An implementation is allowed to fail to load a class as a result of checking CIL instructions in a referenced class for resolution errors. An implementation is permitted to load and initialize a class that has resolution errors in its CIL instructions.

The following exceptions are among those considered resolution exceptions:

  • BadImageFormatException

  • EntryPointNotFoundException

  • MissingFieldException

  • MissingMemberException

  • MissingMethodException

  • NotSupportedException

  • TypeLoadException

  • TypeUnloadedException

For example, when a referenced class cannot be found, a TypeLoadException is thrown. When a referenced method (whose class is found) cannot be found, a MissingMethodException is thrown. If a matching method being used consistently is accessible but violates declared security policy, a SecurityException is thrown.

ANNOTATION

It is possible for a program compiled to IL to reference a type, or members of types, that do not actually exist at runtime. A few situations lead to this problem. One is that in a language that is not statically typed, such as Lisp or Perl, you may not know at runtime whether the type will exist. In cases where the type does not exist at runtime through programmer error, there will be a resolution exception. In a statically typed language, a resolution exception can occur when parts of a program are deployed independently and some parts of the program are not deployed when needed. For example, if a main program is written to assume a certain set of libraries but the program is run without installing the libraries, a resolution exception results.


12.4.2.4 Timing of Exceptions

Certain types of exceptions thrown by CIL instructions may be detected before the instruction is executed. In these cases, the specific time of the throw is not precisely defined, but the exception should be thrown no later than the instruction is executed. That relaxation of the timing of exceptions is provided so that an implementation may choose to detect and throw an exception before any code is run e.g., at the time of CIL-to-native-code conversion.

There is a distinction between the time of detecting the error condition and throwing the associated exception. An error condition may be detected early (e.g., at JIT time), but the condition may be signaled later (e.g., at the execution time of the offending instruction) by throwing an exception.

The following exceptions are among those that may be thrown early by the runtime:

  • MissingFieldException

  • MissingMethodException

  • SecurityException

  • TypeLoadException

12.4.2.5 Overview of Exception Handling

See the Exception Handling specification in Partition II, section 18 for details.

Each method in an executable has associated with it a (possibly empty) array of exception handling information. Each entry in the array describes a protected block, its filter, and its handler (which may be a catch handler, a filter handler, a finally handler, or a fault handler). When an exception occurs, the CLI searches the array for the first protected block

  • That protects a region including the current instruction pointer and

  • That is a catch handler block and

  • Whose filter wishes to handle the exception

If a match is not found in the current method, the calling method is searched, and so on. If no match is found, the CLI will dump a stack trace and abort the program.

NOTE

A debugger can intervene and treat this situation like a breakpoint, before performing any stack unwinding, so that the stack is still available for inspection through the debugger.


If a match is found, the CLI walks the stack back to the point just located, but this time calling the finally and fault handlers. It then starts the corresponding exception handler. Stack frames are discarded either as this second walk occurs or after the handler completes, depending on information in the exception handler array entry associated with the handling block.

Some things to notice are:

  • The ordering of the exception clauses in the Exception Handler Table is important. If handlers are nested, the most deeply nested try blocks shall come before the try blocks that enclose them.

  • Exception handlers may access the local variables and the local memory pool of the routine that catches the exception, but any intermediate results on the evaluation stack at the time the exception was thrown are lost.

  • An exception object describing the exception is automatically created by the CLI and pushed onto the evaluation stack as the first item upon entry of a filter or catch clause.

  • Execution cannot be resumed at the location of the exception, except with a user-filtered handler.

12.4.2.6 CIL Support for Exceptions

The CIL has special instructions to:

  • Throw and rethrow a user-defined exception.

  • Leave a protected block and execute the appropriate finally clauses within a method, without throwing an exception. This is also used to exit a catch clause. Notice that leaving a protected block does not cause the fault clauses to be called.

  • End a user-supplied filter clause (endfilter) and return a value indicating whether to handle the exception.

  • End a finally clause (endfinally) and continue unwinding the stack.

12.4.2.7 Lexical Nesting of Protected Blocks

A protected region (also called a "try block") is described by two addresses: the trystart is the address of the first instruction to be protected, and [the] tryend is the address immediately following the last instruction to be protected. A handler region is described by two addresses: the handlerstart is the address of the first instruction of the handler, and the handlerend is the address immediately following the last instruction of the handler.

There are three kinds of handlers: catch, finally, and fault. A single exception entry consists of

  • Optional: a type token (the type of exception to be handled) or filterstart (the address of the first instruction of the user-supplied filter code)

  • Required: protected region

  • Required: handler region

Every method has associated with it a set of exception entries, called the exception set.

If an exception entry contains a filterstart, then filterstart < handlerstart. The filter region starts at the instruction specified by filterstart and contains all instructions up to (but not including) that specified by handlerstart. If there is no filterstart, then the filter region is empty (hence does not overlap with any region).

No two regions (protected region, handler region, filter region) of a single exception entry may overlap with one another.

For every pair of exception entries in an exception set, one of the following must be true:

  • They nest: all three regions of one entry must be within a single region of the other entry.

  • They are disjoint: all six regions of the two entries are pairwise disjoint (no addresses overlap).

  • They mutually protect: the protected regions are the same, and the other regions are pairwise disjoint.

The encoding of an exception entry in the file format (see Partition II) guarantees that only a catch handler (not a fault handler or finally handler) can have a filter region.

ANNOTATION

Sections 12.4.2.7 and 12.4.2.8 of Partition I have been rewritten more times than any other sections of this standard, because the edge cases of exception handling are incredibly complicated. There continues to be disagreement (on the edge cases) about what is meant by these two sections.

Jim Miller


12.4.2.8 Control Flow Restrictions on Protected Blocks

The following restrictions govern control flow into, out of, and between try blocks and their associated handlers.

  1. CIL code shall not enter a filter, catch, fault, or finally block except through the CLI exception handling mechanism.

  2. There are only two ways to enter a try block from outside its lexical body:

    1. Branching to or falling into the try block's first instruction. The branch may be made using a conditional branch, an unconditional branch, or a leave instruction.

    2. Using a leave instruction from that try's catch block. In this case, correct CIL code may branch to any instruction within the try block, not just its first instruction, so long as that branch target is not protected by yet another try, nested within the first.

  3. Upon entry to a try block, the evaluation stack shall be empty.

  4. The only ways CIL code may leave a try, filter, catch, finally, or fault block are as follows:

    1. throw from any of them.

    2. leave from the body of a try or catch (in this case the destination of the leave shall have an empty evaluation stack and the leave instruction has the side-effect of emptying the evaluation stack).

    3. endfilter may appear only as the lexically last instruction of a filter block, and it shall always be present (even if it is immediately preceded by a throw or other unconditional control flow). If reached, the evaluation stack shall contain an int32 when the endfilter is executed, and the value is used to determine how exception handling should proceed.

    4. endfinally from anywhere within a finally or fault, with the side-effect of emptying the evaluation stack.

    5. rethrow from within a catch block, with the side-effect of emptying the evaluation stack.

  5. When the try block is exited with a leave instruction, the evaluation stack shall be empty.

  6. When a catch or filter clause is exited with a leave instruction, the evaluation stack shall be empty. This involves popping, from the evaluation stack, the exception object that was automatically pushed onto the stack.

  7. CIL code shall not exit any try, filter, catch, finally, or fault block using a ret instruction.

  8. The localloc instruction cannot occur within an exception block: filter, catch, finally, or fault.

ANNOTATION

The edge cases have continued to make it difficult to write these sections precisely. If you have a try block for the catch handler, and inside that catch handler there is another try block and a catch handler, the question is whether one can go from the inner catch to the outer try, and the answer is almost always yes. There are some rules that restrict that. For example, you can't leave from a filter. Those are the kinds of edge cases that have made these sections difficult to write.

Another point that has resulted in confusion in the edge cases is number 5 of the preceding list: "When the try block is exited with a leave instruction, the evaluation stack shall be empty." My understanding is that the intent was to state that the leave instruction causes the evaluation stack to be empty, rather than the alternate reading that you need to empty the evaluation stack.

Jim Miller


12.5 Proxies and Remoting

A remoting boundary exists if it is not possible to share the identity of an object directly across the boundary. For example, if two objects exist on physically separate machines that do not share a common address space, then a remoting boundary will exist between them. There are other administrative mechanisms for creating remoting boundaries.

The VES provides a mechanism, called the application domain, to isolate applications running in the same operating system process from one another. Types loaded into one application domain are distinct from the same type loaded into another application domain, and instances of objects shall not be directly shared from one application domain to another. Hence, the application domain itself forms a remoting boundary.

The VES implements remoting boundaries based on the concept of a proxy. A proxy is an object that exists on one side of the boundary and represents an object on the other side. The proxy forwards references to instance fields and methods to the actual object for interpretation. Proxies do not forward references to static fields or calls to static methods.

The implementation of proxies is provided automatically for instances of types that derive from System.MarshalByRefObject (see the .NET Framework Standard Library Annotated Reference).

12.6 Memory Model and Optimizations

12.6.1 The Memory Store

By "memory store" we mean the regular process memory that the CLI operates within. Conceptually, this store is simply an array of bytes. The index into this array is the address of a data object. The CLI accesses data objects in the memory store via the ldind.* and stind.* instructions.

12.6.2 Alignment

Built-in datatypes shall be properly aligned, which is defined as follows:

  • 1-byte, 2-byte, and 4-byte data is properly aligned when it is stored at a 1-byte, 2-byte, or 4-byte boundary, respectively.

  • 8-byte data is properly aligned when it is stored on the same boundary required by the underlying hardware for atomic access to a native int.

Thus, int16 and unsigned int16 start on even address; int32, unsigned int32, and float32 start on an address divisible by 4; and int64, unsigned int64, and float64 start on an address divisible by 4 or 8, depending on the target architecture. The native size types (native int, native unsigned int, and &) are always naturally aligned (4 bytes or 8 bytes, depending on architecture). When generated externally, these should also be aligned to their natural size, although portable code may use 8-byte alignment to guarantee architecture independence. It is strongly recommended that float64 be aligned on an 8-byte boundary, even when the size of native int is 32 bits.

There is a special prefix instruction, unaligned., that may immediately precede a ldind, stind, initblk, or cpblk instruction. This prefix indicates that the data may have arbitrary alignment; the JIT is required to generate code that correctly performs the effect of the instructions regardless of the actual alignment. Otherwise, if the data is not properly aligned and no unaligned. prefix has been specified, executing the instruction may generate unaligned memory faults or incorrect data.

ANNOTATION

Compiler writers can assume, and VES writers must ensure, that by default, data is aligned. The design point of the CLI is that compilers have to give a heads up to the JIT if data might be unaligned, with the unaligned. prefix. Otherwise, the VES, by default, aligns data.


12.6.3 Byte Ordering

For data types larger than 1 byte, the byte ordering is dependent on the target CPU. Code that depends on byte ordering may not run on all platforms. The PE file format (see Partition I, section 12.2) allows the file to be marked to indicate that it depends on a particular type ordering.

12.6.4 Optimization

Conforming implementations of the CLI are free to execute programs using any technology that guarantees, within a single thread of execution, that side-effects and exceptions generated by a thread are visible in the order specified by the CIL. For this purpose volatile operations (including volatile reads) constitute side-effects. Volatile operations are specified in Partition I, section 12.6.7. There are no ordering guarantees relative to exceptions injected into a thread by another thread (such exceptions are sometimes called "asynchronous exceptions" e.g., System.Threading.ThreadAbortException).

ANNOTATION

The parenthetical comment in the second sentence of this section above "including volatile reads" was intended to be interpreted within the context of the preceding comment "within a single thread of execution." Although that is not a sufficiently strong guarantee to allow I/O drivers to be written using volatile, it restricts a compiler from CIL to native code. It is likely that the next edition of the standard will make this clear.

Jim Miller


RATIONALE

An optimizing compiler is free to reorder side-effects and synchronous exceptions to the extent that this reordering does not change any observable program behavior.


NOTE

An implementation of the CLI is permitted to use an optimizing compiler for example, to convert CIL to native machine code, provided the compiler maintains (within each single thread of execution) the same order of side-effects and synchronous exceptions.

This is a stronger condition than ISO C++ (which permits reordering between a pair of sequence points) or ISO Scheme (which permits reordering of arguments to functions).


12.6.5 Locks and Threads

The logical abstraction of a thread of control is captured by an instance of the System.Threading.Thread object in the class library. Classes beginning with the string "System.Threading" (see the .NET Framework Standard Library Annotated Reference) provide much of the user-visible support for this abstraction.

To create consistency across threads of execution, the CLI provides the following mechanisms:

  1. Synchronized methods. A lock that is visible across threads controls entry to the body of a synchronized method. For instance and virtual methods the lock is associated with the this pointer. For static methods the lock is associated with the type to which the method belongs. The lock is taken by the logical thread (see System.Threading.Thread in the .NET Framework Standard Library Annotated Reference) and may be entered any number of times by the same thread; entry by other threads is prohibited while the first thread is still holding the lock. The CLI shall release the lock when control exits (by any means) the method invocation that first acquired the lock.

  2. Explicit locks and monitors. These are provided in the class library (see System.Threading.Monitor). Many of the methods in the System.Threading.Monitor class accept an Object as argument, allowing direct access to the same lock that is used by synchronized methods. While the CLI is responsible for ensuring correct protocol when this lock is only used by synchronized methods, the user must accept this responsibility when using explicit monitors on these same objects.

  3. Volatile reads and writes. The CIL includes a prefix, volatile., that specifies that the subsequent operation is to be performed with the cross-thread visibility constraints described in Partition I, section 12.6.7. In addition, the class library provides methods to perform explicit volatile reads (System.Thread.VolatileRead) and writes (System.Thread.VolatileWrite), as well as barrier synchronization (System.Thread.MemoryBarrier).

  4. Built-in atomic reads and writes. All reads and writes of certain properly aligned data types are guaranteed to occur atomically. See Partition I, section 12.6.6.

  5. Explicit atomic operations. The class library provides a variety of atomic operations in the System.Threading.Interlocked class.

Acquiring a lock (System.Threading.Monitor.Enter or entering a synchronized method) shall implicitly perform a volatile read operation, and releasing a lock (System.Threading.Monitor.Exit or leaving a synchronized method) shall implicitly perform a volatile write operation. See Partition I, section 12.6.7.

12.6.6 Atomic Reads and Writes

A conforming CLI shall guarantee that read and write access to properly aligned memory locations no larger than the native word size (the size of type native int) is atomic (see Partition I, section 12.6.2). Atomic writes shall alter no bits other than those written. Unless explicit layout control (see Partition II, Controlling Instance Layout [section 9.7]) is used to alter the default behavior, data elements no larger than the natural word size (the size of a native int) shall be properly aligned. Object references shall be treated as though they are stored in the native word size.

NOTE

There is no guarantee about atomic update (read-modify-write) of memory, except for methods provided for that purpose as part of the class library (see the .NET Framework Standard Library Annotated Reference). An atomic write of a "small data item" (an item no larger than the native word size) is required to do an atomic read/write/modify on hardware that does not support direct writes to small data items.


NOTE

There is no guaranteed atomic access to 8-byte data when the size of a native int is 32 bits, even though some implementations may perform atomic operations when the data is aligned on an 8-byte boundary.


12.6.7 Volatile Reads and Writes

The volatile. prefix on certain instructions shall guarantee cross-thread memory ordering rules. They do not provide atomicity, other than that guaranteed by the specification of Partition I, section 12.6.6.

A volatile read has "acquire semantics," meaning that the read is guaranteed to occur prior to any references to memory that occur after the read instruction in the CIL instruction sequence. A volatile write has "release semantics," meaning that the write is guaranteed to happen after any memory references prior to the write instruction in the CIL instruction sequence.

A conforming implementation of the CLI shall guarantee this semantics of volatile operations. This ensures that all threads will observe volatile writes performed by any other thread in the order they were performed. But a conforming implementation is not required to provide a single total ordering of volatile writes as seen from all threads of execution.

ANNOTATION

Microsoft expects to enforce a stronger restriction on its implementations and has proposed that the next edition of the standard also impose that restriction. This restriction says that writes may not be reordered with respect to other writes, independent of whether the writes are volatile. In the proposed model, writes could never be reordered with respect to other writes, and volatile. prefix would, in addition, restrict the reorder of writes relative to reads, as described in this section.


An optimizing compiler that converts CIL to native code shall not remove any volatile operation, nor may it coalesce multiple volatile operations into a single operation.

RATIONALE

One traditional use of volatile operations is to model hardware registers that are visible through direct memory access. In these cases, removing or coalescing the operations may change the behavior of the program.


NOTE

An optimizing compiler from CIL to native code is permitted to reorder code, provided that it guarantees both the single-thread semantics described in Partition I, section 12.6 and the cross-thread semantics of volatile operations.


12.6.8 Other Memory Model Issues

All memory allocated for static variables (other than those assigned RVAs within a PE file; see Partition II) and objects shall be zeroed before they are made visible to any user code.

A conforming implementation of the CLI shall ensure that, even in a multi-threaded environment and without proper user synchronization, objects are allocated in a manner that prevents unauthorized memory access and prevents illegal operations from occurring. In particular, on multi-processor memory systems where explicit synchronization is required to ensure that all relevant data structures are visible (for example, vtable pointers) the VES shall be responsible for either enforcing this synchronization automatically or for converting errors due to lack of synchronization into non-fatal, non-corrupting, user-visible exceptions.

It is explicitly not a requirement that a conforming implementation of the CLI guarantee that all state updates performed within a constructor be uniformly visible before the constructor completes. CIL generators may ensure this requirement themselves by inserting appropriate calls to the memory barrier or volatile write instructions.



The Common Language Infrastructure Annotated Standard (Microsoft. NET Development Series)
The Common Language Infrastructure Annotated Standard (Microsoft. NET Development Series)
ISBN: N/A
EAN: N/A
Year: 2002
Pages: 121

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