A Simple Sample
2011-11-04 14:18 iRead 阅读(972) 评论(0) 编辑 收藏 举报No, the sample is not going to be “Hello, World!” This sample is a simple managed console application that prompts the user to enter an integer and then identifies the integer as odd or even. When the user enters something other than a decimal number, the application responds, “How rude!” and terminates.
The sample uses managed console APIs from the .NET Framework class library for console input and output, and it uses the unmanaged function sscanf from the C run-time library for input string conversion to an integer.
1 //----------- Program header
2 .assembly extern mscorlib { }
3 .assembly OddOrEven { }
4 .module OddOrEven.exe
5 //----------- Class declaration
6 .namespace Odd.or {
7 .class public auto ansi Even extends [mscorlib]System.Object {
8 //----------- Field declaration
9 .field public static int32 val
10 //----------- Method declaration
11 .method public static void check( ) cil managed {
12 .entrypoint
13 .locals init (int32 Retval)
14 AskForNumber:
15 ldstr "Enter a number"
16 call void [mscorlib]System.Console::WriteLine(string)
17 call string [mscorlib]System.Console::ReadLine()
18 ldsflda valuetype CharArray8 Format
19 ldsflda int32 Odd.or.Even::val
20 call vararg int32 sscanf(string,int8*,...,int32*)
21 stloc Retval
22 ldloc Retval
23 brfalse Error
24 ldsfld int32 Odd.or.Even::val
25 ldc.i4 1
26 and
27 brfalse ItsEven
28 ldstr "odd!"
29 br PrintAndReturn
30 ItsEven:
31 ldstr "even!"
32 br PrintAndReturn
33 Error:
34 ldstr "How rude!"
35 PrintAndReturn:
36 call void [mscorlib]System.Console::WriteLine(string)
37 ldloc Retval
38 brtrue AskForNumber
39 ret
40 } // End of method
41 } // End of class
42 } // End of namespace
43 //----------- Global items
44 .field public static valuetype CharArray8 Format at FormatData
45 //----------- Data declaration
46 .data FormatData = bytearray(25 64 00 00 00 00 00 00) //% d . . . . . .
47 //----------- Value type as placeholder
48 .class public explicit CharArray8
49 extends [mscorlib]System.ValueType { .size 8 }
50 //----------- Calling unmanaged code
51 .method public static pinvokeimpl("msvcrt.dll" cdecl)
52 vararg int32 sscanf(string,int8*) cil managed { }
Program Header
.assembly extern mscorlib { }
.assembly OddOrEven { }
.module OddOrEven.exe
.assembly extern mscorlib { } defines a metadata item named Assembly Reference (or AssemblyRef), identifying the external managed application (assembly) used in this program. In this case, the external application is Mscorlib.dll, the main assembly of the .NET Framework classes. (The topic of the .NET Framework class library itself is beyond the scope of this book; for further information, consult the detailed specification of the .NET Framework class library published as Partition IV of the proposed ECMA standard.)
The Mscorlib.dll assembly contains declarations of all the base classes from which all other classes are derived. Although theoretically you could write an application that never uses anything from Mscorlib.dll, I doubt that such an application would be of any use. (One obvious exception is Mscorlib.dll itself.) Thus it’s a good habit to begin a program in ILAsm with a declaration of AssemblyRef to Mscorlib.dll, followed by declarations of other AssemblyRefs (if any).
The scope of an AssemblyRef declaration (between the curly braces) can contain additional information identifying the referenced assembly, such as version or culture (previously known as locale). Because this information is not mandatory for referencing Mscorlib.dll, I have omitted it from this sample. (Chapter 5 describes this additional information in detail.)
Note that although the code references the assembly Mscorlib.dll, AssemblyRef is declared by filename only, without the extension. Including the extension causes the loader to look for Mscorlib.dll.dll or Mscorlib.dll.exe, resulting in a run-time error.
.assembly OddOrEven { } defines a metadata item named Assembly, which, to no one’s surprise, identifies the current application (assembly). Again, you could include additional information identifying the assembly in the assembly declaration—see Chapter 5 for details—but it is not necessary here. Like AssemblyRef, the assembly is identified by its filename, without the extension.
Why must you identify the application as an assembly? If you don’t, it will not be an application at all; rather, it will be a nonprime module—part of some other application (assembly)—and as such will not be able to execute on its own. Giving the module an EXE extension changes nothing; only assemblies can be executed.
.module OddOrEven.exe defines a metadata item named Module, identifying the current module. Each module, prime or otherwise, carries this identification in its metadata. Note that the module is identified by its full filename, including the extension. The path, however, must not be included.
Class Declaration
.namespace Odd.or {
.class public auto ansi Even extends [mscorlib]System.Object {
}
}
.namespace Odd.or { … } declares a namespace. A namespace does not represent a separate metadata item. Rather, a namespace is a common prefix of the full names of all the classes declared within the scope of the namespace declaration.
.class public auto ansi Even extends [mscorlib]System.Object { … } defines a metadata item named Type Definition (TypeDef). Each class, structure, or enumeration defined in the current module is described by a respective TypeDef record in the metadata. The name of the class is Even. Because it is declared within the scope of the namespace Odd.or, its full name, by which it can be referenced elsewhere and by which the loader identifies it, is Odd.or.Even.
The keywords public, auto, and ansi define the flags of the TypeDef item. The keyword public, which defines the visibility of the class, means that the class is visible outside the current assembly. (Another keyword for class visibility is private, the default, which means that the class is for internal use only and cannot be referenced from outside.)
The keyword auto defines the class layout style (automatic, the default), directing the loader to lay out this class however it sees fit. Alternatives are sequential (which preserves the specified sequence of the fields) and explicit (which explicitly specifies the offset for each field, giving the loader exact instructions for laying out the class).
The keyword ansi defines the mode of string conversion within the class, when interoperating with the unmanaged code. This keyword, the default, specifies that the strings will be converted to and from “normal” C-style strings of bytes. Alternative keywords are unicode (strings are converted to and from Unicode) and autochar (the underlying platform determines the mode of string conversion).
The clause extends [mscorlib]System.Object defines the parent, or base class, of the class Odd.or.Even. The code [mscorlib]System.Object represents a metadata item named Type Reference (TypeRef). This particular TypeRef has System as its namespace, Object as its name, and AssemblyRef mscorlib as the resolution scope. Each class defined outside the current module is addressed by TypeRef. You can even address the classes defined in the current module by TypeRefs instead of TypeDefs, which is considered harmless enough but not nice.
By default, all classes are derived from the class System.Object defined in the assembly Mscorlib.dll. Only System.Object itself and the interfaces have no base class, as explained in Chapter 6
The structures—referred to as value types in .NET lingo—are derived from the [mscorlib]System.ValueType class. The enumerations are derived from the [mscorlib]System.Enum class. Because these two distinct kinds of TypeDefs are recognized solely by the classes they extend, you must use the extends clause every time you declare a value type or an enumeration.
Field Declaration
.field public static int32 val
.field public static int32 val defines a metadata item named Field Definition (FieldDef). Because the declaration occurs within the scope of class Odd.or.Even, the declared field belongs to this class.
The keywords public and static define the flags of the FieldDef. The keyword public identifies the accessibility of this field and means that the field can be accessed by any member for whom this class is visible. Alternative accessibility flags are as follows:
-
The assembly flag specifies that the field can be accessed from anywhere within this assembly but not from outside.
-
The family flag specifies that the field can be accessed from any of the classes descending from Odd.or.Even.
-
The famandassem flag specifies that the field can be accessed from any of those descendants of Odd.or.Even that are defined in this assembly.
-
The famorassem flag specifies that the field can be accessed from anywhere within this assembly as well as from any descendant of Odd.or.Even, even if the descendant is declared outside this assembly.
-
The private flag specifies that the field can be accessed from Odd.or.Even only.
-
The privatescope flag is the default. See the Caution reader aid for important information about this flag.
Because the default accessibility is privatescope, which can be a problem, it’s important to remember to specify the accessibility flags.
The keyword static means that the field is static—that is, it is shared by all instances of class Odd.or.Even. If you did not designate the field as static, it would be an instance field, individual to a specific instance of the class.
The keyword int32 defines the type of the field, a 32-bit signed integer. (Types and signatures are described in Chapter 7 And, of course, val is the name of the field.
Method Declaration
.method public static void check( ) cil managed {
.entrypoint
.locals init (int32 Retval)
}
.method public static void check( ) cil managed { … } defines a metadata item named Method Definition (MethodDef). Because it is declared within the scope of Odd.or.Even, this method is a member method of this class.
The keywords public and static define the flags of MethodDef and mean the same as the similarly named flags of FieldDef discussed in the preceding section. Not all the flags of FieldDefs and MethodDefs are identical—see Chapter 8 as well as Chapter 9 for details—but the accessibility flags are, and the keyword static means the same for fields and methods.
The keyword void defines the return type of the method. If the method had a calling convention that differed from the default, you would place the respective keyword after the flags but before the return type. Calling convention, return type, and types of method parameters define the signature of the MethodDef. Note that a lack of parameters is expressed as ( ), never as (void). The notation (void) would mean that the method has one parameter of type void—an illegal signature.
The keywords cil and managed define so-called implementation flags of the MethodDef and indicate that the method body is represented in IL. A method represented in native code rather than in IL would carry the implementation flags native unmanaged.
Now, let’s proceed to the method body. In ILAsm, the method body (or method scope) generally contains three categories of items: instructions (compiled into IL code), labels marking the instructions, and directives (compiled into metadata, header settings, structured exception handling clauses, and so on—in short, anything but IL code). Outside the method body, only directives exist. Every declaration discussed so far has been a directive.
.entrypoint identifies the current method as the entry point of the application (the assembly). Each managed EXE file must have a single entry point. The ILAsm compiler will refuse to compile a module without a specified entry point, unless you use the /DLL command-line option.
.locals init (int32 Retval) defines the single local variable of the current method. The type of the variable is int32, and its name is Retval. The keyword init means that the local variables must be initialized before the method executes. If the local variables are not designated with this keyword in even one of the assembly’s methods, the assembly will fail verification (in a security check performed by the common language runtime) and will be able to run only from a local disk, when verification is disabled. For that reason, you should never forget to use the keyword init with the local variable declaration. If you need more than one local variable, you can list them, comma-separated, within the parentheses—for example, .locals init(int32Retval,stringTempStr).
AskForNumber:
ldstr "Enter a number"
call void [mscorlib]System.Console::WriteLine(string)
AskForNumber: is a label. It needn’t occupy a separate line; the IL Disassembler marks every instruction with a label on the same line as the instruction. Labels are not compiled into metadata or IL; rather, they are used solely for the identification of certain offsets within IL code at compile time.
A label marks the first instruction that follows it. Labels don’t mark directives. In other words, if you moved the AskForNumber label two lines up so that the directives .entrypoint and .locals separated the label and the first instruction, the label would still mark the first instruction.
An important note before we examine the instructions: IL is strictly a stack-based language. Every instruction takes something (or nothing) from the top of the stack and puts something (or nothing) onto the stack. Some instructions have parameters and some don’t, but the general rule does not change: instructions take all required arguments (if any) from the stack and put the results (if any) onto the stack. No IL instruction can address a local variable or a method parameter directly, except the instructions of load and store groups, which, respectively, put the value or the address of a variable or a parameter onto the stack or take the value from the stack and put it into a variable or a parameter.
Elements of the IL stack are not bytes or words, but slots. When we talk about IL stack depth, we are talking in terms of items put onto the stack, with no regard for the size of each item. Each slot of the IL stack carries information about the type of its current “occupant.” And if you put an int32 item on the stack and then invoke an instruction, which expects, for instance, a string, the JIT compiler becomes very unhappy and very outspoken, throwing an Unexpected Type exception and aborting the compilation.
ldstr “Enter a number” is an instruction that loads the specified string constant onto the stack. The string constant in this case is stored in the metadata. We can refer to such strings as common language runtime string constants or metadata string constants. You can store and handle the string constants in another way, as explained in a few moments, but ldstr deals exclusively with common language runtime string constants, which are always stored in Unicode format.
call void [mscorlib]System.Console::WriteLine(string)is an instruction that calls a console output method from the .NET Framework class library. The string is taken from the stack as the method argument, and nothing is put back, because the method returns void.
The parameter of this instruction is a metadata item named Member Reference (MemberRef). It refers to the static method named WriteLine, which has signature void(string); the method is a member of class System.Console, declared in the external assembly mscorlib. The MemberRefs are members of TypeRefs—discussed earlier in this chapter in the section “Class Declaration”—just as FieldDefs and MethodDefs are TypeDef members. However, there are no separate FieldRefs and MethodRefs, the MemberRefs cover references to both fields and methods.
You can distinguish field references from method references by their signatures. MemberRefs for fields and for methods have different calling conventions and different signature structures. Signatures, including those of MemberRefs, are discussed in detail in Chapter 7.
How does the ILAsm compiler know what type of signature should be generated for a MemberRef? Mostly from the context. For example, if a MemberRef is the parameter of a call instruction, it must be a MemberRef for a method. In certain cases in which the context is not clear, the compiler requires explicit specifications, such as method void Odd.or.Even::check( ) or field int32 Odd.or.Even::val.
call string [mscorlib]System.Console::ReadLine()
ldsflda valuetype CharArray8 Format
ldsflda int32 Odd.or.Even::val
call vararg int32 sscanf(string,int8*,...,int32*)
call string [mscorlib]System.Console::ReadLine() is an instruction that calls a console input method from the .NET Framework class library. Nothing is taken from the stack, and a string is put onto the stack as a result of this call.
ldsflda valuetype CharArray8 Format is an instruction that loads the address of the static field Format of type valuetype CharArray8. (Both the field and the value type are declared later in the source code and are discussed later.) IL has separate instructions for loading instance and static fields (ldfld and ldsfld) or their addresses (ldflda and ldsflda). Also note that the “address” loaded onto the stack is not exactly an address (or a C/C++ pointer), but rather a reference to the item (a field in this sample).
As you probably guessed, valuetype CharArray8 Format is another MemberRef, to the field Format of type valuetype CharArray8. Because this MemberRef is not attributed to any TypeRef, it must be a global item. (The following section discusses declaration of global items.) In addition, this MemberRef is not attributed to any external resolution scope, such as [mscorlib]. Hence, it must be a global item defined somewhere in the current module.
ldsflda int32 Odd.or.Even::val is an instruction that loads the address of the static field val, member of the class Odd.or.Even, of type int32. But because the method we’re discussing is also a member of Odd.or.Even, why do we need to specify the full class name when referring to a member of the same class? Such are the rules of ILAsm: all references must be fully qualified. It might look a bit cumbersome, compared to most high-level languages, but it has its advantages. You don’t need to keep track of the context, and all references to the same item look the same throughout the source code.
Because both class Odd.or.Even and its field val have been declared by the time the field is referenced, the ILAsm compiler will not generate a MemberRef item but instead will use a FieldDef item.
call vararg int32 sscanf(string,int8*,...,int32*) is an instruction that calls the global static method sscanf. This method takes three items currently on the stack (the string returned from System.Console::ReadLine, the reference to the global field Format, and the reference to the field Odd.or.Even::val) and puts the result of type int32 onto the stack.
This method call has two major peculiarities. First, it is a call to an unmanaged method from the C runtime library. I’ll defer explanation of this issue until we discuss the declaration of this method. (I have a formal excuse for that because, after all, at the call site managed and unmanaged methods look the same.)
The second peculiarity of this method is its calling convention, vararg, which means that this method has a variable argument list. Vararg methods have some (or no) mandatory parameters, followed by an unspecified number of optional parameters of unspecified types—unspecified, that is, at the moment of the method declaration. When the method is invoked, all the mandatory parameters (if any) plus all the optional parameters used in this invocation (if any) should be explicitly specified.
Let’s take a closer look at the list of arguments in this call. The ellipsis refers to a pseudoargument of a special kind, known as a sentinel. A sentinel’s role can be formulated as “separating the mandatory arguments from the optional ones,” but I think it would be less ambiguous to say that a sentinel immediately precedes the optional arguments and that it is a prefix of the optional part of a vararg signature.
What is the difference? An ironclad common language runtime rule concerning the vararg method signatures dictates that a sentinel cannot be used when no optional arguments are specified. Thus a sentinel can never appear in MethodDef signatures—only mandatory parameters are specified when a method is declared—and it should not appear in call site signatures when only mandatory arguments are supplied. Signatures containing a trailing sentinel are illegal. That’s why I think it is important to look at a sentinel as the beginning of optional arguments and not as a separator between mandatory and optional arguments or (heaven forbid!) as the end of mandatory arguments.
For those less familiar with C runtime functions, I should note that the function sscanf parses and converts the buffer string (first argument) according to the format string (second argument), puts the results in the rest of the pointer arguments, and returns the number of successfully converted items. In our sample, only one item will be converted, so sscanf will return 1 on success or 0 on failure.
stloc Retval
ldloc Retval
brfalse Error
stloc Retval is an instruction that takes the result of the call to sscanf from the stack and stores it in the local variable Retval. We need to save this value in a local variable because we will need it later.
ldloc Retval copies the value of Retval back onto the stack. We need to check this value, which was taken off the stack by the stloc instruction.
brfalse Error takes an item from the stack and, if it is 0, branches (switches the computation flow) to the label Error.
ldsfld int32 Odd.or.Even::val
ldc.i4 1
and
brfalse ItsEven
ldstr "odd!"
br PrintAndReturn
ldsfld int32 Odd.or.Even::val is an instruction that loads the value of the static field Odd.or.Even::val onto the stack. If the code has proceeded this far, the string-to-integer conversion must have been successful, and the value that resulted from this conversion must be sitting in the field val. The last time we addressed this field, we used the instruction ldsflda to load the field address onto the stack. This time we need the value, so we use ldsfld.
ldc.i4 1 is an instruction that loads the constant 1 of type int32 onto the stack.
and takes two items from the stack—the value of the field val and the integer constant 1—performs a bitwise AND operation, and puts the result onto the stack. Performing the bitwise AND operation with 1 zeroes all the bits of the value of val except the least-significant bit.
brfalse ItsEven takes an item from the stack (the result of the bitwise AND operation) and, if it is 0, branches to the label ItsEven. The result of the previous instruction is 0 if the value of val is even, and 1 if the value is odd.
ldstr “odd!” is an instruction that loads the string odd! onto the stack.
br PrintAndReturn is an instruction that does not touch the stack and branches unconditionally to the label PrintAndReturn.
The rest of the code in the Odd.or.Even::check method should be clear. This section has covered all the instructions used in this method except ret, which is fairly obvious: it returns whatever is on the stack. If the method’s return type does not match the type of the item on the stack, the JIT compiler will disapprove, throw an exception, and abort the compilation. It will do the same if the stack contains more than one item by the time ret is reached or if the method is supposed to return void (that is, not return anything) and the stack still contains an item.
Global Items
{
} // End of namespace
.field public static valuetype CharArray8 Format at FormatData
.field public static valuetype CharArray8 Format at FormatData declares a static field named Format of type valuetype CharArray8. As you might remember, we used a reference to this field in the method Odd.or.Even::check.
This field differs from, for example, the field Odd.or.Even::val because it is declared outside any class scope and hence does not belong to any class in particular. It is thus a global item. Global items belong to the module containing their declarations. As you’ve learned, a module is a managed executable file (EXE or DLL); one or more modules constitute an assembly, which is the primary building block of a managed .NET application; and each assembly has one prime module, which carries the assembly identification information in its metadata.
Actually, a little trick is connected with the concept of global items not belonging to any class. In fact, the metadata of every module contains one special TypeDef named <Module>, which represents…any guesses? Yes, you are absolutely right.
This TypeDef is always present in the metadata, and it always holds the honorable first position in the TypeDef table. However, <Module> is not a proper TypeDef, because its attributes are very limited compared to “normal” TypeDefs (classes, value types, and so on). Sounds almost like real life—the more honorable the position you hold, the more limited are your options.
<Module> cannot be private. <Module> can have only static members, which means that all global fields and methods must be static. In addition, <Module> cannot have events or properties because events and properties cannot be static. (Consult Chapter 12, “Events and Properties,” for details.) The reason for this limitation is obvious: given that an assembly always contains exactly one instance of every module, the concept of instantiation becomes meaningless.
The accessibility of global fields and methods differs from the accessibility of member fields and methods belonging to a “normal” class. Even public global items cannot be accessed from outside the assembly. <Module> does not extend anything—that is, it has no base class—and no class can inherit from <Module>. However, all the classes declared within a module have full access to the global items of this module, including the private ones.
This last feature is similar to class nesting and is quite different from class inheritance. (Derived classes don’t have access to the private items of their base classes.) A nested class is a class declared within the scope of another class. That other class is usually referred to as an enclosing class, or an encloser. A nested class is not a member class or an inner class, in the sense that it has no implicit access to the encloser’s instance reference (this). A nested class is connected to its encloser by three facts only: it is declared within the encloser’s lexical scope; its visibility is “filtered” by the encloser’s visibility (that is, if the encloser is private, the nested class will not be visible outside the assembly, regardless of its own visibility); and it has access to all of the encloser’s members.
Because all the classes declared within a module are by definition declared within the lexical scope of the module, it is only logical that the relationship between the module and the classes declared in it is that of an encloser and nested classes.
As a result, global item accessibilities public, assembly, and famorassem all amount to assembly; private, family, and famandassem amount to private; and privatescope is—well, privatescope. The metadata validity rules explicitly state that only three accessibilities are permitted for the global fields and methods: public (which is actually assembly), private, and privatescope. The loader, however, is more serene about the accessibility flags of the global items: it allows any accessibility flags to be set, interpreting them as just described (as assembly, private, or privatescope).
Mapped Fields
.field public static valuetype CharArray8 Format at FormatData
The declaration of the field Format contains one more new item, the clause at FormatData. This clause indicates that the Format field is located in the data section of the module and that its location is identified by the data label FormatData. (Data declaration and labeling are discussed in the following section.)
This technique of mapping fields to data is widely used by the compilers for field initialization. It does have some limitations, however. First, mapped fields must be static. This is logical. After all, the mapping itself is static, as it is done at compile time. And even if you manage to map an instance field, all the different instances of this field will be physically mapped to the same memory, which means that you’ll wind up with a static field anyway. Because the loader, encountering a mapped instance field, decides in favor of “instanceness” and completely ignores the field mapping, the mapped instance fields are laid out just like all other instance fields.
Second, the mapped fields belong in the data section and hence are unreachable for the garbage collection subsystem of the common language runtime, which provides automatic disposal of unused objects. For this reason, mapped fields cannot be of a type that is subject to garbage collection (such as class or array). Value types are permitted as types of the mapped fields, as long as these value types have no members of types that are subject to garbage collection. If this rule is violated, the loader throws a Type Load exception and aborts loading the module.
Third, mapping a field to a predefined memory location leaves this field wide open to access and manipulation. This is perfectly fine from the point of view of security as long as the field does not have an internal structure whose parts are not intended for public access. That’s why the type of a mapped field cannot be any value type that has nonpublic member fields. The loader enforces this rule very strictly and checks for nonpublic fields all the way down. For example, if the type of a mapped field is value type A, the loader will check whether its fields are all public. If among these fields is one field of value type B, the loader will check whether value type B’s fields are also all public. If among these fields are two fields of value types C and D—well, you get the picture. If the loader finds a nonpublic field at any level in the type of a mapped field, it throws a Type Load exception and aborts the loading.
Data Declaration
.field public static valuetype CharArray8 Format at FormatData
.data FormatData = bytearray(25 64 00 00 00 00 00 00)
.data FormatData = bytearray(25 64 00 00 00 00 00 00) defines a data segment labeled FormatData. This segment is 8 bytes long, has ASCII codes of characters % (0x25) and d (0x64) in the first 2 bytes and 0s in the remaining 6 bytes.
The segment is described as bytearray, which is the most ubiquitous way to describe data in ILAsm. The numbers within the parentheses represent the hexadecimal values of the bytes, without the 0x prefix. The byte values should be space-separated, and I recommend that you always use the two-digit form, even if one digit would suffice (as in the case of 0, for example).
It is fairly obvious that you can represent literally any data as a bytearray. For example, instead of using the quoted string in the instruction ldstr "odd!", you could use a bytearray presentation of the string:
.field public static valuetype CharArray8 Format at FormatData
.data FormatData = bytearray(25 64 00 00 00 00 00 00)
The numbers in parentheses represent the Unicode characters o, d, d, !, and zero terminator. When you use ILDASM, you can see bytearrays everywhere. A bytearray is a universal, type-neutral form of data representation, and ILDASM uses it whenever it cannot identify the type associated with the data as one of the elementary types, such as int32.
On the other hand, the data FormatData could be defined as follows:
.data FormatData = int64(0x0000000000006425)
This would result in the same data segment size and contents. When you specify a type declaring a data segment (for instance, int64), no record concerning this type is entered into metadata or anywhere else. The ILAsm compiler uses the specified type for two purposes only: to identify the size of the data segment being allocated and to identify the byte layout within this segment.
Value Type as Placeholder
.field public static valuetype CharArray8 Format at FormatData
.data FormatData = bytearray(25 64 00 00 00 00 00 00)
.class public explicit CharArray8
extends [mscorlib]System.ValueType { .size 8 }
.class public explicit CharArray8 extends [mscorlib]System.ValueType { .size 8 } declares a value type that has no members but that has an explicitly specified size, 8 bytes. Declaring such a value type is a common way to declare “just a piece of memory.” In this case, we don’t need to declare any members of this value type because we aren’t interested in the internal structure of this piece of memory; we simply want to use it as a type of our global field Format, to specify the field’s size. In a sense, this value type is nothing but a placeholder.
Could we use an array of 8 bytes instead and save ourselves the declaration of another value type? We could if we did not intend to map the field to the data. Because arrays are subject to garbage collection, they are not allowed as types of mapped fields.
Using value types as placeholders is popular with managed C/C++ compilers because of the need to store and address numerous ANSI string constants. The Visual C# .NET and Visual Basic .NET compilers, which deal mostly with Unicode strings, are less enthusiastic about this technique because they can directly use the common language runtime string constants, which are stored in metadata in Unicode format.
Calling Unmanaged Code
.method public static pinvokeimpl("msvcrt.dll" cdecl)
vararg int32 sscanf(string,int8*) cil managed { }
The line .method public static pinvokeimpl(“msvcrt.dll” cdecl) vararg int32 sscanf(string,int8*) cil managed { } declares an unmanaged method, to be called from managed code. The attribute pinvokeimpl("msvcrt.dll" cdecl) indicates that this is an unmanaged method, called using the mechanism known as platform invocation, or P/Invoke. This attribute also indicates that this method resides in the unmanaged DLL Msvcrt.dll and has the calling convention cdecl. This calling convention means that the unmanaged method handles the arguments the same way an ANSI C function does.
The method takes two mandatory parameters of types string and int8* (the equivalent of C/C++ char*) and returns int32. Being a vararg method, sscanf can take any number of optional parameters of any type, but, as you know already, neither the optional parameters nor a sentinel is specified when a vararg method is declared.
Platform invocation is the mechanism the common language runtime provides to facilitate the calls from the managed code to unmanaged functions. Behind the scenes, the runtime constructs the so-called stub, or thunk, which allows addressing of the unmanaged function and conversion of managed argument types to appropriate unmanaged types and back. This conversion is known as parameter marshaling.
What is being declared here is not an actual unmanaged method to be called, but a stub generated by runtime, as it is seen from the managed code. Hence the implementation flags cil managed. Specifying the method signature as int32(string, int8*), we specify the “managed side” of parameter marshaling. The unmanaged side of the parameter marshaling is defined by the actual signature of the unmanaged method being invoked.
The actual signature of the unmanaged function sscanf in C is int sscanf(const char*, const char*, …). So the first parameter is marshaled from managed type string to unmanaged type char*. Recall that when we declared the class Odd.or.Even, we specified the ansi flag, which means that the managed strings by default are marshaled as ANSI C strings, that is, char*. And because the call to sscanf is made from a member method of class Odd.or.Even, we don’t need to provide special information about marshaling the managed strings.
Because the second parameter of the sscanf declaration is int8*, which is a direct equivalent of char*, little marshaling is required. (ILAsm has type char as well, but it indicates a Unicode character rather than ANSI, equivalent to “unsigned short” in C, so we cannot use this type here.)
The optional parameters of the original (unmanaged) sscanf are supposed to be the pointers to items (variables) we want to fill while parsing the buffer string. The number and base types of these pointers are defined according to the format specification string (the second argument of sscanf). In this case, given the format specification string "%d", sscanf will expect a single optional argument of type int*. When we call the managed thunk of sscanf, we provide the optional argument of type int32*, which might require marshaling to a native integer pointer only if we are dealing with a platform other than a 32-bit Intel platform (for example, an Alpha or Intel 64-bit platform).
The P/Invoke mechanism is very useful because it gives you full access to rich and numerous native libraries and platform APIs. But don’t overestimate the ubiquity of P/Invoke. Different platforms tend to have different APIs, so overtaxing P/Invoke can easily limit the portability of your applications. It’s better to stick with .NET Framework class library and take some consolation in the thought that by now you can make a fair guess about what lies at the bottom of this library.
Now that we’ve finished analyzing the source code, find the sample file Simple.il on the companion CD, copy it into your working directory, compile it using the console command ilasm simple (assuming that you have installed .NET Framework and the Platform SDK), and try to run the resulting Simple.exe.