More methodology
I was all over the profiling and metadata jigsaw puzzle a couple of weeks ago, foraging for clues on Google in the header files and the specs. I've come to the conclusion that this is a tragically under-explored, under-used and under-documented part of .Net, and Microsoft really need to get their act together. There's so much that can be done with these API's and I'm sure people are scared off by the lack of support.
So, with a view to improving the situation, I will keep writing up my adventures in this hyar blog.
In the last entry I showed how to get a pointer to a method. Now we're going to examine the details of that method:
There are two formats for method blobs - tiny and fat, represented by the structs IMAGE_COR_ILMETHOD_TINY and IMAGE_COR_ILMETHOD_FAT. The tiny format is used for methods less than 64 bytes in size, with no local variables. Most methods you will encounter will be fat, but the two formats need to be approached differently.
One problem I've had is going directly from the raw blob to one of those structs. As far as I can tell, we need to first go through a union called IMAGE_COR_ILMETHOD, which can hold a method in either tiny or fat form:
LPCBYTE pMethodHeader;
ULONG methodSize;
m_pProfilerInfo->GetILFunctionBody(moduleID, functionToken, &pMethodHeader, &methodSize);
IMAGE_COR_ILMETHOD* pMethod = (IMAGE_COR_ILMETHOD*)pMethodHeader;
IMAGE_COR_ILMETHOD_FAT fatImage = pMethod->Fat;
The trouble with this is that the method could be tiny. Luckily, Microsoft provide a couple of structs in the corhlpr.h header which make things a little easier: COR_ILMETHOD_TINY and COR_ILMETHOD_FAT (they both inherit from the IMAGE_xxx types above). They have the methods IsTiny() and IsFat() respectively. Thus:
IMAGE_COR_ILMETHOD* pMethod = (IMAGE_COR_ILMETHOD*)pMethodHeader;
COR_ILMETHOD_FAT* fatImage = (COR_ILMETHOD_FAT*)&pMethod->Fat;
if(!fatImage->IsFat()) {
COR_ILMETHOD_TINY* tinyImage = (COR_ILMETHOD_TINY*)&pMethod->Tiny;
//Handle Tiny method
}
else {
//Handle Fat method
}Now that we've tamed the method blob, we can look at the details of the header, and the code that comes after it. As an example: printf("Flags: %X\n", fatImage->Flags);
printf("Size: %X\n", fatImage->Size);
printf("MaxStack: %X\n", fatImage->MaxStack);
printf ("CodeSize: %X\n", fatImage->CodeSize);
printf("LocalVarSigTok: %X\n", fatImage->LocalVarSigTok);
byte* codeBytes = fatImage->GetCode();
ULONG codeSize = fatImage->CodeSize;
for(ULONG i = 0; i < codeSize; i++) {
if(codeBytes[i] > 0x0F) {
printf("codeBytes[%u] = 0x%X;\n", i, bytes[i]);
}
else {
printf("codeBytes[%u] = 0x0%X;\n", i, bytes[i]);
}
}
The first field, Flags, consists of 12 bits and describes the method format (Fat or Tiny), whether local variables should be initialized before use, and whether there are more sections tagged onto the end of the main code block. Size is the size in DWORDs of the header structure, not the whole method. Currently this is always 3 (12 bytes). MaxStack is, yup, the maximum stack size required by this method (stack size is measured in abstract "items", not in physical memory). CodeSize is the size in bytes of the IL blob following the header, and LocalVarSigTok is a metadata token. It acts as an index into the metadata table of the module containing this method. I'll cover this in more depth next time, and show you how to emit bytecode.