JIT Compilation
AtomVM includes an optional JIT (Just-In-Time) compiler that translates BEAM bytecodes into native machine code for significantly faster execution.
Overview
The JIT compiler works at module load time: when a module is loaded, its BEAM bytecodes are compiled to native code for the target architecture. The compiled native code is stored alongside the BEAM bytecodes and used for execution.
Modules can also be ahead-of-time compiled: the native code is generated at build time and embedded into the .beam file as a avmN chunk. At load time, the precompiled native code is used directly without any compilation step.
Supported architectures
The JIT compiler supports the following target architectures:
Name |
Description and example devices |
|---|---|
|
64-bit x86 (Linux, macOS, FreeBSD) |
|
64-bit ARM (Linux, macOS) |
|
32-bit ARM (Linux) |
|
ARM Cortex-M0+ (Raspberry Pi Pico, STM32 with Cortex-M0/M0+) |
|
ARM Cortex-M3+ with Thumb-2 support, ARMv7-M or later (Raspberry Pi Pico 2, STM32 with Cortex-M3/M4/M7/M33) |
|
32-bit RISC-V (ESP32Cx, ESP32Hx, ESP32P4) |
|
64-bit RISC-V (Linux) |
|
WebAssembly (nodeJS, browsers) |
Requirements
Erlang/OTP 28 or later is required to run the JIT compiler at load time (ahead-of-time compiled modules can be executed with OTP 26+).
Building with JIT support
Generic UNIX
To enable JIT compilation, pass -DAVM_DISABLE_JIT=OFF to CMake:
$ mkdir build
$ cd build
$ cmake -DAVM_DISABLE_JIT=OFF ..
$ make -j 8
The target architecture is auto-detected based on the host platform. To cross-compile or override, use -DAVM_JIT_TARGET_ARCH=<arch>.
DWARF debug information
DWARF debug information support can be enabled to allow debugging JIT-compiled code with LLDB or GDB. It is disabled by default and can be enabled with:
$ cmake -DAVM_DISABLE_JIT=OFF -DAVM_DISABLE_JIT_DWARF=OFF ..
When enabled, each precompiled module includes an ELF object with DWARF debug sections containing:
Function symbols (
module:function/arity)BEAM opcode location symbols
Label symbols
Source file and line number mappings
Context structure type information for inspecting VM registers
Named variable locations (when compiled with
beam_debug_info, OTP 28+)
DWARF debug support
Debugging with LLDB
When DWARF support is enabled and plugin.jit-loader.gdb.enable is turned on, LLDB can set breakpoints on JIT-compiled Erlang functions by name, inspect VM registers, and show backtraces through JIT code.
Setting breakpoints
$ lldb -- tests/test-erlang add
(lldb) settings set plugin.jit-loader.gdb.enable on
(lldb) breakpoint set -n 'add:add/2'
(lldb) run
When the breakpoint is hit, LLDB shows the function name and the Context pointer:
Process stopped
* thread #1, stop reason = breakpoint 1.1
frame #0: 0x00000001002440fa JIT(0x...)`add:add/2(ctx=0x00007f...)
Inspecting VM registers
The DWARF debug information includes location tracking for Erlang x registers. Use frame variable to see the VM state:
(lldb) frame variable
(Context *) ctx = 0x00007fc066804280
(unsigned long) x[0] = 143
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).
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.
Named variable inspection
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.
To enable this, add beam_debug_info as a compile attribute in your module:
-compile([beam_debug_info]).
Or pass it as a compiler flag:
$ erlc +beam_debug_info my_module.erl
Warning
The beam_debug_info option disables several compiler optimizations (constant folding,
binary match optimization, etc.) to preserve variable-to-register mappings. Use it only
for modules you intend to debug, not for production builds.
With beam_debug_info enabled, the debugger shows named variables:
$ lldb -- ./AtomVM my_module.avm
(lldb) settings set plugin.jit-loader.gdb.enable on
(lldb) breakpoint set -f my_module.erl -l 10
(lldb) run
Process stopped
* thread #1, stop reason = breakpoint 1.1
frame #0: JIT`my_module:my_fun/1(ctx=0x...) at my_module.erl:10
(lldb) frame variable N M
(unsigned long) N = 687
(unsigned long) M = 703
The variables are displayed as raw tagged terms, just like x registers. Use term_to_int() to convert small integers:
(lldb) print term_to_int(N)
(avm_int_t) 42
(lldb) print term_to_int(M)
(avm_int_t) 43
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.
Backtraces
(lldb) bt
* frame #0: add:add/2(ctx=0x...)
frame #1: scheduler_entry_point at opcodesswitch.h
frame #2: context_execute_loop at opcodesswitch.h
frame #3: main at test.c
Source line mapping
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:
(lldb) breakpoint set -f my_module.erl -l 15
Note
Upstream LLVM LLDB 19 and Apple LLDB versions earlier than lldb-2100 (shipped in Xcode 26.4 and later) hang when resolving pending source-line breakpoints against JIT-emitted DWARF.
Workarounds without changing lldb:
Set breakpoints by symbol name (
breakpoint set -n 'mod:fun/N') instead of file/lineDefer source-line breakpoints until after the relevant module has been JIT-registered (e.g. break first on a symbol, then add the source-line breakpoint, then continue).
For a fresh lldb:
On macOS 26, the LLDB shipped with the CommandLineTools is already at
lldb-2100(independent of the active Xcode), so a quick fix is to invoke it directly:
$ /Library/Developer/CommandLineTools/usr/bin/lldb -- ...
Alternatively, switch the active Xcode to 26.4 or later (sudo xcode-select -s /Applications/Xcode_26.4.app).
On any platform, install upstream LLDB 20 or later with
sudo port install lldb-20from MacPorts,brew install llvm(Homebrew), or build from the LLVM project source. A self-signedlldbbinary on macOS needs a debugger entitlement.
lldb --version reports the version: lldb-1703.x and earlier are affected; lldb-2100.x and upstream LLDB 20+ are fixed.
### Disassembling precompiled modules
Precompiled `.beam` files contain an ELF object with the native code and symbol table. You can extract and disassemble it to inspect the generated code.
When DWARF is enabled, the ELF is embedded in the `avmN` chunk of the `.beam` file. At runtime, `jit_dwarf:elf/2` produces the ELF binary that can be written to a file for offline analysis:
```erlang
{ok, _TextOffset, ElfBinary} = jit_dwarf:elf(DwarfState, NativeCode),
file:write_file("module.elf", ElfBinary).
The resulting ELF file can be disassembled with standard tools:
$ objdump -d module.elf
This will show the disassembly with function names like module:function/arity, making it easy to correlate the generated machine code with the original Erlang source.
Extracting ELF from a precompiled .beam file
The ELF object is stored in the avmN chunk of the .beam file, after a small header. You can extract it from the Erlang shell using beam_lib:
{ok, {_, [{_, ChunkData}]}} = beam_lib:chunks("module.beam", ["avmN"]),
<<InfoSize:32/big, _Info:InfoSize/binary, ElfData/binary>> = ChunkData,
<<16#7f, "ELF", _/binary>> = ElfData, % verify ELF magic
file:write_file("module.elf", ElfData).
Then disassemble with symbols:
# x86_64
$ objdump -d module.elf
# aarch64
$ aarch64-elf-objdump -d module.elf
# arm32
$ arm-elf-objdump -d module.elf
# armv6m
$ arm-elf-objdump -d --disassembler-options=force-thumb module.elf
# riscv32
$ riscv32-elf-objdump -d module.elf
# riscv64
$ riscv64-elf-objdump -d module.elf
CMake options reference
Option |
Default |
Description |
|---|---|---|
|
|
Disable JIT compilation |
|
|
Disable DWARF debug information in JIT |
|
auto-detected |
Target architecture ( |