Friday, November 22, 2013

Debugger 10: Memory display

To conclude our sessions on debuggers we will add a memory view to our example. These views typically show a dedicated memory range in an arbitrary format. You are not bound to the typical hex view style, still this is what we will use in this tutorial.
  1. A fictional interpreter
  2. The launch framework
  3. A tale of debuggers, processes and threads
  4. Stepping, suspending and other actions
  5. UI breakpoint integration
  6. Debugger breakpoint integration
  7. Display current source code - source lookup
  8. Run to line support
  9. Displaying variables
  10. Displaying memory areas
Source code for this tutorial is available on googlecode as a single zip archive, as a Team Project Set or you can checkout the SVN projects directly. 

Step 1: Implementing Memory representations

As seen in the previous tutorial variables are bound to a StackFrame. Memory dumps instead are bound to the entire DebugTarget. This makes sense as variables may be valid within a dedicated scope only while the memory is a global resource valid for the whole process.

Technically speaking our debugger does not support a memory as such. Therefore we fake it by copying "executed code" into the simulated memory.

Lets start with the memory representation:
package com.codeandme.textinterpreter.debugger.model;

public class TextMemoryBlock extends TextDebugElement implements IMemoryBlock {

 private final long mStartAddress;
 private final long mLength;

 private boolean mDirty = true;
 private byte[] mContent = null;

 public TextMemoryBlock(IDebugTarget target, long startAddress, long length) {
  super(target);
  mStartAddress = startAddress;
  mLength = length;
 }

 @Override
 public long getStartAddress() {
  return mStartAddress;
 }

 @Override
 public long getLength() {
  return mLength;
 }

 @Override
 public byte[] getBytes() {
  if (mDirty) {
   if (mContent == null)
    mContent = new byte[(int) getLength()];

   getDebugTarget().fireModelEvent(new FetchMemoryRequest(getStartAddress(), getLength()));
   mDirty = false;
  }

  return mContent;
 }

 @Override
 public boolean supportsValueModification() {
  return false;
 }

 @Override
 public void setValue(long offset, byte[] bytes) throws DebugException {
  throw new DebugException(new Status(IStatus.ERROR, "com.codeandme.textinterpreter.debugger", "TextMemoryBlock.setValue() not supported"));
 }

 public long getEndAddress() {
  return getStartAddress() + getLength();
 }

 public void update(int startAddress, byte[] data) {
  int fromOffset;
  int toOffset;
  int length;

  if (startAddress <= getStartAddress()) {
   fromOffset = (int) (getStartAddress() - startAddress);
   toOffset = 0;
  } else {
   fromOffset = 0;
   toOffset = (int) (startAddress - getStartAddress());
  }

  length = (int) Math.min(getLength() - toOffset - fromOffset, data.length);

  System.arraycopy(data, fromOffset, mContent, toOffset, length);
 }

 public void setDirty() {
  mDirty = true;
 }
}
The main ingredients are a start address, a length, and the content itself. As before for variables we update the content on demand only.

When the debug framework wants to display a dedicated memory area, it queries the DebugTarget:
package com.codeandme.textinterpreter.debugger.model;

public class TextDebugTarget extends TextDebugElement implements IDebugTarget, IEventProcessor {

 private final Set<TextMemoryBlock> mMemoryBlocks = new HashSet<TextMemoryBlock>();

 @Override
 public void handleEvent(final IDebugEvent event) {

  if (!isDisconnected()) {
   System.out.println("Target.handleEvent() " + event);

   [...]

   } else if (event instanceof MemoryEvent) {
    int startAddress = ((MemoryEvent) event).getStartAddress();
    for (TextMemoryBlock block : mMemoryBlocks) {
     if ((startAddress >= block.getStartAddress()) && (startAddress < block.getEndAddress())) {
      // block affected
      block.update(startAddress, ((MemoryEvent) event).getData());
      block.fireChangeEvent(DebugEvent.CONTENT);
     }
    }
   }

   [...]

  }
 }

 // ************************************************************
 // IMemoryBlockRetrieval
 // ************************************************************

 @Override
 public boolean supportsStorageRetrieval() {
  return true;
 }

 @Override
 public IMemoryBlock getMemoryBlock(final long startAddress, final long length) throws DebugException {
  TextMemoryBlock memoryBlock = new TextMemoryBlock(this, startAddress, length);
  mMemoryBlocks.add(memoryBlock);
  memoryBlock.fireCreationEvent();

  return memoryBlock;
 }
}
An update request is triggered by the MemoryBlock itself, we just need to refresh affected memory areas on an update event.

The implementation on the debugger side is really trivial, so I leave it to you to look at the code.

Step 2: Registering a rendering

To register a memory block for rendering we need to add a new extension to our plugin.xml.
Add a new renderingBindings item to org.eclipse.debug.ui.memoryRenderings. You need to provide a primaryId for the default rendering to be used. The Browse... button will show a nice overview of available renderings.

If you want to support multiple renderings you may add them to the list of renderingIds. In addition you may add an enablement to the rendering that checks for instanceof(TextMemoryBlock).

Defining your own renderings is beyond the scope of this tutorial but may be implemented using the same extension point.

Step 3: Using the Memory view

First you have to activate the Debug / Memory view. After the debugger suspends for the first time, you may add memory areas to monitor. For a selected area the primary rendering is used by default. New Renderings... allows to open additional ones.

Conclusion

I hope these tutorials help you getting started with debugging. Remember that there is good documentation available from eclipse directly (see tutorial 3 for links).

1 comment:

  1. Great tutorials. Thank you very much. I hope to see more of this kind of excellent tutorials on Planet Eclipse.

    ReplyDelete