Upgrade to Pro — share decks privately, control downloads, hide ads and more …

How To Write Your Own Java and Scala Debugger

September 24, 2013

How To Write Your Own Java and Scala Debugger

With this post we’ll explore how Java / Scala debuggers we use everyday are written and work. We'll cover the two key components in the JVM Debugging Architecture - the Java Debugger Wire Protocol(JDWP) and the JVM Tooling Interface (JVMTI) C++ API. Each comes with its own set of capabilities and disadvantages.


September 24, 2013

More Decks by Takipi

Other Decks in Technology


  1. The JVM Debugging Architecture This JVM debugging framework and its

    APIs are completely open, documented and extensible, which means you can write you own debugger fairly easily. The framework’s current design is built out of two main parts – 1. The JDWP protocol 2. The JVMTI API layer Each has its own set of benefits and use-cases for which it works best. takipi.com
  2. The Java Debugger Wire Protocol JWDP is used to pass

    requests and receive events (such as changes in thread states or exceptions) between the debugger and debuggee process. This is done using binary messages, usually over the network. The concept behind this architecture is to create as much separation as possible between the two. The goal is to reduce the Heisenberg effect (Werner that is, not Walt) of having the debugger alter the execution of the target code while it’s running. takipi.com
  3. The Java Debugger Wire Protocol (2) Removing debugger logic from

    the target process also helps make sure that changes in the debuggee state (such as “stop the world” GC, or OutOfMemoryError) do not affect the debugger. The JDK comes with the JDI (Java Debugger Interface) which provides a complete debugger side implementation of the protocol, with the ability to connect, monitor and manipulate the target VM. This protocol is the same one used by Eclipse’s debugger for example. If you look at the command line arguments passed to your java process when it’s debugged by the IDE you’ll notice the arguments (-agentlib:jdwp=transport=dt_socket,…) passed to it to enable JDWP debugging. takipi.com
  4. The JVM Tooling Interface (JVMTI) The 2nd key component in

    the JVM debugger architecture is a set of native APIs covering a wide range of areas relating to the JVM. The JVMTI is designed as a set of C++ APIs along with a JVM mechanism to load libraries (such as a .dll or .so) that use them. This approach differs from JDWP, as it executes the debugger inside the target process. The downside is increased possibility of the debugger impacting the code performance and stability. The key advantage however is the ability to interact directly with the JVM in near real-time. takipi.com
  5. Writing Your Debugger Library Writing your own debugger requires creating

    a native OS library in C++. The headers are available in jvmti.h which comes with the JDK. Your “main” function in this case would look like - JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void*) { } The function will be invoked by the JVM when your debugger agent is loaded by the JVM. The JavaVM pointer passed to you will provide you with everything you need to converse with the JVM. The jvmtiEnv class introduced through the GetEnv method enables interaction with JVMTI through the concept of capabilities and events. takipi.com
  6. JVMTI Capabilities One of the key aspects of writing a

    debugger is to be mindful of its effects on the target process. To help you get fine grained control over how you affect execution of code, the JVMTI specification introduces a concept of capabilities. Using this approach you can tell the JVM in advance which API commands you intend to use (i.e. set breakpoints, suspend threads,..). This enables the JVM to prepare in advance, and gives you more control over your run-time overhead. This also enables JVMs from different vendors to tell you which commands they support out of the entire JVMTI specification. takipi.com
  7. Not All Capabilities Are Created Equal Some JVMTI capabilities come

    at a relatively small performance overhead. Other ones, such as can_generate_exception_events for exception callbacks, or can_generate_monitor_events for object locking come at a higher cost. The reason is they prevent the JVM from optimizing the code during JIT compile to its full extent, and can force the JVM to drop into interpreted mode at run-time. Others such as can_generate_field_modification_events for setting watches come at an even higher cost - slowing code execution by a significant percentage. While HotSpot supports multiple debugger libraries concurrently, some capabilities such as can_suspend for suspending threads can only owned by one library at a time. takipi.com
  8. Setting JVMTI Callbacks Once you’ve received your set of capabilities,

    your next step is to set up callbacks to let you know when things actually happen. Each of those callbacks provides fairly deep information as to the event that has transpired. For an exception callback this includes the bytecode location in which the exception was thrown, the thread, the exception object and if and where it will be caught. void JNICALL ExceptionCallback(jvmtiEnv *jvmti, JNIEnv *jni, jthread thread, jmethodID method, jlocation location, jobject exception, jmethodID catch_method, jlocation catch_location) takipi.com
  9. Setting Breakpoints and Watches The SetBreakpoint method enables you to

    suspend execution at a specific byte code instruction. SetFieldModificationWatch lets you pause execution whenever a field is modified. At that point you can use other functions such as GetStackTrace and GetThreadInfo to learn more about your current position in the code. Most JVMTI functions refer to classes and methods using abstract handles such as a jmethodID and jclass (same ones used by the Java Native Interface API). Additional functions such as GetMethodName and GetClassSignature help you obtain the actual symbol names. takipi.com
  10. Attaching Your Debugger Once you’ve written your debugger library the

    next step is to attach it to a JVM. To connect via JDWP add the following startup argument to debugee process - agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:<port> This details the form of communication (ie.e sockets) to the target and whether to start the debuggee in suspended mode. To attach a JVMTI library pass an agentpath argument to the debuggee process pointing to your library’s location on disk. An alternative way is to append your arguments to the global JAVA_TOOL_OPTIONS environment variable. This var gets picked up by every new JVM and automatically appended to the list of its existing arguments. takipi.com
  11. Remote Attach Another method to attach your debugger is by

    using the remote attach API. This simple and powerful Java API enables you to attach agents to running JVM processes without them being launched with any command line arguments. The downside is that you will not have access to some of the capabilities (such as can_generate_exception_events) as these can only requested at VM startup. takipi.com