《Metadata Tables》第4章 The Other Tables
4. The Other Tables
This chapter goes on to delineate the remaining tables that have not been touched upon and elucidated in the previous chapter. For this purpose, separate programs have been created to explicate each of the disparate tables. Finally, in the last chapter of the book they have all been put in a single program, wherein each one of them has been cross-referenced.
The first program in this chapter explores the Fields table. Enter the following code in the file b.cs and then compile it.
Fields
b.cs
public class zzz { public int i = 10; protected internal string vijay = "hi"; public static void Main() { } } namespace nnn { public class yyy { protected long k = 100; } }
a.cs
using System.Reflection; using System; using System.IO; using System.Configuration.Assemblies; public class zzz { public void DisplayGuid(int st) { Console.Write("{"); Console.Write("{0}{1}{2}{3}", guid[st + 2].ToString("X"), guid[st + 1].ToString("X"), guid[st].ToString("X"), guid[st - 1].ToString("X")); Console.Write("-{0}{1}-", guid[st + 3].ToString("X"), guid[st + 4].ToString("X")); Console.Write("{0}{1}-", guid[st + 6].ToString("X"), guid[st + 5].ToString("X")); Console.Write("{0}{1}-", guid[st + 7].ToString("X"), guid[st + 8].ToString("X")); Console.Write("{0}{1}{2}{3}{4}{5}", guid[st + 9].ToString("X"), guid[st + 10].ToString("X"), guid[st + 11].ToString("X"), guid[st + 12].ToString("X"), guid[st + 13].ToString("X"), guid[st + 14].ToString("X")); Console.Write("}"); } public string GetString(int starting) { int i = starting; while (strings[i] != 0) { i++; } System.Text.Encoding e = System.Text.Encoding.UTF8; string s = e.GetString(strings, starting, i - starting); return s; } public static void Main() { zzz a = new zzz(); a.abc(); } string[] tablenames = new String[]{ "Module" , "TypeRef" , "TypeDef" ,"FieldPtr","Field", "MethodPtr","Method","ParamPtr" , "Param", "InterfaceImpl", "MemberRef", "Constant", "CustomAttribute", "FieldMarshal", "DeclSecurity", "ClassLayout", "FieldLayout", "StandAloneSig" , "EventMap","EventPtr", "Event", "PropertyMap", "PropertyPtr", "Properties","MethodSemantics", "MethodImpl","ModuleRef","TypeSpec", "ImplMap","Field RVA","ENCLog","ENCMap","Assembly","AssemblyProcessor", "AssemblyOS","AssemblyRef","AssemblyRefProcessor", "AssemblyRefOS", "File","ExportedType","ManifestResource", "NestedClass","TypeTyPar","MethodTyPar" }; int tableoffset; int[] rows; int[] offset; int[] ssize; byte[] metadata; byte[] strings; byte[] us; byte[] guid; byte[] blob; long valid; byte[][] names; long sm; public void abc() { long startofmetadata; FileStream s = new FileStream("C:\\mdata\\b.exe", FileMode.Open); BinaryReader r = new BinaryReader(s); s.Seek(360, SeekOrigin.Begin); int rva, size; rva = r.ReadInt32(); size = r.ReadInt32(); int where = rva % 0x2000 + 512; s.Seek(where + 4 + 4, SeekOrigin.Begin); rva = r.ReadInt32(); where = rva % 0x2000 + 512; s.Seek(where, SeekOrigin.Begin); startofmetadata = s.Position; sm = startofmetadata; s.Seek(4 + 2 + 2 + 4 + 4 + 12 + 2, SeekOrigin.Current); int streams = r.ReadInt16(); offset = new int[5]; ssize = new int[5]; names = new byte[5][]; names[0] = new byte[10]; names[1] = new byte[10]; names[2] = new byte[10]; names[3] = new byte[10]; names[4] = new byte[10]; int i = 0; int j; for (i = 0; i < streams; i++) { offset[i] = r.ReadInt32(); ssize[i] = r.ReadInt32(); j = 0; byte bb; while (true) { bb = r.ReadByte(); if (bb == 0) break; names[i][j] = bb; j++; } names[i][j] = bb; while (true) { if (s.Position % 4 == 0) break; byte b = r.ReadByte(); if (b != 0) { s.Seek(-1, SeekOrigin.Current); break; } } } for (i = 0; i < streams; i++) { if (names[i][1] == '~') { metadata = new byte[ssize[i]]; s.Seek(startofmetadata + offset[i], SeekOrigin.Begin); for (int k = 0; k < ssize[i]; k++) metadata[k] = r.ReadByte(); } if (names[i][1] == 'S') { strings = new byte[ssize[i]]; s.Seek(startofmetadata + offset[i], SeekOrigin.Begin); for (int k = 0; k < ssize[i]; k++) strings[k] = r.ReadByte(); } if (names[i][1] == 'U') { us = new byte[ssize[i]]; s.Seek(startofmetadata + offset[i], SeekOrigin.Begin); for (int k = 0; k < ssize[i]; k++) us[k] = r.ReadByte(); } if (names[i][1] == 'G') { guid = new byte[ssize[i]]; s.Seek(startofmetadata + offset[i], SeekOrigin.Begin); for (int k = 0; k < ssize[i]; k++) guid[k] = r.ReadByte(); } if (names[i][1] == 'B') { blob = new byte[ssize[i]]; s.Seek(startofmetadata + offset[i], SeekOrigin.Begin); for (int k = 0; k < ssize[i]; k++) blob[k] = r.ReadByte(); } } valid = BitConverter.ToInt64(metadata, 8); tableoffset = 24; rows = new int[64]; Array.Clear(rows, 0, rows.Length); int cnt = 0; for (int k = 0; k <= 63; k++) { int tablepresent = (int)(valid >> k) & 1; if (tablepresent == 1) { cnt = cnt + 1; rows[k] = BitConverter.ToInt32(metadata, tableoffset); Console.WriteLine("Table {0} present at ind {1} - {2}, Rows in table {3}", cnt, k, tablenames[k], rows[k]); tableoffset += 4; } } xyz(); } public bool tablepresent(byte i) { int p = (int)(valid >> i) & 1; byte[] sizes = { 10, 6, 14, 2, 6, 2, 14, 2, 6, 4, 6, 6, 6, 4, 6, 8, 6, 2, 4, 2, 6, 4, 2, 6, 6, 6, 2, 2, 8, 6, 8, 4, 22, 4, 12, 20, 6, 14, 8, 14, 12, 4 }; for (int j = 0; j < i; j++) { int o = sizes[j] * rows[j]; tableoffset = tableoffset + o; } if (p == 1) return true; else return false; } public void xyz() { int new1 = tableoffset; bool b = tablepresent(4); int offs = tableoffset; tableoffset = new1; if (b) { Console.WriteLine(""); Console.WriteLine("Field Details"); for (int k = 1; k <= rows[4]; k++) { FieldAttributes flags = (FieldAttributes)BitConverter.ToInt16(metadata, offs); offs += 2; int name = BitConverter.ToInt16(metadata, offs); offs += 2; int sig = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row {0}", k); Console.WriteLine("Flags: {0}", flags); Console.WriteLine("Name : {0}", GetString(name)); int count = blob[sig]; Console.Write("Signature [{0}]:Count={1} ", sig, count); if (blob[sig + 1] == 0x06) { Console.Write("Type {0}", GetType(blob[sig + 2])); } Console.WriteLine(); } } } public string GetType(int b) { if (b == 0x01) return "void"; if (b == 0x02) return "boolean"; if (b == 0x03) return "char"; if (b == 0x04) return "byte"; if (b == 0x05) return "ubyte"; if (b == 0x06) return "short"; if (b == 0x07) return "ushort"; if (b == 0x08) return "int"; if (b == 0x09) return "uint"; if (b == 0x0a) return "long"; if (b == 0x0b) return "ulong"; if (b == 0x0c) return "float"; if (b == 0x0d) return "double"; if (b == 0x0e) return "string"; return "unknown"; } }
Output
Table 1 present at ind 0 - Module, Rows in table 1
Table 2 present at ind 1 - TypeRef, Rows in table 2
Table 3 present at ind 2 - TypeDef, Rows in table 3
Table 4 present at ind 4 - Field, Rows in table 3
Table 5 present at ind 6 - Method, Rows in table 3
Table 6 present at ind 10 - MemberRef, Rows in table 2
Table 7 present at ind 12 - CustomAttribute, Rows in table 1
Table 8 present at ind 32 - Assembly, Rows in table 1
Table 9 present at ind 35 - AssemblyRef, Rows in table 1
Field Details
Row 1
Flags: Public
Name : i
Signature [10]:Count=2 Type int
Row 2
Flags: FamORAssem
Name : vijay
Signature [13]:Count=2 Type string
Row 3
Flags: Family
Name : k
Signature [24]:Count=2 Type long
An instance variable is also known as a field. The Field table holds an index of 4 in the valid table. The output shows a count of 3 rows, since the file contains 3 fields spread over 2 classes named zzz and yyy.
The Field table comprises of the following columns:
The first column in the Field table is the FieldAttributes flags. The enum of FieldAttributes displays the string assigned to the number. The second column in the table refers to the name of the field. It is an index to the data contained in the strings stream. The output clearly displays the fact that the fields i and vijay of class zzz are placed earlier, suffixed with the field k from the class yyy in the namespace nnn. This sequence is of utmost significance, as shall be demonstrated by us shortly.
The last field is an index into the Blob heap. It starts with a count byte of 2, thereby indicating that the field signature has a size of 2 bytes. The first byte in the signature of a method establishes the calling convention. Similarly, the first byte in the signature is always 0x06, thereby indicating that it is a field signature. This primarily serves as a sanity check.
This value is followed by the signature byte. It refers to the type of the field. To ascertain its value, we have introduced the function GetType, which returns the data type. As a consequence of this function, the output appropriately reflects the data type of the fields. Section 22.1.15 describes the bits representation of each type. We have saved up the explanation of the especially complicated ones for a rainy day.
Let us revert to the flags byte. The 'protected' accessibility modifier, which allows access only to derived classes, is known as 'Family' in the IL world. The 'internal' access modifier, which restricts access to the same assembly, is christened as 'Assembly' in the IL word.
Life surely would have been significantly more cushy if C# had also resorted to terminology similar to that of IL.
Had we called off our explanation at this juncture, it would have become impossible for us to expose you to the cross-linkages between the tables. So, we have augmented the program with the requisite code essential for the Typedef details.
After calling the xyz function, call the aaa function also, as in xyz(); aaa();
Place the following code prior to the GetType function.
a.cs
public void aaa() { int new1 = tableoffset; bool b = tablepresent(2); int offs = tableoffset; tableoffset = new1; if (b) { Console.WriteLine("TypeDef Details"); for (int k = 1; k <= rows[2]; k++) { TypeAttributes flags = (TypeAttributes)BitConverter.ToInt32(metadata, offs); offs += 4; int name = BitConverter.ToInt16(metadata, offs); offs += 2; int nspace = BitConverter.ToInt16(metadata, offs); offs += 2; int cindex = BitConverter.ToInt16(metadata, offs); offs += 2; int findex = BitConverter.ToInt16(metadata, offs); offs += 2; int mindex = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row:{0}", k); Console.WriteLine("Flags : {0}", flags); Console.WriteLine("Name : {0}", GetString(name)); Console.WriteLine("NameSpace : {0}", GetString(nspace)); Console.Write("Extends:"); int u = cindex & 3; if (u == 0) Console.Write("TypeDef"); if (u == 1) Console.Write("TypeRef"); if (u == 2) Console.Write("TypeSpec"); Console.Write("[{0}]", cindex >> 2); Console.WriteLine(); Console.WriteLine("FieldList Field[{0}]", findex); Console.WriteLine("MethodList Method[{0}]", mindex); } } }
Output
TypeDef Details
Row:1
Flags : Class
Name : <Module>
NameSpace :
Extends:TypeDef[0]
FieldList Field[1]
MethodList Method[1]
Row:2
Flags : AutoLayout, AnsiClass, NotPublic, Public, BeforeFieldInit
Name : zzz
NameSpace :
Extends:TypeRef[1]
FieldList Field[1]
MethodList Method[1]
Row:3
Flags : AutoLayout, AnsiClass, NotPublic, Public, BeforeFieldInit
Name : yyy
NameSpace : nnn
Extends:TypeRef[1]
FieldList Field[3]
MethodList Method[3]
The output of the above program now exhibits the data contained in the TypeDef table, wherein, 3 rows of the table are displayed. Each row of the Field table is owned by one row in the TypeDef table. This is so because the TypeDef table defines a class and the fields belong to a class.
There is no indication as to which Type or class owns the field in the Field table. To determine these linkages, the TypeDef table is examined. The first row represents the global or pseudo class, which can be ignored for the moment. The second row represents the zzz class. The FieldList column in this row points to the first row of the Fields table.
Since the first row in the Fields table represents the variable i, it is logical to conclude that the variable i belongs to the zzz class.
The third row of the TypeDef table represents the class yyy in the namespace nnn. The value for the FieldList field points to the third row of the Field table. Thus, we can safely presume that the first two rows are owned by the class zzz, whereas, from the third row onwards, the fields belong to the class yyy. A row in the Field table can be owned by only one class from the TypeDef table.
This forward pointer method helps in determining the owner of the Field table. Employing this approach, the Fields in a class can be established by reading the FieldList column in the row. All rows in the Fields table belong to one type, until we reach the value given in the FieldList column of the next row. A point to be noted here is that, there can be zero or multiple rows in the Field table. The type encompasses all of them.
This behaviour is akin to a parent-child or one-many relationship wherein, a parent type can have multiple children fields, whereas, a child field can possess only one parent type.
If there exist two instance variables of fields with the same name, but located in different classes, it results in the creation of two separate rows in the Field table, each owned by a different TypeDef row. The same rule is applicable to methods also.
Method Table
Row 1
Name : Main
Row 2
Name : .ctor
Row 3
Name : .ctor
We have displayed only the method names of every row in the method table, since our primary focus at present is on the Field table. Since there are two classes in the file, two constructors are visible. Hence, the method name of .ctor is also displayed twice.
Both the rows 2 and 3 represent a constructor. So, how do we ascertain as to which class each constructor belongs to? Bear in mind that while espying the type that lodges the constructors, you should always begin with the TypeDef table, and not with either the Field table or the Method table. This approach is at variance with the one pursued for the MethodRef table, which has a TypeRef field that reveals the class namespace data.
The type zzz uses a field named MethodList, which has an index value of 1. The second class i.e. yyy has the MethodList with a value of 3. Thus, the first two rows of the Method table belong to the class zzz, while the third row forms a part of the class yyy.
Constant Table
b.cs
public class zzz { const int i1 = 20; const string vijay = "hi"; public static void Main() { } }
a.cs
public void xyz() { int new1 = tableoffset; bool b = tablepresent(11); int offs = tableoffset; tableoffset = new1; if (b) { for (int k = 1; k <= rows[11]; k++) { byte dtype = metadata[offs]; offs += 2; int parent = BitConverter.ToInt16(metadata, offs); offs += 2; int value = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row {0}", k); Console.WriteLine("Type {0}", GetType(dtype)); int tag = parent & 0x03; int rid = (int)((uint)parent >> 2); Console.Write("Parent: "); if (tag == 0) Console.Write("FieldDef"); if (tag == 1) Console.Write("ParamDef"); if (tag == 2) Console.Write("Property"); Console.WriteLine("[{0}]", rid); int count = blob[value]; Console.WriteLine("Value Blob[{0}] Count {1}", value, count); for (int l = 1; l <= count; l++) { Console.Write("{0} ", blob[value + l].ToString("X")); } Console.WriteLine(); } } }
Output
Row 1
Type int
Parent: FieldDef[1]
Value Blob[13] Count 4
14 0 0 0
Row 2
Type string
Parent: FieldDef[2]
Value Blob[21] Count 4
68 0 69 0
Field Details
Row 1
Flags: -32687
Name : i1
Signature [10]:Count=2 Type int
Row 2
Flags: -32687
Name : vijay
Signature [18]:Count=2 Type string
The Constant table has the following columns:
The constant table is at the 11th position in the valid fields, and as its name indicates, it stores the constants that are created in the module.
A constant is also a field; therefore, a corresponding entry gets appended to the Field table. The tables, fields and constants have been displayed, to enable you to refer to them.
The first field in the table refers to the data type of constant. It is a single byte; therefore, the next byte, which contains a value of zero, is used as a padding byte. The trusted GetType function is used to display the type as a readable string.
The next field parent is a HasConst coded index, where the first two bits encode a table and can either be a Field, or a Param or a Property table.
The residual six bits store the index. In this case, both the constants are an index to the Field Table.
The Field table stores the name and the signature. The signature field in the Field table provides the same information as does the type. However, the flags field displays a number and not a string. Thus, the name and the flags of the constant emanate from the Field table.
The last field stores the actual value assigned to the constant. The first byte in this field is the length. If it is an integer, the next four bytes are picked up; however, if it is a string, the length of the string in Unicode is utilized, and not ASCII. The Blob heap is used by the compiler to store the value of the constant.
It is for this very reason that the constants need to be determined at compile time, and not at run time.
Nested Classes
b.cs
public class zzz { class yyy { public int j, k; class xxx { public int i; } } public static void Main() { } }
a.cs
public void xyz() { int new1 = tableoffset; bool b = tablepresent(41); int offs = tableoffset; tableoffset = new1; if (b) { for (int k = 1; k <= rows[41]; k++) { int nestedclass = BitConverter.ToInt16(metadata, offs); offs += 2; int enclosingclass = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row {0}", k); Console.WriteLine("Nested Class TypeDef[{0}]", nestedclass); Console.WriteLine("Enclosing Class TypeDef[{0}]", enclosingclass); } } }
Output
Row 1
Nested Class TypeDef[3]
Enclosing Class TypeDef[2]
Row 2
Nested Class TypeDef[4]
Enclosing Class TypeDef[3]
TypeDef Details
Row:1
Flags : Class
Name : <Module>
NameSpace :
Extends:TypeDef[0]
FieldList Field[1]
MethodList Method[1]
Row:2
Flags : AutoLayout, AnsiClass, NotPublic, Public, BeforeFieldInit
Name : zzz
NameSpace :
Extends:TypeRef[1]
FieldList Field[1]
MethodList Method[1]
Row:3
Flags : AutoLayout, AnsiClass, NotPublic, NestedPrivate, BeforeFieldInit
Name : yyy
NameSpace :
Extends:TypeRef[1]
FieldList Field[1]
MethodList Method[3]
Row:4
Flags : AutoLayout, AnsiClass, NotPublic, NestedPrivate, BeforeFieldInit
Name : xxx
NameSpace :
Extends:TypeRef[1]
FieldList Field[3]
MethodList Method[4]
In the b.cs file, the class zzz encloses the class yyy, which is perfectly legal in the C# world. This concept of enclosing one class within another is termed as 'nesting classes'. The class yyy in turn, contains a nested class named xxx. This too is permissible.
For every nested class, one row gets added to the table Nested Classes, which has an index position of 41. In terms of size, this is the smallest of the tables encountered so far. It contains only two indexes. Both these indexes point to the TypeDef table, which in turn, defines a class.
The TypeDef table contains four rows. Thus, a total of four classes dwell within the file. Apart from one pseudo class, there exist three more classes, which have obviously been created in the file b.cs. Thus, in the TypeDef table, a nested class is a class in its own right.
It is the flags field in the TypeDef table that identifies the class as a nested one, since the NestedPrivate bit is set ON. Further, the three classes are depicted as extending from the class System.Object.
Reverting to the Nested classes table, the first field in the table is the name of the nested class. Therefore, row 1 refers to the third index into the TypeDef table of class yyy and the second row points to the fourth row of class xxx.
The second field in the table is the Enclosing field, which identifies the main class that encloses the nested one. Since the class yyy is nested within the class zzz, the field shows a value of 2, thereby referring to the second row in the TypeDef table. The second class xxx is shown nested within the class yyy, or in the third row.
Thus, the nested classes table is simple to comprehend, as it only stores references to a class and its enclosing class. Both of them index the TypeDef table.
A nested class is defined as being lexically within the text of the enclosing class. However, when no nested class exists in the program module, the nested classes table bit is marked off, thus banishing all traces of the table and the fact that it ever existed.
The two fields of the nested classes must reference a valid row in the TypeDef table, or else it is treated as an error. Furthermore, the Enclosing Class field cannot reference a valid row in the TypeRef table, which shows the type references. Moreover, if the Nested Class and Enclosing class share the same values, a warning is emitted, but not an error.
No two rows can possibly possess the same value for the nested class field. This is the case only with the enclosing type, since multiple nested classes can be enclosed within a single class. A single type may have innumerable nested classes within it, but the inverse is not permitted.
Param Table
b.cs
public class zzz { public static void Main() { } public void abc() { } public long pqr(int i, out byte z) { z = 10; return 0; } public bool xyz(ref byte j, string k, long u) { return true; } }
a.cs
public void xyz() { int new1 = tableoffset; bool b = tablepresent(8); int offs = tableoffset; tableoffset = new1; if (b) { for (int k = 1; k <= rows[8]; k++) { short pattr = BitConverter.ToInt16(metadata, offs); offs += 2; int sequence = BitConverter.ToInt16(metadata, offs); offs += 2; int name = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row {0}", k); Console.WriteLine("ParamAttributes {0} Bytes {1}", GetParamAttributes(pattr), pattr.ToString("X")); Console.WriteLine("Sequence {0}", sequence); Console.WriteLine("Name {0}", GetString(name)); } } } public string GetParamAttributes(short a) { if (a == 0x00) return "None"; if ((a & 0x01) == 0x01) return "[In]"; if ((a & 0x02) == 0x02) return "[Out]"; if ((a & 0x04) == 0x04) return "[Optional]"; if ((a & 0x1000) == 0x1000) return "[Default]"; if ((a & 0x2000) == 0x2000) return "[Field Marshal]"; if ((a & 0xcfe0) == 0xcfe0) return "[Field Marshall]"; return "Unknown"; } public void aaa() { int new1 = tableoffset; bool b = tablepresent(6); int offs = tableoffset; tableoffset = new1; if (b) { for (int k = 1; k <= rows[6]; k++) { int rva = BitConverter.ToInt32(metadata, offs); offs += 4; MethodImplAttributes impflags = (MethodImplAttributes)BitConverter.ToInt16(metadata, offs); offs += 2; int flags = (int)BitConverter.ToInt16(metadata, offs); offs += 2; int name = BitConverter.ToInt16(metadata, offs); offs += 2; int signature = BitConverter.ToInt16(metadata, offs); offs += 2; int param = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row {0}", k); Console.WriteLine("RVA :{0}", rva.ToString("X")); Console.WriteLine("Name : {0}", GetString(name)); Console.WriteLine("ImpFlags :{0}", impflags); Console.WriteLine("Flags :{0}", flags.ToString("X")); Type t = typeof(System.Reflection.MethodAttributes); FieldInfo[] f = t.GetFields(BindingFlags.Public | BindingFlags.Static); for (int i = 0; i < f.Length; i++) { int fv = (int)f[i].GetValue(null); if ((fv & flags) == fv) Console.Write("{0} ", f[i].Name); } Console.WriteLine(); Console.WriteLine("Signature: #Blob[{0}]", signature); byte count = blob[signature]; Console.Write("Blob:{0} Count:{1} Bytes ", signature, count); for (int l = 1; l <= count; l++) { Console.Write("{0} ", blob[signature + l].ToString("X")); } Console.WriteLine(); Console.WriteLine("ParamList: Param[{0}]", param); Console.WriteLine(); } } }
Output
Row 1
ParamAttributes None Bytes 0
Sequence 1
Name i
Row 2
ParamAttributes [Out] Bytes 2
Sequence 2
Name z
Row 3
ParamAttributes None Bytes 0
Sequence 1
Name j
Row 4
ParamAttributes None Bytes 0
Sequence 2
Name k
Row 5
ParamAttributes None Bytes 0
Sequence 3
Name u
Row 1
RVA :2050
Name : Main
ImpFlags :Managed
Flags :96
PrivateScope FamANDAssem Family Public Static HideBySig ReuseSlot
Signature: #Blob[10]
Blob:10 Count:3 Bytes 0 0 1
ParamList: Param[1]
Row 2
RVA :2060
Name : abc
ImpFlags :Managed
Flags :86
PrivateScope FamANDAssem Family Public HideBySig ReuseSlot
Signature: #Blob[14]
Blob:14 Count:3 Bytes 20 0 1
ParamList: Param[1]
Row 3
RVA :2070
Name : pqr
ImpFlags :Managed
Flags :86
PrivateScope FamANDAssem Family Public HideBySig ReuseSlot
Signature: #Blob[18]
Blob:18 Count:6 Bytes 20 2 A 8 10 5
ParamList: Param[1]
Row 4
RVA :2088
Name : xyz
ImpFlags :Managed
Flags :86
PrivateScope FamANDAssem Family Public HideBySig ReuseSlot
Signature: #Blob[25]
Blob:25 Count:7 Bytes 20 3 2 10 5 E A
ParamList: Param[3]
Row 5
RVA :209C
Name : .ctor
ImpFlags :Managed
Flags :1886
PrivateScope FamANDAssem Family Public HideBySig ReuseSlot SpecialName RTSpecialName
Signature: #Blob[14]
Blob:14 Count:3 Bytes 20 0 1
ParamList: Param[6]
The file b.cs now contains three functions, viz. abc with no parameters, pqr with two parameters and xyz with three parameters. The parameters assigned to the methods get stored in the Param Table, which has an index position of 8 in the valid field. Since there are a total of 5 parameters, the Param table displays a count of 5 rows.
The Param table has the following three fields:
The first field of FlagAttributes describes the attributes assigned to the function parameters.
For this purpose, a special function named GetParamAttributes is provided, whose sole task is to return a string, depending upon the bit that is switched ON in the flag byte.
The second field is a sequence number, while the third field is the name of the parameter.
Let us first take a closer peek at the method table. The Rows 2 and 3 stand for the methods abc and pqr, respectively. They both point to Row 1 of the ParamList. It appears deceptive at this stage, since the method abc does not take any parameters, while the method pqr takes two parameters. The constructor in Row 1 also takes no parameters, and yet, it points to the first row in the parameter list.
However, as we have learnt in the previous chapter, the second byte of the Blob heap must be examined to determine the actual number of parameters that the function is being passed. Neither the constructor nor the abc function takes any parameters, since the value specified in the second byte is 0.
The pqr method takes two parameters. Thus, in the Param Table, the parameters for the function are present from the first row onwards. The sequence number identifies the ordering of the parameters, which is why the first parameter i has a value of 1, the second parameter z has a value of 2, and so on.
The fourth row of the method table represents the method xyz. The number of parameters in the Blob heap is shown as 3, thereby referring to the third row in the Param table. The third row in the Param table has the parameter named j with the sequence number as 1. The next row for parameter k has a sequence number of 2.
The sequence number thus provides the order or the sequence of the parameters. It starts with 1, and thereafter, gets reset to 1 for every new method. The fitting approach is that the method table's Blob field is read first, in order to determine the number of parameters and its type. Then, depending upon the value in the Params field, the appropriate row in the Params table is accessed. The Params table provides the name of the parameter, its attribute and the order in the param list. Thereafter, contingent on the number of parameters in the Blob field, the next set of rows is picked up from the Params table.
The number of parameters can be re-ascertained by examining the value in the sequence number. For reasons unknown, the attribute bits are not set accurately.
The IN parameter is the default in C#. The OUT parameter is employed when the calling function is authorized to modify the value of the parameters. Ref is a variation in C#. It is considered to be a variant of IN. Therefore, it does not display the attribute of OUT. However, as per our interpretation, Ref is a variant of OUT. You may blame this misconception on our lack of insight. Another possibility could be that the C# compiler may be taking a break.
Conceptually, every row in the method table owns one row in the param table, with the singular exception of Row 1. A row cannot have two owners in the method table. Thus, if there are two functions abc and pqr with one parameter, i.e. an int i, there will be two identical rows in the param table. This is not considered erroneous at all, since duplicate rows are acceptable.
The sequence number can be zero, signifying the owner's methods return type. The sequence numbers are arranged as per increasing sequence values. Resultantly, there will be omissions in the sequence numbers, which is perfectly valid.
The parameters in the .Net world cannot have default values. So, the HasDefault flag will always be zero.
Properties Table
b.cs
public class zzz { public int aa { set { } get { return 10; } } public string bb { set { } get { return "hi"; } } public static void Main() { } } public class yyy { public byte cc { set { } get { return 10; } } }
a.cs
public void xyz() { int new1 = tableoffset; int old = tableoffset; bool b = tablepresent(21); int offs = tableoffset; Console.WriteLine("Properties Map Table "); if (b) { for (int k = 1; k <= rows[21]; k++) { short parent = BitConverter.ToInt16(metadata, offs); offs += 2; short propertylist = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row {0}", k); Console.WriteLine("Parent TypeDef[{0}]", parent); Console.WriteLine("PropertyList Property[{0}]", propertylist); } } Console.WriteLine("Properties Table"); tableoffset = old; b = tablepresent(23); offs = tableoffset; if (b) { for (int k = 1; k <= rows[23]; k++) { PropertyAttributes flags = (PropertyAttributes)BitConverter.ToInt16(metadata, offs); offs += 2; int name = BitConverter.ToInt16(metadata, offs); offs += 2; int type = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row {0}", k); Console.WriteLine("Flags [{0}]", flags); Console.WriteLine("Name {0}", GetString(name)); int count = blob[type]; Console.Write("Type BLOB[{0}] Count={1} ", type, count); for (int l = 1; l <= count; l++) { Console.Write("{0} ", blob[type + l].ToString("X")); } Console.WriteLine(); } } Console.WriteLine("MethodSematics Table"); tableoffset = old; b = tablepresent(24); offs = tableoffset; if (b) { for (int k = 1; k <= rows[24]; k++) { short methodsemanticsattributes = BitConverter.ToInt16(metadata, offs); offs += 2; int methodindex = BitConverter.ToInt16(metadata, offs); offs += 2; int association = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row {0}", k); Console.WriteLine("Semantics {0} ", GetMethodSemantics(methodsemanticsattributes)); Console.WriteLine("Method Method[{0}]", methodindex); int tag = association & 0x01; Console.Write("Association "); if (tag == 0) Console.Write("Events"); if (tag == 1) Console.Write("Properties"); int riid = association >> 1; Console.WriteLine("[{0}]", riid); } } tableoffset = new1; } public string GetMethodSemantics(short a) { string s = ""; if ((a & 0x01) == 0x01) s = s + "Setter"; if ((a & 0x02) == 0x02) s = s + "Getter"; if ((a & 0x04) == 0x04) s = s + "Other"; if ((a & 0x08) == 0x08) s = s + "Event Addon"; if ((a & 0x10) == 0x10) s = s + "Event Remove"; if ((a & 0x20) == 0x20) s = s + "Event Fire"; return s;
Output
Properties Map Table
Row 1
Parent TypeDef[2]
PropertyList Property[1]
Row 2
Parent TypeDef[3]
PropertyList Property[3]
Properties Table
Row 1
Flags [None]
Name aa
Type BLOB[36] Count=3 28 0 8
Row 2
Flags [None]
Name bb
Type BLOB[40] Count=3 28 0 E
Row 3
Flags [None]
Name cc
Type BLOB[53] Count=3 28 0 5
MethodSematics Table
Row 1
Semantics Getter
Method Method[2]
Association Properties[1]
Row 2
Semantics Setter
Method Method[1]
Association Properties[1]
Row 3
Semantics Getter
Method Method[4]
Association Properties[2]
Row 4
Semantics Setter
Method Method[3]
Association Properties[2]
Row 5
Semantics Getter
Method Method[8]
Association Properties[3]
Row 6
Semantics Setter
Method Method[7]
Association Properties[3]
As always, let us commence with the b.cs file. In the class zzz, there exist two properties, viz. aa and bb; while in the class yyy, there exists a single property named cc.
In a.cs, prior to exhibiting the values in the Property table, we initially display the details of the table Properties Map. This table is at the 21st position in the valid table.
The value of the tableoffset variable is also stored in the variable old, apart from new1.
The PropertyMap table possesses the following two columns:
The first field is called the parent, which is an index into the TypeDef table. Since there are two classes that contain properties within them, two rows come into view. The first row points to the second row in the TypeDef table zzz. The parent field in the second row points to the third row in the TypeDef table yyy.
The second field of the Properties Map table indexes into the Properties table. The table is displayed below.
The Property ( 0x17 ) table has the following columns:
The Properties Table is the 23rd bit in the valid field. The first field in this table is an Enum of PropertyAttributes. The second is the name of the property. The third field is a series of bytes in the Blob.
Let us revert to the Properties Map table. The first row for the class zzz has an index value of 1 in the Properties table, whereas, the second row has the index value of 3. Since the value is not 2, it means that the first two rows of the properties table are owned by TypeDef[2], or the class zzz.
The properties table includes one row for each property. Thus, one link is TypeDef-PropertyMap-Property. Now, let us examine the rows in the Method Table.
Method Table
Row 1
Name : set_aa
Signature: #Blob[10]
Blob:10 Count:4 Bytes 20 1 1 8
ParamList: Param[1]
Row 2
Name : get_aa
Signature: #Blob[15]
Blob:15 Count:3 Bytes 20 0 8
ParamList: Param[2]
Row 3
Name : set_bb
Signature: #Blob[19]
Blob:19 Count:4 Bytes 20 1 1 E
ParamList: Param[2]
Row 4
Name : get_bb
Signature: #Blob[24]
Blob:24 Count:3 Bytes 20 0 E
ParamList: Param[3]
Row 5
Name : Main
Row 6
Name : .ctor
Row 7
Name : set_cc
Signature: #Blob[44]
Blob:44 Count:4 Bytes 20 1 1 5
ParamList: Param[3]
Row 8
Name : get_cc
Signature: #Blob[49]
Blob:49 Count:3 Bytes 20 0 5
ParamList: Param[4]
Row 9
Name : .ctor
Signature: #Blob[32]
Blob:32 Count:3 Bytes 20 0 1
ParamList: Param[4]
Param Table
Row 1
Name value
Row 2
Name value
Row 3
Name value
This table is adorned with nine functions. Hey, wait a minute! We expected only three functions, viz. a Main and a constructor each for the classes zzz and yyy. Now, the moment of truth has dawned upon us. For each occurrence of a property called aa, two methods get created: one called set_aa for the set accessor, and the other known as get_aa, for the get accessor.
Since three properties prevail within the file, a total of 6 functions get created; and in the wake of it, 6 rows get added to the method table.
Although the C# programming language is capable of comprehending properties, they subsist in the form of functions in the IL world. Thus, all properties get transformed into simple function calls. The set function or the set accessor is passed one parameter, as the signature represents this introduction. This parameter named 'value' is the row 1 in the Param table.
The people who designed the C# compiler chose to call the parameter by no other name but 'value'.
The set_aa function is passed a parameter called 'value', with the type being set to the type of the property, as indicated by the signature. The get accepts no parameters, and even though the ParamList field makes a mention of an index in the param table, it can safely be ignored. The signature of the method is to be read first. The Param table owns only a single parameter called value, despite the existence of three rows, one for each property.
So far so good, but the link between the Properties table and the method table is conspicuous by its absence. This relation is perceptible in a table called the MethodSematics, which has a bit position of 24 in the valid field.
The MethodSemantics table has the following columns:
This table commences with a two-byte attributes mask. We have created a function called GetMethodSemantics, which checks if the bits are ON or not. The whole idea behind creating functions is that they may be of utility in the future too.
In this function, the coding is done using a slightly unusual technique. On most occasions, a combination of bits may be ON. Till now, an inspection was being carried out in order to verify if a specific bit was ON or not, and accordingly, a value was returned. However, this approach proves ineffective if we aspire to establish whether multiples bits are ON or not.
Innovation is the order of the day! Accordingly, in the function, we keep adding or concatenating to the string 's' in case the bit is ON. The final outcome is that the attribute flag furnishes information as to whether it is a 'getter' or a 'setter' function. The possible values for this mask are stipulated below.
The second field in the semantics table points to a row in the method table. Thus, the first row in the MethodSematics table is a 'getter'. It refers to Row 2 in the method table, which represents the function get_aa.
The last field called 'association' is a link to the properties table. It wields a coded index of 1 bit, thereby resulting in a value of 1. Since the first row of the properties table is the property aa, it associates the getter flag with this property.
Thus, the deficient link between the method and the properties is established with the help of the MethodSematics table. The field association is considerably more complex and it deals with events as well. We shall inquire into it in a little while from now.
To sum up, the PropertiesMap table talks about the classes in the typedef table, which own properties in the Properties Table.
Thereafter, the Methods table merely lists the methods that are created as an outcome of the properties. It is the MethodSematics table that links up the Properties and the Methods table, by pointing each of them to the function and the property.
FieldLayout Table
b.cs
using System.Runtime.InteropServices; [StructLayoutAttribute(LayoutKind.Explicit)] public class zzz { [FieldOffset(2)] int i; [FieldOffset(20)] long j; public static void Main() { } }
a.cs
public void xyz() { bool b = tablepresent(16); int offs = tableoffset; if (b) { for (int k = 1; k <= rows[16]; k++) { int offset = BitConverter.ToInt32(metadata, offs); offs += 4; int fieldindex = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row {0}", k); Console.WriteLine("Offset:{0}", offset); Console.WriteLine("Field :Field[{0}]", fieldindex); } } }
Output
Row 1
Offset:2
Field :Field[1]
Row 2
Offset:20
Field :Field[2]
Field Table
Row 1
Name : i
Signature [10]:Count=2 Type int
Row 2
Name : j
Signature [13]:Count=2 Type long
In the normal course, by default, memory locations are allocated to the fields present in a class or structure by the runtime. Under specific circumstances, we are required to determine these memory locations manually. To accomplish this, an attribute named StructLayoutAttribute in the program, has to be prefixed to the class name, which acquires the value of Explicit from an enum named LayoutKind.
It is the attribute FieldOffset that contains the offset and the field, which is the ultimate authority that determines the layout. In b.cs, we have specified the first field i to be laid out at a starting position of 2, in place of 0. Further, we have also stipulated the fact that the second field, which normally starts at the end of the first field, should start at offset 20 instead. The FieldOffset attribute must be placed on each of the instance members.
On each occasion that the fields are laid out manually, rows get supplemented to the FieldLayout table. They have an index position of 16 in the valid table. The FieldLayout table has the following columns:
The first field is an int, which stores the offset. The second field is an index into the Field table. Thus, the first row points to the first field i in the Field table, while the second row in the FieldLayout refers to the field j.
It is as straightforward as this!
Events and Delegates
b.cs
public void xyz() { bool b = tablepresent(16); int offs = tableoffset; if (b) { for (int k = 1; k <= rows[16]; k++) { int offset = BitConverter.ToInt32(metadata, offs); offs += 4; int fieldindex = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row {0}", k); Console.WriteLine("Offset:{0}", offset); Console.WriteLine("Field :Field[{0}]", fieldindex); } } }
The above example defines a delegate called pqr at the namespace level. A delegate is brought into play to call methods unconventionally, in a type-safe manner. They go hand in glove with Events. Let us scrutinize the assorted tables that are created.
TypeRef Table
Row[1]
Name :Object,0x20
Namespace :System,0x19
Row[2]
Name :MulticastDelegate,0x2B
Namespace :System,0x19
Row[3]
Name : IAsyncResult,0x53
Namespace :System,0x19
Row[4]
Name : AsyncCallback,0x60
Namespace :System,0x19
Row[5]
Name :DebuggableAttribute,0x97
Namespace :System.Diagnostics,0x84
The TypeRef table reveals the fact that 5 types are being referred to in the assembly. The first and the last types are invariably present. However, with the creation of a delegate, all the three types, viz. MulticastDelegate, IAsyncResult and AsyncCallback, which belong to the System namespace, get augmented. These references have come about as a result of the code being introduced by the delegate class.
TypeDef Table
Row:1
Flags : Class
Name : <Module>
Row:2
Flags : AutoLayout, AnsiClass, NotPublic, Public, BeforeFieldInit
Name : zzz
Row:3
Flags : AutoLayout, AnsiClass, NotPublic, Public, Sealed
Name : pqr
NameSpace :
Extends:TypeRef[2]
FieldList Field[1]
MethodList Method[3]
In the previous chapter, we examined the rows in the TypeDef table. The pseudo and zzz types are created, just as before. With the ushering in of the delegate, a new row with the type name of pqr, gets supplemented to the table. This becomes a first class type, to which the flag Sealed gets added, thereby preventing access to all other classes to derive from it.
The Extends field reveals the type that pqr is derived from. It points to the second row in the TypeRef table, i.e. the class MulticastDelegate. Thus, it can be competently established that a delegate class derives from the MulticastDelegate class.
Now, we take a look at the Method table from the third row onwards, to unfurl the methods that a delegate class introduces.
Methods Table
Row 1
Name : Main
Row 2
Name : .ctor
Row 3
Name : .ctor
ImpFlags :Runtime
PrivateScope FamANDAssem Family Public HideBySig ReuseSlot SpecialName RTSpecialName
Signature: #Blob[18]
Blob:18 Count:5 Bytes 20 2 1 1C 18
ParamList: Param[1]
Row 4
Name : Invoke
ImpFlags :Runtime
PrivateScope FamANDAssem Family Public Virtual HideBySig ReuseSlot
Signature: #Blob[24]
Blob:24 Count:4 Bytes 20 1 1 8
ParamList: Param[3]
Row 5
Name : BeginInvoke
ImpFlags :Runtime
PrivateScope FamANDAssem Family Public Virtual HideBySig VtableLayoutMask ReuseSlot NewSlot
Signature: #Blob[29]
Blob:29 Count:8 Bytes 20 3 12 D 8 12 11 1C
ParamList: Param[4]
Row 6
Name : EndInvoke
ImpFlags :Runtime
PrivateScope FamANDAssem Family Public Virtual HideBySig VtableLayoutMask ReuseSlot NewSlot
Signature: #Blob[38]
Blob:38 Count:5 Bytes 20 1 1 12 D
ParamList: Param[7]
Param Table
Row 1
Name object
Row 2
Name method
Row 3
Name p
Row 4
Name p
Row 5
Name callback
Row 6
Name object
Row 7
Name result
The third row in the method table is a constructor. Since none of these functions have been entered manually, the ImpFlags for all the four functions are Runtime. This is the first occasion on which we have encountered this flag. We shall explicate the other flags of Virtual and NewSlot a little later.
The signature of the constructor reveals two parameters. On inspecting the params table, we discover that the row 1 is a parameter called object, and the row 2 is a parameter named method. In the same vein, the second function of Invoke has one parameter called 'p'. The third function in the delegate is BeginInvoke, which accepts three parameters i.e. 'p', callback and object. Finally, we come upon the EndInvoke function that takes one parameter named result.
By now, it would have become amply evident to you that reading metadata tables is becoming progressively easier.
b.cs
using System; public class zzz { public event EventHandler a; public static void Main() { } }
In the file b.cs, the field 'a' is declared to be of type event. The EventHandler delegate is present in the System namespace. Let us take a look at the rows inserted in the metadata tables.
TypeRef Table
Row[1]
Name :Object,0x20
Row[2]
Name :EventHandler,0x2B
Namespace :System,0x19
Row[3]
Name :DebuggableAttribute,0x67
Row[4]
Name :Delegate,0x83
Namespace :System,0x19
There are two extra type refs that get introduced at rows 2 and 4, viz. the EventHandler delegate that the event uses, and the Delegate type. We shall revert to them in no time.
The TypeDef table contains the rows of the pseudo class and the zzz class. Hence, they have not been displayed. An event, unlike a delegate, is basically treated as a field. Therefore, a row gets added to the Field table.
Field Table
Row 1
Flags: Private
Name : a
Signature [10]:Count=3 Type unknown
However, the signature assigned to the field is an obscure one. This is because we have not implemented all the types, objects in particular.
Method Table
Row 1
Name : add_a
ImpFlags :Synchronized
Flags :886
PrivateScope FamANDAssem Family Public HideBySig ReuseSlot SpecialName
Signature: #Blob[14]
Blob:14 Count:5 Bytes 20 1 1 12 9
ParamList: Param[1]
Row 2
Name : remove_a
ImpFlags :Synchronized
Flags :886
PrivateScope FamANDAssem Family Public HideBySig ReuseSlot SpecialName
Signature: #Blob[14]
Blob:14 Count:5 Bytes 20 1 1 12 9
ParamList: Param[2]
Row 3
Name : Main
Row 4
Name : .ctor
With the introduction of events in the program, two rows get added to the method table, viz. add_a and remove_a. We will handle the residual Flag bits in a single stroke, a little later.
The add_a function takes one parameter whose name is 'value', as verified by the params table. The second method named remove_a also takes a parameter called 'value'.
Thus, an event eventually disintegrates into two methods, i.e. add_eventname and remove_eventname.
MemberRef
Row 1
Name:.ctor
Row 2
Class:TypeRef[4]
Name:Combine
Signature #BLOB[34] Count 8 0 2 12 11 12 11 12 11
Row 3
Class:TypeRef[4]
Name:Remove
Signature #BLOB[34] Count 8 0 2 12 11 12 11 12 11
Row 4
Name:.ctor
The MemberRef table unveils the fact that the event refers to the two methods named Combine and Remove. These methods index into row 4 of the TypeRef table, which represents the System.Delegate class. The signature shall be explained in the subsequent chapters, since it is too convoluted to be handled at this point in time.
a.cs
public void xyz() { int old = tableoffset; bool b = tablepresent(20); int offs = tableoffset; if (b) { Console.WriteLine("Event"); for (int k = 1; k <= rows[20]; k++) { short attr = BitConverter.ToInt16(metadata, offs); offs += 2; int name = BitConverter.ToInt16(metadata, offs); offs += 2; int coded = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row {0}", k); Console.WriteLine("Event Flags: {0}", GetEventsAttributes(attr)); Console.WriteLine("Name: {0}", GetString(name)); Console.Write("Event Type: "); int tag = coded & 0x03; if (tag == 0) Console.Write("TypeDef"); if (tag == 1) Console.Write("TypeRef"); if (tag == 2) Console.Write("TypeSpec"); int riid = coded >> 2; Console.WriteLine("[{0}]", riid); } } tableoffset = old; b = tablepresent(18); offs = tableoffset; if (b) { Console.WriteLine("EventMap Table"); for (int k = 1; k <= rows[18]; k++) { short index = BitConverter.ToInt16(metadata, offs); offs += 2; short eindex = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row {0}", k); Console.WriteLine("Parent TypeDef[{0}]", index); Console.WriteLine("EventList Event[{0}]", eindex); } } } public string GetEventsAttributes(short a) { string s = ""; if ((a & 0x0200) == 0x0200) s = s + "Special Name"; if ((a & 0x0400) == 0x0400) s = s + "RTSpecialName"; if (s.Length == 0) return "None"; else return s; }
Output
Event
Row 1
Event Flags: None
Name: a
Event Type: TypeRef[2]
EventMap Table
Row 1
Parent TypeDef[2]
EventList Event[1]
MethodSematics
Row 1
Semantics Event Addon
Method Method[1]
Association Events[1]
Row 2
Semantics Event Remove
Method Method[2]
Association Events[1]
TypeDef Table
Row:1
Name : <Module>
Row:2
Name : zzz
Each time that we add an event to our code, two tables get added to our metadata. To begin with, events are conceptually treated as properties. The first table is the Event Table, with an id of 20.
The Event table has the following columns:
The first field is always a flag field, which has either none or one of the two bits ON.
The flag value of 0x0200 is a special event, whereas, the second value of 0x0400 solicits the runtime to treat the event as special, thus handling the event in a special manner.
The function GetEventAttributes returns the event attributes in a string form. The second field is the name of the event, which in our case is 'a'. The third field is a TypeDefOrRef coded index, wherein two bits determine whether the table is a TypeDef or TypeRef or TypeSpec. In this case, it is an index to the 2nd row of the TypeRef table, which represents the class EventHandler in the System namespace.
Every event must necessarily be of some delegate type. The second table is the EventMap table, which has an id of 18.
The EventMap table has the following columns:
The first field is an index into the TypeDef table. It has a value of 2, thereby pointing to the second row, which has the class name of zzz in the table. The event 'a' has been created in the class zzz. The second field in the EventMap table is an index into the event table. It refers to the event index in the event table.
Finally, the MethodSematics class links the methods and the events together. You may recall that the MethodSemantics table encompasses the columns of Semantics, Method and Association.
The first field of MethodSemanticsAttributes may either be Addon or Remove. It is followed by an index to the method table. Thereafter, we come across add_a and remove_a, and finally, we encounter the Event index in the event table, to which the above methods are linked. This table acts as a conduit between properties and events, and their methods.
b.cs
using System; public delegate int pqr(); public class zzz { public event EventHandler a; public event pqr b; public event EventHandler c; public static void Main() { } } public class yyy { public event pqr b1; public event EventHandler c1; } public class xxx { }
Output
Event
Row 1
Name: a
Row 2
Name: b
Row 3
Name: c
Row 4
Name: b1
Row 5
Name: c1
EventMap Table
Row 1
Parent TypeDef[3]
EventList Event[1]
Row 2
Parent TypeDef[4]
EventList Event[4]
In the above example, we have merely added three events to the class zzz and two events to the class yyy. The Event table stores the five events, while the Event Map table stores the classes that contain these events. The class xxx does not contain any events, which explains its absence in Event Map table.
Associating an event with a class is indeed a tricky and a sticky issue, since a count of the events is not stored anywhere. The Event Map table has to be accessed to determine the Event row in the EventList. The difference between the event index for the first row and that of the second row establishes the number of events that exist in the first class.
Pinvoke Table
b.cs
using System.Runtime.InteropServices; using System; public class zzz { [DllImport("user32.dll")] public static extern int MessageBox(int hWnd, String text, String caption, uint type); public static void Main() { } }
a.cs
public void xyz() { int old = tableoffset; bool b = tablepresent(26); int offs = tableoffset; if (b) { Console.WriteLine("ModuleRef"); for (int k = 1; k <= rows[26]; k++) { short name = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row {0}", k); Console.WriteLine("Name {0}", GetString(name)); } } tableoffset = old; b = tablepresent(28); offs = tableoffset; if (b) { Console.WriteLine("ImplMap"); for (int k = 1; k <= rows[28]; k++) { short attr = BitConverter.ToInt16(metadata, offs); offs += 2; short cindex = BitConverter.ToInt16(metadata, offs); offs += 2; short name = BitConverter.ToInt16(metadata, offs); offs += 2; short scope = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row {0}", k); Console.WriteLine("Mapping Flags :{0}", GetPInvokeAttributes(attr)); Console.Write("MemberForwarded:"); int tag = cindex & 0x01; if (tag == 0) Console.Write("Field"); if (tag == 1) Console.Write("Method"); int riid = cindex >> 1; Console.WriteLine("[{0}]", riid); Console.WriteLine("Name :{0}", GetString(name)); Console.WriteLine("Import Scope :ModuleRef[{0}]", scope); } } } public string GetPInvokeAttributes(short a) { string s = ""; if ((a & 0x0001) == 0x0001) s = s + "NoMangle "; if ((a & 0x0006) == 0x0006) s = s + "CharSetMask "; if ((a & 0x0000) == 0x0000) s = s + "CharSetNotSpec "; if ((a & 0x0002) == 0x0002) s = s + "CharSetAnsi "; if ((a & 0x0004) == 0x0004) s = s + "CharSetUnicode "; if ((a & 0x0004) == 0x0004) s = s + "CharSetAuto "; if ((a & 0x0040) == 0x0040) s = s + "SupportsLastError "; if ((a & 0x0700) == 0x0700) s = s + "CallConvMask "; if ((a & 0x0100) == 0x0100) s = s + "CallConvWinapi "; if ((a & 0x0200) == 0x0200) s = s + "CallConvCdecl "; if ((a & 0x0300) == 0x0300) s = s + "CallConvStdcall "; if ((a & 0x0400) == 0x0400) s = s + "CallConvThiscall "; if ((a & 0x0500) == 0x0500) s = s + "CallConvFastcall "; return s; }
Output
ModuleRef
Row 1
Name user32.dll
ImplRef
Row 1
Mapping Flags :CharSetNotSpec CallConvWinapi
MemberForwarded:Method[1]
Name :MessageBox
Import Scope :ModuleRef[1]
TypeRef Table
Row[3]
Name :DllImportAttribute,0x89
Namespace :System.Runtime.InteropServices,0x6A
Method Table
Row 1
RVA :0
Name : MessageBox
ImpFlags :PreserveSig
PrivateScope FamANDAssem Family Public Static HideBySig ReuseSlot PinvokeImpl
Signature: #Blob[10]
Blob:10 Count:7 Bytes 0 4 8 8 E E 9
ParamList: Param[1]
In the above example, there is a method named MessageBox that takes four parameters. It is composed of two ints and two strings. The code of this method is not placed within open and close braces; instead, it terminates with a semicolon. Moreover, there is an attribute named DllImport placed above this function.
Code is normally placed in a Dynamic Link Library (DLL). The code that runs Windows is also posited in dlls, such as User32.dll and Kernerl32.dll. This code has mainly been written in the C programming language. In the days of yore, there were functions infinite that were penned down in the C language, and then, compiled into the Intel assembler, but not in IL. We can gain access to this code either from our C# program or from any other .Net application.
In order to execute this code, the PInvoke functionality of the .Net world was introduced. The addition of the DllImport attribute results in the addition of a row to the ModuleRef table, which has a bit position of 26. The ModuleRef table has only one column i.e. Name, which is an index into the String heap.
Currently, this table has a single row. The index into the string table furnishes the name of the DLL as user32.dll.
Yet another table that is affected by the interop service is the ImplMap table. This table is at a bit position of 28. It contains the details of the method present in the DLL.
The ImplMap table has the following columns:
The first field is the flags or the attribute field, which is resolved with the help of a function called GetPInvokeAttributes.
The Charset value represents the character set that is employed. The standard of Unicode is an international standard, which facilitates the representation of any language in the world, on the computer. This standard is also known as I18n, since there are 18 characters between the letters I and N in the word 'InternationalizatioN'.
The calling convention takes a look at parameters that are posited on the stack and thereafter, it elects the parameter that would be conferred with the onus of cleaning up the stack.
The Winapi calling convention has been used while developing Windows, where the parameters are pushed on the stack in the reverse order. Further, it is the 'called' function that scours the stack clean or restores it back to its original location, when the function is called.
The second field in the implmap table is the MemberForwarded. It is a coded index. Thus, the first two bits resolve the dilemma of whether it is an index to the Field or to the Method table. The field export is not supported.
The first method in the methoddef table has an RVA of zero, since the code for the function is present in the dll named User32.dll, and not within the current file. The Flags, in no uncertain terms, indicate the fact that the method has the PinvokeImpl bit set ON. Also, the Signature states that the method will be called with four parameters, and that the offset in the param table begins at 1.
The last field is an offset in the ModuleRef table, which provides the name of the DLL where the method resides.
Interfaces
b.cs
public class zzz : yyy, xxx { void yyy.abc() { } void xxx.abc() { } public static void Main() { } } interface yyy { void abc(); } interface xxx { void abc(); }
a.cs
public void xyz() { int old = tableoffset; bool b = tablepresent(9); int offs = tableoffset; if (b) { Console.WriteLine("InterfaceImpl"); for (int k = 1; k <= rows[9]; k++) { short classindex = BitConverter.ToInt16(metadata, offs); offs += 2; short interfaceindex = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row {0}", k); Console.WriteLine("Class TypeDef[{0}]", classindex); Console.WriteLine("Interface {0}[{1}]", GetTypeDefOrRefTable(interfaceindex), GetTypeDefOrRefValue(interfaceindex)); } } tableoffset = old; b = tablepresent(25); offs = tableoffset; if (b) { Console.WriteLine("MethodImpl"); for (int k = 1; k <= rows[25]; k++) { short classindex = BitConverter.ToInt16(metadata, offs); offs += 2; short codedbody = BitConverter.ToInt16(metadata, offs); offs += 2; short codeddef = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row {0}", k); Console.WriteLine("Class TypeDef[{0}]", classindex); Console.WriteLine("MethodBody {0}[{1}]", GetMethodDefTable(codedbody), GetMethodDefValue(codedbody)); Console.WriteLine("MethodDeclaration {0}[{1}]", GetMethodDefTable(codeddef), GetMethodDefValue(codeddef)); } } } public int GetMethodDefValue(short a) { return a >> 1; } public string GetMethodDefTable(short a) { string s = ""; short tag = (short)(a & (short)0x01); if (tag == 0) s = s + "MethodDef"; if (tag == 1) s = s + "MethodRef"; return s; } public int GetTypeDefOrRefValue(short a) { return a >> 2; } public string GetTypeDefOrRefTable(short a) { string s = ""; short tag = (short)(a & (short)0x03); if (tag == 0) s = s + "TypeDef"; if (tag == 1) s = s + "TypeRef"; if (tag == 2) s = s + "TypeRef"; return s; }
Output
InterfaceImpl
Row 1
Class TypeDef[4]
Interface TypeDef[2]
Row 2
Class TypeDef[4]
Interface TypeDef[3]
MethodImpl
Row 1
Class TypeDef[4]
MethodBody MethodDef[3]
MethodDeclaration MethodDef[1]
Row 2
Class TypeDef[4]
MethodBody MethodDef[4]
MethodDeclaration MethodDef[2]
TypeDef Table
Row:2
Flags : AutoLayout, AnsiClass, NotPublic, ClassSemanticsMask, Abstract
Name : yyy
Row:3
Flags : AutoLayout, AnsiClass, NotPublic, ClassSemanticsMask, Abstract
Name : xxx
Row:4
Name : zzz
Method Table
Row 1
RVA :0
Name : abc
Row 2
RVA :0
Name : abc
Row 3
RVA :2050
Name : yyy.abc
Row 4
RVA :2060
Name : xxx.abc
Row 5
Row 6
The b.cs file has two interfaces named yyy and xxx, which in turn have one function each, bearing the same name of abc. The class zzz is then derived from the two interfaces. Since the function names are identical, the method name abc in the class must be preceded with the name of the interface.
Let us now pore over the tables that get created. The first table that gets affected is the InterfaceImpl, which has a bit index of 9 in the valid table.
The InterfaceImpl table has the following columns:
The first field is an offset to the TypeDef table. Row 4 in this table refers to the class name zzz itself. The second parameter is a 2-bit coded index of TypeDefOrRef. Hereinafter, a function is used to decode the coded index, which checks the bits and returns the table name. Another function is utilized to right shift and return the index value.
The TypeDefOrRef coded index indexes into one of the following three tables: TypeDef, TypeRef or TypeSpec.
In the first row, the index value points to the second row of the TypeDef table. The second row has an entry for the interface yyy. The second row of the InterfaceImpl also points to the fourth row in the TypeDef table, i.e. class zzz; but the interface index is now Row 3 of the TypeDef table, which is interface xxx.
Thus, every class that derives from an interface, has one row in the InterfaceImpl table. Since we derive from two interfaces, two rows are present. The interface is a type in the TypeDef table with the Abstract and ClassSemanticsMask bits ON.
The second table that gets populated is the MethodImpl table at the bit index of 25. The MethodImpl table has the following columns:
The first field is an index to the TypeDef table. As the methods are present in the class zzz, both rows point to this class or row 4, in the TypeDef table. The next two fields use the same MethodDefOrRef coded index, which has a single bit that chooses from amongst the two, i.e. a MethodDef (which is a definition), and a MethodRef (which is a reference to a method).
Two functions, which are similar to the intefaceimpl table, are implemented to return the string and the value. The MethodBody index points to the third method in the method table, i.e. yyy.abc, while the MethodDeclaration points to the first function i.e. abc.
This function has an RVA of zero, since it belongs to the interface yyy. This is so because the field of MethodList in the TypeDef table has a value of 1.
The second row of the MethodImpl table has the MethodBody index to the fourth row or the xxx.abc method. The MethodDeclaration field is the function abc from the interface xxx.
Thus, the methodimpl table provides information about the original function in the MethodDeclaration field, which gets overridden by the function in the MethodBody field in the class zzz.
If the interface encompasses 100 methods, each would then be overridden in the class zzz, thereby resulting in the addition of 100 rows in the MethodImpl table.
StandAloneSig Table :
b.cs
public class zzz { public static void Main() { int i; string j; } public void abc() { bool b; } }
a.cs
public void xyz() { int old = tableoffset; bool b = tablepresent(17); int offs = tableoffset; if (b) { Console.WriteLine("StandAloneSig"); for (int k = 1; k <= rows[17]; k++) { short index = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row {0}", k); byte count = blob[index]; Console.Write("Signature BLOB[{0}] Count {1} ", index, count); if (blob[index + 1] == 0x07) { for (int l = 3; l <= count; l++) { Console.Write("{0} ", GetType(blob[index + l])); } Console.WriteLine(); } } } }
Output
StandAloneSig
Row 1
Signature BLOB[24] Count 4 int string
Row 2
Signature BLOB[29] Count 3 boolean
In the file b.cs, the function Main possesses two local variables, an int and a string. The function abc in turn has one local variable of type bool.
Each time that a local variable is created in a method, one row gets added to the StandAloneSig table, which holds the position of 17 in the valid field. This table has a field called signature, which is an index into the Blob heap.
This index starts out with the count followed by the reserved number 7. We have already encountered this in one of the earlier examples. Next is the count of the number of parameters and the actual data types. The GetType function is again exploited to decipher the data types.
Signatures that are stored in the Blob heap can be indexed from numerous other tables. We could be faced with an eventuality wherein, a signature in a Blob heap has no metadata table indexing it. One such case presents itself when there are variables present in a function. In due course, we would be evincing the procedure employed to access these signatures.
Security and Unsafe
b.cs
public class zzz { public static void Main() { } static public unsafe void abc(int j) { } public unsafe void pqr() { } }
>csc b.cs /unsafe
a.cs
public void xyz() { int old = tableoffset; bool b = tablepresent(14); int offs = tableoffset; if (b) { Console.WriteLine("DeclSecurity"); for (int k = 1; k <= rows[14]; k++) { int action = BitConverter.ToInt16(metadata, offs); offs += 2; int coded = BitConverter.ToInt16(metadata, offs); offs += 2; int bindex = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row {0}", k); Console.WriteLine("Action {0}", action); Console.WriteLine("Parent: {0}[{1}]", GetDeclSecurityTable(coded), GetDeclSecurityValue(coded)); int count = blob[bindex]; Console.WriteLine("Permission Set BLOB[{0}] Count={1}", bindex, count); for (int l = 1; l <= count; l++) { Console.Write("{0}", (char)blob[bindex + l]); } Console.WriteLine(); } } } public int GetDeclSecurityValue(int a) { return a >> 2; } public string GetDeclSecurityTable(int a) { string s = ""; short tag = (short)(a & (short)0x03); if (tag == 0) s = s + "TypeDef"; if (tag == 1) s = s + "MethodDef"; if (tag == 2) s = s + "Assembly"; return s; }
Output
DeclSecurity
Row 1
Action 8
Parent: Assembly[1]
Permission Set BLOB[47] Count=130
<PermissionSet class="System.Security.PermissionSet"
TypeRef Table
Row[1]
Row[2]
Name :SecurityPermissionAttribute,0x5A
Namespace :System.Security.Permissions,0x3E
Row[3]
Name :SecurityAction,0x76
Namespace :System.Security.Permissions,0x3E
Row[4]
Row[5]
Name :UnverifiableCodeAttribute,0xC0
Namespace :System.Security,0xB0
The b.cs program now contains two methods that have been tagged with the 'unsafe' parameter. Whenever pointers are used in the program code, it is mandatory for the 'unsafe' modifier to be implemented. Furthermore, while compiling the above program, the /unsafe option is to be added to the compiler. The use of a single unsafe method adds one row to the DeclSecurity Table that has a bit index of 14.
The DeclSecurity table has the following columns:
Besides the DeclSecurity table, the TypeRef table also gets augmented with three rows, thereby resulting in three references. All the three attributes are located within the Security namespace.
The first field in the DeclSecurity table is a short, which stands for Actions. The value assigned to it originates from the enum SecurityAction in the System.Security.Permissions namespace. The values from 0 to 0xff are reserved for use by future standards. Since the current value is 8, we are incapable of explaining the Action that it represents, at this stage.
The second field is a 2-bit coded index of HasDeclSecurity that points to one of the following three entities, viz. TypeDef, MethodDef or Assembly.
This field is called the parent. Since the value of Assembly has been assigned, the security permissions apply to the entire assembly. The last parameter is an index into the Blob heap. The value is a Unicode string System.Security.PermissionSet, which is a valid serialized CLI object graph.
Resources
a.txt
sonal=mukhi
vijay=ram
A text file named a.txt is created to store the string resource. A resource is merely a name-value pair. There are a total of two names, i.e. sonal and vijay, with the values of mukhi and ram. These resources are put away in our exe file and retrieved later, using some API.
Now, run the Resgen program as in Resgen a.txt to create a file named a.resources. Thereafter, create one more text file named b.txt that contains the line net=yes, and then, run the Resgen program on it too, as in Resgen b.txt.
Now, to add both resource files to the exe file, issue the following command:
b.cs
public class zzz { public static void Main() { } }
Csc b.cs /res:a.resources /res:b.resources
The /res command option adds the resources to the exe file. Now, let us explore the metadata tables that get impinged on.
a.cs
public void xyz() { int old = tableoffset; bool b = tablepresent(40); int offs = tableoffset; if (b) { Console.WriteLine("ManifestResource"); for (int k = 1; k <= rows[40]; k++) { int offset = BitConverter.ToInt32(metadata, offs); offs += 4; int flags = BitConverter.ToInt32(metadata, offs); offs += 4; short name = BitConverter.ToInt16(metadata, offs); offs += 2; short coded = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row {0}", k); Console.WriteLine("Offset {0}", offset); Console.WriteLine("Flags {0}", GetManifestResource(flags)); Console.WriteLine("Name {0}", GetString(name)); Console.WriteLine("Implementation: {0}[{1}]", GetManifestResourceTable(coded), GetManifestResourceValue(coded)); } } } public int GetManifestResourceValue(int a) { return a >> 2; } public string GetManifestResourceTable(int a) { string s = ""; short tag = (short)(a & (short)0x03); if (tag == 0) s = s + "File"; if (tag == 1) s = s + "AssemblyRef"; if (tag == 2) s = s + "ExportedType"; return s; } public string GetManifestResource(int a) { string s = ""; if ((a & 0x007) == 0x007) s = s + "VisibilityMask "; if ((a & 0x001) == 0x001) s = s + "Public "; if ((a & 0x002) == 0x002) s = s + "Private "; return s; }
Output
ManifestResources
Row 1
Offset 0
Flags Public
Name a.resources
Implementation: File[0]
Row 2
Offset 348
Flags Public
Name b.resources
Implementation: File[0]
Each time a resource is added to the exe file using the /res option, a row gets added to the ManifestResource table, whose bit index is 40 in the valid field.
The ManifestResource table has the following columns:
The first field is an offset to the location, where the resource is stored in the executable file. This offset is obtained from the Resource data directory in the ImageOptional header.
The COR header contains an entry for the resources, along with their offset.
The first resource is stored at an offset of zero from this value. The size of our file a.resources is 342 bytes. As a result of the accumulation of bytes for the header, the second resource reveals an offset value of 348. The second resource begins immediately after the first. Thus, the offset field contains the byte offset from where the resource begins.
The second field is a flags field, which renders information about whether the resource is 'public' or 'exported' from the assembly, or if it is 'private' to the assembly.
Using the function GetManifestResource, the relevant string is returned.
The third field is the name of the resource file. The fourth field is an Implementation code index, which points to the File or AssemblyRef table.
The documentation clearly states that if the implementation is an index into the File table, the index value must be zero. A zero index in a table signifies that the index is invalid.
Exported Type
c.cs
public class yyy { public int i; } namespace ccc { public class xxx { public string j; } }
>csc /t:module c.cs
This gives us a file c.netmodule
b.cs
public class zzz { public static void Main() { } }
csc /AddModule:c.netmodule b.cs
a.cs
public void xyz() { int old = tableoffset; bool b = tablepresent(39); int offs = tableoffset; tableoffset = old; if (b) { Console.WriteLine(tablenames[39]); for (int k = 1; k <= rows[39]; k++) { TypeAttributes flags = (TypeAttributes)BitConverter.ToInt32(metadata, offs); offs += 4; int typedefindex = BitConverter.ToInt32(metadata, offs); offs += 4; int name = BitConverter.ToInt16(metadata, offs); offs += 2; int nspace = BitConverter.ToInt16(metadata, offs); offs += 2; short coded = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row {0}", k); Console.WriteLine("Flags {0}", flags); Console.WriteLine("TypeDef TYPEDEF[{0}]", typedefindex); Console.WriteLine("TypeName {0}", GetString(name)); Console.WriteLine("NameSpace {0}", GetString(nspace)); Console.WriteLine("Implementation {0}[{1}] ", GetImplementationTable(coded), GetImplementationValue(coded)); } } } public int GetImplementationValue(short a) { return a >> 2; } public string GetImplementationTable(short a) { string s = ""; int tag = a & 0x03; if (tag == 0) s = s + "File"; if (tag == 1) s = s + "AssemblyRef"; if (tag == 2) s = s + "ExportedType"; return s;
}
Output
ExportedType
Row 1
Flags AutoLayout, AnsiClass, NotPublic, Public, BeforeFieldInit
TypeDef TYPEDEF[33554434]
TypeName yyy
NameSpace
Implementation File[1]
Row 2
Flags AutoLayout, AnsiClass, NotPublic, Public, BeforeFieldInit
TypeDef TYPEDEF[33554435]
TypeName xxx
NameSpace ccc
Implementation File[1]
Two classes are created in the file c.cs. They are subsequently compiled into a module using the module option, along with the /target option of the compiler. This results in the creation of a module file having an extension of .netmodule. Thereafter, the file b.cs is compiled using the .AddModule option of the compiler.
This results in the addition of two rows to the exported type table having a bit index of 39 in the valid field.
The ExportedType table has the following columns:
The first field is the flags attribute, which is of type TypeAttributes. It displays the normal flags that every class possesses. The second field is slightly more complex. It is known as TypeDefId. The value is an index into a TypeDef table of another module in the assembly. However, it is to be used merely as a reference.
Before proceeding further, it is imperative to verify if the other table also contains the same name and namespace. In this case, the value shown is exceedingly large; and as a result, we could not accomplish anything worthwhile. The third is the name of the class. The fourth is the name of the namespace. The fifth is an implementation coded index that points to the first row of the file table.
Thus, to summarize, the exported type table holds a row only for a type that is defined within other modules of our assembly, and is exported. These types are obviously marked as 'public'.