Search code examples
javadebuggingbreakpointsjvmtijdi

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


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();

Solution

  • 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}