📜 ⬆️ ⬇️

Java application debugging with JDI

Introduction

In the process of debugging applications running on a JVM through a debugger in Eclipse, I was always impressed by how much access you can get to application data — streams, variable values, and so on. And at the same time, from time to time there was a desire to “script” some actions or to gain more control over them.

For example, sometimes in order to “monitor” the state of a variable that changes in a cycle, I used a conditional breakpoint, the condition for which was a code like “System.out.println (theVariable); return false. This hack made it possible to get a log of the values ​​of a variable almost without interrupting the operation of the application (it, of course, was still interrupted while the condition code was being executed, but no more). Plus, often when viewing any data through the Display view, it was annoying that the result of the Evolution code in the Display entered was added immediately after it.

In general, I wanted to be able to do the same thing, for example, via Bean Shell or Groovy Shell, which is basically similar to a program debug. Logically, it shouldn't have been difficult - after all, Eclipse does it somehow, right?
')
Having done some research, I was able to access the debugging information of the JVM programmatically, and I hasten to share an example.


About JPDA and JDI

To debug the JVM, special standards have been invented, brought together under the umbrella term JPDA - Java Platform Debugger Architecture. They include JVMTI — the native interface for debugging applications in the JVM by calling functions, JDWP — the data transfer protocol between the debugger and the JVM, the applications in which are debugged, etc.

All of this didn’t look particularly relevant. But beyond that, JPDA includes a JDI - the Java Debug Interface. This is a Java API for debugging JVM applications - what the doctor ordered. The official JPDA page has confirmed the existence of Sun / Oracle JDI reference implementations. So, it only remained to start using it.

Example

As proof of concept, I decided to try running two Groovy Shells — one in debug mode as an “experimental” one, the second as a debugger. In the experimental shell, a lower case variable was set, the value of which was to be obtained from the “debugger” shell.

Subject was run with the following parameters:
-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=7896
Those. The JVM was launched in remote debugging mode via TCP / IP, and was awaiting a connection from the debugger on port 7896.

Also in the experimental Groovy Shell the following command was executed:
 myVar = “Some special value”; 

Accordingly, the value “Some special value” should have been obtained in the debugger.

Since this is not just the value of the field of some object, in order to get it, it was necessary to know a little the insides of Groovy Shell (or at least pry into the sources), but the more interesting and realistic the task seemed to me.

Next, it was for the "debugger":

Consider everything step by step:

JVM connection

Using JDI, we connect with the JVM which we decided to debug (host == localhost since I did everything on the same machine, but it works the same way with the remote one; the same port that was set up in the experimental JVM debug parameters).
JDI allows you to join the JVM through sockets and directly to the local process. Therefore, VirtualMachineManager returns more than one AttachingConnector. We select the desired connector by the name of the transport ("dt_socket")
 vmm = com.sun.jdi.Bootstrap.virtualMachineManager(); vmm.attachingConnectors().each{ if("dt_socket".equalsIgnoreCase(it.transport().name())) { atconn = it; } } args = atconn.defaultArguments(); args.get("port").setValue(7896); args.get("hostname").setValue("127.0.0.1"); vm = atconn.attach(args); 


Getting stream stream main

The resulting interface to the remote JVM allows you to see the threads running in it, suspend them, etc. But in order to be able to make method calls in a remote JVM, we need a thread in it that would be stopped by exactly breakpoint. What actually says the following paragraph JDI javadoc :
“The method of invocation can be interrupted. It has been suspended through VirtualMachine.suspend () or it has been suspended through ThreadReference.suspend (). ”

To install a breakpoint, I went in a somewhat specific way - not to look into the Groovy Shell sorts, but simply to see what is happening in the JVM right now and set the breakpoint right in what is happening.

The main stream was detected in the JVM experimental streams, and I looked into his window frame. The flow was previously stopped - so that the stackrack remains relevant during subsequent manipulations.
 // Find thread by name "main" vm.allThreads().each{ if(it.name().equals("main")) mainThread = it } // Suspend it mainThread.suspend() // Look what's in it's stack trace i=0; mainThread.frames().each{ println String.valueOf(i++)+": "+it; }; println ""; 


As a result, I got this:
 0: java.io.FileInputStream.readBytes(byte[], int, int)+-1 in thread instance of java.lang.Thread(name='main', id=1) 1: java.io.FileInputStream:220 in thread instance of java.lang.Thread(name='main', id=1) 2: java.io.BufferedInputStream:218 in thread instance of java.lang.Thread(name='main', id=1) 3: java.io.BufferedInputStream:237 in thread instance of java.lang.Thread(name='main', id=1) 4: jline.Terminal:99 in thread instance of java.lang.Thread(name='main', id=1) 5: jline.UnixTerminal:128 in thread instance of java.lang.Thread(name='main', id=1) 6: jline.ConsoleReader:1453 in thread instance of java.lang.Thread(name='main', id=1) 7: jline.ConsoleReader:654 in thread instance of java.lang.Thread(name='main', id=1) 8: jline.ConsoleReader:494 in thread instance of java.lang.Thread(name='main', id=1) 9: jline.ConsoleReader:448 in thread instance of java.lang.Thread(name='main', id=1) 10: jline.ConsoleReader$readLine.call(java.lang.Object, java.lang.Object)+17 in thread instance of java.lang.Thread(name='main', id=1) 11: org.codehaus.groovy.tools.shell.InteractiveShellRunner:89 in thread instance of java.lang.Thread(name='main', id=1) 12: org.codehaus.groovy.tools.shell.ShellRunner:75 in thread instance of java.lang.Thread(name='main', id=1) 13: org.codehaus.groovy.tools.shell.InteractiveShellRunner.super$2$work()+1 in thread instance of java.lang.Thread(name='main', id=1) ....  .., 65   


Setting breakpoint

So, we have the maintrack of the stopped thread main. The JDI API returns for threads the so-called StackFrame, from which you can get their Location. Actually, this Location is required for setting breakpoint.
Without hesitation, I took the location from jline.ConsoleReader $ readLine.call, and set a breakpoint in it, and then I started the main thread to continue:
 evReqMan = vm.eventRequestManager(); frame = mainThread.frames().get(10); bpReq = evReqMan.createBreakpointRequest(frame.location()); mainThread.resume(); bpReq.enable(); 


Breakpoint now installed. Switching to the experimental Groovy Shell and pressing enter I saw that it really stopped. We have a breakpoint flow at breakpoint - everything is ready to interfere with the experimental JVM.

Getting a reference to a Groovy Shell object

The JDI API allows StackFrame to get variables visible in them. To get the value of a variable from the context of Groovy Shell, it was necessary to first pull the link to the shell itself. But where is he?

We spy all visible variables in all stack frames:
 i=0; mainThread.frames().each{ println String.valueOf(i++)+": "+it; try{ it.visibleVariables().each{var-> println " - "+var; }} catch(Exception e) {} }; println; 


A frame stack was detected in the object “org.codehaus.groovy.tools.shell.Main” with a visible shell variable:
"48: org.codehaus.groovy.tools.shell.Main: 131 in thread instance of java.lang.Thread (name = 'main', id = 1)".

Getting the value from the Groovy Shell

The shell.Main has an interpreter field. Knowing a bit of Groovy Shell internals, I knew in advance that the context GroovyShell variables are stored in an object of the type groovy.lang.Binding , which can be obtained by calling getContext () from the Interpreter (method call is necessary because the corresponding field with reference to groovy.lang.Binding in Interpreter no).

From the Binding, the value of a variable can be obtained by calling the getVariable (String varName) method.

 frame = mainThread.frames().get(48); vShell = frame.getValue(frame.visibleVariableByName("shell")); vInterp = vShell.getValue(vShell.referenceType().fieldByName("interp")); vContext = vInterp.invokeMethod(mainThread, vInterp.referenceType().methodsByName("getContext").get(0), [], 0) varVal = vContext.invokeMethod(mainThread, vContext.referenceType().methodsByName("getVariable").get(0), [vm.mirrorOf("myVar")], 0) 


The last line of the script returned to us the expected value “Some special value” - everything works!

Finishing touch

For fun, I decided to also change the value of this variable from the debugger - for this it was enough to call the Binding method setVariable (String varName, Object varValue). What could be easier?
 varVal = vContext.invokeMethod(mainThread, vContext.referenceType().methodsByName("setVariable").get(0), [vm.mirrorOf("myVar"), vm.mirrorOf("Surprise!")], 0); bpReq.disable(); mainThread.resume(); 

To make sure that everything worked, I also zadizeyblil breakpoint and launched back the main thread suspended earlier at breakpoint.

Switching the last time to the Groovy Shell test subject, I checked the value of the myVar variable, and it turned out to be “Surprise!”.

findings

Being a Java programmer is a blessing, for Sun gave us powerful tools - which means great opportunities (-:
And if you add to Groovy comfortable wrappers (metaclasses) for JDI, you can make the program debugging from Groovy Shell quite enjoyable. Unfortunately, for now, it looks somewhere the same as, for example, accessing fields and methods through the reflection API.

UPD:
Some vague and defective wrappers for Groovy were found here: youdebug.kenai.com
I started writing my own - github.com/mvmn/groovyjdi

Source: https://habr.com/ru/post/155505/


All Articles