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

How To Write Your Own Java and Scala Debugger

Takipi
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.

Takipi

September 24, 2013
Tweet

More Decks by Takipi

Other Decks in Technology

Transcript

  1. How To Write Your Own Java
    and Scala Debugger
    www.takipi.com

    View full-size slide

  2. 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

    View full-size slide

  3. 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

    View full-size slide

  4. 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

    View full-size slide

  5. 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

    View full-size slide

  6. 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

    View full-size slide

  7. 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

    View full-size slide

  8. 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

    View full-size slide

  9. 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

    View full-size slide

  10. 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

    View full-size slide

  11. 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:
    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

    View full-size slide

  12. 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

    View full-size slide

  13. www.takipi.com
    @takipid

    View full-size slide