Copyright © 2002 Southern Storm Software, Pty Ltd.
Permission to distribute copies of this work under the terms of the
GNU Free Documentation License is hereby granted.
struct,
union, and other special C types must be mangled to
conform with CLI conventions, but such mangling should still be
readable to a human debugging the compiler.
Note 1. Given the nature of C, it is always possible for a programmer to write code that depends upon platform-specific word sizes, endianness, and operating system facilities. Our goal is that C code written to commonly used C coding standards should not be aware of such platform differences.Some things are deliberately outside the scope of this ABI definition. We do not describe the facilities that are provided by the "libc" implementation, or the contents of standard header files, for example.
Note 2. This doesn't preclude the application programmer from using native code facilities such as PInvoke. But the compiler itself will not use such features to implement the ABI.
Note 3. If the ABI avoids vendor-specific naming, it is more likely to be adopted by other vendors.
In the sections below, we suggest extended syntax for the C language to enable access to CLI-specific features. This syntax is only a suggestion. Two compilers that use different syntax for the same feature can still interoperate if they translate their syntax into the same ABI conventions.
All extension keywords begin and end in "__", following
standard C practice. We recommend that compiler vendors seriously consider
adopting the proposed keywords to make it easier to port source code from
one compiler to another.
Note: Some of the features described in this document haven't been fully implemented by Portable.NET's C compiler yet. This document is therefore subject to change.
int's are 32 bits in size,
long's and pointers are 64 bits in size.int's, long's, and
pointers are all 32 bits in size.
Note: A "Model 64" program will fail to work on a 128-bit CLI implementation, for the same reason that "Model 32" programs fail on 64-bit CLI implementations. When and if 128-bit CLI implementations become common-place, it will be easy to extend this ABI to include a "Model 128".When the compiler builds an object file, application, or library, it MUST tag the corresponding module with the memory model. For example, the following module is tagged as "Model 64":
.module test.exe
.custom instance void [OpenSystem.C]OpenSystem.C.MemoryModelAttribute::.ctor
(int32) = (01 00 40 00 00 00 00 00)
Linkers can use the presence of this attribute to detect that a C
application is being linked, rather than a C# application, and then
modify their behaviour accordingly. For example, by adding additional
libraries to the link that aren't normally required by C# applications.The two primary memory models are designed to mirror existing 64-bit and 32-bit CPU architectures. But sometimes the programmer will want to exactly match the memory model to the underlying operating system, to improve interoperability with native code. In the process, portability is sacrificed, so this must only be used when absolutely necessary.
To match an underlying operating system, the compiler chooses either
"Model 64" or "Model 32", based on the size of the system's
"void *" type. The compiler then applies a number of
"model modifiers" to alter type alignment values to match the
system. These modifiers are as follows:
0x00000001short values are aligned on 1-byte boundaries,
rather than 2-byte boundaries.0x00000002int values are aligned on 1-byte boundaries,
rather than 4-byte boundaries.0x00000004int values are aligned on 2-byte boundaries,
rather than 4-byte boundaries.0x00000008long long values are aligned on 1-byte boundaries,
rather than 8-byte boundaries.0x00000010long long values are aligned on 2-byte boundaries,
rather than 8-byte boundaries.0x00000020long long values are aligned on 4-byte boundaries,
rather than 8-byte boundaries.0x00000040float values are aligned on 1-byte boundaries,
rather than 4-byte boundaries.0x00000080float values are aligned on 2-byte boundaries,
rather than 4-byte boundaries.0x00000100double values are aligned on 1-byte boundaries,
rather than 8-byte boundaries.0x00000200double values are aligned on 2-byte boundaries,
rather than 8-byte boundaries.0x00000400double values are aligned on 4-byte boundaries,
rather than 8-byte boundaries.0x00000800long double values are aligned on 1-byte boundaries.0x00001000long double values are aligned on 2-byte boundaries.0x00002000long double values are aligned on 4-byte boundaries.0x00004000long double values are aligned on 8-byte boundaries.0x00008000long double values are aligned on 16-byte boundaries.0x000100000x000200000x000400000x00080000These modifiers describe how the actual memory model differs from either "Model 32" or "Model 64". Programs that are compiled with non-zero modifier values are unlikely to work on runtime engines that use a different combination of flags. Programs with zero modifier values should work on all runtime engines that support the memory model.
When the object file is generated, the modifier flags are written into
the "MemoryModel" attribute declaration, as an optional
argument:
.module test.exe
.custom instance void [OpenSystem.C]OpenSystem.C.MemoryModelAttribute::.ctor
(int32, int32) = (01 00 20 00 00 00 20 04 00 00 00 00)
This indicates "Model 32 with 4-byte alignment of 64-bit integers
and doubles". If the second parameter is not present, the default
modifier flag value is zero.
MemoryModelAttribute" example in the previous section
demonstrated the use of the "OpenSystem.C" assembly, which
provides a number of classes for tagging C applications, and for implementing
ABI support facilities. The following summarises the important classes
in the "OpenSystem.C" namespace:
IsConstconst".IsFunctionPointerBitFieldAttributeWeakAliasForAttributeStrongAliasForAttributeInitializerAttribute.cctor methods), in that initializers are
guaranteed to be executed before main.InitializerOrderAttributeFinalizerAttributeFinalize"
methods in garbage-collected objects. The order of garbage-collected
object finalization is indeterminate with respect to C finalizers.FinalizerOrderAttributeMemoryModelAttributeOriginalNameAttributeLongJmpExceptionsetjmp/longjmp operations.Crt0LongDoublenative float" type. The standard C# base
class library lacks a suitable class. This class has
a constructor that takes a single "native float"
argument, and an "Unpack" method that returns
a "native float" when applied to an instance.FloatComplex, DoubleComplex,
LongDoubleComplexFloatImaginary, DoubleImaginary,
LongDoubleImaginaryCNameAttributeFloatComplex" is marked as
"float _Complex".To simplify discussion, we will use an abbreviated syntax to describe attributes in CIL assembly code examples. For example, the memory model designation in the previous section can also be written as:
This abbreviation is for exposition purposes only. It isn't intended to suggest an alternative syntax for CIL assemblers..module test.exe .custom [OpenSystem.C.MemoryModel(64)]
| Type | Model 64 Size/Align1 |
Model 32 Size/Align1 | Description |
void |
1/1 2 | 1/1 | Void type |
_Bool |
1/1 | 1/1 | 8-bit boolean value (C# "bool") |
char |
1/1 | 1/1 | Signed 8-bit integer |
unsigned char |
1/1 | 1/1 | Unsigned 8-bit integer |
short |
2/2 | 2/2 | Signed 16-bit integer |
unsigned short |
2/2 | 2/2 | Unsigned 16-bit |
__wchar__ |
2/2 | 2/2 | 16-bit wide character value (C# "char") |
int |
4/4 | 4/4 | Signed 32-bit integer |
unsigned int |
4/4 | 4/4 | Unsigned 32-bit integer |
long |
8/8 | 4/4 | Signed 64-bit or 32-bit integer |
unsigned long |
8/8 | 4/4 | Unsigned 64-bit or 32-bit integer |
long long |
8/8 | 8/8 | Signed 64-bit integer |
unsigned long long |
8/8 | 8/8 | Unsigned 64-bit integer |
float |
4/4 | 4/4 | 32-bit IEEE 754 floating-point |
double |
8/8 | 8/8 | 64-bit IEEE 754 floating-point |
type * |
8/8 | 4/4 | Pointer to "type" |
float _Complex |
8/4 | 8/4 | Complex number type based on float |
double _Complex |
16/8 | 16/8 | Complex number type based on double |
float _Imaginary |
4/4 | 4/4 | Imaginary number type based on float |
double _Imaginary |
8/8 | 8/8 | Imaginary number type based on double |
Note 1. These size and alignment values refer to the
primary memory model. The values may be different if there are non-zero
model modifier flags in effect.
Note 2. The size of "void" is 1, to be
consistent with gcc.
In "Model 64", pointers are allocated 8 bytes of memory, and aligned
on an 8-byte boundary, even on platforms that only support 32-bit pointers.
The expression "sizeof(void *)" will always return 8.
This behaviour is necessary to provide a consistent "struct"
layout on all CLI implementations, as we will see in the following sections.
const" and "volatile" qualifiers are
represented using the "OpenSystem.C.IsConst" and
"System.Runtime.CompilerServices.IsVolatile" modifiers.
The following table provides some examples:
| Declaration | Representation |
const int x; |
int32 modopt(OpenSystem.C.IsConst) x |
void * volatile y; |
void * modreq(System.Runtime.CompilerServices.IsVolatile) y |
const char *s; |
int8 modopt(OpenSystem.C.IsConst) * s |
char * const s; |
int8 * modopt(OpenSystem.C.IsConst) s |
The placement of the type modifier is important. A qualifier at the
outer-most level of a type applies to the field or variable. A qualifier
at an inner level applies to a referenced type. In the last example
above, the variable "s" cannot be modified, but it points
at a string that can be modified. In the second last example, the
variable can be modified, but not the string.
The "IsVolatile" modifier is required, to be consistent
with other CLI-compatible languages. The "IsConst" modifier
is optional, because other CLI-compatible languages can safely ignore it
(the programmer on the other hand probably shouldn't ignore it).
Fixed types have a constant size and alignment. The expression
"sizeof(T)" can be evaluated to a constant at compile time.
Dynamic types have a constant size and alignment, but these values are not known until runtime. Native types (described in a later section) are an example, as are C# value types.
Unknown types have no known size. An example is "char[]",
which cannot be used as the type of a structure field, as its storage
size cannot be determined.
Traditional C compilers only have "fixed" and "unknown" types. One of
the goals of this ABI is to minimize the occurence of "dynamic" types
so that "struct" layout can be computed efficiently.
struct A") are converted into a value
type called "struct A", with no namespace qualifier. This
value type is marked as having explicit layout, with pre-computed class
packing and size values. Each field within the structure has a
pre-computed offset. For example, on a "Model 64" system:
struct A
{
int item;
struct A *next;
};
.class public explicit sealed ansi 'struct A' extends System.ValueType
{
.pack 8
.size 16
.field [0] public int32 item
.field [8] public 'struct A' * next
}
On a "Model 32" system, the structure would be encoded as:
.class public explicit sealed ansi 'struct A' extends System.ValueType
{
.pack 4
.size 8
.field [0] public int32 item
.field [4] public 'struct A' * next
}
Structures may only contain fields with "fixed" layout. Native structures
(described later) can contain fields with both "fixed" and "dynamic"
layout, but their usage is restricted.If a structure contains sub-structures, they are converted into companion structure types:
struct A
{
int x;
struct
{
int y;
} z;
struct B
{
int w;
} v;
};
.class public explicit sealed ansi 'struct A' extends System.ValueType
{
.pack 4
.size 12
.field [0] public int32 x
.class public explicit sealed ansi 'struct (1)'
extends System.ValueType
{
.pack 4
.size 4
.field [0] public int32 y
}
.field [4] public valuetype 'struct A'/'struct (1)' z
.field [8] public valuetype 'struct B' v
}
.class public explicit sealed ansi 'struct B' extends System.ValueType
{
.pack 4
.size 4
.field [0] public int32 w
}
As can be seen, anonymous structures are assigned a unique numeric code,
and are encoded as nested types. The code is unique to the surrounding
structure, so that the same code will be generated each time the program
is compiled.
Note: It would be desirable to allow the runtime engine to perform structure layout dynamically, rather than fix types to specific sizes and fields to specific offsets. Readers who think it may be possible to do so may like to ponder how to efficiently compile the following code so that it will work regardless of the runtime size of "void *" and the runtime alignment of "y":
struct item { char x[sizeof(void *)]; long long y; }; long long get_y(struct item *i) { return i->y; }
union A") are represented as value types with
the name "union A", and all fields explicitly laid out to
start at offset 0.
union A
{
int x;
double y;
}
.class public explicit sealed ansi 'union A' extends System.ValueType
{
.pack 8
.size 8
.field [0] public int32 x
.field [0] public float64 y
}
Unions may only contain fields with "fixed" layout.
struct A
{
int x : 8;
int y : 1;
unsigned int z : 16;
int w;
}
.class public explicit sealed ansi 'struct A' extends System.ValueType
{
.custom [OpenSystem.C.BitField("x", ".bitfield-1", 0, 8)]
.custom [OpenSystem.C.BitField("y", ".bitfield-1", 8, 1)]
.custom [OpenSystem.C.BitField("z", ".bitfield-2", 0, 16)]
.pack 4
.size 12
.field [0] public int32 '.bitfield-1'
.field [4] public unsigned int32 '.bitfield-2'
.field [8] public int32 w
}
A[]" are mapped to a value
type called "array A[]". For example, "int[]"
is encoded as follows:
.class public explicit sealed ansi 'array int[]'
extends System.ValueType
{
.pack 4
.size 0
.field private static specialname int32 elem__
}
The value type must have a field called "elem__", which
defines the element type, and it must have the attributes
"private static specialname".
Note. It will be rare to find a type of the form "A[]" in a generated object file, because such types normally decay to pointer types when used as function arguments. The encoding is specified here because the compiler does need to distinguish "A[]" from "A *" in certain circumstances.
If the array type includes a non-zero size value, then it is encoded
as a value type with an explicit size defining the total size of the
array. For example, "int[100] is encoded as follows:
.class public explicit sealed ansi 'array int[100]'
extends System.ValueType
{
.pack 4
.size 400
.field [0] public specialname int32 elem__
}
The size of the array is determined by dividing ".size"
by the size of the element type. The "elem__" field in
this case must be "public specialname". Arrays with
a zero size are encoded as follows:
.class public explicit sealed ansi 'array int[0]'
extends System.ValueType
{
.pack 4
.size 0
.field [0] public static specialname int32 elem__
}
Here, the "elem__" field is "static". This
type can be distinguished from the encoding for "int[]"
because the "elem__" field is "public"
instead of "private".
Array element types must have "fixed" layout. The programmer can
allocate arrays of "dynamic" types using "malloc"
or "alloca".
The following is an example of encoding the two-dimensional array type
"int [300][400]":
.class public explicit sealed ansi 'array int[300][400]'
extends System.ValueType
{
.pack 4
.size 480000 // == 300 * 400 * 4
.field [0] public specialname valuetype 'array int[400]' elem__
}
.class public explicit sealed ansi 'array int[400]'
extends System.ValueType
{
.pack 4
.size 1600 // == 400 * 4
.field [0] public specialname int32 elem__
}
All of the types that are described in this section have "dynamic" layout. They cannot be used as array element types, or as the members of non-native structures and unions.
The use of these native types is highly discouraged, except where it is absolutely essential to interoperate with other system components:
| Type | Description |
__native__ int |
Signed native integer (C# "IntPtr") |
unsigned __native__ int |
Unsigned native integer (C# "UIntPtr") |
long double |
Native floating-point |
long double _Complex |
Complex number type based on long double |
long double _Imaginary |
Imaginary number type based on long double |
The native integer types are "__native__ int" and
"unsigned __native__ int". They may be either 4 or 8 bytes in
size, depending upon the underlying platform. The expressions
"sizeof(__native__ int)" and
"sizeof(unsigned __native__ int)" are evaluated at run time.
The native floating point type is "long double", and
is guaranteed to have precision greater than or equal to "double".
The expression "sizeof(long double)" is computed at runtime.
Structures can be specified to have native layout at declaration time:
struct __native__ A
{
int item;
struct A *next;
};This is represented by a sequential type definition in the program's metadata:
.class public sequential sealed ansi 'struct A' extends System.ValueType
{
.field public int32 item
.field public 'struct A' * next
}
The runtime engine will lay this out using platform-specific type sizes
and alignment. The expression "sizeof(struct A)" will be
evaluated at runtime.Unions can also be specified to have native layout at declaration time:
union __native__ A
{
int x;
void *y;
}
.class public explicit sealed ansi 'union A' extends System.ValueType
{
.field [0] public int32 x
.field [0] public void * y
}
The type is declared explicit, so that all fields can be defined with
an offset of zero, but the type does not have an overall size.Types with "fixed" and "dynamic" layout may be used as the members of native structures and unions.
It is recommended that the compiler issue a warning when bit fields are used in native structures and unions, and the memory model does not have an appropriate memory model modifier set. The compiler's bit order may not match the native platform's bit order, leading to problems with PInvoke'd functions.
OpenSystem.C.IsFunctionPointer" modifier:
void (*func)(int); .field public static method void * (int32) modopt(IsFunctionPointer) func
An array argument to a function will be converted into its "decayed" pointer form. For example:
int main(int argc, char *argv[])
{
...
}
.method public static int32 main
(int32 argc, int8 * * argv) cil managed
{
...
}
Functions that take a variable number of arguments must be declared
with "vararg" calling conventions:
int printf(const char *format, ...)
{
...
}
.method public static vararg int32 printf
(int8 modopt(IsConst) * format) cil managed
{
...
}When arguments are passed to a variable-argument function, they must be converted into their "natural passing type" first:
| Type | Natural Passing Type |
_Bool |
_Bool |
char |
int |
unsigned char |
int |
short |
int |
unsigned short |
int |
__wchar__ |
int |
int |
int |
unsigned int |
int |
__native__ int |
long |
unsigned __native__ int |
long |
long |
long |
unsigned long |
long |
long long |
long long |
unsigned long long |
long long |
float |
double |
double |
double |
long double |
OpenSystem.C.LongDouble |
type * |
long |
struct and union |
Same as input type |
Natural passing types help to properly implement cases where a value is passed as unsigned, but unpacked as signed, or is passed using a smaller type than the unpacking type.
The compiler must convert all variable arguments to their natural passing
types at the point of the call. The "va_arg" operator is then
responsible for casting the natural passing type back to the programmer's
requested type.
The "va_list" type is implemented by the C#
"System.ArgIterator" class, and has "dynamic" layout.
The runtime engine will throw an exception if an attempt is made to
unpack an argument using the wrong natural passing type.
<Module>"
type. However, there are some "undefined" issues that we now
deal with.
<Module>"
type within a foreign assembly. This appears to be a hard-wired constraint.
Other CLR's (e.g. Portable.NET) make no distinction between the module type
and all other types.
To achieve interoperability with Microsoft's CLR, library assemblies must
use the "$Module$" type for their global field and method
definitions instead of "<Module>". The
"$Module$" type must have the "public" and
"sealed" flags.
Executables still use the "<Module>" type, as it appears
to work in all CLR's that have been tested so far. The
"<Module>" type should have the "public"
and "abstract" flags.
When the assembler sees a dangling reference to something in the
"<Module>" class, it will convert it into a member
reference on the "<ModuleExtern>" class. For example:
.method public static void hello() cil managed
{
call void hello2()
}
If hello2 remains undefined at the end of the assembly
process, then the resulting object file will look like this:
.method public static void hello() cil managed
{
call void '<ModuleExtern>'::hello2()
}
When the linker loads this object file, it will resolve references to
"<ModuleExtern>" by looking for a matching definition
and changing the type reference appropriately. The new reference
may be to the linked executable's "<Module>" type,
or to a foreign library's "$Module$" type.
The "<ModuleExtern>" type will itself be dangling.
The exact means by which this is accomplished is compiler-dependent, as the
ECMA specification does not define an object file format for the CLI.
Portable.NET's assembler encodes dangling types as a
TypeRef, scoped to the current module, but with no corresponding TypeDef.
The object file format is based on the native PE/COFF object file format,
with CIL metadata stored in the ".text$il" section.
Portable.NET's linker fixes up dangling TypeRef's at link time.
static" are converted
into "private" fields or methods within the
"<Module>" object file's class. All other variables
or functions are converted into "public" definitions.
If the "<Module>" class has any "public"
members, then the class will also be declared "public".
This ensures that a library will export its definitions correctly to
applications that link against the library.
private" definition for the same function
or variable. Alternatively, one may be "private" and
the other "public".
We resolve this situation by renaming one of the "private"
definitions to something else, and then redirecting all references to
the original to the renamed version. From an external user's point of
view, the "public" definition (if any) will become the
visible definition. For example:
File 1:If two or more object files have conflicting ".field public static int32 xFile 2:.field private static float64 x .method public static float64 getx() cil managed { ldsfld float64 x ret }Result:.field public static int32 x .field private static float64 'x-1' .method public static float64 getx() cil managed { ldsfld float64 'x-1' ret }
public"
definitions for a function or variable, then a linker error will occur.
Structure, union, and array types may also conflict when two
object files are linked together. In most cases, the two definitions
will be the same, because the same type is being used in both object
files (e.g. "struct _IO_FILE" in glibc's stdio implementation).
When two types have identical definitions, the linker will copy one into the output file and ignore the other. When the two types have different definitions, the linker chooses one to become the primary copy, and the other is renamed.
If one of the types has the same definition as a type from a library, the linker should favour the library's definition, as it is the most likely candidate. If neither definition duplicates a library definition, the linker can choose either one, and probably should also report a warning to the programmer.
When program items are renamed, the resultant binary will not be in
sync with the source code. This can make source-level debugging
difficult. To alleviate this problem, the linker can add
"OriginalName" attribute values to all renamed items:
.field public static int32 x
.field private static float64 'x-1'
.custom [OpenSystem.C.OriginalName("x")]
.method public static float64 getx() cil managed
{
ldsfld float64 'x-1'
ret
}
Normally this is only required if an object file contained debug
symbol information prior to renaming.
getuid" function (paraphrased a little):
int __getuid(void)
{
...
}
weak_alias(__getuid, getuid)
This will be compiled as follows:
.method public static int32 __getuid() cil managed
{
...
}
.field public specialname static .method int32 * () 'getuid-alias'
.method public static int32 getuid() cil managed
{
.custom [OpenSystem.C.WeakAliasFor("__getuid")]
.maxstack 1
ldsfld .method int32 * () 'getuid-alias'
tail.
calli int32 ()
ret
}
.method private specialname static void '.init-1'() cil managed
{
.custom [OpenSystem.C.Initializer]
.maxstack 1
ldftn void __getuid()
stsfld .method int32 * () 'getuid-alias'
ret
}
When a program is linked against this definition, the
"WeakAliasFor" attribute is used to redirect the
reference to the actual definition if the system does not contain
any other definitions for the function.
When a library that does not supply its own "getuid"
is linked against this definition, the "getuid"
method is called directly, which will then redirect control to
the actual "getuid".
A program or library that defines its own "getuid"
is compiled as normal:
.method public static int32 getuid() cil managed
{
...
}
At link time, the linker will insert an initializer which updates the
"getuid-alias" field with the new value:
.method private specialname static void '.init-1'() cil managed
{
.custom [OpenSystem.C.Initializer]
.maxstack 1
ldftn void getuid()
stsfld .method int32 * () [library]'$Module$'::'getuid-alias'
ret
}
where "library" is the name of the library that defines
the "getuid-alias" variable.Strong aliases for functions are defined in a similar manner:
.method public static vararg int32 _IO_printf
(int8 modopt(OpenSystem.C.IsConst) *format) cil managed
{
...
}
.method public static vararg int32 printf
(int8 modopt(OpenSystem.C.IsConst) *format) cil managed
{
.custom [OpenSystem.C.StrongAliasFor("_IO_printf")]
}
In this case, whenever the linker sees a reference to "printf",
it will redirect the caller to "_IO_printf". The body of
the alias function is empty, because it will never be called at runtime.Global variables may also have strong aliases associated with them:
char **__environ;
strong_alias(__environ, environ);
.field public static int8 * * __environ
.field public static int8 * * environ
.custom [OpenSystem.C.StrongAliasFor("__environ")]
When the linker sees a reference to "environ", it will
substitute "__environ".
Weak aliases are not supported for global variables. Weak aliases
exist in libc libraries primarily for legacy reasons. There are
existing C programs that depend upon variables like "environ",
"timezone", etc, being weak aliases, but they are rarer
than programs that depend upon functions being weak aliases.
It is recommended that if the compiler sees a weak alias definition for a variable that it output a strong alias instead.
specialname" flag, have no parameters or return
values, and are marked with the "Initializer"
attribute.
Finalizers are compiled into static methods that have the
"specialname" flag, have no parameters or return
values, and are marked with the "Finalizer" attribute.
The linker collects up all initializers and finalizers in a program or library and does the following:
public methods in the
"<Module>" class: ".init"
and ".fini"..init" method calls the ".init"
methods of all libraries that the program or library itself
depends upon..init" method then calls all of the
locally-defined initializers..fini" method calls all of the locally-defined
finalizers..fini" method then calls the ".fini"
methods of all libraries that the program or library itself
depends upon, in reverse order..init" methods are called is usually
indeterminable. The compiler can alter the ordering using the
"InitializerOrder" attribute:
.method private specialname static void '.init-1'() cil managed
{
.custom [OpenSystem.C.Initializer]
.custom [OpenSystem.C.InitializerOrder(-1)]
...
}
This initializer will be executed before all "normal" initializers, which
have a default order value of zero.
The "FinalizerOrder" attribute can used to alter the
ordering of finalizers. A finalizer with an order value of -1 will
be executed after the normal finalizers.
When the linker generates the ".init" and ".fini"
methods, it must also insert some reference counting code. The body
of the ".init" method will only be executed upon the first
call, and the body of the ".fini" method will only be
executed upon the last call. Appendix A contains some sample code
that demonstrates this.
Usually, locally-defined initializers and finalizers are declared
"private". The renaming logic described in a previous
section will take care of resolving ambiguities in naming.
main" function is compiled,
a small amount of CIL code is added to define the application entry point.
This code calls facilities in the "OpenSystem.C.Crt0" class
to initialize the application, to invoke "main", and to
handle shutdown tasks when "main" exits. Using C# syntax,
the startup code looks like this:
public static void .start(String[] args)
{
try
{
int argc;
IntPtr argv;
IntPtr envp;
argv = Crt0.GetArgV(args, sizeof(void *), out argc);
envp = Crt0.GetEnvironment();
Crt0.Startup("libcNN");
Crt0.Shutdown(main(argc, argv, envp));
}
catch(OutOfMemoryException)
{
throw;
}
catch(Object e)
{
throw Crt0.ShutdownWithException(e);
}
}
where "libcNN" is the name of the "libc" implementation
that the program was compiled against. This will normally be
"libc64" for "Model 64" and "libc32" for
"Model 32". The compiler will only pass those parameters to
"main" that the programmer specified in their source code.
The startup code in the application is kept deliberately simple,
with most of the real work being done in the "OpenSystem.C.Crt0"
class. This allows the crt0 code to be modified to accomodate new "libc"
requirements in the future, without needing all existing applications
to be recompiled.
.class private sealed '.init-count' extends System.Object
{
.field private static int32 count
}
.method public specialname static void '.init'() cil managed
{
.maxstack 2
.locals (class System.Type)
// Lock down '.init-count' to synchronize access.
ldtoken '.init-count'
call class System.Type System.Type::GetTypeFromHandle
(valuetype System.RuntimeTypeHandle)
dup
stloc 0
call void System.Threading.Monitor::Enter(class System.Object)
.try
{
// Increase the reference count, and check for the first call.
ldsfld '.init-count'::count
dup
ldc.i4.1
add
stsfld '.init-count'::count
brtrue L1
leave runinit
L1:
leave exit
}
finally
{
ldloc 0
call void System.Threading.Monitor::Exit(class System.Object)
endfinally
}
runinit:
// Run the initializers for the libraries.
call void [libc64]'<Module>'::'.init'()
// Run the local initializers.
call void '<Module>'::'.init-1'()
call void '<Module>'::'.init-2'()
...
call void '<Module>'::'.init-N'()
exit:
// Initialization has finished.
ret
}
.method public specialname static void '.fini'() cil managed
{
.maxstack 2
.locals (class System.Type)
// Lock down '.init-count' to synchronize access.
ldtoken '.init-count'
call class System.Type System.Type::GetTypeFromHandle
(valuetype System.RuntimeTypeHandle)
dup
stloc 0
call void System.Threading.Monitor::Enter(class System.Object)
.try
{
// Decrease the reference count, and check for the last call.
ldsfld '.init-count'::count
ldc.i4.1
sub
dup
stsfld '.init-count'::count
brtrue L1
leave runfini
L1:
leave exit
}
finally
{
ldloc 0
call void System.Threading.Monitor::Exit(class System.Object)
endfinally
}
runfini:
// Run the local finalizers.
call void '<Module>'::'.fini-1'()
call void '<Module>'::'.fini-2'()
...
call void '<Module>'::'.fini-N'()
// Run the finalizers for the libraries.
call void [libc64]'<Module>'::'.fini'()
exit:
// Finalization has finished.
ret
}