Skip to content

Commit 0fd10c9

Browse files
committed
Implement nif_start (179), executable_line (183) and debug_line (184)
- `nif_start` is implemented as no-op, preventing crash - `executable_line` is implemented on jit+dwarf for additional line information for debuggers - `debug_line` is implemented on jit+dwarf for additional line information and variable mapping for debuggers Also fix DWARF generation by emitting DW_LNS_set_prologue_end which lldb needs to resolve breakpoints at function lines with `executable_line`. Also clean up `jit_dwarf.erl` Signed-off-by: Paul Guyot <pguyot@kallisys.net>
1 parent 8ea71cc commit 0fd10c9

23 files changed

Lines changed: 1020 additions & 235 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
@@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
### Added
1010
- Added Erlang distribution over serial (uart)
11+
- Added support for `nif_start`, `executable_line` and `debug_line` opcodes
12+
- Added named variable debugging support in DWARF when modules are compiled with `beam_debug_info`
1113

1214
### Fixed
1315
- Stop using deprecated `term_from_int32` on STM32 platform

doc/src/jit.md

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ 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
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) |
2830

2931
### Requirements
3032

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

6467
## DWARF debug support
6568

@@ -96,7 +99,58 @@ The DWARF debug information includes location tracking for Erlang x registers. U
9699

97100
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`).
98101

99-
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.
102+
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+
104+
#### Named variable inspection
105+
106+
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.
107+
108+
To enable this, add `beam_debug_info` as a compile attribute in your module:
109+
110+
```erlang
111+
-compile([beam_debug_info]).
112+
```
113+
114+
Or pass it as a compiler flag:
115+
116+
```shell
117+
$ erlc +beam_debug_info my_module.erl
118+
```
119+
120+
```{warning}
121+
The `beam_debug_info` option disables several compiler optimizations (constant folding,
122+
binary match optimization, etc.) to preserve variable-to-register mappings. Use it only
123+
for modules you intend to debug, not for production builds.
124+
```
125+
126+
With `beam_debug_info` enabled, the debugger shows named variables:
127+
128+
```
129+
$ lldb -- ./AtomVM my_module.avm
130+
(lldb) settings set plugin.jit-loader.gdb.enable on
131+
(lldb) breakpoint set -f my_module.erl -l 10
132+
(lldb) run
133+
```
134+
135+
```
136+
Process stopped
137+
* thread #1, stop reason = breakpoint 1.1
138+
frame #0: JIT`my_module:my_fun/1(ctx=0x...) at my_module.erl:10
139+
(lldb) frame variable N M
140+
(unsigned long) N = 687
141+
(unsigned long) M = 703
142+
```
143+
144+
The variables are displayed as raw tagged terms, just like x registers. Use `term_to_int()` to convert small integers:
145+
146+
```
147+
(lldb) print term_to_int(N)
148+
(avm_int_t) 42
149+
(lldb) print term_to_int(M)
150+
(avm_int_t) 43
151+
```
152+
153+
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.
100154

101155
#### Backtraces
102156

@@ -110,12 +164,15 @@ When the JIT compiler has cached an x register in a native CPU register, the deb
110164

111165
#### Source line mapping
112166

113-
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.
167+
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:
168+
169+
```
170+
(lldb) breakpoint set -f my_module.erl -l 15
171+
```
114172

115173
```{note}
116-
LLDB 19 (including Apple's system LLDB shipped with Xcode) has a regression in the JIT loader
117-
that causes hangs when resolving breakpoints in JIT-loaded modules. Use LLDB 20 or later.
118-
On macOS, install it from [MacPorts](https://www.macports.org/) (`port install lldb-20`) or
174+
LLDB 19 has a regression in the JIT loader that causes hangs when resolving breakpoints in JIT-loaded modules. Use LLDB 20 or later.
175+
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
119176
build from the [LLVM project source](https://github.com/llvm/llvm-project).
120177
```
121178

@@ -178,4 +235,3 @@ $ riscv64-elf-objdump -d module.elf
178235
| `AVM_DISABLE_JIT` | `ON` | Disable JIT compilation |
179236
| `AVM_DISABLE_JIT_DWARF` | `ON` | Disable DWARF debug information in JIT |
180237
| `AVM_JIT_TARGET_ARCH` | auto-detected | Target architecture (`x86_64`, `aarch64`, `arm32`, `armv6m`, `armv6m+thumb2`, `riscv32`, `riscv64`) |
181-
| `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: 56 additions & 2 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).
@@ -90,6 +92,9 @@
9092
literal_resolver :: fun((integer()) -> any()),
9193
type_resolver :: fun((integer()) -> any()),
9294
import_resolver :: fun((integer()) -> {atom(), atom(), non_neg_integer()}),
95+
debug_info_resolver :: fun(
96+
(integer()) -> [{binary(), {x, integer()} | {y, integer()} | {value, any()}}] | false
97+
),
9398
tail_cache :: [{tuple(), non_neg_integer()}]
9499
}).
95100

@@ -124,6 +129,7 @@ compile(
124129
LiteralResolver,
125130
TypeResolver,
126131
ImportResolver,
132+
DebugInfoResolver,
127133
MMod,
128134
MSt0
129135
) when OpcodeMax =< ?OPCODE_MAX ->
@@ -134,6 +140,7 @@ compile(
134140
literal_resolver = LiteralResolver,
135141
type_resolver = TypeResolver,
136142
import_resolver = ImportResolver,
143+
debug_info_resolver = DebugInfoResolver,
137144
tail_cache = []
138145
},
139146
MSt1 = MMod:jump_table(MSt0, LabelsCount),
@@ -147,11 +154,21 @@ compile(
147154
_LiteralResolver,
148155
_TypeResolver,
149156
_ImportResolver,
157+
_DebugInfoResolver,
150158
_MMod,
151159
_MSt
152160
) ->
153161
error(badarg, [OpcodeMax]);
154-
compile(CodeChunk, _AtomResolver, _LiteralResolver, _TypeResolver, _ImportResolver, _MMod, _MSt) ->
162+
compile(
163+
CodeChunk,
164+
_AtomResolver,
165+
_LiteralResolver,
166+
_TypeResolver,
167+
_ImportResolver,
168+
_DebugInfoResolver,
169+
_MMod,
170+
_MSt
171+
) ->
155172
error(badarg, [CodeChunk]).
156173

157174
% 1
@@ -2433,6 +2450,11 @@ first_pass(<<?OP_CALL_FUN2, Rest0/binary>>, MMod, MSt0, State0) ->
24332450
]),
24342451
?ASSERT_ALL_NATIVE_FREE(MSt6),
24352452
first_pass(Rest3, MMod, MSt6, State0);
2453+
% 179
2454+
first_pass(<<?OP_NIF_START, Rest0/binary>>, MMod, MSt0, State0) ->
2455+
?ASSERT_ALL_NATIVE_FREE(MSt0),
2456+
?TRACE("OP_NIF_START\n", []),
2457+
first_pass(Rest0, MMod, MSt0, State0);
24362458
% 180
24372459
first_pass(<<?OP_BADRECORD, Rest0/binary>>, MMod, MSt0, State0) ->
24382460
?ASSERT_ALL_NATIVE_FREE(MSt0),
@@ -2541,6 +2563,38 @@ first_pass(<<?OP_BS_MATCH, Rest0/binary>>, MMod, MSt0, State0) ->
25412563
MSt9 = MMod:free_native_registers(MSt8, [BSBinaryReg, NewBSOffsetReg, MatchStateReg2]),
25422564
?ASSERT_ALL_NATIVE_FREE(MSt9),
25432565
first_pass(Rest4, MMod, MSt9, State0);
2566+
% 183
2567+
first_pass(<<?OP_EXECUTABLE_LINE, Rest0/binary>>, MMod, MSt0, State0) ->
2568+
?ASSERT_ALL_NATIVE_FREE(MSt0),
2569+
{MSt1, {literal, _Location}, Rest1} = decode_compact_term(Rest0, MMod, MSt0, State0),
2570+
{_LineNum, Rest2} = decode_literal(Rest1),
2571+
?TRACE("OP_EXECUTABLE_LINE ~p, ~p\n", [_Location, _LineNum]),
2572+
MSt2 = ?DWARF_LINE(MMod, MSt1, _Location),
2573+
?ASSERT_ALL_NATIVE_FREE(MSt2),
2574+
first_pass(Rest2, MMod, MSt2, State0);
2575+
% 184
2576+
first_pass(
2577+
<<?OP_DEBUG_LINE, Rest0/binary>>,
2578+
MMod,
2579+
MSt0,
2580+
#state{debug_info_resolver = DebugInfoResolver} = State0
2581+
) ->
2582+
?ASSERT_ALL_NATIVE_FREE(MSt0),
2583+
Rest1 = skip_compact_term(Rest0),
2584+
{MSt1, {literal, _Location}, Rest2} = decode_compact_term(Rest1, MMod, MSt0, State0),
2585+
{Index, Rest3} = decode_literal(Rest2),
2586+
{_Live, Rest4} = decode_literal(Rest3),
2587+
?TRACE("OP_DEBUG_LINE ~p, ~p, ~p\n", [_Location, Index, _Live]),
2588+
MSt2 = ?DWARF_LINE(MMod, MSt1, _Location),
2589+
MSt3 =
2590+
case DebugInfoResolver(Index) of
2591+
false ->
2592+
MSt2;
2593+
_VarMappings ->
2594+
?DWARF_VARIABLES(MMod, MSt2, _VarMappings)
2595+
end,
2596+
?ASSERT_ALL_NATIVE_FREE(MSt3),
2597+
first_pass(Rest4, MMod, MSt3, State0);
25442598
% 185
25452599
first_pass(<<?OP_BIF3, Rest0/binary>>, MMod, MSt0, State0) ->
25462600
?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)