Improve your managed debugging skills: examining registers and memory

 

I was helping a colleague and we were deep in the middle of a debug session, single stepping some code and we wanted to see a value in the debugger. The debugger showed either nothing, because the intermediate value has been optimized out, or a message like “cannot display value, possibly because it has been optimized away”.

 

This occurs especially often when debugging some code you don’t own, (perhaps you’ve stepped into .Net framework code: (Tools->Options->Debugging->Enable .Net Framework source stepping) or some library you’re using.

 

Well, often you can get around this and actually see the values.

 

Fire up Visual Studio: File->New->Project->C# Windows Console Application, paste the code below.

 

To simulate the scenario above, we’ll set Build->Configuration Manager->Active Solution Configuration: Release, so the intermediate values are optimized out.

 

Now try hitting F10 to single step the code. Notice that the intermediate return values are not displayed in the debug window.

 

The first thing we’ll do is set up a few debug windows. While at a breakpoint, open the registers window and a few memory windows:

Debug->Windows-> Registers

Debug->Windows-> Memory->Memory 1

Debug->Windows-> Memory->Memory 2

Debug->Windows-> Memory->Memory 3

 

Rearrange these windows as you like: you’ll be dragging/dropping values between them.

 

Note: you can right click on a Memory window and display the values in various formats: like 1 byte integer, 4 byte integer, etc. Switch them to 4 byte integer.

 

The code instantiates a class and calls various methods on it, each of which return a value.

 

retString returns a string: the debugger doesn’t show the value at all because it’s been optimized away (in fact, since the return value is unused, it seems the compiler did a good thing to optimize it away).

 

In fact, managed and native code methods/function calls that return simple values that will fit in a single WORD (32 bit for a32 bit process, or 64 bit on 64 bit process) will return the value in EAX for 32bit or RAX for 64 bit.

 

Thus EAX is one of the most important registers to watch, so in all my debugging sessions, I always have the Register window visible, so I can see EAX.

 

<Aside>

Back in 1979, I had a summer job programming an embedded 8080 in Assembly code, using an In Circuit Emulator, or ICE. I was manipulating the 8 bit A, B, C, D registers in my programs. This was just before the days of the TRS 80 radio shack computer, and the $50 Timex Sinclair. I remember thinking that the 8 bit 8080 was backwards compatible with the 4 bit Intel 4004, which allowed the amazing 3 levels of subroutines!

 

I remember Intel providing us with an advanced spec for a new 16 bit processor architecture: the 8086, which would have an 8 bit bus version: the 8088. I remember perusing the doc and seeing the register names changing from the 8 bit A to the 16 bit AX. The high and low 8 bits could be addressed as AH and AL respectively. Sure enough, the original IBM PC came out about 2 years later, using the 8088 and AX, BX, CX and DX, general purpose registers, with SI and DI Source and Destination Index registers.

Since then, in the 32 bit world, they’re called EAX for Extended AX. Now for 64 bit, the 64 bit A register is called RAX.

</Aside>

 

After calling retString, observe what’s in the registers window.

 

EAX = 02475720 EBX = 00000000 ECX = 02475714 EDX = 050724A8 ESI = 050724A8 EDI = 0650E5A0 EIP = 00292484 ESP = 0650E568 EBP = 0650E574 EFL = 00000246

 

Note that the changed registers are Red. EAX changed, so we have a return value. EIP changed as well: that’s the Instruction Pointer, which we know from our school computer classes, changes to point to the next instruction to execute.

 

Now let’s examine the returned value: drag/drop the red EAX value into the Memory 1 window.

0x02475720 6125f92c 00000010 00680054 00730069 00690020 00200073 ,ù%a....T.h.i.s. .i.s. .

0x02475738 00200061 00740073 00690072 0067006e 00000000 00000000 a. .s.t.r.i.n.g.........

 

You can see the returned string value! The first number (6125f92c) is the ClassId of System.String. This ClassId is constant from debug session to debug session. The next value (00000010) is the length of the string in hex (16 Unicode characters)

After that, are the characters themselves (00680054) represents “T” (0x54) and “h” (0x68) in little endian order. You can right-click, choose 1-byte integer to read individual byte values more easily.

 

Just because we’re running managed code doesn’t mean you can’t examine memory and registers!

 

More complex return values are returned in different ways: a 64 bit result (such as a Real) on 32 typically will use EAX and EDX.

 

For the returned String Array:

0x022F57E0 61216ba8 00000003 6125f92c 022f57a0 022f57b4 022f57c8 ¨k!a....,ù%a W/.´W/.ÈW/.

0x022F57F8 00000000 00000000 00000000 00000000 00000000 00000000 ........................

 

The first value () is the ClassID of the array, (00000003) is the numer of elements, (6125f92c) is the ClassId for System.Sting(note it’s the same as above), and then the 3 string values. Drag one of those string values to a different memory window to see its value.

 

The returned Int array is simpler:

0x022F57FC 612628b8 00000003 00000001 00000002 00000003 00000000 ¸(&a....................

 

You can see there are 3 values, 1,2, and 3.

 

 

You can examine other values too, not just return values, if you practice and enhance this technique and pay careful attention to the disassembly.

 

Exercise for the reader: how is something like a GUID returned?

hint: examine the disassembly of the call: right click the source code->Go to Diasassembly

00000069 lea edi,[ebp-18h]

               “lea” means Load Effective Address”

 

 

 

 

Note: on a 64 bit OS, you can target 64 bit (Project->Properties->Build->Platform Target->Any CPU (or x64) and the above works, but the registers and values are all 64 bit long, rather than 32:

 

RAX = 0000000002695498 RBX = 0000000000000000 RCX = 0000000002695480 RDX = 0000000002695498 RSI = 000000001D35E220 RDI = 000000001D35E0B8 R8 = 000000001D35E110 R9 = 000007FEEE344DD5 R10 = 000007FF001859B0 R11 = 000000001D35DF40 R12 = 0000000000000000 R13 = 0000000000000000 R14 = 0000000000000000 R15 = 000000000000001D RIP = 000007FF0014337E RSP = 000000001D35E040 RBP = 000000001D35E0E0 EFL = 00000202

 

000000001D35E060 = 0000000002695480

 

 

 

 

Note: this debugging technique does NOT require Project->Properties->Debug->Enable unmanaged code debugging.

 

Note: this works with VB code too.

 

See also:

Why was the original IBM PC 4.77 Megahertz?

Thirty years ago, computers were quite different

 

<Sample Code>

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

 

namespace ConsoleApplication1

{

    class Program

    {

        static void Main(string[] args)

        {

            var testClass = new TestClass();

 

            var str = testClass.retString();

 

            testClass.retStringArr(); // works even if we don't assign the return value

 

            testClass.retIntArr();

 

            testClass.retDict();

 

            // let's get all the files in "MyDocuments"

            System.IO.Directory.GetFiles(Environment.GetFolderPath( System.Environment.SpecialFolder.MyDocuments));

 

            var guid = new Guid("11111111111111111111111111111111");

           

        }

        class TestClass

        {

            public string retString()

            {

                return "This is a string";

            }

            public string[] retStringArr()

            {

                return new string[] {"one","two","three"};

            }

            public int[] retIntArr()

            {

                return new int[] {1,2,3};

            }

            public Dictionary<string, string> retDict()

            {

                return new Dictionary<string, string>() {

                    {"zero", "Sunday" },

                    {"one", "Monday" }, // i like that C# doesn't complain

                                        //about an extra comma here,

                                        //so copy/paste new items works fine

                };

            }

        }

    }

}

 

 

</Sample Code>