locked
Problems reading ICorDebugStringValue's returned from ICorDebugProcess5::GetObject() RRS feed

  • Question

  • Hi

    Some quick background: I've got a small test application (some code further down) that I load into WinDbg and then use ".dump /map ..." to create a crash dump. The test app spawns a thread that has some local varibales and then has the thread throw an exception (which is when control returns to WinDbg and I save the crash dump).

    I've been experimenting with ICorDebugProcess5::EnumerateHeap() (see here) and I've run into a problem reading the values (strings in this case) of the returned COR_HEAPOBJECT's using ICorDebugProcess5::GetObject().

    My process at the moment is as follows:
    1. Enumerate each item on the heap using ICorDebugProcess5::EnumerateHeap()
    2. For each COR_HEAPOBJECT returned I call ICorDebugProcess5::GetTypeForTypeID() to retrieve the ICorDebugType interface
    3. I then use ICorDebugType::GetType() to determine the type of the returned heap object.
    4. If the type is ELEMENT_TYPE_CLASS then I use the metadata to determine the name of the class.

    Based on the above steps I display a list of all of the heap objects and their types. My problem occurs when I select a "System.String" object and attempt to read it's value.

    The steps I follow for this are:
    1. Call ICorDebugProcess5::GetObject() using the address of the returned COR_HEAPOBJECT to get a ICorDebugObjectValue interface
    2. Check if the returned ICorDebugObjectValue supports ICorDebugStringValue
    3. Use ICorDebugStringValue::GetLength() to read the length of the string

    This is where my problems started. When I call GetLength() I get a huge value returned e.g. 1830167992, which clearly isn't right. I thought this was a problem with my declaration of ICorDebugStringValue, but further testing shows that my declaration is correct (more on this below).

    So even though I could successfully query for ICorDebugStringValue, the value returned from GetLength() doesn't seem  right. I tried a few "System.String" values, and had similar results.

    Since I couldn't get ICorDebugStringValue::GetLength() to work I thought I'd try reading the COR_FIELD's of the "System.String" COR_HEAPOBJECT instead (since a string internally has m_stringLength & m_firstChar fields). When I call ICorDebugObjectValue::GetFieldValue() for either field I get CORDBG_E_FIELD_NOT_AVAILABLE returned.

    Since that wasn't working I thought I would try updating my sample app to include a custom class with a string as one of its fields, like this:

    public void DoSomething()
    {
        SomeParameters blah = new SomeParameters();
        blah.anInt = 5;
        blah.aFloat = 11.23F;
        blah.aString = "I am a test string";
        string aLocalString = "I am a local string";
        // *snip* throw an exception  code *snip*
        MessageBox.Show(aLocalString); // hopefully this stops any optimization of aLocalString... just in case
    }
    
    private class SomeParameters
    {
        public int anInt;
        public float aFloat;
        public string aString;
    }

    Luckily it was quite easy to find the instance of "SomeParameters" (blah) in the returned heap objects. My process in this case is (I've skipped a few steps to keep it brief):
    1. Call GetObject() on the address of the heap object for "SomeParameters"
    2. Call ICorDebugObjectValue::GetFieldValue() to get the ICorDebugValue of the "aString" field
    3. Deference the returned value as needed
    4. Check if the dereferenced value supports ICorDebugStringValue
    5. If ICorDebugStringValue is supported then check GetLength() and GetString()

    The above process works without problems, I can correctly read the length of the string and it's value: "I am a test string".

    Looking at theap I can see that blah.aString and aLocalString both exist on the heap as COR_HEAPOBJECT's themselves (as well as "blah" itself). I can't read either "blah.aString" or "aLocalString" directly, but I can read "blah.aString" if I read it as a field of "blah".

    In summary:
    1. I have COR_HEAPOBJECT's of type ELEMENT_TYPE_CLASS ("System.String") that support ICorDebugStringValue, but GetLength() returns an invalid length. I can't read the fields of the object either.
    2. I have COR_FIELD's of type ELEMENT_TYPE_STRING inside a COR_HEAPOBJECT (ELEMENT_TYPE_CLASS) that support ICorDebugStringValue that works correctly.

    My question is: what is the difference between COR_HEAPOBJECT's of type ELEMENT_TYPE_CLASS ("System.String") and COR_FIELDS of type ELEMENT_TYPE_STRING that both support ICorDebugStringValue but only one ICorDebugStringValue actually works?

    In an attempt to explain this a bit clearer I copied the output from WinDbg of what I'm trying to do using the COR_HEAPOBJECT's. My code can read "aString" as a field of object 0x02A0C05C, but I can't read "aString" directly from the heap i.e. I can't read object 0x02a0bff4 directly off the heap even though it is the same object as the "aString" field inside 0x02A0C05C (which I can read).

    Thanks, Greg

    0:006> !do  0x02A0BFF4
    Name:        System.String
    MethodTable: 6b237dfc
    EEClass:     6af0185c
    Size:        50(0x32) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    String:      I am a test string
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    6b236844  40000aa        4         System.Int32  1 instance       18 m_stringLength
    6b238940  40000ab        8          System.Char  1 instance       49 m_firstChar
    6b237dfc  40000ac        c        System.String  0   shared   static Empty
        >> Domain:Value  00ea5738:NotInit  <<
    0:006> !do  0x02A0C028
    Name:        System.String
    MethodTable: 6b237dfc
    EEClass:     6af0185c
    Size:        52(0x34) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    String:      I am a local string
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    6b236844  40000aa        4         System.Int32  1 instance       19 m_stringLength
    6b238940  40000ab        8          System.Char  1 instance       49 m_firstChar
    6b237dfc  40000ac        c        System.String  0   shared   static Empty
        >> Domain:Value  00ea5738:NotInit  <<
    0:006> !do 0x02A0C05C
    Name:        CrashTestDummy.Form1+SomeParameters
    MethodTable: 00d773c4
    EEClass:     00e81898
    Size:        20(0x14) bytes
    File:        C:\Users\Greg\Documents\Visual Studio 11\Projects\CrashTestDummy\bin\Debug\CrashTestDummy.exe
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    6b236844  4000005        8         System.Int32  1 instance        5 anInt
    6b257f74  4000006        c        System.Single  1 instance 11.230000 aFloat
    6b237dfc  4000007        4        System.String  0 instance 02a0bff4 aString


    • Edited by Greg Nagel Tuesday, May 15, 2012 5:28 AM
    Monday, May 14, 2012 6:53 PM

Answers

  • Just wanted to follow up - sadly it looks like the ICorDebugProcess5::GetObject() is buggy in several cases and it will give you an ICorDebugObjectValue that behaves incorrectly in future calls. Your string example above is one of them, but it looks like it also misreports the CorElementType, array data seems erroneous, and GetFieldValue isn't working properly (also as you noticed). 

    If you need an ICorDebugValue an onerous workaround is get an ICorDebugReferenceValue from somewhere, use the SetValue() function to set its pointer to the desired object address, then dereference it to get a well behaved ICorDebugValue. Afterwards you could set the pointer back. Take care that you either match object's AppDomain and ICorDebugReferenceValue AppDomain or expect the ICorDebugValue to report the wrong AppDomain (ie whichever one ICorDebugReferenceValue had). Using ICorDebugHeapValue2::CreateHandle may corrupt the target process if used on an ICorDebugValue that has the wrong AppDomain. Overall I can't really recommend going this route, but it is possible if you wanted to dodge the pitfalls.

    Alternatively the better workaround is probably to use the new layout APIs and never call ICorDebugProcess5::GetObject() at all. Above I described how to do it for string, but you can use a similar technique on any array. For classes with fields it looks like you already discovered how to get the COR_FIELD structures for each field. Instead of ICorDebugObjectValue::GetFieldValue() you can compute object_address + COR_FIELD.offset to get the memory address holding data for that field. Using ICorDebugProcess5::GetTypeForTypeID(COR_FIELD.Id) will provide an ICorDebugType for each field. You can use that to determine how large the field is and how to interpret the memory contents.

    I know we don't have a great sample yet showing how all of this fits together, but feel free to ping me if you get stuck and I'll help as best I can.

    -Noah

    Wednesday, July 11, 2012 9:50 AM
    Moderator

All replies

  • Hi Greg,

    I find the method: http://msdn.microsoft.com/en-us/library/ms232059.aspx

    Do you have the right call?

    Have a nice day.


    Ghost,
    Call me ghost for short, Thanks
    To get the better answer, it should be a better question.

    Tuesday, May 15, 2012 12:39 PM
  • Thanks, I tried this call but unfortunately it's the one that's not working.

    If I call it on the objects at 0x02A0BFF4 or 0x02A0C028 (see above) it doesn't work, I get e.g. 1830167992 returned as the length. If I call it on field 3 of object 0x02A0C05C (field 3 is actually a reference to 0x02A0BFF4), then it works correctly.

    Wednesday, May 16, 2012 5:51 AM
  • Hi Greq,

    Did you check what does this value means?

    Is it is the address of that object or is it the pointer address of that object?

    Have a nice day.


    Ghost,
    Call me ghost for short, Thanks
    To get the better answer, it should be a better question.

    Wednesday, May 16, 2012 7:53 AM
  • Sorry, I don't think I explained it very well.

    The signature for GetLength() is:

    HRESULT GetLength (
        [out] ULONG32   *pcchString
    );

    In my case the HRESULT returned is 0, while "pcchString" is 1830167992, which indicates that the method returned successfully, and that the string is 1,830,167,992 characters long. Which is quite long ;)

    Saturday, May 19, 2012 10:10 AM
  • Try making the variables in SomeParameters static variables. Presently the variables are local variables which is on the execution stack which the code may not be able to reach.  Making the variables static puts the variables in global memory.

    I suspect you are using Visual Studio 2010 and not 2008.  Microsoft tried to improve the C# library in 2010 by eliminate the annoying error messages.  In some cases 2010 is not reporting errors which is giving results like what you are having.


    jdweng

    Saturday, May 19, 2012 11:25 AM
  • Hi Greg,

    I see you have unmarked Joel's reply. How about the result? 

    If you get further questions, please feel free to follow up.

    Best regards,


    Mike Feng
    MSDN Community Support | Feedback to us
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.

    Wednesday, June 6, 2012 11:14 AM
    Moderator
  • Hi

    Sorry for the long silence, real life interupted :(

    I haven't had a chance to look at Joel's proposal properly yet, thus me unmarking it as unanswered for the moment.

    I don't think it's going to solve my problem though. The example I quoted above was just an easy way to demonstrate my problem: I seem to be able to read System.String members when they're a member of an object, but I can't read a System.String when it's directly on the heap (as opposed to being a member of another object).

    I hope that makes sense, it's a bit difficult to explain. I'm going to do a bit more digging using WinDbg, hopefully that can shed some more light on the situation. I'll post more detail soon.

    Cheers, Greg

    Wednesday, June 6, 2012 12:58 PM
  • I you are putting  string on a heap then you have to make a "new" string every time you place the string on the heap.


    jdweng

    Wednesday, June 6, 2012 1:16 PM
  • Hi Joel

    That's where it gets interesting - I'm not having this problem with my objects only, I'm having it with other System.String's that I didn't allocate.

    I've quickly done some more investigation and have found something very interesting. I'll update this post with all of the details over the weekend when I've been able to investigate properly, but here's a quick preview.

    As I mentioned I'm enumerating the heap and displaying the address and type of each object I find. The problem I'm having is with objects of type "System.String". When I take the address of the object and look up it's value (see my first post), I see it supposrts ICorDebugStringValue, when I call GetLength() I get a number well into the millions i.e. according to GetLength() the "System.String" is millions of characters long.

    I've taken a few of the addresses of the "System.String"'s (as mentioned above) and looked them up in WinDbg. WinDbg has no problem reading the value of the string, so the address I'm see in my test app is definately a valid System.String.

    I then decided to run !gcroot to see where the System.String was rooted. This is where it got interesting. I tried aproximately 10 addresses and each one was rooted in by a "System.Object[]". In addition to that most of the handles seemed to be "pinned" (one was "strong handle").

    The fact that they're all rooted by System.Object[]'s could be a complete fluke, but I am starting to grasp at straws now.

    I'll do some investigation over the weekend and post all of the details here (including WinDbg output.

    Cheers, Greg

    Thursday, June 7, 2012 6:37 PM
  • when yo run your tests you might weant to try an array of bytes (byte[]) or characters (char[])instread of a string.  Strings are a class that might behave different from an array.

    jdweng

    Thursday, June 7, 2012 7:48 PM
  • That's where the problem is: these are not strings that I've defined, they're just random heap objects returned by ICorDebugProcess5::EnumerateHeap(). Here is the detail I promised previously:

    One of the COR_HEAPOBJECT's returned by EnumerateHeap() has an address of 0x02E41228. "!do" in WinDbg returns the following:

    0:005> !do 0x02E41254
    Name:        System.String
    MethodTable: 7270b808
    EEClass:     72314ec8
    Size:        196(0xc4) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    String:      C:\Users\Greg\Documents\Visual Studio 2012\Projects\ShowMe\ShowMe.CrashTestDummy\bin\Debug\
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    7270cfc8  40000aa        4         System.Int32  1 instance       91 m_stringLength
    7270c200  40000ab        8          System.Char  1 instance       43 m_firstChar
    7270b808  40000ac        c        System.String  0   shared   static Empty
        >> Domain:Value  012ec188:NotInit  <<

    So the object at address 0x02E41254 is a System.String and WinDbg can correctly read it. Notice that it's 196 bytes, 96 characters long (including the null char).

    Now I try to read the same string in my code (see comments for details):

    // txtObjectAddress.Text is "02E41254"
    ulong objectAddress = ulong.Parse(this.txtObjectAddress.Text, System.Globalization.NumberStyles.HexNumber); 
    COR_TYPEID objectType = _corDebugProcess.GetTypeId(objectAddress);
    CorDebugType heapObjectType = _corDebugProcess.GetTypeForTypeID(objectType);
    
    // heapObjectType.Type is "System.String" (CorDebugType is my wrapper for ICorDebugType)
    this.textBox1.AppendText(String.Format("Object at address 0x{0:X8} is of type {1}\r\n", objectAddress, heapObjectType.Type));
    
    ICorDebugObjectValue objectValue = _corDebugProcess.GetObject(objectAddress);
    // I use this to display which ICorDebug*Value interfaces are implemented by the returned ICorDebugObjectValue
    DisplayValueInterfaces(objectValue); 
    
    ICorDebugStringValue stringValue = objectValue as ICorDebugStringValue;
    uint hresult;
    
    if (stringValue != null)
    {
        uint stringLength;
    
    	// GetLength() returns successfully, but "stringLength" is a huge number
        hresult = stringValue.GetLength(out stringLength);
        if (hresult != 0) throw new Exception("ICorDebugStringValue::GetLength failed: " + hresult);
    
        this.textBox1.AppendText("\t\tValue is '" + stringLength + " characters long'.\r\n");
    }

    My code runs successfully (no erros or exceptions), but the returned value of "stringLength" is 1933329624. So even though WinDbg can read the System.String, I can not.

    Out of interested I ran "!gcroot" on 0x02E41254:

    0:005> !gcroot 0x02E41254
    HandleTable:
        010311fc (strong handle)
        -> 02e4141c System.AppDomain
        -> 02e414f4 System.AppDomainSetup
        -> 02e4169c System.Object[]
        -> 02e41254 System.String

    Found 1 unique roots (run '!GCRoot -all' to see all roots).

    As you can see the System.String is rooted by a System.Object[]:

    0:005> !do 02e4169c
    Name:        System.String[]
    MethodTable: 726bc684
    EEClass:     7237b5c4
    Size:        88(0x58) bytes
    Array:       Rank 1, Number of elements 18, Type CLASS
    Fields:
    None
    0:005> !dumpmt 726bc684
    EEClass:         7237b5c4
    Module:          72311000
    Name:            System.Object[]
    mdToken:         02000000
    File:            C:\WINDOWS\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    BaseSize:        0x10
    ComponentSize:   0x4
    Slots in VTable: 28
    Number of IFaces in IFaceMap: 6

    The same seems to happen for any COR_HEAPOBJECT of type System.String that gets returned by EnumerateHeap().

    Now consider this:

    COR_HEAPOBJECT at address 0x02E8A05C is an ELEMENT_TYPE_CLASS, of name "SomeParametersForTheException". This is a class I have defined in my CrashTestDummy app (of which is the crash dump I am using).

    Here is the "!do" output from WinDbg:

    0:005> !do 0x02E8A05C
    Name:        ShowMe.CrashTestDummy.Form1+SomeParametersForTheException
    MethodTable: 010573f4
    EEClass:     010e1718
    Size:        20(0x14) bytes
    File:        C:\Users\Greg\Documents\Visual Studio 2012\Projects\ShowMe\ShowMe.CrashTestDummy\bin\Debug\ShowMe.CrashTestDummy.exe
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    7270cfc8  4000005        8         System.Int32  1 instance        5 anInt
    727014ac  4000006        c        System.Single  1 instance 11.230000 aFloat
    7270b808  4000007        4        System.String  0 instance 02e89ff4 aString

    Note that the 3rd field is a System.String, at address 02e89ff4:

    0:005> !do 02e89ff4
    Name:        System.String
    MethodTable: 7270b808
    EEClass:     72314ec8
    Size:        50(0x32) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    String:      I am a test string
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    7270cfc8  40000aa        4         System.Int32  1 instance       18 m_stringLength
    7270c200  40000ab        8          System.Char  1 instance       49 m_firstChar
    7270b808  40000ac        c        System.String  0   shared   static Empty
        >> Domain:Value  012ec188:NotInit  <<

    If I try and read 02e89ff4 using the code I mentioned above "stringLength" is returned as 1933329624.

    BUT

    If I read field 3 using ICorDebugObjectValue.GetFieldValue() everything works fine. I have to dereference the value returned from GetFieldValue(), but the dereferenced ICorDebugStringValue works without any problems: I can the value "I am a test string".

    So after all that how come I read a string through it's parent (GetFieldValue()), but I can't read it directly (using ICorDebugProcess5::GetObject()) ?

    I hope this all makes sense!

    Sunday, June 10, 2012 12:32 PM
  • I don't know how heaps are built in the Net Library.  Normally a heap is stored as a binary tree to make it quicker to locate an object and retrive the object quickly.  Normal allocates store everything sequential which which increases the access time.  It appears that the string class isn't compatible with the heab binary treee structure.  I went looking for any refernces to this problem

    Here is an interesting article.  I don't think it has anything to do with your issue, but not sure.

    http://www.codeproject.com/Articles/6489/Debug-Tutorial-Part-3-The-Heap

    I appeas you are multi-threading and may need to add a lock.  See this webpage

    http://stackoverflow.com/questions/1771813/lock-before-reading-a-global-string

    Here is one more article on strings you may be interested in reading

    http://stackoverflow.com/questions/636932/in-c-why-is-string-a-reference-type-that-behaves-like-a-value-type


    jdweng

    Sunday, June 10, 2012 12:56 PM
  • Thanks Joel. I found this article a while back which is also pretty good, it's goes into more detail on the actual CLR implementation: http://msdn.microsoft.com/en-us/magazine/cc534993.aspx

    It still seems to me that ICorDebugProcess5::GetObject() is failing, or there is something wrong with the value returned.

    The input to GetObject() is a CORDB_ADDRESS. EnumerateHeap() returns an enumeration of COR_HEAPOBJECT, of which one of the fields is the address (CORDB_ADDRESS).  I should be able to use that address as input to GetObject() and get a valid object back (or an error).

    In my case I'm passing in a valid CORDB_ADDRESS and GetObject() is returning a ICorDebugObjectValue interface without error. I can QueryInterface the returned ICorDebugObjectValue for ICorDebugStringValue without error. It's only when I call GetLength() on the returned ICorDebugStringValue that I get a returned value that is wrong i.e. 1933329624

    There are few thigns I'm going to try next:

    1. Check which memory locations are read when I call GetLength()
    2. Check if I can read arrays, using GetObject(), a quick glance previously looked like it wasn't working
    3. See if I can access the MetaData for the object
    4. Try and figure out why the type of the System.String is ELEMENT_TYPE_CLASS and not ELEMENT_TYPE_STRING

    Tuesday, June 12, 2012 8:07 AM
  • I like to be able to control the processes that are running in my code and never like automatic features that run in the background without the users beng aware that they are running.  Microsoft like to have features that the user don't know are running without asking a Guru.

    If what the article says is true that any object wiht over 85,000 bytes is considered a heap, then any string object over 85,000 bytes is built as a heap.  Am I correct that you are putting a string into a heap which means you have a heap inside a heap?


    jdweng

    Tuesday, June 12, 2012 9:15 AM
  • I've got a "crash test dummy" WinForms application, which is a very basic WinForms application, there is almost no code in it. I'll post the code when I get home.

    I load that application in WinDbg and run it. Once it runs I click a button that throws an exception which WinDbg then catches. I then run ".dump /map filename.hdmp" to create a crash dump of the application.

    I then use the various CorDebug coclasses/interfaces to load the crash dump. This is where ICorDebugProcess5 comes into the picture. One of the methods in ICorDebugProcess5 is EnumerateHeap which enumerates "objects on the managed heap". So I'm trying to use EnumerateHeap to enumerate whatever objects are on the managed heap, some are created by me while others are created by the CLR/Framework.

    EDIT:

    Here is the code for my test app, as promised. As you can see there are just a few local variables for testing that my code can handle the different types. Although I haven't even got past strings yet.

    namespace ShowMe.CrashTestDummy
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
    
            private void button1_Click(object sender, EventArgs e)
            {
                ExceptionThread newThread = new ExceptionThread();
    
                Thread runningThread = new Thread(new ThreadStart(newThread.Run));
                runningThread.Start();
            }
    
            private class ExceptionThread
            {
                public ExceptionThread()
                {
                }
    
                public void Run()
                {
                    SomeParametersForTheException blah = new SomeParametersForTheException();
                    blah.anInt = 5;
                    blah.aFloat = 11.23F;
                    blah.aString = "I am a test string";
                    string aLocalString = "I am a local string";
                    
                    ThrowAnException<string>(blah, 56);
    
                    Thread.Sleep(10 * 1000);
    
                    MessageBox.Show(aLocalString);
                }
    
                private void ThrowAnException<T>(SomeParametersForTheException someParameters, int anotherIntParameter)
                {
                    T aGenericParameter = default(T);
    
                    Debug.WriteLine(aGenericParameter.ToString()); // Don't want var optimised away
    
                    throw new Exception("A am an exception");
                }
            }
    
            private class SomeParametersForTheException
            {
                public int anInt;
                public float aFloat;
                public string aString;
            }
    
        }
    }
    

    • Edited by Greg Nagel Tuesday, June 12, 2012 3:02 PM Added sample code
    Tuesday, June 12, 2012 1:02 PM
  • A quick update on some new things I've discovered.

    I mentioned previously that the strings I was trying to read mostly seem to be owned by an array. I also mentioned that I could read strings using ICorDebugObjectValue::GetFieldValue(), but I couldn't read them using ICorDebugProcess5::GetObject().

    So based on that I decided to see what would happen if I tried to read the string via the array that owns it i.e. read the string as an element of an array, instead of reading the string as a value on the heap. (Hopefully that makes sense!)

    In one of my previous posts I mentioned this output from WinDbg:

    0:005> !gcroot 0x02E41254
    HandleTable:
        010311fc (strong handle)
        -> 02e4141c System.AppDomain
        -> 02e414f4 System.AppDomainSetup
        -> 02e4169c System.Object[]
        -> 02e41254 System.String

    What I've done now is to try and access the fields of the object at 02e414f4, which should be the fields of System.AppDomainSetup. One of those fields should be the System.Object[] located at 02e4169c.

    I've hooked up the code and what I'm noticing is that 02e414f4 (AppDomainSetup) has 17 fields, including a couple which seem to be of type ELEMENT_TYPE_SZARRAY.

    I use ICorDebugProcess5::GetTypeFields() to see how many fields there are in the object and then loop through each one. For each field I call ICorDebugObjectValue::GetFieldValue().

    The interesting thing is that GetFieldValue() returns CORDBG_E_FIELD_NOT_AVAILABLE for each field I try and read?

    That's as much as far as I've gotten for the moment. I'll post more detail once I've had a chance to double check in WinDbg and see what it's output is.

    Does this seem familier to anyone? Does it sound like expected behaviour?

    Greg

    EDIT:

    I did a "!do" on " 02e414f4 System.AppDomainSetup" (mentioned above) and got the output below. You'll notice that there are 17 fields, which matches what I get. The difference is that I get CORDBG_E_FIELD_NOT_AVAILABLE for each field, where as WinDbg can see the actual details.

    If I call GetObject() for 02e4169c (the System.Object[]), I get a ICorDebugObjectValue that doesn't support ICorDebugArrayValue, even though WinDbg can see it's Rank is 1 and has 18 elements (See further below).

    If I call GetObject() on 02e48ce0 I get a ICorDebugObjectValue interface that supports ICorDebugStringValue. However calling GetLength() on ICorDebugStringValue returns 1729774808.

    0:005> !do 0x02e414f4
    Name:        System.AppDomainSetup
    MethodTable: 7270c02c
    EEClass:     7237b618
    Size:        68(0x44) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    726bc684  40001c5        4      System.Object[]  0 instance 02e4169c _Entries
    726cd888  40001c6       38         System.Int32  1 instance        0 _LoaderOptimization
    7270b808  40001c7        8        System.String  0 instance 00000000 _AppBase
    726ee098  40001c8        c ...DomainInitializer  0 instance 00000000 _AppDomainInitializer
    726bc684  40001c9       10      System.Object[]  0 instance 00000000 _AppDomainInitializerArguments
    7270b120  40001ca       14 ...tivationArguments  0 instance 00000000 _ActivationArguments
    7270b808  40001cb       18        System.String  0 instance 00000000 _ApplicationTrust
    7270da9c  40001cc       1c        System.Byte[]  0 instance 00000000 _ConfigurationBytes
    727012b8  40001cd       3c       System.Boolean  1 instance        0 _DisableInterfaceCache
    7270b808  40001ce       20        System.String  0 instance 00000000 _AppDomainManagerAssembly
    7270b808  40001cf       24        System.String  0 instance 00000000 _AppDomainManagerType
    726bc684  40001d0       28      System.Object[]  0 instance 00000000 _AptcaVisibleAssemblies
    72321370  40001d1       2c ...bject, mscorlib]]  0 instance 00000000 _CompatFlags
    7270b808  40001d2       30        System.String  0 instance 02e48ce0 _TargetFrameworkName
    726ee774  40001d3       34 ...nSortingSetupInfo  0 instance 00000000 _AppDomainSortingSetupInfo
    727012b8  40001d4       3d       System.Boolean  1 instance        1 _CheckedForTargetFrameworkName
    727012b8  40001d5       3e       System.Boolean  1 instance        0 _UseRandomizedStringHashing

    0:005> !do 02e4169c
    Name:        System.String[]
    MethodTable: 726bc684
    EEClass:     7237b5c4
    Size:        88(0x58) bytes
    Array:       Rank 1, Number of elements 18, Type CLASS
    Fields:
    None
    0:005> !do 02e48ce0
    Name:        System.String
    MethodTable: 7270b808
    EEClass:     72314ec8
    Size:        66(0x42) bytes
    File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    String:      .NETFramework,Version=v4.5
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    7270cfc8  40000aa        4         System.Int32  1 instance       26 m_stringLength
    7270c200  40000ab        8          System.Char  1 instance       2e m_firstChar
    7270b808  40000ac        c        System.String  0   shared   static Empty
        >> Domain:Value  012ec188:NotInit  <<

    • Edited by Greg Nagel Monday, June 18, 2012 5:42 PM Added WinDbg output
    Monday, June 18, 2012 3:07 PM
  • Hi Greg,

      Sorry to see this has gone unresolved so long, if you are still interested let me try assisting.

    The behavior you describe sounds like it could potentially be bug(s) in ICorDebugProcess5::GetObject(), but I'd like to take a peak tomorrow in the office. You've got ample description of what you are doing so I should be able to replicate it.

    If it is bug(s) you may be able to work around it in the meantime. Rather than calling ICorDebugProcess5::GetObject(), you can directly inspect the memory contents of the object with very minimal abstraction from ICorDebug. This was actually our intended scenario for the heap inspection APIs, whereas ICorDebugProcess5::GetObject() was aimed as a compatibility bridge back to our older APIs. Because all the layout information is cacheable on a per-type basis you can do a high performance scan of the heap without the overhead of creating COM objects in the debugger to wrap every object in the target process. On large heaps that will make huge differences in both time and memory consumed by the debugger.

    For strings in particular, we expose their layout similar to other arrays.

    1) To discover you have a string (without using ICorDebugProcess5::GetObject) use ICorDebugProcess5::GetLayout() and then check the type field in the COR_TYPE_LAYOUT to see if it is ELEMENT_TYPE_STRING. You can cache all layout info per type so in the future whenever you see an object with this COR_TYPEID, it is a string.

    2) Call ICorDebugProcess5::GetArrayLayout() http://msdn.microsoft.com/en-us/library/hh361100(VS.110).aspx. The array information you get back will include the offset to where length is stored and the offset to where the character buffer starts. Assuming you have an object at address object_addr:

    uint object_addr = <object's address in target process>

    COR_ARRAY_LAYOUT stringLayoutInfo = <layout for the string type>

    uint string_length_addr = object_addr + stringLayoutInfo.countOffset;

    uint string_char_data_addr = object_addr + stringLayoutInfo.firstElementOffset;

    uint length;

    ReadProcessMemory(string_length_addr, &length, 4);

    wchar* buffer = new wchar[length];

    ReadProcessMemory(string_char_data_addr, &buffer, length*sizeof(wchar));

    If you wanted to you should also be able to confirm that the remaining data in the string array layout structure tells you the array component is the char type, size 2 bytes, and array rank is 1. I make implicit use of some of that instead of writing a more general purpose array reader. I hope that helps unblock you at least and I'll let you know what I find when I test ICorDebugProcess5::GetObject.

    HTH,

    -Noah

    Tuesday, July 10, 2012 10:35 AM
    Moderator
  • Hi Noah

    Thanks, for your response. I ran a quick test of your suggestion and it worked first time, I can successfully read strings now! :D

    I need to have a proper look and double check everything, but it looks good. The one place I need to do a bit more thinking is around the encoding of the string (I'm doing this all from c#). At first I thought of using the System.Text.Encoding class, but in theory I don't actually know how the string has been encoded in memory, all I have is a byte[], so I don't know which encoding to use e.g. Unicode, UTF8, UTF7, etc, etc.

    Then I remembered that converting from wchar* to System.String is pretty easy in interop, so I had a look at the System.Runtime.InteropServices.Marshal class. I found PtrToStringAuto() which seems to be a safe bet. I tried it, and it does work, but in theory it can only convert ANSI or Unicode strings.

    I need to do a bit more research on this, but do you have any thoughts?

    btw - Thanks for the performance tips, that'll make quite an improvement to my app!

    Cheers

    Greg

    Tuesday, July 10, 2012 5:42 PM
  • Awesome, glad we got you moving forward : )

    Quick answer - its always encoded UTF16, host endianess

    Longer answer - .Net defines the char type to be a 16 bit value corresponding to a UTF16 code point, and System.String can be viewed as a System.Char[] (it has an indexer that returns char) which is the UTF16 encoding of that string. Nowhere does it say that the internal representation in memory will be UTF16 though. ICorDebug's use of the array layout with component type = System.Char is intended to make that additional claim, that the memory format is also an array of UTF16 code points.

    HTH,

    -Noah

    PS. If you have more questions as you doing these heap memory APIs, feel free to ping me on email to come take a look at forum posts. I flag posts in progress to give me email alerts, but I haven't found something that notifies me about new posts. My email is noahfalk AT microsoft DOT com

    Tuesday, July 10, 2012 7:30 PM
    Moderator
  • Just wanted to follow up - sadly it looks like the ICorDebugProcess5::GetObject() is buggy in several cases and it will give you an ICorDebugObjectValue that behaves incorrectly in future calls. Your string example above is one of them, but it looks like it also misreports the CorElementType, array data seems erroneous, and GetFieldValue isn't working properly (also as you noticed). 

    If you need an ICorDebugValue an onerous workaround is get an ICorDebugReferenceValue from somewhere, use the SetValue() function to set its pointer to the desired object address, then dereference it to get a well behaved ICorDebugValue. Afterwards you could set the pointer back. Take care that you either match object's AppDomain and ICorDebugReferenceValue AppDomain or expect the ICorDebugValue to report the wrong AppDomain (ie whichever one ICorDebugReferenceValue had). Using ICorDebugHeapValue2::CreateHandle may corrupt the target process if used on an ICorDebugValue that has the wrong AppDomain. Overall I can't really recommend going this route, but it is possible if you wanted to dodge the pitfalls.

    Alternatively the better workaround is probably to use the new layout APIs and never call ICorDebugProcess5::GetObject() at all. Above I described how to do it for string, but you can use a similar technique on any array. For classes with fields it looks like you already discovered how to get the COR_FIELD structures for each field. Instead of ICorDebugObjectValue::GetFieldValue() you can compute object_address + COR_FIELD.offset to get the memory address holding data for that field. Using ICorDebugProcess5::GetTypeForTypeID(COR_FIELD.Id) will provide an ICorDebugType for each field. You can use that to determine how large the field is and how to interpret the memory contents.

    I know we don't have a great sample yet showing how all of this fits together, but feel free to ping me if you get stuck and I'll help as best I can.

    -Noah

    Wednesday, July 11, 2012 9:50 AM
    Moderator