Program47.csc

Public void DisplayAllMethods(int typeindex)

{

methodstring = methodstring + CreateSpaces(spacesforrest + 2 + spacesfornested) +  "// Code size       0 (0x0)" + "\r\n";

if ( IsGlobalMethod(methodindex))

methodstring = methodstring + CreateSpaces(spacesforrest+ spacesfornested) + "} // end of global method " + NameReserved(GetString(MethodStruct[methodindex].name)) + "\r\n";

else

methodstring = methodstring + CreateSpaces(spacesforrest+ spacesfornested) + "} // end of method " + NameReserved(GetString(TypeDefStruct[typeindex].name)) + "::" + NameReserved(GetString(MethodStruct[methodindex].name)) + "\r\n";

Console.WriteLine(methodstring);

}

public void DisplayTypeDefsAndMethods ()

{

notprototype = true;

if ( TypeDefStruct.Length != 2)

{

Console.WriteLine();

Console.WriteLine("// =============================================================");

Console.WriteLine();

}

Console.WriteLine();

Console.WriteLine("// =============== GLOBAL FIELDS AND METHODS ===================");

Console.WriteLine();

DisplayGlobalFields();

DisplayGlobalMethods();

if ( TypeDefStruct.Length != 2)

{

Console.WriteLine();

Console.WriteLine("// =============================================================");

}

 

public void DisplayGlobalFields ()

{

int start , startofnext =0;

if ( TypeDefStruct == null || FieldStruct == null)

return;

start =  TypeDefStruct[1].findex ;

if ( TypeDefStruct.Length == 2 )

startofnext = FieldStruct.Length;

else

startofnext = TypeDefStruct[2].findex ;

if ( start != startofnext )

{

Console.WriteLine("//Global fields");

Console.WriteLine("//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");

DisplayAllFields (1);

}

}

public void DisplayGlobalMethods ()

{

int start , startofnext=0;

start =  TypeDefStruct[1].mindex ;

if ( TypeDefStruct == null || MethodStruct == null)

return;

 

if ( TypeDefStruct.Length == 2 )

startofnext= MethodStruct.Length;

else

startofnext = TypeDefStruct[2].mindex ;

if ( start != startofnext )

{

Console.WriteLine("//Global methods");

Console.WriteLine("//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");

spacesforrest = 0;

DisplayAllMethods(1);

spacesforrest = 2;

}

}

public bool IsGlobalMethod (int methodrow)

{

int start , startofnext=0;

methodrow , TypeDefStruct.Length);

if ( TypeDefStruct.Length == 2)

return true;

start =  TypeDefStruct[1].mindex ;

if ( TypeDefStruct.Length == 1 )

{

startofnext= MethodStruct.Length;

}

else

startofnext = TypeDefStruct[2].mindex;

if ( methodrow >= start && methodrow < startofnext )

return true;

else

return false;

}

 

e.il

.namespace kkk

{

.method static private void a1()

{

}

.field static private int32 i

}

.class zzz

{

}

 

Output

// =============================================================

 

 

// =============== GLOBAL FIELDS AND METHODS ===================

 

//Global fields

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.field /*04000001*/ private static int32 i

//Global methods

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.method /*06000001*/ private static void

        a1() cil managed

// SIG: 00 00 01

{

  // Method begins at RVA 0x2050

  // Code size       0 (0x0)

} // end of global method a1

 

 

// =============================================================

 

In this small program we display all the global method and fields that we have in the il file. Most newer languages do not support the concept of global fields and variables but we are fortunate that il does. We told you a long time ago that we always have a extra type that will handle global entities.

 

We have in the DisplayAllMethods a if statement at the very end that checks whether we have a global function or not. We have written a function IsGlobalMethod that tells us whether the current method is global or not by passing it the method number. In this method we first check if there are only global methods in the il file.

 

This happens if the length of the TypeDef array is 2, remember it is always larger by one. We also told you that there is always a global type available. We then find out as before the first and last method owned by this global type. If the method row falls within this range we return true or else we return false.

 

The only change is that as always we no not check for the last type but for the first type. If this method returns true, we do display the type name that is Module but add the words global instead. In the method DisplayTypeDefsAndMethods we have add two lines in the beginning that call the methods DisplayGlobalFields and DisplayGlobalMethods.

 

The method DisplayGlobalFields first checks whether they are global fields available. It does this by setting start to the findex of the first TypeDef entity that is module. Then it sets the startofnext to the second Typedef.

 

At times there is only a single type and hence we then set startofnext to the length of the number of rows in the Field table. If start and startofnext are the same, we have global fields and therefore display some text and call the DisplayAllFields method with the type row number as 1.

 

We do the same for the DisplayGlobalMethods but now also set the spacesforrest at 2 for the rest of the method that will follow the global methods. The setting of the variable has no effect on the global methods as there is no global namespace for global fields or methods and if we even add one, it gets removed.

 

Program48.csc

 

e.il

.assembly extern mscorlib

{

}

.mresource aa

{

.custom instance void [mscorlib]a18::.ctor() = (65)

}

.assembly extern vijay

{

.custom instance void [mscorlib]a15::.ctor() = (65)

}

.assembly e.dll

{

.custom instance void [vijay]a14::.ctor() = (65)

}

.module extern kk.dll

.custom instance void [vijay]a12::.ctor() = (65)

.module aa.dll

.custom instance void [vijay]a7::.ctor() = (65)

.file aa

.custom instance void [mscorlib]a177::.ctor() = (65)

.class extern xxx

{

.custom instance void [mscorlib]a17::.ctor() = (65)

.file aa

}

.class uuu

{

.permissionset assert = (12 )

.custom instance void [vijay]a8::.ctor() = (65)

}

.class zzz

{

.custom instance void [vijay]a4::.ctor() = (65)

.field public int32 pqr

.custom instance void [vijay]a2::.ctor() = (65)

.method public void abc(int32 i , int32 j)

{

.param [1] = int8(10)

.custom instance void [vijay]a5::.ctor() = (65)

}

.method public void abc1()

{

.custom instance void [vijay]a1::.ctor() = (65)

}

/*

.method void xyz()

{

call void [vijay]yyy::pqr()

.custom instance void [vijay]a6::.ctor() = (65)

}

*/

.property instance int32 aa()

{

.custom instance void [vijay]a9::.ctor() = (65)

.get instance void zzz::abc1()

}

.event [vijay]re a

{

.custom instance void [vijay]a10::.ctor() = (65)

.addon instance void zzz::abc1()

}

}

.class interface iii

{

.custom instance void [vijay]a5::.ctor() = (65)

}

.custom ([vijay]a3) instance void [vijay]a33::.ctor() = (65)

//.custom ([vijay]a6::abc()) instance void [vijay]a66::.ctor() = (65)

 

We have not made any major additions to this program but simply added a series of DisplayCustomAttribute methods calls at many places. Thus we will start with the il file and show you code fragments of the functions where we have added the call of the method DisplayCustomAttribute. We have also tried to show you the il output and the method together. The first is the function abc where we move the method CreateSignatures above the method DisplayModuleRefs as we would like all the arrays to be filled up earlier.

 

public void abc(string [] args)

{

CreateSignatures();

DisplayModuleRefs ();

DisplayAssembleyRefs();

}

 

public void DisplayAssembleyRefs ()

{

if (AssemblyRefStruct == null)

return;

for ( int i = 1 ; i < AssemblyRefStruct.Length ; i++)

{

Console.WriteLine(".assembly extern /*23{0}*/ {1}", i.ToString("X6") , NameReserved(GetString(AssemblyRefStruct[i].name)));

Console.WriteLine("{");

DisplayCustomAttribute ("AssemblyRef" , i , 2 );

 

.assembly extern /*23000002*/ vijay

{

  .custom /*0C00000A:0A000002*/ instance void [mscorlib/* 23000001 */]a15/* 01000002 */::.ctor() /* 0A000002 */ = "e"

  .ver 0:0:0:0

}

 

We have a custom attribute in the AssemblyRef table and thus we add the method DisplayCustomAttribute just after the { brace.

 

public void DisplayAssembley ()

{

if (AssemblyStruct.Length == 1)

return;

Console.WriteLine(".assembly /*20000001*/ {0}" , NameReserved(GetString(AssemblyStruct[1].name)));

Console.WriteLine("{");

DisplayCustomAttribute("Assembly" , 1 , 2 + spacefornamespace);

 

.assembly /*20000001*/ e.dll

{

  .custom /*0C000006:0A000003*/ instance void [vijay/* 23000002 */]a14/* 01000003 */::.ctor() /* 0A000003 */ = "e"

  .ver 0:0:0:0

}

 

The second place is the Assembly table as this table has only one row we have used the row number as 1.

 

public void DisplayClassExtern ()

{

 

Console.Write(".class extern /*27{0}*/ {1}" , ii.ToString("X6") , ss1 );

Console.WriteLine(ss);

Console.WriteLine("{");

DisplayCustomAttribute("ExportedType" , ii , 2);

 

}

 

.class extern /*27000001*/ xxx

{

  .custom /*0C000007:0A000006*/ instance void [mscorlib/* 23000001 */]a17/* 01000006 */::.ctor() /* 0A000006 */ = "e"

  .file aa/*26000001*/

}

 

The third place is the Class Extern directive or what we call the Exported Type.

 

public void DisplayModuleAndMore()

{

Console.WriteLine(".module {0}" , NameReserved(GetString(ModuleStruct[1].Name)));

Console.Write("// MVID: ");

DisplayGuid(ModuleStruct[1].Mvid);

Console.WriteLine();

DisplayCustomAttribute("Module"  , 1 , 0);

 

.module aa.dll

// MVID: {AF4812DF-31BB-48D0-87D5-964909ECF751}

.custom /*0C000003:0A000005*/ instance void [vijay/* 23000002 */]a7/* 01000005 */::.ctor() /* 0A000005 */ = "e"

 

The fourth place is the Module table and once again the row number is 1.

 

public void DisplayAllFields (int typeindex)

{

Console.WriteLine("{0}" , returnstring);

DisplayCustomAttribute("FieldDef" , fieldindex , spacesfornested + spacesforrest);

}

}

 

  .field /*04000001*/ public int32 pqr

  .custom /*0C000001:0A000009*/ instance void [vijay/* 23000002 */]a2/* 0100000A */::.ctor() /* 0A000009 */ = "e"

 

The fifth one is the attribute with fields and we place it right at the end of the function.

 

Console.WriteLine("{");

DisplayCustomAttribute("Event" , eventrow , spacesfornested + spacesforrest+2);

for ( int kk = 1 ; kk < MethodSemanticsStruct.Length ; kk++)

{

 

  .event /*14000001*/ [vijay/* 23000002 */]re/* 0100000E */ /*0100000E*/ a

  {

    .custom /*0C000005:0A00000D*/ instance void [vijay/* 23000002 */]a10/* 0100000F */::.ctor() /* 0A00000D */ = "e"

    .addon instance void zzz/* 02000003 */::abc1() /* 06000002 */

  } // end of event zzz::a

 

With an event the attribute is placed at the beginning like that of  a property.

 

public void DisplayAllProperties (int typeindex)

{

DisplayCustomAttribute("Property" , propertyrow , 2 + spacesforrest + spacesfornested);

for ( int kk = 1 ; kk < MethodSemanticsStruct.Length ; kk++)

{

 

}

 

  .property /*17000001*/ instance int32 aa()

  {

    .custom /*0C000004:0A00000C*/ instance void [vijay/* 23000002 */]a9/* 0100000D */::.ctor() /* 0A00000C */ = "e"

    .get /*06000002*/ instance void zzz/* 02000003 */::abc1() /* 06000002 */

  } // end of property zzz::aa

 

public void DisplayEnd()

{

string nspace = NameReserved(GetString(TypeDefStruct[TypeDefStruct.Length-1].nspace));

if ( ! placedend)

{

Console.WriteLine();

Console.WriteLine("// =============================================================");

Console.WriteLine();

placedend = true;

}

if ( nspace == "")

DisplayCustomAttribute("TypeRef" , 0 , 0);

 

.custom /*0C00000D*/ ([vijay/* 23000002 */]a3/* 01000010 *//*01000010*/ ) instance void [vijay/* 23000002 */]a33/* 01000011 */::.ctor() /* 0A00000E */ = "e"

//*********** DISASSEMBLY COMPLETE ***********************

 

We then need to add the TypeRef custom attribute at the very end of the output and thus use  the DisplayEnd if the namespace is null. Also we need the TypeRef for the case where the namespace is not null.

 

public void DisplayOneTypeDefEnd (int typeindex )

{

if ( nspace1 != nspace2 )

{

if ( lasttypedisplayed == typeindex && notprototype  )

{

Console.WriteLine();

Console.WriteLine("// =============================================================");

Console.WriteLine();

placedend = true;

DisplayCustomAttribute("TypeRef" , 0 , 2);

}

}

 

public void DisplayFileTable ()

{

 

DisplayCustomAttribute ("File" , ii , 0);

}

}

 

.file /*26000001*/ aa

.custom /*0C000007:0A000006*/ instance void [mscorlib/* 23000001 */]a177/* 01000006 */::.ctor() /* 0A000006 */ = "e"

 

We have added the method DisplayCustomAttribute at the end of the DisplayFileTable method.

 

Customs that did not happen

 

e.il

.assembly extern vijay

{

}

.mresource aa

{

.custom instance void [vijay]a18::.ctor() = (65)

}

.class zzz

{

}

 

.mresource /*28000001*/ aa

{

  // WARNING: managed resource file aa created

}

 

We have a custom attribute in the mresource directive and ilasm just does not create an entry in the CustomAttribute table for it. In the next series of examples we will manually add a custom attribute and figure out how ildasm handles it.

 

e.il

.assembly extern vijay

{

}

.mresource aa

{

}

.class zzz

{

.custom instance void [vijay]a18::.ctor() = (65)

}

 

We create a simple il file with a custom attribute in a type and a single mresource directive. We print out the initial bytes of the custom attribute table and search for these bytes in a hex editor. We used Ultra Edit as explained earlier. In our case the first six bytes of the CustomAttribute table are 43 0 B 0 5 0. The first two bytes are the parent coded index. The first 5 bits of the first byte 43 are the table number. This is 3 the table number of a TypeDef and the last 13 bits have a value of 2. This is as the first TypeDef we create is number as the global type def is number 1. We change the first byte to 32 as the first 5 bits are 18, the code for a manifest resource and the last three bits taken by themselves are 1, to stand for row number 1. When we run ildasm on e.dll, we get the following output.

 

.mresource /*28000001*/ aa

{

  .custom /*0C000001:0A000001*/ instance void [vijay/* 23000001 */]a18/* 01000002 */::.ctor() /* 0A000001 */ = "e"

  // WARNING: managed resource file aa created

}

 

Thus ildasm recognizes a custom attribute and ilasm does not. We have not added code to our mresource directive to display a custom attribute.

 

.assembly extern vijay

{

}

.module extern aa.dll

.custom instance void [vijay]a18::.ctor() = (65)

.class zzz

{

}

 

We have added a custom attribute to the module extern directive and ilasm ignores it. We once again get back to the earlier program and now change the first byte from 43 to 2C as we want the first 5 bits to be 12 the code for a module ref table and the last three bits to be 1.

 

.module extern aa.dll /*1A000001*/

.custom /*0C000001:0A000001*/ instance void [vijay/* 23000001 */]a18/* 01000002 */::.ctor() /* 0A000001 */ = "e"

 

When we run ildasm on e.dll, we see the custom attribute below the module extern directive.

 

.assembly extern vijay

{

}

.class zzz

{

.permissionset assert = ( 41 42)

.custom instance void [vijay]a18::.ctor() = (65)

}

 

We have added the .permissionset directive and a custom attribute below it. Ilasm instead of associating the custom directive with the Decl security associates it with the type. We once again change the first byte to 28 as 8 is the number for security. When we run ildasm we get the following output.

 

  .permissionset assert = (41 42 )                                           // AB

  .custom /*0C000001:0A000001*/ instance void [vijay/* 23000001 */]a18/* 01000002 */::.ctor() /* 0A000001 */ = "e"

 

Once again ildasm passes where ilasm fails.

 

.assembly extern vijay

{

}

.class zzz implements iii

{

}

.class interface iii

{

}

.class yyy

{

.custom instance void [vijay]a18::.ctor() = (65)

}

 

In this case the  first byte is 83 as the TypeDef row is now 3 and not 2. We change it to 25 as we want row 1 and 5 is the code for the interface implementation. In this case the custom attribute comes at the bottom as.

 

.custom /*0C000001*/ (UNKNOWN_OWNER/*09000001*/ ) instance void [vijay/* 23000001 */]a18/* 01000003 */::.ctor() /* 0A000001 */ = "e"

 

The InterfaceImpl table has a number of 9. We have three more MemberRef, TypeSpec and StandAloneSig that we will do later.

 

Program49.csc

Public void DisplayAllMethods(int typeindex)

{

string vtentrystring = GetVtentryString(methodindex);

methodstring = vtentrystring;

methodstring = methodstring + CreateSpaces(spacesforrest + 2 + spacesfornested) +  "// Method begins at RVA 0x" + MethodStruct[methodindex].rva.ToString("x4");

Console.Write(methodstring);

long fileoffset = ConvertRVA(MethodStruct[methodindex].rva);

mfilestream.Seek(fileoffset , SeekOrigin.Begin);

DisplayFatFormat (NameReserved(GetString(TypeDefStruct[typeindex].name)) , NameReserved(GetString(MethodStruct[methodindex].name)) ,  methodindex );

}

}

public void DisplayFatFormat(string classname , string methodname , int methodindex)

{

DisplayInitialMethodHeader(methodindex);

DisplayMethodILCode(classname , methodname , methodindex);

}

public void DisplayInitialMethodHeader (int methodindex)

{

int tiny = mbinaryreader.ReadByte();

if ( (tiny & 0x03) == 0x03)

tinyformat = false;

else

{

Console.WriteLine(".....{0}" , tiny);

tinyformat = true;

}

mfilestream.Seek(-1,SeekOrigin.Current);

string methodstring = "";

if ( !tinyformat )

{

long where = mfilestream.Position;

short first = mbinaryreader.ReadInt16();

first12 = (short)(first & 0x0fff);

short stacksize = mbinaryreader.ReadInt16();

codesize = mbinaryreader.ReadInt32();

methodstring =  "\r\n" + CreateSpaces(spacesforrest + 2 + spacesfornested) + "// Code size       " + codesize.ToString() + " (0x" + codesize.ToString("x") + ")\r\n";

methodstring = methodstring + CreateSpaces(spacesforrest + 2+ spacesfornested) + ".maxstack  " + stacksize.ToString();

int standalonesig = mbinaryreader.ReadInt32();

int rowstandalonesig = standalonesig  & 0x00ffffff;

Console.WriteLine(".....{0}" , standalonesig.ToString("X8"));

if (StandAloneSigStruct != null && standalonesigarray == null)

standalonesigarray = new string[StandAloneSigStruct.Length];

if ( rowstandalonesig != 0 )

{

CreateSignatureForEachType (5 , StandAloneSigStruct[rowstandalonesig ].index , rowstandalonesig);

if (standalonesigarray [rowstandalonesig ] != "")

{

methodstring = methodstring + "\r\n" + CreateSpaces(spacesforrest + 2+ spacesfornested) +  ".locals /*11" + rowstandalonesig.ToString("X6") + "*/ ";

if ((first12&0x10) == 0x10 )

methodstring = methodstring + "init (" ;

else

methodstring = methodstring + "(" ;

methodstring = methodstring + standalonesigarray[rowstandalonesig ];

methodstring = methodstring + ")";

}

}

}

else

{

mbinaryreader.ReadByte();

codesize = tiny >> 2;

methodstring = methodstring + "\r\n" + CreateSpaces(spacesforrest + 2 + spacesfornested ) +  "// Code size       " + codesize.ToString() + " (0x" + codesize.ToString("x") + ")";

if (codesize != 0)

methodstring = methodstring + "\r\n" + CreateSpaces(spacesforrest + 2+ spacesfornested) +  ".maxstack  8" ;

}

Console.WriteLine(methodstring);

}

public void CreateSignatureForEachType (byte type , int index , int row)

{

int uncompressedbyte , count , howmanybytes;

howmanybytes = CorSigUncompressData(blob , index , out uncompressedbyte);

count = uncompressedbyte;

byte [] blob1 = new byte[count];

Array.Copy(blob , index + howmanybytes , blob1 , 0 , count);

if ( type == 7)

CreatePropertySignature(blob1 , row);

if ( type == 6)

CreateFieldSignature(blob1 , row);

if ( type == 5)

CreateLocalVarSignature(blob1 , row);

if ( type == 1)

CreateMethodDefSignature(blob1 , row);

if ( type == 2)

CreateMethodRefSignature(blob1 , row);

}

public void CreateLocalVarSignature (byte [] blobarray , int row)

{

int index = 0;

standalonesigarray[row] = "";

if ( blobarray[index] != 0x07)

return;

index++;

int howmanybytes,uncompressedbyte ;

howmanybytes = CorSigUncompressData(blobarray , index , out uncompressedbyte);

index = index + howmanybytes;

string returnstring = "";

for ( int l = 1 ; l <= uncompressedbyte ; l++)

{

string typestring = GetElementType(index , blobarray ,  out howmanybytes , 0 , "");

typestring = typestring.Replace("^",",");

int variableVindex = l - 1 ;

returnstring = returnstring + typestring +  " V_" + variableVindex.ToString() ;

if ( l != uncompressedbyte)

returnstring = returnstring + ",\r\n" + CreateSpaces(spacesforrest + 2 + spacesfornested) + CreateSpaces(9);

index = index + howmanybytes;

}

standalonesigarray [row] = returnstring;

}

public void DisplayMethodILCode ( string classname , string methodname  ,int methodindex)

{

Console.Write(CreateSpaces(spacesforrest+ spacesfornested));

Console.Write("}");

if (GetTypeForMethod (methodindex)== 1)

Console.WriteLine(" // end of global method {0}\r\n" , methodname);

else

Console.WriteLine(" // end of method {0}::{1}\r\n" , classname , methodname);

}

string [] standalonesigarray;

int codesize ;

bool tinyformat;

int first12;

}

 

e.il

.class zzz

{

.method void xyz()

{

}

.method void xyz()

{

.locals ( int32 V_1 , int8 j)

.maxstack 2

ret

}

.method void xyz()

{

.locals init (  int8 j)

ret

}

}

 

  .method /*06000001*/ privatescope instance void

          xyz$PST06000001() cil managed

  // SIG: 20 00 01

  {

    // Method begins at RVA 0x2050.....2

    // Code size       0 (0x0)

  } // end of method zzz::xyz

 

  .method /*06000002*/ privatescope instance void

          xyz$PST06000002() cil managed

  // SIG: 20 00 01

  {

    // Method begins at RVA 0x2054.....11000001

    // Code size       1 (0x1)

    .maxstack  2

    .locals /*11000001*/ (int32 V_0,

             int8 V_1)

  } // end of method zzz::xyz

 

  .method /*06000003*/ privatescope instance void

          xyz$PST06000003() cil managed

  // SIG: 20 00 01

  {

    // Method begins at RVA 0x2064.....11000002

    // Code size       1 (0x1)

    .maxstack  8

    .locals /*11000002*/ init (int8 V_0)

  } // end of method zzz::xyz

 

 

Now is when the fun starts as we are actually moving to display or disassemble all the il code in a method. We first start off with the DisplayAllMethods methods where for the last time we make any changes. We replace the last lines with the above lines. The Method array has a field rva that tells us where the code for this method is to be found in memory.

 

We use the ConvertRVA method to tell us where this location is on disk. We then use the Seek function to move the file pointer to this position and call a method named DisplayFatFormat to write out all the il code for us. We pass this method the typename, the method name and the method index.

 

When we arrive at this function, we find that we are calling two more functions, so lets look at the first DisplayInitialMethodHeader which is simply passed the methodindex variable. The first thing we do is read the first byte of the method structure. This is the byte that tells us what is the type of method structure, tiny or large.

 

Actually we only need to look at the first bits of the first byte. If its value is 2, then method header is tiny or else for the large type is 3. As there is no third type, we get away with a simple if and else. This distinction is made mainly depending upon the size of code.

 

We have a instance variable tinyformat that is set to true if the first three bits are not 3. Now that we have read the first byte, we need to move the file pointer back one and then reread the bytes again depending upon whether it is a tiny or large format.

 

Thus the method DisplayFatFormat should have be renamed DisplayFatAndTinyFormat as it reads both. Lets start with the more complex format the fat one. This is normally the default format and applies to methods where the code size is larger than 5 bits or larger than 32 bytes.

 

Also methods that have exceptions, local variables, extra data sections and the operand stack needs to handle more than 8  entities all need the fat format. As we said before the tint format is foe the methods that really do nothing. We store in the variable where the current position of the file pointer that we will use later.

 

We also re-read the first short whose three bytes told us the type of format. We need only the first 12 bits and thus we mask off the last four bits. The top most four bits gives us a size of the header that we will see later. Remember these 12 bits are called flags even though we have only 4 flags of which the tiny and fat have already been done.

 

Thus we have two flags only to decipher. This flags is followed by a short that tells us the size of the stack. Then we have a int that is the size of the code and we immediately first add the Code Size to the methodstring variable. This is followed by the size of the stack. The codesize is not a directive but maxstack is.

 

Then a method may have variables that are local. These are enclosed within the .locals directive and have the same format as parameters, but they cannot have names like parameters but computer generated names beginning with V_ and the local variable ordinal number. The last int gives us a token which tells us where the local variables can be found.

 

For those of you with a short memory, the top byte of a token is the table number and the remaining three bytes the row number. As we have displayed the signature token, the first byte will be 11 the stand alone signature table number. If this row number is zero, then we know that the method has no local variables.

 

Earlier we made a simple mistake. Before our program started displaying stuff we calculated all the method signatures in the CreateSignatures method. Now that way was written in stone and hence he have used another way. We first check whether the array StandAloneSigStruct is not null and our standalonesigarray string array is also not null.

 

The first check is to make sure that we have some method that has local variables and the second to make sure that we create our string array standalonesigarray only once.

 

Then we check whether this method has local variables and if yes, we call the method CreateSignatureForEachType with a number 5, the type for stand alone signatures and then followed by the blob array offset and the StandAloneSig table row number rowstandalonesig to set the standalonesigarray array member to.

 

This simply sets one member of the array to a signature. We will come to the CreateSignatureForEachType method a little later. Now if the corresponding standalonesigarray array member is not null we write out the directive locals along with the signature.

 

The locals directive has one more keyword init that will call the default constructors for all the local variables. This flags has a value of 0x10 and we then write out the open and close brackets of the locals directive. This completes the fat format the tiny format is really very simple.

 

We read the first byte and then right shift it by two to remove the first two bits that were the flags. This now give us the size of the code that we write out and if the code size is not zero, the size of the stack is 8. This completes the initial reading of the method header.

 

In the CreateSignatureForEachType method, we simply call the method CreateLocalVarSignature. This is once again a signature that is easy for us as the first byte is always 7. The next byte is the count of the number of local variables that make up the signature. This is followed by the individual local variable data types that we pick up using the GetElementType method.

 

We also replace the ^ with the comma for arrays. We then need to write out the name of the variable and hence start with V_ and the loop variable l minus 1 as the count starts with 0. We then need to place a comma, followed with a enter as each of these variables needs to be on a new line with the right amount of spaces.

 

Thus this code cannot be called for the earlier signatures as then we do not have the value of the variables spacesfornested and spacesforrest. We finally set the standalonesigarray array with the returnstring variable.

 

We have the method DisplayMethodILCode that only displays some spaces and then depending upon whether it is a global method or not displays the end of the method. We could have used the method IsGlobalMethod instead of GetTypeForMethod at all times.

 

Now coming to the IL code generated by our program, we have the first function show us that it has the tiny format as the else is true and the firstbyte has a value of 2 which also gives the code a size of zero.

 

For the second two methods, the local var token starts with 0x11 and then we have the row numbers in the StandAloneSig table and not the method table. If we specify a maxstack, then ours gets used or else a default value of 8 is taken. Also for the tint format the maxstack has no place where it can be stored.

 

If we make the mistake of giving a local variable a name, ilasm removes the name and we then have to generate a standard name for it.

 

Program50.csc

public void DisplayMethodILCode ( string classname , string methodname  ,int methodindex)

{

byte [] codearray = new byte[codesize];

for ( int i = 1 ; i <= codesize ; i++)

{

codearray[i-1] = mbinaryreader.ReadByte();

Console.Write(" {0}" , codearray[i-1].ToString("X") );

}

Console.WriteLine();

Console.Write(CreateSpaces(spacesforrest+ spacesfornested));

Console.Write("}");

}

 

e.il

.class zzz

{

.method void xyz()

{

.locals ( int32 V_1 , int8 j)

.maxstack 2

ret

nop

ret

nop

ret

}

}

 

.method /*06000001*/ privatescope instance void

          xyz$PST06000001() cil managed

  // SIG: 20 00 01

  {

    // Method begins at RVA 0x2050

    // Code size       5 (0x5)

    .maxstack  2

    .locals /*11000001*/ (int32 V_0,

             int8 V_1)

 2A 0 2A 0 2A

  } // end of method zzz::xyz

 

Now is when we start out book. It is in the method DisplayMethodILCode that we get our hands on the il code. This il code is available just after the initial method structure.  We need to store this code in an array codearray that we initialize to the size of the code that we have stored in the instance variable codesize.

 

We read each byte into the array and then simply display the members of the array.  What we see is the ret instruction that must be present as the last instruction of every method has a value of 0x2a. The nop instruction that behaves like most of the world has a value of zero as it is supposed to do nothing.

 

Thus after the method structure we have a series of bytes that we will have to decipher into il code. The next program does just that.

 

Program51.csc

using System.Reflection;

using System.Reflection.Emit;

 

public void abc(string [] args)

{

ReadPEStructures(args);

DisplayPEStructures();

ReadandDisplayImportAdressTable();

ReadandDisplayCLRHeader();

ReadStreamsData();

FillTableSizes();

ReadTablesIntoStructures();

DisplayTablesForDebugging();

ReadandDisplayVTableFixup();

ReadandDisplayExportAddressTableJumps();

FillArray();

FillOpCodeArray ();

CreateSignatures();

}

OpCode [] OpCodesArray; 

OpCode [] OpCodesArray1; 

public void FillOpCodeArray()

{

OpCodesArray = new OpCode[66000];

OpCodesArray1 = new OpCode[32];

FieldInfo[] fields = typeof(OpCodes).GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly);

foreach (FieldInfo f in fields)

{

OpCode o = (OpCode) f.GetValue(null);

if ( o.Value <= 255 && o.Value >= 0)

OpCodesArray[(short)o.Value] = o;

}

}

public void DisplayMethodILCode ( string classname , string methodname  ,int methodindex)

{

byte []codearray = new byte[codesize];

for ( int i = 1 ; i <= codesize ; i++)

codearray[i-1] = mbinaryreader.ReadByte();

for ( int arrayoffset = 0 ; arrayoffset  < codesize ; )

{

int instructionbyte  = codearray[arrayoffset];

OpCode opcode;

opcode = OpCodesArray [instructionbyte];

string strings = CreateSpaces(spacesforrest+2+ spacesfornested);

strings = strings + "IL_" + arrayoffset .ToString("x4") + ":  /* " + opcode.Value.ToString("X2") + "   | ";

int sizeofinstructionanddata  = DecodeILInstrcution2( opcode , arrayoffset  , codearray , methodindex , strings);

Console.WriteLine();

arrayoffset  = arrayoffset  + sizeofinstructionanddata;

}

Console.Write(CreateSpaces(spacesforrest+ spacesfornested));

Console.Write("}");

if ( GetTypeForMethod(methodindex)== 1)

Console.WriteLine(" // end of global method {0}\r\n" , methodname);

else

Console.WriteLine(" // end of method {0}::{1}\r\n" , classname , methodname);

}

public int DecodeILInstrcution2 (OpCode opcode , int codeindex , byte [] codearray , int methodindex , string strings)

{

int sizeofinstructiondata = 0;

if ( opcode.OperandType == OperandType.InlineNone)

{

Console.Write(strings);

Console.Write("{0}*/ {1}",CreateSpaces(17) ,  opcode.Name);

if ( opcode.Name == "ret" && codeindex != (codearray.Length -1) )

Console.WriteLine();

if ( opcode.Name == "throw" && codeindex != (codearray.Length -1) )

Console.WriteLine();

sizeofinstructiondata =1;

}

return sizeofinstructiondata;

}

}

 

e.il

.class zzz

{

.method void xyz()

{

.locals ( int32 V_1 , int8 j)

.maxstack 2

sub

ret

add

mul

nop

ret

}

}

 

  .method /*06000001*/ privatescope instance void

          xyz$PST06000001() cil managed

  // SIG: 20 00 01

  {

    // Method begins at RVA 0x2050

    // Code size       6 (0x6)

    .maxstack  2

    .locals /*11000001*/ (int32 V_0,

             int8 V_1)

    IL_0000:  /* 59   |                  */ sub

    IL_0001:  /* 2A   |                  */ ret

 

    IL_0002:  /* 58   |                  */ add

    IL_0003:  /* 5A   |                  */ mul

    IL_0004:  /* 00   |                  */ nop

    IL_0005:  /* 2A   |                  */ ret

  } // end of method zzz::xyz

 

Finally we have some il code disassembled. When you look at the il file, we have a series of il instructions popularly  called opcodes. We have used ones like ret and nop earlier and now add, mul etc. These opcodes have one thing in common, they all take no parameters. The il world has divided the il instructions into a family depending upon the parameters or operands that they take.

 

All il instructions that have the same operand type behave in a similar manner. The il world has also given us a enum called OperandType that tells us how many different types of operands a il instruction takes. What we now need to do is handle each operand type.

 

What we cannot do is tell ourselves that we have a series of if statements that takes each opcode and figures out what the name of the opcode is and what is its operand type. We need a more generic way of handling the above problem. Lets take a look at method abc first.

 

We have introduced a new method FillOpCodeArray just before we create the method signatures. We have also defined two instance arrays OpCodesArray and OpCodesArray1 of type OpCode. These classes are from the System.Reflection namespace. The OpCode structure represents a Il instruction.

 

It has properties like Name that tells us the name of the Il instruction, OperandType that tell us the Operand types that this instruction takes and Value that tell us the byte value or number of the il instruction. For example the above program showed us that 0x2a is the value of the ret instruction, and 0 for nop.

 

Thus we now need fill up the OpCodesArray with a valid OpCode object that represents each opcode. As 0x2A represents a ret instruction, the 2a member of the OpCodesArray array should stand for the ret instruction. In the method FillOpCodeArray, this is what we do.

 

We first initialize the OpCodesArray array to a very large value of members 66000 and the second array to 32 members only. This second array will be used later. The foreach loop lets us iterate through an object. The FieldInfo class simply provides access to the metadata for field attributes.

 

The GetFields function which is part of the Type class gets us all the fields from the Type that calls it. In this case we require all the fields from the class OpCodes. Every valid opcode in the IL instruction set is a field in the OpCode class. Thus if we have 500 il instructions we will have 500 fields in the OpCode class.

 

These fields are static and read only. To be specific, the OpCode class has 221 fields in all. This way we can figure out all the fields that a certain class has. The parameter to the GetFields function allows us to get at those fields that meet certain conditions like they must be static or public etc.

 

Now that we have all the IL instructions with us in the FieldInfo array we simply use the GetValue method to give us the actual field. The array on the left is type OpCode and GetValue returns an object and hence the cast. Thus variable o now is the OpCode object where o.Name is the name of the OpCode say ret and o.Value is its byte value 0x2a.

 

Now all that we have to do is initialize the 2a member of the array with o which is what we do in the next line. We only take those opcodes that lie between 0 and 255 only. The other cases we will handle differently a little later. Thus we now have an array with all the opcode values, name and types of parameters.

 

Lets now move on to the method DisplayMethodILCode which first reads all the byte codes into the codearray array. Then we use another for loop to iterate through this array. We first pick up the first or zeroth byte from the codearray using the loop variable arrayoffset.

 

We know that this byte is an opcode value and not data and thus pick up the relevant OpCode structure that represents it from the OpCodesArray array. Thus if variable instructionbyte had a value 0x2a, then opcode would stand for the OpCode for the ret instruction.

 

We write out the initial spaces as always and then the words IL_ followed by the instruction offset within the code bytes. This is represented by the loop variable arrayoffset. We then have to add a colon some spaces and then in comments the actual opcode value, 2a for a ret.

 

Then some more spaces and a or sign | . Some instructions write some data after the or sign. Thus what we have done so far is what is the same for each instruction. We now call the method DecodeILInstrcution2 to write out the actual instruction for us.

 

We pass it the OpCode representing the instruction, the array offset from the beginning of the first byte of code arrayoffset, the entire codearray of all the bytes, the methodindex of the method and finally the initial string that we have just created of the instruction stored in the variables strings.

 

This function after writing out the entire single instruction, will then return a answer that will give us how many bytes this instruction has taken. This is important as the il instructions take up a variable number of bytes.

 

We add this return value to arrayoffset  so that the arrayoffset  variable points to the next il instruction at the start of the loop. Thus all our focus from now on will be on the DecodeILInstrcution2 method. Lets move there now.

 

In this method we have a series of if statements that handle each operand type. The first one we check for is InLineNone where we have no operands at all. Thus we use the parameter opcode and check whether its field OperandType equals the value of the enum OperandType InLineLine.

 

If we find a match we first write out the initial bytes that we are passed and then the name of the opcode. The name does not have to ret as there are a hundred opcodes that have a type InLineNone. This is why we simply use the Name property and we do have to worry about the name of the OpCode.

 

This way we do have a hundred if statements that check each opcode value and write out a name. The only problem is that only if the opcode name is ret or throw and we are not the last instruction, we should write out a enter. Thus if a ret or throw is the last instruction, a extra enter will now be written out, else it will be.

 

Also the variable sizeofinstructiondata will return the number of bytes taken up by this instruction type and for no operands the value is obviously one. This way we will handle each Operand Type. Lets sum up what we have done so far. As there are far too many IL instructions for our liking, we create an array of OpCode structures that will handle each IL instruction. What we need to know is the name and operand types that this IL instruction will take. We read each byte from the codesize array and then read the corresponding OpCode from the OpCodesArray array. Then we figure out its operand type and handle each operand type differently. Lets see it work with the next example. 

 

e.il

.class zzz

{

.method void xyz()

{

arglist

}

}

 

  .method /*06000001*/ privatescope instance void

          xyz$PST06000001() cil managed

  // SIG: 20 00 01

  {

    // Method begins at RVA 0x2050

    // Code size       2 (0x2)

    .maxstack  8

    IL_0000:  /* FE   |                  */ prefix1

    IL_0001:  /* 00   |                  */ nop

  } // end of method zzz::xyz

 

Things just do not cut it. We have used a simple IL instruction arglist and for some reason we get two instructions. Why. The guys who designed the IL instruction set toke some inspiration from the Java guys who had the concept of a byte code which said that every instruction should not be larger than one byte or have a value ranging from 0 to 255.

 

Unfortunately there are too many IL instructions and hence they

designed a different way of giving them values. The first 225 or 0 to 224 IL opcodes have a direct value. From now on things change. The arglist has a value FE 00 as FE stands for Prefix 1 and there are seven such prefixes.

 

Thus we first need to check the first byte and if it is larger than F8 we have a prefix byte. FF is the last single byte instruction and is called the prefixref. Thus for two byte instructions we have to ignore the first byte and simply read the second. Thus we have to make modifications in our program to handle two byte instructions.

 

Program52.csc

public void FillOpCodeArray()

{

OpCodesArray = new OpCode[256];

OpCodesArray1 = new OpCode[32];

FieldInfo[] fields = typeof(OpCodes).GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly);

foreach (FieldInfo f in fields)

{

OpCode o = (OpCode) f.GetValue(null);

if ( o.Value <= 255 && o.Value >= 0)

OpCodesArray[(short)o.Value] = o;

OpCodesArray1[(ushort) 0x00] = OpCodes.Arglist;

OpCodesArray1[(ushort) 0x01] = OpCodes.Ceq;

OpCodesArray1[(ushort) 0x02] = OpCodes.Cgt;

OpCodesArray1[(ushort) 0x03] = OpCodes.Cgt_Un;

OpCodesArray1[(ushort) 0x04] = OpCodes.Clt;

OpCodesArray1[(ushort) 0x05] = OpCodes.Clt_Un;

OpCodesArray1[(ushort) 0x06] = OpCodes.Ldftn;

OpCodesArray1[(ushort) 0x07] = OpCodes.Ldvirtftn;

OpCodesArray1[(ushort) 0x09] = OpCodes.Ldarg;

OpCodesArray1[(ushort) 0x0A] = OpCodes.Ldarga;

OpCodesArray1[(ushort) 0x0B] = OpCodes.Starg;

OpCodesArray1[(ushort) 0x0C] = OpCodes.Ldloc;

OpCodesArray1[(ushort) 0x0D] = OpCodes.Ldloca;

OpCodesArray1[(ushort) 0x0E] = OpCodes.Stloc;

OpCodesArray1[(ushort) 0x0F] = OpCodes.Localloc;

OpCodesArray1[(ushort) 0x11] = OpCodes.Endfilter;

OpCodesArray1[(ushort) 0x12] = OpCodes.Unaligned;

OpCodesArray1[(ushort) 0x13] = OpCodes.Volatile;

OpCodesArray1[(ushort) 0x14] = OpCodes.Tailcall;

OpCodesArray1[(ushort) 0x15] = OpCodes.Initobj;

OpCodesArray1[(ushort) 0x17] = OpCodes.Cpblk;

OpCodesArray1[(ushort) 0x18] = OpCodes.Initblk;

OpCodesArray1[(ushort) 0x1A] = OpCodes.Rethrow;

OpCodesArray1[(ushort) 0x1C] = OpCodes.Sizeof;

OpCodesArray1[(ushort) 0x1D] = OpCodes.Refanytype;

}

}

public void DisplayMethodILCode ( string classname , string methodname  ,int methodindex)

{

byte [] codearray = new byte[codesize];

for ( int i = 1 ; i <= codesize ; i++)

codearray[i-1] = mbinaryreader.ReadByte();

for ( int arrayoffset = 0 ; arrayoffset  < codesize ; )

{

int instructionbyte  = codearray[arrayoffset ];

OpCode opcode;

string strings = "";

if ( instructionbyte == 0xFE)

{

instructionbyte = codearray[arrayoffset +1];

opcode = OpCodesArray1 [instructionbyte] ;

strings = CreateSpaces(spacesforrest+2+ spacesfornested);

strings = strings + "IL_" + arrayoffset.ToString("x4") + ":  /* " + opcode.Value.ToString("X") + " | ";

arrayoffset  = arrayoffset  + 1;

}

else

{

opcode = OpCodesArray[instructionbyte];

strings = CreateSpaces(spacesforrest+2+ spacesfornested);

strings = strings + "IL_" + arrayoffset .ToString("x4") + ":  /* " + opcode.Value.ToString("X2") + "   | ";

}

int sizeofinstructionanddata  = DecodeILInstrcution2 ( opcode , arrayoffset  , codearray , methodindex , strings);

Console.WriteLine();

arrayoffset  = arrayoffset  + sizeofinstructionanddata;

}

Console.Write(CreateSpaces(spacesforrest+ spacesfornested));

Console.Write("}");

if ( GetTypeForMethod(methodindex)== 1)

Console.WriteLine(" // end of global method {0}\r\n" , methodname);

else

Console.WriteLine(" // end of method {0}::{1}\r\n" , classname , methodname);

}

 

e.il

.class zzz

{

.method void xyz()

{

ret

arglist

}

}

 

  .method /*06000001*/ privatescope instance void

          xyz$PST06000001() cil managed

  // SIG: 20 00 01

  {

    // Method begins at RVA 0x2050

    // Code size       3 (0x3)

    .maxstack  8

    IL_0000:  /* 2A   |                  */ ret

 

    IL_0001:  /* FE00 |                  */ arglist

  } // end of method zzz::xyz

 

Lets start with the method FillOpCodeArray. Here we have reduced the size of the first array to a reasonable 256 as the one byte opcodes will not exceed 255. Thus the array OpCodesArray will store for us all the single byte opcodes.

 

Then we have a second array OpCodesArray1 that will store the two byte opcodes. These unfortunately have to be keyed in manually and they are 30 of them. Thus we will now have to search in two arrays for our opcode. This is what we do in the method DisplayMethodILCode.

 

We read the first byte from the codearray and then check if it is the prefix1 opcode. If it is, we then pick up the second or the next byte and use this as an offset into the OpCodesArray1 array to give us the right opcode.

 

The key point is that we are reading the second byte and then picking the opcode from the second and not the first opcode array. We then set the strings variable to the initial number of spaces as well as the initial parts of the Il instruction which is the IL_ and the line number stored in the arrayoffset variable.

 

We stop at the or sign as before. As we are dealing with a 2 byte instruction, the arrayoffset variable is increased by 1. If it is  a one byte instruction, we do what we did earlier.

 

Thus when we call the method DecodeILInstrcution2, it does not care whether it is a one byte or two byte instruction set. The strings variable has a extra space for a one byte instruction set.

 

Finally we will list out all the InLineNone operand types or those that take up no parameters. We are decoding instructions depending upon the types of parameters they take. They are 146 instructions that take no operand. These are nop , break , ldarg.0 , ldarg.1 , ldarg.2 , ldarg.3 , ldloc.0 , ldloc.1 , ldloc.2 , ldloc.3 , stloc.0 , stloc.1 , stloc.2 , stloc.3 , ldnull , ldc.i4.m1 , ldc.i4.0 , ldc.i4.1 , ldc.i4.2 , ldc.i4.3 , ldc.i4.4 , ldc.i4.5 , ldc.i4.6 , ldc.i4.7 , ldc.i4.8 , dup , pop , ret , ldind.i1 , ldind.u1 , ldind.i2 , ldind.u2 , ldind.i4 , ldind.u4 , ldind.i8 , ldind.i , ldind.r4 , ldind.r8 , ldind.ref , stind.ref , stind.i1 , stind.i2 , stind.i4 , stind.i8 , stind.r4 , stind.r8 , add , sub , mul , div , div.un , rem , rem.un , and , or , xor , shl , shr , shr.un , neg , not , conv.i1 , conv.i2 , conv.i4 , conv.i8 , conv.r4 , conv.r8 , conv.u4 , conv.u8 , conv.r.un , throw , conv.ovf.i1.un , conv.ovf.i2.un , conv.ovf.i4.un , conv.ovf.i8.un , conv.ovf.u1.un , conv.ovf.u2.un , conv.ovf.u4.un , conv.ovf.u8.un , conv.ovf.i.un , conv.ovf.u.un , ldlen , ldelem.i1 , ldelem.u1 , ldelem.i2 , ldelem.u2 , ldelem.i4 , ldelem.u4 , ldelem.i8 , ldelem.i , ldelem.r4 , ldelem.r8 , ldelem.ref , stelem.i , stelem.i1 , stelem.i2 , stelem.i4 , stelem.i8 , stelem.r4 , stelem.r8 , stelem.ref , conv.ovf.i1 , conv.ovf.u1 , conv.ovf.i2 , conv.ovf.u2 , conv.ovf.i4 , conv.ovf.u4 , conv.ovf.i8 , conv.ovf.u8 , ckfinite , conv.u2 , conv.u1 , conv.i , conv.ovf.i , conv.ovf.u , add.ovf , add.ovf.un , mul.ovf , mul.ovf.un , sub.ovf , sub.ovf.un , endfinally , stind.i , conv.u , prefix7 , prefix6 , prefix5 , prefix4 , prefix3 , prefix2 , prefix1 , prefixref , arglist , ceq , cgt , cgt.un , clt , clt.un , localloc , endfilter , volatile. , tail. , cpblk , initblk , rethrow , refanytype. This is just for completeness.

 

Program53.csc

public int DecodeILInstrcution2 (OpCode opcode , int codeindex , byte [] codearray , int methodindex , string strings)

{

int sizeofinstructiondata = 0;

if ( opcode.OperandType == OperandType.InlineI)

{

Console.Write(strings);

int token = BitConverter.ToInt32( codearray , codeindex + 1 );

byte b1 = codearray[codeindex+1];

byte b2 = codearray[codeindex+2];

byte b3 = codearray[codeindex+3];

byte b4 = codearray[codeindex+4];

Console.Write("{0}{1}{2}{3}",b1.ToString("X2"),b2.ToString("X2"),b3.ToString("X2"),b4.ToString("X2"));

Console.Write(CreateSpaces(8))