Skip to content

Commit 038dd84

Browse files
committed
Merge pull request #2264 from pguyot/w15/add-opcodes
Improve DWARF debugging support These changes are made under both the "Apache 2.0" and the "GNU Lesser General Public License 2.1 or later" license terms (dual license). SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
2 parents 74873bc + 31e7e90 commit 038dd84

23 files changed

Lines changed: 1024 additions & 239 deletions

.github/workflows/build-and-test-macos.yaml

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ jobs:
6060
cmake_opts_other: "-DAVM_DISABLE_JIT=OFF"
6161

6262
# JIT + DWARF build (macOS aarch64)
63-
- os: "macos-15"
63+
- os: "macos-26"
6464
otp: "28"
6565
mbedtls: "mbedtls@3"
6666
cmake_opts_other: "-DAVM_DISABLE_JIT=OFF -DAVM_DISABLE_JIT_DWARF=OFF"
@@ -145,6 +145,62 @@ jobs:
145145
run: |
146146
./tests/test-erlang
147147
148+
- name: "Test: dwarf (test_executable_line)"
149+
if: matrix.cmake_opts_other == '-DAVM_DISABLE_JIT=OFF -DAVM_DISABLE_JIT_DWARF=OFF'
150+
working-directory: build
151+
run: |
152+
OUTPUT=$(lldb -b \
153+
-o "settings set plugin.jit-loader.gdb.enable on" \
154+
-o "breakpoint set -f test_executable_line.erl -l 49" \
155+
-o "breakpoint set -f test_executable_line.erl -l 52" \
156+
-o "run" \
157+
-o "print term_to_int(ctx->x[0])" \
158+
-o "c" \
159+
-o "print term_to_int(ctx->x[0])" \
160+
-- ./tests/test-erlang test_executable_line 2>&1)
161+
echo "$OUTPUT"
162+
# Extract printed values in order
163+
VALUES=$(echo "$OUTPUT" | sed -n 's/.*(\(avm_int_t\)) \(-\{0,1\}[0-9][0-9]*\).*/\2/p')
164+
FIRST=$(echo "$VALUES" | sed -n '1p')
165+
SECOND=$(echo "$VALUES" | sed -n '2p')
166+
if [ "$FIRST" != "42" ]; then
167+
echo "FAIL: expected 42 at line 49, got '$FIRST'"
168+
exit 1
169+
fi
170+
if [ "$SECOND" != "2" ]; then
171+
echo "FAIL: expected 2 at line 52, got '$SECOND'"
172+
exit 1
173+
fi
174+
echo "PASS: test_executable_line dwarf test"
175+
176+
- name: "Test: dwarf (test_debug_line)"
177+
if: matrix.cmake_opts_other == '-DAVM_DISABLE_JIT=OFF -DAVM_DISABLE_JIT_DWARF=OFF'
178+
working-directory: build
179+
run: |
180+
OUTPUT=$(lldb -b \
181+
-o "settings set plugin.jit-loader.gdb.enable on" \
182+
-o "breakpoint set -f test_debug_line.erl -l 49" \
183+
-o "breakpoint set -f test_debug_line.erl -l 52" \
184+
-o "run" \
185+
-o "print term_to_int(N)" \
186+
-o "c" \
187+
-o "print term_to_int(Z)" \
188+
-- ./tests/test-erlang test_debug_line 2>&1)
189+
echo "$OUTPUT"
190+
# Extract printed values in order
191+
VALUES=$(echo "$OUTPUT" | sed -n 's/.*(\(avm_int_t\)) \(-\{0,1\}[0-9][0-9]*\).*/\2/p')
192+
FIRST=$(echo "$VALUES" | sed -n '1p')
193+
SECOND=$(echo "$VALUES" | sed -n '2p')
194+
if [ "$FIRST" != "42" ]; then
195+
echo "FAIL: expected 42 at line 49, got '$FIRST'"
196+
exit 1
197+
fi
198+
if [ "$SECOND" != "2" ]; then
199+
echo "FAIL: expected 2 at line 52, got '$SECOND'"
200+
exit 1
201+
fi
202+
echo "PASS: test_debug_line dwarf test"
203+
148204
- name: "Test: test-enif"
149205
working-directory: build
150206
run: |

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
- Added Erlang distribution over serial (uart)
1111
- Added WASM32 JIT backend for Emscripten platform
1212
- Added `network:wifi_scan/0,1` to ESP32 network driver to scan available APs when in sta or sta+ap mode.
13+
- Added support for `nif_start`, `executable_line` and `debug_line` opcodes
14+
- Added named variable debugging support in DWARF when modules are compiled with `beam_debug_info`
1315

1416
### Changed
1517
- Updated network type db() to dbm() to reflect the actual representation of the type

doc/src/jit.md

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,16 @@ Modules can also be ahead-of-time compiled: the native code is generated at buil
1818

1919
The JIT compiler supports the following target architectures:
2020

21-
* `x86_64` — 64-bit x86 (Linux, macOS, FreeBSD)
22-
* `aarch64` — 64-bit ARM (Linux, macOS)
23-
* `arm32` — 32-bit ARM (Linux)
24-
* `armv6m` — ARM Cortex-M0+ (Raspberry Pi Pico, STM32 with Cortex-M0/M0+)
25-
* `armv6m+thumb2` — ARM Cortex-M3+ with Thumb-2 support, ARMv7-M or later (Raspberry Pi Pico 2, STM32 with Cortex-M3/M4/M7/M33)
26-
* `riscv32` — 32-bit RISC-V
27-
* `riscv64` — 64-bit RISC-V
28-
* `wasm32` — WebAssembly (Emscripten)
21+
| Name | Description and example devices |
22+
|-----------------|------------------------------------|
23+
| `x86_64` | 64-bit x86 (Linux, macOS, FreeBSD) |
24+
| `aarch64` | 64-bit ARM (Linux, macOS) |
25+
| `arm32` | 32-bit ARM (Linux) |
26+
| `armv6m` | ARM Cortex-M0+ (Raspberry Pi Pico, STM32 with Cortex-M0/M0+) |
27+
| `armv6m+thumb2` | ARM Cortex-M3+ with Thumb-2 support, ARMv7-M or later (Raspberry Pi Pico 2, STM32 with Cortex-M3/M4/M7/M33) |
28+
| `riscv32` | 32-bit RISC-V (ESP32Cx, ESP32Hx, ESP32P4) |
29+
| `riscv64` | 64-bit RISC-V (Linux) |
30+
| `wasm32` | WebAssembly (nodeJS, browsers) |
2931

3032
### Requirements
3133

@@ -61,6 +63,7 @@ When enabled, each precompiled module includes an ELF object with DWARF debug se
6163
* Label symbols
6264
* Source file and line number mappings
6365
* Context structure type information for inspecting VM registers
66+
* Named variable locations (when compiled with `beam_debug_info`, OTP 28+)
6467

6568
## DWARF debug support
6669

@@ -97,7 +100,58 @@ The DWARF debug information includes location tracking for Erlang x registers. U
97100

98101
The x register values are displayed as raw tagged terms. For small integers, the value is `(term >> 4)`, so `x[0] = 143` means the integer `8` (since `143 = 8 << 4 | 0xf`).
99102

100-
When the JIT compiler has cached an x register in a native CPU register, the debugger reads it directly from the CPU register instead of memory — this is tracked automatically through DWARF location lists.
103+
When the JIT compiler has cached an x register in a native CPU register, the debugger reads it directly from the CPU register instead of memory, this is tracked automatically through DWARF location lists.
104+
105+
#### Named variable inspection
106+
107+
When an Erlang module is compiled with the `beam_debug_info` option (OTP 28+), the compiler emits `debug_line` opcodes that carry variable-to-register mappings. The JIT DWARF backend uses these to generate named variable entries, so the debugger can display Erlang variable names instead of raw register indices.
108+
109+
To enable this, add `beam_debug_info` as a compile attribute in your module:
110+
111+
```erlang
112+
-compile([beam_debug_info]).
113+
```
114+
115+
Or pass it as a compiler flag:
116+
117+
```shell
118+
$ erlc +beam_debug_info my_module.erl
119+
```
120+
121+
```{warning}
122+
The `beam_debug_info` option disables several compiler optimizations (constant folding,
123+
binary match optimization, etc.) to preserve variable-to-register mappings. Use it only
124+
for modules you intend to debug, not for production builds.
125+
```
126+
127+
With `beam_debug_info` enabled, the debugger shows named variables:
128+
129+
```
130+
$ lldb -- ./AtomVM my_module.avm
131+
(lldb) settings set plugin.jit-loader.gdb.enable on
132+
(lldb) breakpoint set -f my_module.erl -l 10
133+
(lldb) run
134+
```
135+
136+
```
137+
Process stopped
138+
* thread #1, stop reason = breakpoint 1.1
139+
frame #0: JIT`my_module:my_fun/1(ctx=0x...) at my_module.erl:10
140+
(lldb) frame variable N M
141+
(unsigned long) N = 687
142+
(unsigned long) M = 703
143+
```
144+
145+
The variables are displayed as raw tagged terms, just like x registers. Use `term_to_int()` to convert small integers:
146+
147+
```
148+
(lldb) print term_to_int(N)
149+
(avm_int_t) 42
150+
(lldb) print term_to_int(M)
151+
(avm_int_t) 43
152+
```
153+
154+
The variable locations are tracked through DWARF location lists, so the debugger shows the correct value at each point in the function. A variable that moves from one register to another (or goes out of scope) is handled automatically.
101155

102156
#### Backtraces
103157

@@ -111,12 +165,15 @@ When the JIT compiler has cached an x register in a native CPU register, the deb
111165

112166
#### Source line mapping
113167

114-
If the Erlang source was compiled with debug information and the BEAM Line chunk is present, the debugger maps JIT code addresses to source file and line numbers.
168+
If the Erlang source was compiled with debug information and the BEAM Line chunk is present, the debugger maps JIT code addresses to source file and line numbers. You can set breakpoints by file and line:
169+
170+
```
171+
(lldb) breakpoint set -f my_module.erl -l 15
172+
```
115173

116174
```{note}
117-
LLDB 19 (including Apple's system LLDB shipped with Xcode) has a regression in the JIT loader
118-
that causes hangs when resolving breakpoints in JIT-loaded modules. Use LLDB 20 or later.
119-
On macOS, install it from [MacPorts](https://www.macports.org/) (`port install lldb-20`) or
175+
LLDB 19 has a regression in the JIT loader that causes hangs when resolving breakpoints in JIT-loaded modules. Use LLDB 20 or later.
176+
On macOS, you can use lldb that ships with Xcode 26+ or install lldb 20 from [MacPorts](https://www.macports.org/) (`port install lldb-20`) or
120177
build from the [LLVM project source](https://github.com/llvm/llvm-project).
121178
```
122179

@@ -179,4 +236,3 @@ $ riscv64-elf-objdump -d module.elf
179236
| `AVM_DISABLE_JIT` | `ON` | Disable JIT compilation |
180237
| `AVM_DISABLE_JIT_DWARF` | `ON` | Disable DWARF debug information in JIT |
181238
| `AVM_JIT_TARGET_ARCH` | auto-detected | Target architecture (`x86_64`, `aarch64`, `arm32`, `armv6m`, `armv6m+thumb2`, `riscv32`, `riscv64`) |
182-
| `AVM_DISABLE_SMP` | `OFF` | Disable SMP support |

libs/estdlib/src/code_server.erl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ load(Module) ->
177177
ImportResolver = fun(Index) ->
178178
code_server:import_resolver(Module, Index)
179179
end,
180+
DebugInfoResolver = fun(_Index) -> false end,
180181
{StreamModule, Stream0} = jit:stream(jit_mmap_size(byte_size(Code))),
181182
{BackendModule, BackendState0} = jit:backend(StreamModule, Stream0),
182183
{LabelsCount, BackendState1} = jit:compile(
@@ -185,6 +186,7 @@ load(Module) ->
185186
LiteralResolver,
186187
TypeResolver,
187188
ImportResolver,
189+
DebugInfoResolver,
188190
BackendModule,
189191
BackendState0
190192
),

libs/jit/src/jit.erl

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
stream/1,
2525
backend/2,
2626
beam_chunk_header/3,
27-
compile/7,
27+
compile/8,
2828
decode_value64/1
2929
]).
3030

@@ -61,10 +61,12 @@
6161
MMod:dwarf_function(MSt, (State0#state.atom_resolver)(FunctionName), Arity)
6262
).
6363
-define(DWARF_LINE(MMod, MSt, Line), MMod:dwarf_line(MSt, Line)).
64+
-define(DWARF_VARIABLES(MMod, MSt, Vars), MMod:dwarf_variables(MSt, Vars)).
6465
-else.
6566
-define(DWARF_LABEL(_MMod, MSt, _Label), MSt).
6667
-define(DWARF_FUNCTION(_MMod, MSt, _FunctionName, _Arity), MSt).
6768
-define(DWARF_LINE(_MMod, MSt, _Line), MSt).
69+
-define(DWARF_VARIABLES(_MMod, MSt, _Vars), MSt).
6870
-endif.
6971

7072
-define(BOXED_FUN_SIZE, 3).
@@ -91,6 +93,9 @@
9193
literal_resolver :: fun((integer()) -> any()),
9294
type_resolver :: fun((integer()) -> any()),
9395
import_resolver :: fun((integer()) -> {atom(), atom(), non_neg_integer()}),
96+
debug_info_resolver :: fun(
97+
(integer()) -> [{binary(), {x, integer()} | {y, integer()} | {value, any()}}] | false
98+
),
9499
tail_cache :: tail_cache()
95100
}).
96101

@@ -132,6 +137,7 @@ compile(
132137
LiteralResolver,
133138
TypeResolver,
134139
ImportResolver,
140+
DebugInfoResolver,
135141
MMod,
136142
MSt0
137143
) when OpcodeMax =< ?OPCODE_MAX ->
@@ -143,6 +149,7 @@ compile(
143149
literal_resolver = LiteralResolver,
144150
type_resolver = TypeResolver,
145151
import_resolver = ImportResolver,
152+
debug_info_resolver = DebugInfoResolver,
146153
tail_cache =
147154
case erlang:function_exported(MMod, supports_tail_cache, 0) of
148155
true ->
@@ -165,11 +172,21 @@ compile(
165172
_LiteralResolver,
166173
_TypeResolver,
167174
_ImportResolver,
175+
_DebugInfoResolver,
168176
_MMod,
169177
_MSt
170178
) ->
171179
error(badarg, [OpcodeMax]);
172-
compile(CodeChunk, _AtomResolver, _LiteralResolver, _TypeResolver, _ImportResolver, _MMod, _MSt) ->
180+
compile(
181+
CodeChunk,
182+
_AtomResolver,
183+
_LiteralResolver,
184+
_TypeResolver,
185+
_ImportResolver,
186+
_DebugInfoResolver,
187+
_MMod,
188+
_MSt
189+
) ->
173190
error(badarg, [CodeChunk]).
174191

175192
% 1
@@ -2455,14 +2472,19 @@ first_pass(<<?OP_CALL_FUN2, Rest0/binary>>, MMod, MSt0, State0) ->
24552472
% We ignore Tag (could be literal 0 or atom unsafe)
24562473
MSt2 = MMod:free_native_registers(MSt1, [Tag]),
24572474
MSt3 = MMod:decrement_reductions_and_maybe_schedule_next(MSt2),
2458-
State1a = record_continuation_line(MMod, MSt3, State0),
2459-
{MSt4, Fun, Rest3} = decode_typed_compact_term(Rest2, MMod, MSt3, State1a),
2475+
State1 = record_continuation_line(MMod, MSt3, State0),
2476+
{MSt4, Fun, Rest3} = decode_typed_compact_term(Rest2, MMod, MSt3, State1),
24602477
{MSt5, Reg} = verify_is_function(Fun, MMod, MSt4),
24612478
MSt6 = MMod:call_primitive_with_cp(MSt5, ?PRIM_CALL_FUN, [
24622479
ctx, jit_state, offset, {free, Reg}, ArgsCount
24632480
]),
24642481
?ASSERT_ALL_NATIVE_FREE(MSt6),
2465-
first_pass(Rest3, MMod, MSt6, State1a);
2482+
first_pass(Rest3, MMod, MSt6, State1);
2483+
% 179
2484+
first_pass(<<?OP_NIF_START, Rest0/binary>>, MMod, MSt0, State0) ->
2485+
?ASSERT_ALL_NATIVE_FREE(MSt0),
2486+
?TRACE("OP_NIF_START\n", []),
2487+
first_pass(Rest0, MMod, MSt0, State0);
24662488
% 180
24672489
first_pass(<<?OP_BADRECORD, Rest0/binary>>, MMod, MSt0, State0) ->
24682490
?ASSERT_ALL_NATIVE_FREE(MSt0),
@@ -2571,6 +2593,38 @@ first_pass(<<?OP_BS_MATCH, Rest0/binary>>, MMod, MSt0, State0) ->
25712593
MSt9 = MMod:free_native_registers(MSt8, [BSBinaryReg, NewBSOffsetReg, MatchStateReg2]),
25722594
?ASSERT_ALL_NATIVE_FREE(MSt9),
25732595
first_pass(Rest4, MMod, MSt9, State0);
2596+
% 183
2597+
first_pass(<<?OP_EXECUTABLE_LINE, Rest0/binary>>, MMod, MSt0, State0) ->
2598+
?ASSERT_ALL_NATIVE_FREE(MSt0),
2599+
{MSt1, {literal, _Location}, Rest1} = decode_compact_term(Rest0, MMod, MSt0, State0),
2600+
{_LineNum, Rest2} = decode_literal(Rest1),
2601+
?TRACE("OP_EXECUTABLE_LINE ~p, ~p\n", [_Location, _LineNum]),
2602+
MSt2 = ?DWARF_LINE(MMod, MSt1, _Location),
2603+
?ASSERT_ALL_NATIVE_FREE(MSt2),
2604+
first_pass(Rest2, MMod, MSt2, State0);
2605+
% 184
2606+
first_pass(
2607+
<<?OP_DEBUG_LINE, Rest0/binary>>,
2608+
MMod,
2609+
MSt0,
2610+
#state{debug_info_resolver = DebugInfoResolver} = State0
2611+
) ->
2612+
?ASSERT_ALL_NATIVE_FREE(MSt0),
2613+
Rest1 = skip_compact_term(Rest0),
2614+
{MSt1, {literal, _Location}, Rest2} = decode_compact_term(Rest1, MMod, MSt0, State0),
2615+
{Index, Rest3} = decode_literal(Rest2),
2616+
{_Live, Rest4} = decode_literal(Rest3),
2617+
?TRACE("OP_DEBUG_LINE ~p, ~p, ~p\n", [_Location, Index, _Live]),
2618+
MSt2 = ?DWARF_LINE(MMod, MSt1, _Location),
2619+
MSt3 =
2620+
case DebugInfoResolver(Index) of
2621+
false ->
2622+
MSt2;
2623+
_VarMappings ->
2624+
?DWARF_VARIABLES(MMod, MSt2, _VarMappings)
2625+
end,
2626+
?ASSERT_ALL_NATIVE_FREE(MSt3),
2627+
first_pass(Rest4, MMod, MSt3, State0);
25742628
% 185
25752629
first_pass(<<?OP_BIF3, Rest0/binary>>, MMod, MSt0, State0) ->
25762630
?ASSERT_ALL_NATIVE_FREE(MSt0),

libs/jit/src/jit_aarch64.erl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
dwarf_label/2,
8484
dwarf_function/3,
8585
dwarf_line/2,
86+
dwarf_variables/2,
8687
dwarf_ctx_register/0
8788
]).
8889
-endif.

libs/jit/src/jit_arm32.erl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
dwarf_label/2,
8282
dwarf_function/3,
8383
dwarf_line/2,
84+
dwarf_variables/2,
8485
dwarf_ctx_register/0
8586
]).
8687
-endif.

libs/jit/src/jit_armv6m.erl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
dwarf_label/2,
8282
dwarf_function/3,
8383
dwarf_line/2,
84+
dwarf_variables/2,
8485
dwarf_ctx_register/0
8586
]).
8687
-endif.

libs/jit/src/jit_backend_dwarf_impl.hrl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,8 @@ dwarf_function(#state{stream = Stream0} = State, FunctionName, Arity) ->
3737
Stream1 = jit_dwarf:function(Stream0, FunctionName, Arity),
3838
State#state{stream = Stream1}.
3939

40+
dwarf_variables(#state{stream = Stream0} = State, VarMappings) ->
41+
Stream1 = jit_dwarf:variables(Stream0, VarMappings),
42+
State#state{stream = Stream1}.
43+
4044
-endif.

0 commit comments

Comments
 (0)