DBX vs. Visual Studio and WinDbg: Part 2C, Memory Access Breakpoints, Revisited
This is where we are through the series:
- Calling a function
- Configuring breakpoints
- Tracing execution
- Execution control
- Displaying data, including STL collections
- Runtime application checking
- Miscellaneous commands
Today’s post revisits the concept of memory access breakpoints. Before I started this series, I wrote a post showing a proof of concept technique for setting a memory breakpoint on a wide range of memory using SDbgExt’s !vprotect command, PAGE_GUARD pages, and WinDbg. However, there’s more to be said about memory access breakpoints than meets the eye.
As I have mentioned in the previous post, hardware breakpoints on Intel x86/x64 CPUs can be configured to fire on reads or writes to a specific memory location, whose size cannot exceed the size of the system word*. DBX, on the other hand, allows access breakpoints on memory ranges of arbitrary size. Furthermore, DBX allows you to configure the breakpoint to fire before or after the memory access has taken place, as opposed to hardware breakpoints that are always fired after the event has occurred.
Below is an example of using DBX’s memory access breakpoints:
29 ms2.next = &ms2;
31 ms.y = 14;
32 ms1.y = 13;
(dbx) stop access wb &ms
(3) stop access wb &ms, 12
watchpoint wb &ms (0x8068f04) at line 31 in file "stl.cc"
31 ms.y = 14;
(dbx) print ms.y
ms.y = 0
27 a.arr = 'a';
(dbx) stop access w &a
(3) stop access wa &a, 1000
watchpoint wa &a (0x8047846) at line 27 in file "stl.cc"
27 a.arr = 'a';
(dbx) print a.arr
a.arr = 'a'
There are two things worth noting here: first, there is no need to specify the size of anything—DBX deduces size information from the variable itself; second, the memory breakpoint indeed works for a range that is larger than what hardware breakpoints allow.
There’s yet another feature of DBX that has to do with watching memory locations. You can use the stop change myVar command to watch for any change in the value of a particular variable. Unlike memory breakpoints, this feature relies on automatically single-stepping through your entire program, checking the variable’s value after each step, and therefore considered expensive.
Memory Breakpoints in the Managed World
Both DBX and Visual Studio have support for debugging managed languages—in DBX it’s Java, in Visual Studio it’s any .NET language such as C#. The problem with these languages is that support garbage collection, so that objects keep moving around in memory, making it difficult to track them using memory breakpoints.
Indeed, the only memory-breakpoint-related feature that DBX supports for Java debugging is setting a breakpoint on modification of class fields:
> 9 System.in.read();
10 x = 43;
11 e.field = x;
(dbx) stop access wb Example.field
(3) java stop access wb Example.field
stopped in Example.main at line 11 in file "Example.java"
11 e.field = x;
(2) java stop inmethod main
*(3) java stop access wb Example.field
However, there is no support for watching a local variable, or watching modifications to the fields of a particular instance.
Visual Studio, on the other hand, has no support at all for setting memory breakpoints on managed objects. And while WinDbg can be tricked into setting such breakpoints using the ba command, they will not follow the object around when the garbage collector decides to move it.
Still, not all is lost. It is possible (at least theoretically) to set a memory breakpoint on a managed object that would follow it around in memory. I can think of at least two approaches—which aren’t easy to implement, don’t get me wrong—which can come close to accomplishing this:
- A WinDbg extension can receive some identifying information about the object on which a breakpoint needs to be set (e.g. the name of a local variable in a live stack frame). After each garbage collection—of which the extension can be aware by setting a breakpoint on mscorwks!*RestartEE—the extension can reset the hardware breakpoint using the identifying information above.
- [a more robust approach] Use the CLR profiling callback interfaces, specifically the ICorProfilerCallback::MovedReferences method, to implement object tracking and follow the object around memory, resetting the hardware breakpoint every time the object is moved.
If anyone is interested in collaborating on the development of either approach, feel free to let me know through the contact form or the comments :-)
* Curiously, this limitation does not apply to Itanium processors, where a memory access breakpoint can be set on a range of up to 2^55 bytes. [cf. Intel Itanium Manual, Volume 2, Part 1, pp. 2:152-153]