Skip to content

Commit 8e0bbf1

Browse files
committed
[GR-61548] Add documentation for the SubstrateVM JDWP implementation
1 parent 43d4eb6 commit 8e0bbf1

File tree

4 files changed

+338
-1
lines changed

4 files changed

+338
-1
lines changed

docs/reference-manual/native-image/DebuggingAndDiagnostics.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ Native Image provides utilities for debugging and inspecting the produced binary
1616
- For performance analysis, see [Linux Perf Profiler Support in Native Image](PerfProfiling.md)
1717
- For an overall insight regarding build phases and the contents of a native executable, use [Build Reports](BuildReport.md)
1818
- For native memory tracking, see [Native Memory Tracking (NMT)](NMT.md)
19-
- See the [Java Diagnostic Command documentation](JCmd.md) for instructions on using `jcmd`.
19+
- See the [Java Diagnostic Command documentation](JCmd.md) for instructions on using `jcmd`.
20+
- For Java Debug Wire Protocol (JDWP) support in Native Image to enable debugging with standard Java tooling, see [Java Debug Wire Protocol (JDWP)](JDWP.md).
+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
---
2+
layout: docs
3+
toc_group: debugging-and-diagnostics
4+
link_title: Java Debug Wire Protocol (JDWP) with Native Image
5+
permalink: /reference-manual/native-image/debugging-and-diagnostics/JDWP/
6+
---
7+
8+
# Java Debug Wire Protocol (JDWP) with Native Image
9+
10+
## Overview
11+
12+
This document describes the Java Debug Wire Protocol (JDWP) debugging support for Native Image, a feature that enables debugging of native images using standard Java tooling.
13+
14+
## Installation
15+
16+
The JDWP feature is not included by default as part of Native Image. To use it, you need to [build GraalVM from source](https://github.com/oracle/graal/blob/master/vm/README.md) using the following [mx](https://github.com/graalvm/mx/) commands:
17+
```shell
18+
mx --dynamicimports /substratevm build
19+
export JAVA_HOME=$(mx --dynamicimports /substratevm graalvm-home)
20+
```
21+
22+
## Usage
23+
24+
> Note: JDWP debugging for Native Image is currently under development.
25+
26+
To include JDWP support in a native image, add the `--macro:svmjdwp` option to your `native-image` command:
27+
28+
```shell
29+
native-image --macro:svmjdwp ... -cp <class/path> YourApplication ...
30+
```
31+
32+
This command produces:
33+
1. The native executable
34+
2. An `<image-name>.metadata` file
35+
3. The `lib:svmjdwp` (`libsvmjdwp.so`, `libsvmjdwp.dylib` or `svmjdwp.dll`) shared library that will be necessary when debugging is also copied next to those files.
36+
37+
### Launching in Debug Mode
38+
39+
To launch the native image in debug mode, use the `-XX:JDWPOptions=` option, similar to HotSpot's `-agentlib:jdwp=`:
40+
41+
```shell
42+
./your-application -XX:JDWPOptions=transport=dt_socket,server=y,address=8000
43+
```
44+
45+
> Note: Debugging requires the _image-name.metadata_ file generated at build time and the `svmjdwp` shared library in the same directory as the native executable.
46+
47+
For a complete list of supported JDWP options on Native Image, run:
48+
49+
```shell
50+
./your-application -XX:JDWPOptions=help
51+
```
52+
53+
### Additional JDWP Options
54+
55+
Native Image supports additional non-standard JDWP options:
56+
57+
- `mode=native:<path>`: Specifies the path to the `svmjdwp` library. This can be:
58+
- A direct path to `lib:svmjdwp`
59+
- A directory containing `lib:svmjdwp`
60+
- A GraalVM installation containing `lib:svmjdwp` in the `lib` or `bin` directory
61+
62+
If no path is specified, `lib:svmjdwp` is searched for beside the native executable.
63+
64+
Examples:
65+
- `-XX:JDWPOptions=...,mode=native:<path/to/lib:svmjdwp>`
66+
- `-XX:JDWPOptions=...,mode=native:<path/to/directory/containing/lib:svmjdwp>`
67+
- `-XX:JDWPOptions=...,mode=native:<path/to/java/home>`: Search `lib:svmjdwp` inside `JAVA_HOME`, for example `lib|bin/lib:svmjdwp`.
68+
- `-XX:JDWPOptions=...,mode=native`: Search `lib:svmjdwp` besides the native executable directory.
69+
70+
- `-XX:JDWPOptions=...,vm.options=...`: VM options, separated by whitespaces, passed to the JDWP server isolate/JVM, should not include a `,` character.
71+
- `-XX:JDWPOptions=...,vm.options=@argfile`: Also supports [Java Command-Line Argument Files](https://docs.oracle.com/en/java/javase/21/docs/specs/man/java.html#java-command-line-argument-files).
72+
73+
Note: If `lib:svmjdwp` cannot be found, the application will terminate with error code 1.
74+
75+
### Build `lib:svmjdwp`
76+
77+
Run `mx --native-images=lib:svmjdwp build` to build the library.
78+
79+
### Build a Native Executable with JDWP Support
80+
81+
Add the `--macro:svmjdwp` option to the `native-image` command:
82+
```shell
83+
mx native-image --macro:svmjdwp -cp <class/path> MainClass ...
84+
```
85+
86+
To build and include `lib:svmjdwp` as a build artifact, run:
87+
```shell
88+
mx --native-images=lib:svmjdwp native-image --macro:svmjdwp -cp <class/path> MainClass ...
89+
```
90+
91+
Both commands produce a binary, an `<image-name>.metadata` file.
92+
93+
## Goals and Constraints
94+
95+
The JDWP debugging support for Native Image aims to:
96+
97+
1. Expose Native Image through JDWP as-is, maintaining its assumptions and constraints
98+
2. Incur minimal or no performance overhead when not in use
99+
3. Add minimal size overhead to the native binary
100+
4. Be available on all Graal-supported platforms, including Linux, macOS, and Windows, across x64 and AArch64 architectures
101+
5. Provide a debugging experience similar to HotSpot, without requiring additional steps (e.g., setting permissions, environment variables)
102+
103+
## Architecture
104+
105+
The JDWP debugging support is implemented using a Java bytecode interpreter, adapted from [Espresso](https://github.com/oracle/graal/tree/master/espresso) to work with Native Image. Key components include:
106+
107+
1. **Interpreter**: Derived from Espresso and adapted for [SubstrateVM](https://github.com/oracle/graal/tree/master/substratevm/). It does not enable any dynamic features beyond what Native Image already supports.
108+
109+
2. **PLT/GOT Feature**: Used to divert execution to the interpreter. This implementation detail may change for some platforms.
110+
111+
3. **Metadata File**: An external _.metadata_ file produced at build time, containing information required for runtime method interpretation.
112+
113+
4. **JDWP Server**: Implemented as a native library (`lib:svmjdwp`), handling network connections and implementing JDWP commands.
114+
115+
5. **JDWP Resident**: A component within the application providing access to locals, fields, stack traces, and other runtime information.
116+
117+
## Limitations
118+
119+
The JDWP debugger for Native Image is designed to align with Native Image's architecture and principles.
120+
While many limitations are a natural consequence of Native Image's design, others may be due to the current implementation of the debugger itself.
121+
Here are the key limitations to be aware of:
122+
123+
- The debugger follows Native Image closed-world assumptions:
124+
- Only classes, methods, and fields included in the image are accessible.
125+
- Some types may not be instantiable at runtime, even if there are instances in the image heap.
126+
- Some fields cannot be written to.
127+
- No support for dynamic class loading.
128+
- No class or method redefinition.
129+
- There's no runtime class-path `System.getProperty("java.class.path") == null`
130+
- No exception breakpoints.
131+
- No field watchpoints.
132+
- No early return or frame popping.
133+
- Some methods cannot be interpreted by the debugger (see below):
134+
- Methods that use "System Java".
135+
- Methods that contain a call to an intrinsic without compiled entry-point.
136+
- Breakpoints cannot be set in non-interpretable methods.
137+
- Stepping through non-interpretable methods is not possible, these are effectively treated as if they were Java "native" methods, with no guarantee to break/step on the next executed method, only on the next interpreted method.
138+
- Not all execution paths are executable/interpretable.
139+
- Interpreting "dead-code" may work, but only on a best-effort basis.
140+
- Violating compiled-code assumptions, for example, passing a null argument where a non-null was expected, is considered undefined behavior and prone to crashes.
141+
- Cannot write locals of compiled frames.
142+
- Cannot hit breakpoints or stepping events on actively executing compiled methods.
143+
- Step-out operations only work for interpreter frames, not compiled frames.
144+
- Can only debug the first isolate of a native image.
145+
- Step-into does not work for target methods of a `MethodHandle` object, for example, lambdas.
146+
147+
These limitations reflect the current state of JDWP debugging support in Native Image.
148+
Some may be addressed in future iterations of the debugger, while others are fundamental to Native Image's design.
149+
150+
### Further Reading
151+
152+
- Instructions on how to run the JDWP server in HotSpot, so that the debugger can be debugged: [JDWP server](https://github.com/oracle/graal/tree/master/substratevm/src/com.oracle.svm.jdwp.server/README.md)
153+
- Implementation details about the interpreter and the transitions from and to compiled code: [SubstrateVM Interpreter](https://github.com/oracle/graal/tree/master/substratevm/src/com.oracle.svm.interpreter/README.md)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# Java bytecode interpreter for SubstrateVM
2+
3+
## How execution is diverged from compiled code to interpreter
4+
5+
Execution of a native-image binary will start out in AOT compiled code as usual.
6+
Execution of specific methods can be diverted into the interpreter via `InterpreterDirectives`.
7+
8+
There are two cases to consider:
9+
10+
1. There is an activation frame on a thread stack: We have to deoptimize that frame so that whenever execution returns to this activation frame it will transfer execution to the interpreter.
11+
2. Whenever a call to this method is done, we need to make sure that its execution will happen in the interpreter.
12+
13+
14+
For 1. the procedure is similar to the `Deoptimizer` implemented for run-time compiled methods: We patch the return address in the upper frame, so that it points to a deopt stub that will do the deoptimization of the target frame. Once the deopt stub executes it collects the locals and arguments on the native frame accordingly, and tail calls into a small stub that will create a new frame and resumes execution in the interpreter with the right `bci` and locals in place.
15+
16+
For 2. we piggyback on the PLT+GOT work done in the context of code compression (see GR-40476). That means every call has an extra indirection through a PLT stub. There are other approaches that work on some platforms (e.g., map `.text` section `rwx` on Linux and patch the prologue or call-sites accordingly), that will be reconsidered once interpretation is enabled by default.
17+
18+
### Transition from compiled code to interpreter (`c2i`)
19+
20+
If we want a method to be executed in the interpreter, we can update its slot in the GOT accordingly, since each call-site for that method will go through the PLT+GOT mechanism. But _what_ do we patch there? For each method that we want to run in the interpreter we allocate an `InterpreterMethod` at image build-time and store it in a table. We then create a `interp_enter_trampoline` that injects its index to the `InterpreterMethod` as a hidden argument.
21+
22+
```c
23+
interp_enter_trampoline:
24+
mov $interp_reg, <index to InterpreterMethod>
25+
tailcall interp_enter_stub
26+
```
27+
28+
The `interp_enter_stub` then collects all ABI registers into a `InterpreterEnterData` stackvalue:
29+
30+
```c
31+
struct InterpreterEnterData {
32+
InterpreterMethod method;
33+
Pointer stack;
34+
Word abi_arg_gp_regs[];
35+
Word abi_arg_fp_regs[];
36+
Word abi_ret_gp_reg;
37+
Word abi_ret_fp_reg;
38+
}
39+
40+
interp_enter_stub:
41+
InterpreterEnterData enterData = stack_allocate();
42+
enterData->stack = $original_sp;
43+
enterData->method = interpreter_methods_table[$interp_reg]
44+
45+
for (int i = 0; enterData->abi_arg_gp_regs.length; i++) {
46+
enterData->abi_arg_gp_regs[i] = getABIRegGP(i);
47+
}
48+
for (int i = 0; enterData->abi_arg_fp_regs.length; i++) {
49+
enterData->abi_arg_fp_regs[i] = getABIRegFP(i);
50+
}
51+
52+
mov arg_0_reg, enterData
53+
call interp_enter
54+
55+
mov return_gp_reg, enterData.abi_ret_gp_reg
56+
mov return_fp_reg, enterData.abi_ret_fp_reg
57+
ret
58+
```
59+
60+
And then `interp_enter` allocates the actual `InterpreterFrame`, populates it with arguments and calls the interpreter main loop:
61+
62+
```java
63+
static void interp_enter(InterpreterEnterData enterData) {
64+
InterpreterMethod method = enterData.method;
65+
InterpreterFrame frame = allocate();
66+
67+
CallInfo cinfo = method.getCallInfo();
68+
for (int i = 0; i < method.signature.length; i++) {
69+
// cinfo.getArg does the "heavy" ABI work
70+
frame.setArg(i, cinfo.getArg(i, method.signature[i], enterData));
71+
}
72+
73+
new Interpreter(method).execute(frame);
74+
75+
enterData.setReturnValue(cinfo.getReturnValue(frame));
76+
}
77+
```
78+
79+
Note that:
80+
* Per method there is one `interp_enter_trampoline`, and it is architecture specific.
81+
* There is one `interp_enter_stub` globally, and it is architecture specific.
82+
* There is one `interp_enter` globally, and is written in System Java.
83+
* `InterpreterEnterData` is a `CStruct`.
84+
85+
### Transition from interpreter to compiled code (`i2c`)
86+
87+
Similar approach as for `c2i`: We have `interp_exit` that builds a `InterpreterExitData` struct, which is then passed down to an `interp_exit_stub` that does the ABI related work. Eventually it calls the AOT compiled method indirectly via its GOT offset (i.e. we do _not_ go through the PLT stub) and makes sure the return values are correctly passed up.
88+
89+
For exit we do not require a trampoline per method. However the `interp_exit_stub` is a bit special, as it does not have a fixed frame layout, something the Graal backend cannot deal with well. Therefore there is special handling around this, for example the stack walker needs to know how much it should unwind. The variable length of such frames is stored at a known offset.
90+
91+
### Call dispatch
92+
93+
Let's consider multiple scenarios.
94+
95+
#### Scenario A: Static call, no inlining
96+
97+
Consider:
98+
```java
99+
class Calls {
100+
static void foo() {
101+
bar();
102+
}
103+
static void bar() {
104+
print("bar");
105+
}
106+
}
107+
```
108+
109+
Cases:
110+
1. If we want `Calls#bar` to be executed in the interpreter, its GOT slot will be patched to an `interp_enter_trampoline` that injects the `InterpreterMethod` for `Calls#bar`.
111+
2. If `Calls#foo` is executed in the interpreter, and `Calls#bar` should run as compiled code, we will do a GOT dispatch with the `i2c` primitive described above. A prerequisite for that is that we can determine the right GOT slot at the time we create the constant pool entry for that `invokestatic` in `Calls#foo`, which is effectively a `MethodPointer` attached to the `InterpreterMethod`.
112+
3. If both should be executed in the interpreter we can either go through two transitions via `i2c <> c2i`. There is some optimization potential here by having an `i2i` transition.
113+
114+
115+
#### Scenario B: virtual call, no inlining
116+
117+
Consider:
118+
```java
119+
class VCalls {
120+
abstract void baz();
121+
122+
static void dispatch(VCalls o) {
123+
o.baz();
124+
}
125+
}
126+
127+
class VCalls_A extends VCalls {
128+
void baz() {
129+
print("VCalls_A");
130+
}
131+
}
132+
133+
class VCalls_B extends VCalls {
134+
void baz() {
135+
print("VCalls_B");
136+
}
137+
}
138+
```
139+
140+
Cases:
141+
1. Assume `VCalls#dispatch` runs as compiled code and `VCalls_A#baz` should run with the interpreter. Since the virtual dispatch goes through the PLT+GOT mechanism, we can patch the GOT slot accordingly.
142+
2. Assume `VCalls#dispatch` runs in the interpreter and both `VCalls_A#baz` and `VCalls_B#baz` run as compiled code. We do the virtual dispatch by access the entrypoint via `o->hub->vtable[vtableIndex]`, and then pass that through the `i2c` mechanism above. Additionally, at build-time we need to attach the right `vtableIndex` for the constant pool entry associated with that `invokevirtual` instruction.
143+
3. Assume `VCalls#dispatch` runs in the interpreter, `VCalls_A#baz` runs in the interpreter too but `VCalls_B#baz` runs as compiled code. In this case we do the same as in 2. and accept the overhead of a `i2c <> c2i` transition.
144+
145+
#### Scenario C: Static call, with inlining
146+
147+
Consider:
148+
```java
149+
class Calls {
150+
static void foo() {
151+
bar();
152+
}
153+
static void bar() {
154+
print("bar");
155+
}
156+
}
157+
```
158+
159+
But this time `Calls#bar` is inlined. Cases:
160+
1. Assume `Calls#bar` should be executed in the interpreter. In this case we need to make sure that the execution of `Calls#foo` already happens in the interpreter, i.e. patch its GOT slot and deoptimize activation frames of it.
161+
2. Assume `Calls#foo` should be executed in the interpreter. As soon as we reach the `invokestatic` to call `Calls#bar` we cannot do the same as in Scenario A, because there is no AOT compiled version for `Calls#bar` (the only usage was inlined). So we have to make sure to include a `InterpreterMethod` for `Calls#bar` at build-time, and do its execution in the interpreter. Since there is no `MethodPointer` attached to the `InterpreterMethod` of `Calls#bar`, we know that we must execute `Calls#bar` in the interpreter too then.
162+
163+
For 2. we need to track inlining decisions during image building and persist that information in a `.metadata` companion file.
164+
165+
#### Scenario D: Virtual calls, with inlining
166+
167+
If virtual callees "disappear" due to inlining, SVM still reserves a vtable slot for it but populates it with a bailout stub.
168+
We detect such cases upon invocation and fall back to a "side vtable" populated with `InterpreterMethod`s. We construct that table during image build time and include that in the `.metadata` companion file.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# JDWP for SubstrateVM
2+
3+
For a user oriented documentation see [JDWP](/docs/JDWP.md).
4+
5+
6+
## JDWP Development, running the server on HotSpot
7+
8+
The default configuration is that the JDWP server is another shared library built with Native Image. However, to ease debugging the debugger, it is possible to run the JDWP server on HotSpot.
9+
10+
An additional non-standard mode is available to enable that:
11+
12+
- `-XX:JDWPOptions=...,mode=jvm`
13+
- `-XX:JDWPOptions=...,mode=jvm:<path/to/lib:jvm>
14+
- `-XX:JDWPOptions=...,mode=jvm:<path/to/java/home>
15+

0 commit comments

Comments
 (0)