BeamSpy
objdump, strings, and readelf for the BEAM VM
BeamSpy is a BEAM file analysis tool that provides commands for inspecting compiled BEAM modules.
Features
- Atom extraction - Extract and filter the atom table
- Export/Import analysis - List module interfaces
- Bytecode disassembly - Human-readable assembly with opcode categorization
- Source interleaving - See source code alongside bytecode
- Call graph generation - Build DOT graphs of function calls
- Chunk inspection - Examine raw BEAM file structure
- Themeable output - Syntax highlighting with customizable themes
Installation
Add beam_spy to your list of dependencies in mix.exs:
def deps do
[
{:beam_spy, "~> 0.1.0"}
]
endAs a CLI tool
git clone https://github.com/QuinnWilton/beam_spy
cd beam_spy
mix deps.get && mix escript.build
This creates a beam_spy executable in the project directory.
Quick Start
# Show module metadata
./beam_spy info Enum
# List exported functions, filtered by name
./beam_spy exports lists --filter=map
# Disassemble with source interleaving
./beam_spy disasm GenServer -f "handle_call*" --source
# Generate a call graph as SVG
./beam_spy callgraph Enum --format=dot | dot -Tsvg -o graph.svgModule Resolution
All commands accept either a file path or a module name:
# Module names (resolved automatically via code path)
./beam_spy info Enum # Elixir module
./beam_spy info lists # Erlang module
./beam_spy info GenServer # Standard library
# Direct file paths
./beam_spy info ./my_module.beam
./beam_spy info /path/to/module.beamCommands Reference
| Command | Purpose |
|---|---|
atoms | Extract atom table |
exports | List exported functions |
imports | List imported functions |
info | Show module metadata |
chunks | List/inspect BEAM chunks |
disasm | Disassemble bytecode |
callgraph | Build function call graph |
atoms
Extract the atom table from a BEAM file.
./beam_spy atoms Enum
./beam_spy atoms Enum --filter=map
./beam_spy atoms Enum --format=jsonOptions:
| Option | Short | Description |
|--------|-------|-------------|
| --format | -o | Output format: text, json |
| --filter | -F | Filter pattern (substring, re:regex, or glob:pattern) |
exports
List exported functions from a module.
./beam_spy exports lists
./beam_spy exports Enum --filter=map
./beam_spy exports Enum --plain # One per line (for piping)Options:
| Option | Short | Description |
|--------|-------|-------------|
| --format | -o | Output format: text, json |
| --filter | -F | Filter pattern |
| --plain | | Output plain text (one per line) |
imports
List imported functions from a module.
./beam_spy imports GenServer
./beam_spy imports GenServer --group # Group by moduleOptions:
| Option | Short | Description |
|--------|-------|-------------|
| --format | -o | Output format: text, json |
| --filter | -F | Filter pattern |
| --group | -g | Group imports by module |
info
Show module metadata.
./beam_spy info EnumOutput includes:
- Module name and source file
- Compile time and OTP version
- MD5 checksum and file size
- Count of chunks, exports, imports, atoms
chunks
List BEAM file chunks or dump raw chunk data.
./beam_spy chunks Enum # List all chunks
./beam_spy chunks Enum --raw AtU8 # Hex dump of atom tableOptions:
| Option | Short | Description |
|--------|-------|-------------|
| --format | -o | Output format: text, json |
| --raw | -r | Hex dump of specific chunk (e.g., AtU8) |
Common chunk IDs:
| Chunk | Description |
|-------|-------------|
| AtU8 | Atom table (UTF-8) |
| Code | Bytecode |
| StrT | String table |
| ImpT | Import table |
| ExpT | Export table |
| FunT | Lambda/fun table |
| LitT | Literal table (compressed) |
| Dbgi | Debug info |
| Docs | Documentation |
| Line | Line number table |
disasm
Disassemble BEAM bytecode into human-readable assembly.
./beam_spy disasm lists -f "reverse/1"
./beam_spy disasm Enum -f "map*" --source
./beam_spy disasm GenServer --format=jsonOptions:
| Option | Short | Description |
|--------|-------|-------------|
| --format | -o | Output format: text, json |
| --function | -f | Filter to specific function (supports globs) |
| --source | -S | Interleave source code with disassembly |
Function filter patterns:
map/2- Exact match (name and arity)handle_*- Glob patternmap- Substring match
callgraph
Build a function call graph.
./beam_spy callgraph Enum
./beam_spy callgraph Enum --format=dot | dot -Tsvg -o graph.svg
./beam_spy callgraph Enum --format=jsonOptions:
| Option | Short | Description |
|--------|-------|-------------|
| --format | -o | Output format: text, json, dot |
Assembler Syntax Reference
The disasm command outputs BEAM bytecode in a human-readable assembler syntax. This section documents the syntax so you can understand the output.
Registers
BEAM uses several register types:
| Type | Syntax | Purpose |
|---|---|---|
| X registers | x(0), x(1), ... | Arguments, return values, and temporaries |
| Y registers | y(0), y(1), ... | Stack frame slots (preserved across calls) |
| Float registers | fr(0), fr(1), ... | Floating-point operations |
| Labels | f(19), f(23), ... | Branch targets (failure labels) |
X registers are the primary working registers. x(0) through x(N-1) hold function arguments on entry. x(0) holds the return value.
Y registers are stack slots allocated with allocate. They persist across function calls and must be deallocated before returning.
Atoms
Atoms are displayed with Elixir-style colon prefix:
:ok
:error
:handle_call
Enum # Module atoms may omit the colonExternal Function References
External function calls use the format :module:function/arity:
:erlang:error/1
:lists:reverse/2
Enum:map/2Lists
Lists use square bracket notation:
[] # Empty list
[x(0), x(1)] # List of registers
[:ok, x(0)] # Mixed listMap Operations
Map instructions use special syntax:
get_map_elements - Extract map values:
get_map_elements f(425), x(0), [step => x(5), last => x(4), first => x(3)]put_map_assoc/put_map_exact - Create/update maps:
put_map_assoc f(0), x(1), x(2), 2, %{key: x(3), other: x(4)}Allocation Tuples
Stack allocation uses compact notation:
alloc(w:2) # Allocate 2 words
alloc(w:1, fn:1) # 1 word + 1 fun slot
alloc(w:2, fl:1) # 2 words + 1 float slotLabels
Labels mark branch targets and function entry points:
label 19:
func_info :lists, :reverse, 1
label 20: # Entry point (from function header)
test :is_nonempty_list, f(23), [x(0)]
The number in f(x) in test instructions refers to the label to jump to on failure.
Opcode Categories
BeamSpy categorizes opcodes for syntax highlighting:
| Category | Opcodes | Description |
|---|---|---|
| call | call, call_ext, call_only, call_fun2, apply, bif1, gc_bif2 | Function invocation |
| control | label, jump, select_val, is_* tests | Control flow |
| data | move, swap, get_list, put_list, get_tuple_element | Data manipulation |
| stack | allocate, deallocate, test_heap, trim | Stack management |
| return | return | Function return |
| exception | try, catch, raise, build_stacktrace | Exception handling |
| error | func_info, badmatch, case_end | Error generation |
| message | send, receive, wait, timeout | Message passing |
| binary | bs_get_*, bs_match, bs_create_bin | Binary operations |
| float | fadd, fsub, fmul, fdiv, fconv | Floating-point |
| meta | line, executable_line | Metadata |
Common Instruction Patterns
Function entry:
label 19:
func_info :lists, :reverse, 1 # Error info (called on pattern match failure)
label 20: # Actual entry pointType tests:
test :is_nonempty_list, f(23), [x(0)] # Test x(0), jump to label 23 on failure
test :is_nil, f(21), [x(2)]
test :is_eq_exact, f(425), [x(2), Range]List operations:
get_list x(0), x(1), x(2) # x(1) = hd(x(0)), x(2) = tl(x(0))
put_list x(1), [], x(1) # x(1) = [x(1) | []]
put_list x(0), x(1), x(0) # x(0) = [x(0) | x(1)]Stack management:
allocate 4, 6 # Allocate 4 Y slots, preserve 6 X regs
test_heap 4, 2 # Ensure 4 words heap space, preserve 2 X regs
deallocate 1 # Free 1 Y slot
trim 3, 1 # Remove 3 Y slots, keep 1Function calls:
call 2, {Enum, :map_range, 4} # Local call
call_ext 3, :Elixir.Enum:reduce/3 # External call
call_only 2, {Enum, :"-map/2-...", 2} # Tail call (local)
call_ext_only 2, :lists:reverse/2 # Tail call (external)Example: lists:reverse/1
function reverse/1 (entry: 20)
────────────────────────────────────────────────────────
line 15
label 19:
func_info :lists, :reverse, 1
label 20:
test :is_nonempty_list, f(23), [x(0)]
get_list x(0), x(1), x(2)
test :is_nonempty_list, f(22), [x(2)]
get_list x(2), x(0), x(2)
test :is_nil, f(21), [x(2)]
test_heap 4, 2
put_list x(1), [], x(1)
put_list x(0), x(1), x(0)
return
label 21:
test_heap 4, 3
put_list x(1), [], x(1)
put_list x(0), x(1), x(1)
move x(2), x(0)
line 16
call_ext_only 2, :lists:reverse/2
label 22:
test :is_nil, f(19), [x(2)]
return
label 23:
test :is_nil, f(19), [x(0)]
returnThis shows the optimized implementation:
- Labels 22, 23 handle edge cases (empty list, single element)
- Label 20 is the main entry point
- The function inlines the first two iterations of the loop for common cases
-
Label 21 falls through to
reverse/2for lists longer than 2 elements
Source Interleaving
With --source, BeamSpy shows source code alongside bytecode:
function map/2 (entry: 420)
────────────────────────────────────────────────────────
1687 │ def map(enumerable, fun)
│ label 419:
│ func_info Enum, :map, 2
│ label 420:
│ test :is_list, f(421), [x(0)]
│ call_only 2, {Enum, :"-map/2-lists^map/1-1-", 2}Format:
-
Line numbers appear in the left margin with a
│border - Source lines are shown in italic (in supported terminals)
- Bytecode is indented under the corresponding source
- Line numbers are clickable hyperlinks in supported terminals
Distant references:
When bytecode references lines far from the current function (e.g., inlined code), BeamSpy shows a compact reference:
→ map_range (line 4539)
│ call_fun 1
This indicates the bytecode came from the map_range function at line 4539.
Common Options
These options are available on all subcommands:
| Option | Short | Description |
|---|---|---|
--theme | -t |
Color theme to use (default: default) |
--paging |
Paging mode: auto, always, never |
The --list-themes flag is available at the top level to list available themes.
Environment Variables
NO_COLOR- Disable all color output when set (any value)
Output Formats
Most commands support multiple output formats:
- text (default) - Human-readable, styled with colors
- json - Machine-readable JSON
- dot - GraphViz DOT format (callgraph only)
./beam_spy exports Enum --format=json | jq '.[] | select(.arity == 2)'
./beam_spy callgraph Enum --format=dot | dot -Tpng -o graph.pngThemes
BeamSpy supports color themes defined in TOML format. Themes are stored in priv/themes/.
# List available themes
./beam_spy --list-themes
# Use a specific theme
./beam_spy disasm Enum --theme=monokaiTheme files define colors for:
- UI elements (headers, borders, keys)
- Data types (atoms, modules, functions, numbers)
- Opcode categories (call, control, data, etc.)
- Register types (x, y, fr)
Development
Built with AI assistance. See CLAUDE.md for contribution guidelines.
License
MIT