Before I begin talking about how I've implemented some of the commands in SPT, I need to talk about some of the supporting methods that make doing things much easier. The IXCLRDataProcess/Dacp*Request API is not very user friendly and very verbose, by creating wrappers around them, we can spend more time building extensions and less time worrying about implementation.
The first thing we need to do is get an instance of an IXCLRDataProcess. This is the core interface we'll be using for everything. See my previous posts for more information on it. Conveniently, WinDBG has a semi-undocumented IoCtrl request to give us an instance of this object. It internally handles creating an ICLRDataTarget implementation, finding and loading the correct mscordacwks dll, and calling CLRDataCreateInstance for you. Using it is simple, the IoCtrl request code is documented, so we simply need to make a request and call Ioctl. (Note: Ioctl is defined in wdbgexts.h)
HRESULT InitIXCLRDataFromWinDBG(IXCLRDataProcess **ppDac)
ixDataQuery.Iid = &__uuidof(IXCLRDataProcess);
if (!Ioctl(IG_GET_CLR_DATA_INTERFACE, &ixDataQuery, sizeof(ixDataQuery)))
*ppDac = (IXCLRDataProcess*)ixDataQuery.Iface;
Next, we'll need an easy way to read the value of a field from a managed object. This is a multi-step process:
- Make sure the memory address (lets call it pObj) is pointing at a real managed object.
- Get the MethodTable for the object.
- Find the field by name, note: this may require traversing up the type hierarchy looking at parent classes for their fields as well.
- Figure out the field offset from the start of the managed object.
- Read the value from the target process's memory.
1/2 - Validate the address and get the MethodTable
This is the easiest part. One of the Dacp requests, (they're all defined in dacpriv.h in the SSCLI), is DacpObjectData, which most closely maps to !DumpObj in SOS. Requesting this with our pObj will give us a few useful things. 1) The MethodTable of the object, 2) if the object isn't a managed object, it'll return a failure code.
3 - Find the field by name
This is by far the hardest. It's an 8 step recursive process:
- Use a DacpMethodTableData request to get the parent method table of the current MT.
- If the parent MT isn't NULL, call ourselves recursively. This will allow us to start at System.Object and work up, the reason for doing this will make sense later.
- Get the number of static + instance fields on the current class. The DacpMethodTableData request has an EEClass in the return information, and we can use DacpEEClassData to get the field information and module information. Important data here is: FirstField, wNumInstanceFields, wNumStaticFields, Module.
- Get an instance of IMetaDataImport. We can use DacpModuleData and the Module token from our EEClass data to get an instance of the IXCLRDataModule containing the EEClass. This can then be QI'd to an IMetaDataImport object.
- Iterate over the fields (they're a linked list, start a FirstField and keep going) until you hit (already seen fields from previous recurison) + wNumInstanceFields + wNumStaticFields.
- The condition needs some explaining. Imagine a 2 class hierarchy, Foo and Bar, where Bar inherits Foo. Foo defines 2 fields, a and b, and Bar defines one, c. If we look at the EEClass data on Bar, we'll see wNumInstanceFields = 3, but traversing the field list will only give us 1 field, since the other 2 are on the Foo EEClass. This is where the recursion comes in, by traversing from the bottom of the type hierarchy up and keeping count of how many fields we've seen, by the time we visit Bar we've already looked at Foo and seen 2 fields, so that means we only need to look at one field on Bar, which is exactly how many fields are valid in the linked list.
- Static fields are simpler because they don't inherit up the type hierarchy. There's no need to keep track of the number of fields you've seen in previous recursion steps.
- For each field, use DacpFieldDescData to get the FieldDesc for it. We'll use this to figure out 1) if the field is static/instance, 2) the field token ("mb"), 3) the pointer to the next field.
- Use the IMetaDataImport object we got in step 3 to get the field name for the field token. I used IMetaDataImport::GetMemberProps, GetFieldProps might work too?
- Compare to the input field name
- Continue if not a match.
4/5 - Phew, figure out the field offset and read the value!
Compared to step 3, this is a walk in the park. There's only two conditions we need to worry about. 1) If the type is a value type, dwOffset on the field is relative to the object address. 2) If it's a reference type, dwOffset is relative to the start of the object + sizeof(void*). This is because reference types have a methodtable pointer at the start of their instance.
To read the value from the target process, we can use IDebugDataSpaces::ReadVirtual and pass it the computed address from above.