Skip to content

Commit f4eb4e6

Browse files
fadushinbettio
authored andcommitted
Merge pull request #418 from fadushin/stacktrace
Implement stack traces This PR adds support for stacktraces, both in the display of crash reports, as well as programmatic access to stacktrace data as terms in catch clauses. Stacktrace data is represented as a list of tuples, each of which represents a stack “frame”. Each tuple is of the form: ``` [{Module :: module(), Function :: atom(), Arity :: non_neg_integer(), AuxData :: aux_data()}] ``` where `aux_data()` is a (possibly empty) properties list containing the following elements: ``` [{file, File :: string(), line, Line :: pos_integer()}] ``` Stack frames are ordered from the frame “closest“ to the point of failure (the “top” of the stack) to the frame furthest from the point of failure (the “bottom” of the stack). Stack frames will contain file and line information in the `AuxData` list if the BEAM files (typically embedded in AVM files) include `<<“Line”>>` chunks generated by the compiler. Otherwise, the `AuxData` will be empty. Note that adding line information to BEAM files not only increases the size of BEAM files in storage, but calculation of file and line information can have a non-negligible impact on memory usage. Memory-sensitive applications should consider not including line information in BEAM files. Addresses issue #254. 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
1 parent af8e2f6 commit f4eb4e6

File tree

22 files changed

+1275
-56
lines changed

22 files changed

+1275
-56
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ jobs:
120120
cflags: "-m32 -O3"
121121
otp: "23"
122122
elixir_version: "1.11"
123-
cmake_opts: "-DOPENSSL_CRYPTO_LIBRARY=/usr/lib/i386-linux-gnu/libcrypto.so"
123+
cmake_opts: "-DOPENSSL_CRYPTO_LIBRARY=/usr/lib/i386-linux-gnu/libcrypto.so -DAVM_CREATE_STACKTRACES=off"
124124
arch: "i386"
125125
compiler_pkgs: "gcc-10 g++-10 gcc-10-multilib g++-10-multilib libc6-dev-i386
126126
libc6-dbg:i386 zlib1g-dev:i386 libssl-dev:i386"

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3333
- Added support for `erlang:integer_to_list/2` and `erlang:integer_to_binary/2`
3434
- Added functions `esp:sleep_enable_ext0_wakeup/2` and `esp:sleep_enable_ext1_wakeup/2.`
3535
- Added support for FP opcodes 94-102 thus removing the need for `AVM_DISABLE_FP=On` with OTP-22+
36+
- Added support for stacktraces
3637

3738
### Fixed
3839
- Fixed issue with formatting integers with io:format() on STM32 platform

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ find_package(Elixir)
2828
option(AVM_DISABLE_FP "Disable floating point support." OFF)
2929
option(AVM_USE_32BIT_FLOAT "Use 32 bit floats." OFF)
3030
option(AVM_VERBOSE_ABORT "Print module and line number on VM abort" OFF)
31+
option(AVM_RELEASE "Build an AtomVM release" OFF)
32+
option(AVM_CREATE_STACKTRACES "Create stacktraces" ON)
3133
option(COVERAGE "Build for code coverage" OFF)
3234

3335
if((${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") OR

CMakeModules/BuildElixir.cmake

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,16 @@ macro(pack_archive avm_name)
3737
DEPENDS ${BEAMS}
3838
)
3939

40+
if(AVM_RELEASE)
41+
set(INCLUDE_LINES "")
42+
else()
43+
set(INCLUDE_LINES "-i")
44+
endif()
45+
4046
add_custom_target(
4147
${avm_name} ALL
4248
DEPENDS ${avm_name}_beams PackBEAM
43-
COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM -a ${avm_name}.avm ${BEAMS}
49+
COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM -a ${INCLUDE_LINES} ${avm_name}.avm ${BEAMS}
4450
COMMENT "Packing archive ${avm_name}.avm"
4551
VERBATIM
4652
)
@@ -64,6 +70,12 @@ macro(pack_runnable avm_name main)
6470
DEPENDS Elixir.${main}.beam
6571
)
6672

73+
if(AVM_RELEASE)
74+
set(INCLUDE_LINES "")
75+
else()
76+
set(INCLUDE_LINES "-i")
77+
endif()
78+
6779
foreach(archive_name ${ARGN})
6880
if(${archive_name} STREQUAL "exavmlib")
6981
set(ARCHIVES ${ARCHIVES} ${CMAKE_BINARY_DIR}/libs/${archive_name}/lib/${archive_name}.avm)
@@ -75,7 +87,7 @@ macro(pack_runnable avm_name main)
7587

7688
add_custom_target(
7789
${avm_name} ALL
78-
COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM ${avm_name}.avm Elixir.${main}.beam ${ARCHIVES}
90+
COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM ${INCLUDE_LINES} ${avm_name}.avm Elixir.${main}.beam ${ARCHIVES}
7991
COMMENT "Packing runnable ${avm_name}.avm"
8092
VERBATIM
8193
)

CMakeModules/BuildErlang.cmake

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,17 @@ macro(pack_archive avm_name)
3636
DEPENDS ${BEAMS}
3737
)
3838

39+
if(AVM_RELEASE)
40+
set(INCLUDE_LINES "")
41+
else()
42+
set(INCLUDE_LINES "-i")
43+
endif()
44+
3945
add_custom_target(
4046
${avm_name} ALL
4147
DEPENDS ${avm_name}_beams PackBEAM
4248
#DEPENDS ${BEAMS}
43-
COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM -a ${avm_name}.avm ${BEAMS}
49+
COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM -a ${INCLUDE_LINES} ${avm_name}.avm ${BEAMS}
4450
COMMENT "Packing archive ${avm_name}.avm"
4551
VERBATIM
4652
)
@@ -60,9 +66,15 @@ macro(pack_lib avm_name)
6066
set(ARCHIVE_TARGETS ${ARCHIVE_TARGETS} ${archive_name})
6167
endforeach()
6268

69+
if(AVM_RELEASE)
70+
set(INCLUDE_LINES "")
71+
else()
72+
set(INCLUDE_LINES "-i")
73+
endif()
74+
6375
add_custom_target(
6476
${avm_name} ALL
65-
COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM -a ${avm_name}.avm ${ARCHIVES}
77+
COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM -a ${INCLUDE_LINES} ${avm_name}.avm ${ARCHIVES}
6678
COMMENT "Packing runnable ${avm_name}.avm"
6779
VERBATIM
6880
)
@@ -98,9 +110,15 @@ macro(pack_runnable avm_name main)
98110
set(ARCHIVE_TARGETS ${ARCHIVE_TARGETS} ${archive_name})
99111
endforeach()
100112

113+
if(AVM_RELEASE)
114+
set(INCLUDE_LINES "")
115+
else()
116+
set(INCLUDE_LINES "-i")
117+
endif()
118+
101119
add_custom_target(
102120
${avm_name} ALL
103-
COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM ${avm_name}.avm ${main}.beam ${ARCHIVES}
121+
COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM ${INCLUDE_LINES} ${avm_name}.avm ${main}.beam ${ARCHIVES}
104122
COMMENT "Packing runnable ${avm_name}.avm"
105123
VERBATIM
106124
)
@@ -118,10 +136,16 @@ macro(pack_test test_avm_name)
118136
set(ARCHIVE_TARGETS ${ARCHIVE_TARGETS} ${archive_name})
119137
endforeach()
120138

139+
if(AVM_RELEASE)
140+
set(INCLUDE_LINES "")
141+
else()
142+
set(INCLUDE_LINES "-i")
143+
endif()
144+
121145
add_custom_target(
122146
${test_avm_name} ALL
123147
COMMAND erlc -I ${CMAKE_SOURCE_DIR}/libs/include ${CMAKE_CURRENT_SOURCE_DIR}/tests.erl
124-
COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM ${CMAKE_CURRENT_BINARY_DIR}/${test_avm_name}.avm ${CMAKE_CURRENT_BINARY_DIR}/tests.beam ${ARCHIVES}
148+
COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM ${INCLUDE_LINES} ${CMAKE_CURRENT_BINARY_DIR}/${test_avm_name}.avm ${CMAKE_CURRENT_BINARY_DIR}/tests.beam ${ARCHIVES}
125149
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/tests.erl
126150
COMMENT "Packing runnable ${test_avm_name}.avm"
127151
VERBATIM

doc/src/atomvm-internals.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,41 @@ The relationship between the `GlobalContext` fields that manage BEAM processes a
8989
## Exception Handling
9090

9191
## The Scheduler
92+
93+
## Stacktraces
94+
95+
Stacktraces are computed from information gathered at load time from BEAM modules loaded into the application, together with information in the runtime stack that is maintained during the execution of a program. In addition, if a BEAM file contains a `Line` chunk, additional information is added to stack traces, including the file name (as defined at compile time), as well as the line number of a function call.
96+
97+
> Note. Adding line information to a BEAM file adds non-trivial memory overhead to applications and should only be used when necessary (e.g., during the development process). For applications to make the best use of memory in tightly constrained environments, packagers should consider removing line information all together from BEAM files and rely instead on logging or other mechanisms for diagnosing problems in the field.
98+
99+
Newcomers to Erlang may find stacktraces slightly confusing, because some optimizations taken by the Erlang compiler and runtime can result in stack frames "missing" from stack traces. For example, tail-recursive function calls, as well as function calls that occur as the last expression in a function clause, don't involve the creation of frames in the runtime stack, and consequently will not appear in a stacktrace.
100+
101+
### Line Numbers
102+
103+
Including file and line number information in stacktraces adds considerable overhead to both the BEAM file data, as well as the memory consumed at module load time. The data structures used to track line numbers and file names are described below and are only created if the associated BEAM file contains a `Line` chunk.
104+
105+
#### The line-refs table
106+
107+
The line-refs table is an array of 16-bit integers, mapping line references (as they occur in BEAM instructions) to the actual line numbers in a file. (Internally, BEAM instructions do not reference line numbers directly, but instead are indirected through a line index). This table is stored on the `Module` structure.
108+
109+
This table is populated when the BEAM file is loaded. The table is created from information in the `Line` chunk in the BEAM file, if it exists. Note that if there is no `Line` chunk in a BEAM file, this table is not created.
110+
111+
The memory cost of this table is `num_line_refs * 2` bytes, for each loaded module, or 0, if there is no `Line` chunk in the associated BEAM file.
112+
113+
#### The filenames table
114+
115+
The filenames table is a table of (usually only 1?) file name. This table maps filename indices to `ModuleFilename` structures, which is essentially a pointer and a length (of type `size_t`). This table generally only contains 1 entry, the file name of the Erlang source code module from which the BEAM file was generated. This table is stored on the `Module` structure.
116+
117+
Note that a `ModuleFilename` structure points to data directly in the `Line` chunk of the BEAM file. Therefore, for ports of AtomVM that memory-map BEAM file data (e.g., ESP32), the actual file name data does not consume any memory.
118+
119+
The memory cost of this table is `num_filenames * sizeof(struct ModuleFilename)`, where `struct ModuleFilename` is a pointer and length, for each loaded module, or 0, if there is no `Line` chunk in the associated BEAM file.
120+
121+
#### The line-ref-offsets list
122+
123+
The line-ref-offsets list is a sequence of `LineRefOffset` structures, where each structure contains a ListHead (for list book-keeping), a 16-bit line-ref, and an unsigned integer value designating the code offset at which the line reference occurs in the code chunk of the BEAM file. This list is stored on the `Module` structure.
124+
125+
This list is populated at code load time. When a line reference is encountered during code loading, a `LineRefOffset` structure is allocated and added to the line-ref-offsets list. This list is used at a later time to find the line number at which a stack frame is called, in a manner described below.
126+
127+
The memory cost of this list is `num_line_refs * sizeof(struct LineRefOffset)`, for each loaded module, or 0, if there is no `Line` chunk in the associated BEAM file.
128+
129+
### Raw Stacktraces

doc/src/programmers-guide.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ The `PackBEAM` tool is a command-line application that can be used to create Pac
202202
shell$ PackBEAM -h
203203
Usage: PackBEAM [-h] [-l] <avm-file> [<options>]
204204
-h Print this help menu.
205+
-i Include file and line information.
205206
-l <input-avm-file> List the contents of an AVM file.
206207
[-a] <output-avm-file> <input-beam-or-avm-file>+ Create an AVM file (archive if -a specified).
207208

@@ -387,6 +388,49 @@ Use `base64:encode/1` and `base64:decode/1` to encode to and decode from Base64
387388

388389
You can Use `base64:encode_to_string/1` and `base64:decode_to_string/1` to perform the same encoding, but to return values as Erlang list structures, instead of as binaries.
389390

391+
### StackTraces
392+
393+
You can obtain information about the current state of a process via stacktraces, which provide information about the location of function calls (possibly including file names and line numbers) in your program.
394+
395+
Currently in AtomVM, stack traces can be obtained in one of following ways:
396+
397+
* via try-catch blocks
398+
* via catch blocks, when an error has been raised via the `error` Bif.
399+
400+
> Note. AtomVM does not support `erlang:get_stacktrace/0` which was deprecated in Erlang/OTP 21 and 22, stopped working in Erlang/OTP 23 and was removed in Erlang/OTP 24. Support for accessing the current stacktrace via `erlang:process_info/2` may be added in the future.
401+
402+
For example a stack trace can be bound to a variable in the catch clause in a try-catch block:
403+
404+
try
405+
do_something()
406+
catch
407+
_Class:_Error:Stacktrace ->
408+
io:format("Stacktrace: ~p~n", [Stacktrace])
409+
end
410+
411+
Alternatively, a stack trace can be bound to the result of a `catch` expression, but only when the error is raised by the `error` Bif. For example,
412+
413+
{'EXIT', {foo, Stacktrace}} = (catch error(foo)),
414+
io:format("Stacktrace: ~p~n", [Stacktrace])
415+
416+
Stack traces are printed to the console in a crash report, for example, when a process dies unexpectedly.
417+
418+
Stacktrace data is represented as a list of tuples, each of which represents a stack “frame”. Each tuple is of the form:
419+
420+
[{Module :: module(), Function :: atom(), Arity :: non_neg_integer(), AuxData :: aux_data()}]
421+
422+
where `aux_data()` is a (possibly empty) properties list containing the following elements:
423+
424+
[{file, File :: string(), line, Line :: pos_integer()}]
425+
426+
Stack frames are ordered from the frame “closest“ to the point of failure (the “top” of the stack) to the frame furthest from the point of failure (the “bottom” of the stack).
427+
428+
Stack frames will contain file and line information in the AuxData list if the BEAM files (typically embedded in AVM files) include <<“Line”>> chunks generated by the compiler. Otherwise, the AuxData will be an empty list.
429+
430+
> Note. Adding line information to BEAM files not only increases the size of BEAM files in storage, but calculation of file and line information can have a non-negligible impact on memory usage. Memory-sensitive applications should consider not including line information in BEAM files.
431+
432+
The PackBEAM tool does not include file and line information in the AVM files it creates, but file and line information can be included via a command line option. For information about the PackBEAM too, see the [`PackBEAM` tool](#Packbeam_tool).
433+
390434
### Math
391435

392436
AtomvVM supports the following standard functions from the OTP `math` module:

src/libAtomVM/CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ set(HEADER_FILES
5050
port.h
5151
refc_binary.h
5252
scheduler.h
53+
stacktrace.h
5354
sys.h
5455
term_typedef.h
5556
term.h
@@ -81,6 +82,7 @@ set(SOURCE_FILES
8182
port.c
8283
refc_binary.c
8384
scheduler.c
85+
stacktrace.c
8486
term.c
8587
timer_wheel.c
8688
valueshashtable.c
@@ -127,6 +129,10 @@ if (AVM_VERBOSE_ABORT)
127129
target_compile_definitions(libAtomVM PUBLIC AVM_VERBOSE_ABORT)
128130
endif()
129131

132+
if(AVM_CREATE_STACKTRACES)
133+
target_compile_definitions(libAtomVM PUBLIC AVM_CREATE_STACKTRACES)
134+
endif()
135+
130136
# Automatically use zlib if present to load .beam files
131137
if (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR ${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
132138
find_package(ZLIB)

src/libAtomVM/iff.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ void scan_iff(const void *iff_binary, int buf_size, unsigned long *offsets, unsi
9292
} else if (!memcmp(current_record->name, "StrT", 4)) {
9393
offsets[STRT] = current_pos;
9494
sizes[STRT] = ENDIAN_SWAP_32(current_record->size);
95+
} else if (!memcmp(current_record->name, "Line", 4)) {
96+
offsets[LINT] = current_pos;
97+
sizes[LINT] = ENDIAN_SWAP_32(current_record->size);
9598
}
9699

97100
current_pos += iff_align(ENDIAN_SWAP_32(current_record->size) + 8);

src/libAtomVM/iff.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,13 @@ extern "C" {
5353
#define FUNT 7
5454
/** Str table section */
5555
#define STRT 8
56+
/** Str table section */
57+
#define LINT 9
5658

5759
/** Required size for offsets array */
58-
#define MAX_OFFS 9
60+
#define MAX_OFFS 10
5961
/** Required size for sizes array */
60-
#define MAX_SIZES 9
62+
#define MAX_SIZES 10
6163

6264
/** sizeof IFF section header in bytes */
6365
#define IFF_SECTION_HEADER_SIZE 8

0 commit comments

Comments
 (0)