Is it possible to get JDI's current StackFrame in Java at the debuggee side?
Asked Answered
B

1

2

So, JDI allows us to set a breakpoint in the debuggee app and then get the current StackFrame via JDWP. To my understanding, JVMTI is used at the debuggee side to send the requested information to JDI through JDWP.

Is it possible to get the current StackFrame from the debuggee itself (so without sending it to the debugger... the debuggee will be its own debugger)?

For example consider this code:

//client code
int a = 5;
StackFrame frame = ...

//list will contain variable "a"
List<LocalVariable> visibleVariables = frame.visibleVariables();
Botfly answered 17/6, 2020 at 16:20 Comment(0)
G
6

It’s possible, with some catches.

Debugging must have been enable for the JVM at launch time already. To connect with your own JVM, you need to use either, a predefined port that the applications knows or the attach feature, which requires self-attach to be enabled explicitly for recent JVMs.

Then, since you have to suspend the thread you want to inspect, it can’t be the same thread performing the inspection. So you have to delegate the task to a different thread.

For example

public static void main(String[] args) throws Exception {
    Object o = null;
    int test = 42;
    String s = "hello";
    Map<String, Object> vars = variables();
    System.out.println(vars);
}
// get the variables in the caller’s frame
static Map<String,Object> variables() throws Exception {
    Thread th = Thread.currentThread();
    String oldName = th.getName(), tmpName = UUID.randomUUID().toString();
    th.setName(tmpName);
    long depth = StackWalker.getInstance(
        StackWalker.Option.SHOW_HIDDEN_FRAMES).walk(Stream::count) - 1;

    ExecutorService es = Executors.newSingleThreadExecutor();
    try {
        return es.<Map<String,Object>>submit(() -> {
            VirtualMachineManager m = Bootstrap.virtualMachineManager();
            for(var ac: m.attachingConnectors()) {
                Map<String, Connector.Argument> arg = ac.defaultArguments();
                Connector.Argument a = arg.get("pid");
                if(a == null) continue;
                a.setValue(String.valueOf(ProcessHandle.current().pid()));
                VirtualMachine vm = ac.attach(arg);
                return getVariableValues(vm, tmpName, depth);
            }
            return Map.of();
        }).get();
    } finally {
        th.setName(oldName);
        es.shutdown();
    }
}

private static Map<String,Object> getVariableValues(
        VirtualMachine vm, String tmpName, long depth)
        throws IncompatibleThreadStateException, AbsentInformationException {

    for(ThreadReference r: vm.allThreads()) {
        if(!r.name().equals(tmpName)) continue;
        r.suspend();
        try {
            StackFrame frame = r.frame((int)(r.frameCount() - depth));
            return frame.getValues(frame.visibleVariables())
                .entrySet().stream().collect(HashMap::new,
                    (m,e) -> m.put(e.getKey().name(), t(e.getValue())), Map::putAll);
        } finally {
            r.resume();
        }
    }
    return Map.of();
}
private static Object t(Value v) {
    if(v == null) return null;
    switch(v.type().signature()) {
        case "Z": return ((PrimitiveValue)v).booleanValue();
        case "B": return ((PrimitiveValue)v).byteValue();
        case "S": return ((PrimitiveValue)v).shortValue();
        case "C": return ((PrimitiveValue)v).charValue();
        case "I": return ((PrimitiveValue)v).intValue();
        case "J": return ((PrimitiveValue)v).longValue();
        case "F": return ((PrimitiveValue)v).floatValue();
        case "D": return ((PrimitiveValue)v).doubleValue();
        case "Ljava/lang/String;": return ((StringReference)v).value();
    }
    if(v instanceof ArrayReference)
        return ((ArrayReference)v).getValues().stream().map(e -> t(e)).toArray();
    return v.type().name()+'@'+Integer.toHexString(v.hashCode());
}

When I run this on my machine with JDK 12 using the options
-Djdk.attach.allowAttachSelf -agentlib:jdwp=transport=dt_socket,server=y,suspend=n, it prints

Listening for transport dt_socket at address: 50961
{args=[Ljava.lang.Object;@146ba0ac, s=hello, test=42, o=null}
Gavra answered 19/6, 2020 at 10:24 Comment(2)
would this be any faster than attaching from an external JVM? The most important thing for me is to achieve this in a way which is really fast (not requiring VM stop or communication through sockets)...Botfly
I don’t think so. An application debugging itself has not been foreseen by the developers, so they surely didn’t implement any optimizations for this case. It even seems to me that the attach API usage only eliminates the need to know the port number beforehand, but will end up using socket communication. If your task is as narrow as described in the question (getting your own current stack frame), byte code instrumentation would allow much faster solutions, not needing the JVM’s debug features at all.Gavra

© 2022 - 2024 — McMap. All rights reserved.