diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index 4eb693bfa4..5ba8175aa7 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -120,7 +120,7 @@ jobs: cflags: "-m32 -O3" otp: "23" elixir_version: "1.11" - cmake_opts: "-DOPENSSL_CRYPTO_LIBRARY=/usr/lib/i386-linux-gnu/libcrypto.so" + cmake_opts: "-DOPENSSL_CRYPTO_LIBRARY=/usr/lib/i386-linux-gnu/libcrypto.so -DAVM_CREATE_STACKTRACES=off" arch: "i386" compiler_pkgs: "gcc-10 g++-10 gcc-10-multilib g++-10-multilib libc6-dev-i386 libc6-dbg:i386 zlib1g-dev:i386 libssl-dev:i386" diff --git a/CHANGELOG.md b/CHANGELOG.md index 83ad56c3cd..7ddf57b14c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for `erlang:integer_to_list/2` and `erlang:integer_to_binary/2` - Added functions `esp:sleep_enable_ext0_wakeup/2` and `esp:sleep_enable_ext1_wakeup/2.` - Added support for FP opcodes 94-102 thus removing the need for `AVM_DISABLE_FP=On` with OTP-22+ +- Added support for stacktraces ### Fixed - Fixed issue with formatting integers with io:format() on STM32 platform diff --git a/CMakeLists.txt b/CMakeLists.txt index e2a3406c48..3b464e4379 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,8 @@ find_package(Elixir) option(AVM_DISABLE_FP "Disable floating point support." OFF) option(AVM_USE_32BIT_FLOAT "Use 32 bit floats." OFF) option(AVM_VERBOSE_ABORT "Print module and line number on VM abort" OFF) +option(AVM_RELEASE "Build an AtomVM release" OFF) +option(AVM_CREATE_STACKTRACES "Create stacktraces" ON) option(COVERAGE "Build for code coverage" OFF) if((${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") OR diff --git a/CMakeModules/BuildElixir.cmake b/CMakeModules/BuildElixir.cmake index 85d51b4054..4006507225 100644 --- a/CMakeModules/BuildElixir.cmake +++ b/CMakeModules/BuildElixir.cmake @@ -37,10 +37,16 @@ macro(pack_archive avm_name) DEPENDS ${BEAMS} ) + if(AVM_RELEASE) + set(INCLUDE_LINES "") + else() + set(INCLUDE_LINES "-i") + endif() + add_custom_target( ${avm_name} ALL DEPENDS ${avm_name}_beams PackBEAM - COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM -a ${avm_name}.avm ${BEAMS} + COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM -a ${INCLUDE_LINES} ${avm_name}.avm ${BEAMS} COMMENT "Packing archive ${avm_name}.avm" VERBATIM ) @@ -64,6 +70,12 @@ macro(pack_runnable avm_name main) DEPENDS Elixir.${main}.beam ) + if(AVM_RELEASE) + set(INCLUDE_LINES "") + else() + set(INCLUDE_LINES "-i") + endif() + foreach(archive_name ${ARGN}) if(${archive_name} STREQUAL "exavmlib") set(ARCHIVES ${ARCHIVES} ${CMAKE_BINARY_DIR}/libs/${archive_name}/lib/${archive_name}.avm) @@ -75,7 +87,7 @@ macro(pack_runnable avm_name main) add_custom_target( ${avm_name} ALL - COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM ${avm_name}.avm Elixir.${main}.beam ${ARCHIVES} + COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM ${INCLUDE_LINES} ${avm_name}.avm Elixir.${main}.beam ${ARCHIVES} COMMENT "Packing runnable ${avm_name}.avm" VERBATIM ) diff --git a/CMakeModules/BuildErlang.cmake b/CMakeModules/BuildErlang.cmake index ce70617820..e3fdf81475 100644 --- a/CMakeModules/BuildErlang.cmake +++ b/CMakeModules/BuildErlang.cmake @@ -36,11 +36,17 @@ macro(pack_archive avm_name) DEPENDS ${BEAMS} ) + if(AVM_RELEASE) + set(INCLUDE_LINES "") + else() + set(INCLUDE_LINES "-i") + endif() + add_custom_target( ${avm_name} ALL DEPENDS ${avm_name}_beams PackBEAM #DEPENDS ${BEAMS} - COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM -a ${avm_name}.avm ${BEAMS} + COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM -a ${INCLUDE_LINES} ${avm_name}.avm ${BEAMS} COMMENT "Packing archive ${avm_name}.avm" VERBATIM ) @@ -60,9 +66,15 @@ macro(pack_lib avm_name) set(ARCHIVE_TARGETS ${ARCHIVE_TARGETS} ${archive_name}) endforeach() + if(AVM_RELEASE) + set(INCLUDE_LINES "") + else() + set(INCLUDE_LINES "-i") + endif() + add_custom_target( ${avm_name} ALL - COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM -a ${avm_name}.avm ${ARCHIVES} + COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM -a ${INCLUDE_LINES} ${avm_name}.avm ${ARCHIVES} COMMENT "Packing runnable ${avm_name}.avm" VERBATIM ) @@ -98,9 +110,15 @@ macro(pack_runnable avm_name main) set(ARCHIVE_TARGETS ${ARCHIVE_TARGETS} ${archive_name}) endforeach() + if(AVM_RELEASE) + set(INCLUDE_LINES "") + else() + set(INCLUDE_LINES "-i") + endif() + add_custom_target( ${avm_name} ALL - COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM ${avm_name}.avm ${main}.beam ${ARCHIVES} + COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM ${INCLUDE_LINES} ${avm_name}.avm ${main}.beam ${ARCHIVES} COMMENT "Packing runnable ${avm_name}.avm" VERBATIM ) @@ -118,10 +136,16 @@ macro(pack_test test_avm_name) set(ARCHIVE_TARGETS ${ARCHIVE_TARGETS} ${archive_name}) endforeach() + if(AVM_RELEASE) + set(INCLUDE_LINES "") + else() + set(INCLUDE_LINES "-i") + endif() + add_custom_target( ${test_avm_name} ALL COMMAND erlc -I ${CMAKE_SOURCE_DIR}/libs/include ${CMAKE_CURRENT_SOURCE_DIR}/tests.erl - COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM ${CMAKE_CURRENT_BINARY_DIR}/${test_avm_name}.avm ${CMAKE_CURRENT_BINARY_DIR}/tests.beam ${ARCHIVES} + COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/PackBEAM ${INCLUDE_LINES} ${CMAKE_CURRENT_BINARY_DIR}/${test_avm_name}.avm ${CMAKE_CURRENT_BINARY_DIR}/tests.beam ${ARCHIVES} DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/tests.erl COMMENT "Packing runnable ${test_avm_name}.avm" VERBATIM diff --git a/doc/src/atomvm-internals.md b/doc/src/atomvm-internals.md index 1ce824e811..5398959a44 100644 --- a/doc/src/atomvm-internals.md +++ b/doc/src/atomvm-internals.md @@ -89,3 +89,41 @@ The relationship between the `GlobalContext` fields that manage BEAM processes a ## Exception Handling ## The Scheduler + +## Stacktraces + +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. + +> 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. + +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. + +### Line Numbers + +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. + +#### The line-refs table + +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. + +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. + +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. + +#### The filenames table + +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. + +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. + +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. + +#### The line-ref-offsets list + +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. + +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. + +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. + +### Raw Stacktraces diff --git a/doc/src/programmers-guide.md b/doc/src/programmers-guide.md index 4928129199..bfe67cbb60 100644 --- a/doc/src/programmers-guide.md +++ b/doc/src/programmers-guide.md @@ -202,6 +202,7 @@ The `PackBEAM` tool is a command-line application that can be used to create Pac shell$ PackBEAM -h Usage: PackBEAM [-h] [-l] [] -h Print this help menu. + -i Include file and line information. -l List the contents of an AVM file. [-a] + Create an AVM file (archive if -a specified). @@ -387,6 +388,49 @@ Use `base64:encode/1` and `base64:decode/1` to encode to and decode from Base64 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. +### StackTraces + +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. + +Currently in AtomVM, stack traces can be obtained in one of following ways: + +* via try-catch blocks +* via catch blocks, when an error has been raised via the `error` Bif. + +> 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. + +For example a stack trace can be bound to a variable in the catch clause in a try-catch block: + + try + do_something() + catch + _Class:_Error:Stacktrace -> + io:format("Stacktrace: ~p~n", [Stacktrace]) + end + +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, + + {'EXIT', {foo, Stacktrace}} = (catch error(foo)), + io:format("Stacktrace: ~p~n", [Stacktrace]) + +Stack traces are printed to the console in a crash report, for example, when a process dies unexpectedly. + +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 an empty list. + +> 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. + +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). + ### Math AtomvVM supports the following standard functions from the OTP `math` module: diff --git a/src/libAtomVM/CMakeLists.txt b/src/libAtomVM/CMakeLists.txt index 096d94d5f1..76d0629712 100644 --- a/src/libAtomVM/CMakeLists.txt +++ b/src/libAtomVM/CMakeLists.txt @@ -50,6 +50,7 @@ set(HEADER_FILES port.h refc_binary.h scheduler.h + stacktrace.h sys.h term_typedef.h term.h @@ -81,6 +82,7 @@ set(SOURCE_FILES port.c refc_binary.c scheduler.c + stacktrace.c term.c timer_wheel.c valueshashtable.c @@ -127,6 +129,10 @@ if (AVM_VERBOSE_ABORT) target_compile_definitions(libAtomVM PUBLIC AVM_VERBOSE_ABORT) endif() +if(AVM_CREATE_STACKTRACES) + target_compile_definitions(libAtomVM PUBLIC AVM_CREATE_STACKTRACES) +endif() + # Automatically use zlib if present to load .beam files if (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR ${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") find_package(ZLIB) diff --git a/src/libAtomVM/iff.c b/src/libAtomVM/iff.c index 6d9b160aed..374a945035 100644 --- a/src/libAtomVM/iff.c +++ b/src/libAtomVM/iff.c @@ -92,6 +92,9 @@ void scan_iff(const void *iff_binary, int buf_size, unsigned long *offsets, unsi } else if (!memcmp(current_record->name, "StrT", 4)) { offsets[STRT] = current_pos; sizes[STRT] = ENDIAN_SWAP_32(current_record->size); + } else if (!memcmp(current_record->name, "Line", 4)) { + offsets[LINT] = current_pos; + sizes[LINT] = ENDIAN_SWAP_32(current_record->size); } current_pos += iff_align(ENDIAN_SWAP_32(current_record->size) + 8); diff --git a/src/libAtomVM/iff.h b/src/libAtomVM/iff.h index fc5b193ac7..049d620811 100644 --- a/src/libAtomVM/iff.h +++ b/src/libAtomVM/iff.h @@ -53,11 +53,13 @@ extern "C" { #define FUNT 7 /** Str table section */ #define STRT 8 +/** Str table section */ +#define LINT 9 /** Required size for offsets array */ -#define MAX_OFFS 9 +#define MAX_OFFS 10 /** Required size for sizes array */ -#define MAX_SIZES 9 +#define MAX_SIZES 10 /** sizeof IFF section header in bytes */ #define IFF_SECTION_HEADER_SIZE 8 diff --git a/src/libAtomVM/module.c b/src/libAtomVM/module.c index fc522ce72d..62bb58fb60 100644 --- a/src/libAtomVM/module.c +++ b/src/libAtomVM/module.c @@ -25,6 +25,8 @@ #include "context.h" #include "externalterm.h" #include "iff.h" +#include "linkedlist.h" +#include "list.h" #include "nifs.h" #include "utils.h" @@ -38,6 +40,19 @@ #define LITT_UNCOMPRESSED_SIZE_OFFSET 8 #define LITT_HEADER_SIZE 12 +// TODO Constants similar to these are defined in opcodesswitch.h and should +// be refactored so they can be used here, as well. +#define TAG_COMPACT_INT 0x01 +#define TAG_COMPACT_ATOM 0x02 +#define TAG_EXTENDED_INT 0x09 +#define TAG_EXTENDED_ATOM 0x0A + +#define CHECK_FREE_SPACE(space, error) \ + if ((size_t) ((pos + space) - data) > len) { \ + fprintf(stderr, error); \ + return; \ + } + #ifdef WITH_ZLIB static void *module_uncompress_literals(const uint8_t *litT, int size); #endif @@ -45,6 +60,7 @@ static struct LiteralEntry *module_build_literals_table(const void *literalsBuf) static void module_add_label(Module *mod, int index, void *ptr); static enum ModuleLoadResult module_build_imported_functions_table(Module *this_module, uint8_t *table_data); static void module_add_label(Module *mod, int index, void *ptr); +static void parse_line_table(uint16_t **line_refs, struct ModuleFilename **filenames, uint8_t *data, size_t len); #define IMPL_CODE_LOADER 1 #include "opcodesswitch.h" @@ -141,6 +157,41 @@ void module_get_imported_function_module_and_name(const Module *this_module, int } #endif +bool module_get_function_from_label(Module *this_module, int label, AtomString *function_name, int *arity) +{ + int best_label = -1; + const uint8_t *export_table_data = (const uint8_t *) this_module->export_table; + int exports_count = READ_32_ALIGNED(export_table_data + 8); + for (int export_index = exports_count - 1; export_index >= 0; export_index--) { + int fun_atom_index = READ_32_ALIGNED(export_table_data + (export_index * 12) + 12); + int fun_arity = READ_32_ALIGNED(export_table_data + (export_index * 12) + 4 + 12); + int fun_label = READ_32_ALIGNED(export_table_data + (export_index * 12) + 8 + 12); + if (fun_label <= label && best_label < fun_label) { + best_label = fun_label; + *arity = fun_arity; + *function_name = module_get_atom_string_by_id(this_module, fun_atom_index); + } + } + + const uint8_t *local_table_data = (const uint8_t *) this_module->local_table; + int locals_count = READ_32_ALIGNED(local_table_data + 8); + for (int local_index = locals_count - 1; local_index >= 0; local_index--) { + int fun_atom_index = READ_32_ALIGNED(local_table_data + (local_index * 12) + 12); + int fun_arity = READ_32_ALIGNED(local_table_data + (local_index * 12) + 4 + 12); + int fun_label = READ_32_ALIGNED(local_table_data + (local_index * 12) + 8 + 12); + if (fun_label <= label && best_label < fun_label) { + best_label = fun_label; + *arity = fun_arity; + *function_name = module_get_atom_string_by_id(this_module, fun_atom_index); + } + } + if (UNLIKELY(best_label == -1)) { + // Couldn't find the function. + return false; + } + return true; +} + uint32_t module_search_exported_function(Module *this_module, AtomString func_name, int func_arity) { const uint8_t *table_data = (const uint8_t *) this_module->export_table; @@ -198,17 +249,22 @@ Module *module_new_from_iff_binary(GlobalContext *global, const void *iff_binary #endif mod->code = (CodeChunk *) (beam_file + offsets[CODE]); mod->export_table = beam_file + offsets[EXPT]; + mod->local_table = beam_file + offsets[LOCT]; mod->atom_table = beam_file + offsets[AT8U]; mod->fun_table = beam_file + offsets[FUNT]; mod->str_table = beam_file + offsets[STRT]; mod->str_table_len = sizes[STRT]; - mod->labels = calloc(ENDIAN_SWAP_32(mod->code->labels), sizeof(void *)); + uint32_t num_labels = ENDIAN_SWAP_32(mod->code->labels); + mod->labels = calloc(num_labels, sizeof(void *)); if (IS_NULL_PTR(mod->labels)) { fprintf(stderr, "Error: Null module labels: %s:%i.\n", __FILE__, __LINE__); module_destroy(mod); return NULL; } + parse_line_table(&mod->line_refs, &mod->filenames, beam_file + offsets[LINT] + 8, sizes[LINT]); + list_init(&mod->line_ref_offsets); + if (offsets[LITT]) { #ifdef WITH_ZLIB mod->literals_data = module_uncompress_literals(beam_file + offsets[LITT], sizes[LITT]); @@ -358,3 +414,200 @@ const struct ExportedFunction *module_resolve_function(Module *mod, int import_t return NULL; } } + +static uint16_t *parse_line_refs(uint8_t **data, size_t num_refs, size_t len) +{ + uint16_t *ref_table = malloc((num_refs + 1) * sizeof(uint16_t)); + if (IS_NULL_PTR(ref_table)) { + return NULL; + } + + // assert pos >= *data + uint8_t *pos = *data; + for (size_t i = 0; i < num_refs + 1; ++i) { + if ((size_t) (pos - *data) > len) { + fprintf(stderr, "Invalid line_ref: expected tag.\n"); + free(ref_table); + return NULL; + } + uint8_t tag = *pos; + switch (tag & 0x0F) { + case TAG_COMPACT_INT: { + uint16_t line_idx = ((tag & 0xF0) >> 4); + ref_table[i] = line_idx; + ++pos; + break; + } + case TAG_COMPACT_ATOM: { + uint16_t line_idx = ((tag & 0xF0) >> 4); + ref_table[i] = line_idx; + ++pos; + break; + } + case TAG_EXTENDED_INT: { + uint16_t high_order_3_bits = (tag & 0xE0); + ++pos; + if ((size_t) (pos - *data) > len) { + fprintf(stderr, "Invalid line_ref: expected extended int.\n"); + free(ref_table); + return NULL; + } + uint8_t next_byte = *pos; + uint16_t line_idx = ((high_order_3_bits << 3) | next_byte); + ++pos; + ref_table[i] = line_idx; + break; + } + case TAG_EXTENDED_ATOM: { + uint16_t file_idx = ((tag & 0xF0) >> 4); + ++pos; + if ((size_t) (pos - *data) > len) { + fprintf(stderr, "Invalid line_ref: expected extended atom.\n"); + free(ref_table); + return NULL; + } + uint8_t next_byte = *pos; + uint16_t line_idx = ((next_byte & 0xF0) >> 4); + ++pos; + ref_table[file_idx - 1] = line_idx; + break; + } + default: + // TODO handle integer compact encodings > 2048 + fprintf(stderr, "Unsupported line_ref tag: %u\n", tag); + free(ref_table); + return NULL; + } + } + + *data = pos; + return ref_table; +} + +struct ModuleFilename *parse_filename_table(uint8_t **data, size_t num_filenames, size_t len) +{ + struct ModuleFilename *filenames = malloc(num_filenames * sizeof(struct ModuleFilename)); + if (IS_NULL_PTR(filenames)) { + return NULL; + } + + // assert pos >= *data + uint8_t *pos = *data; + for (size_t i = 0; i < num_filenames; ++i) { + if ((size_t) ((pos + 2) - *data) > len) { + fprintf(stderr, "Invalid filename: expected 16-bit size.\n"); + free(filenames); + return NULL; + } + uint16_t size = READ_16_UNALIGNED(pos); + pos +=2; + if ((size_t) ((pos + size) - *data) > len) { + fprintf(stderr, "Invalid filename: expected filename data (%u bytes).\n", size); + free(filenames); + return NULL; + } + filenames[i].len = size; + filenames[i].data = pos; + pos += size; + } + + *data = pos; + return filenames; +} + +static void parse_line_table(uint16_t **line_refs, struct ModuleFilename **filenames, uint8_t *data, size_t len) +{ + + *line_refs = NULL; + *filenames = NULL; + + if (len == 0) { + return; + } + + uint8_t *pos = data; + + CHECK_FREE_SPACE(4, "Error reading Line chunk: version\n"); + uint32_t version = READ_32_UNALIGNED(pos); + if (UNLIKELY(version != 0)) { + fprintf(stderr, "Warning: Unsupported line version %u. Line information in stacktraces may be missing\n", version); + return; + } + pos += 4; + + CHECK_FREE_SPACE(4, "Error reading Line chunk: flags\n"); + uint32_t _flags = READ_32_UNALIGNED(pos); + UNUSED(_flags); + pos += 4; + + CHECK_FREE_SPACE(4, "Error reading Line chunk: num_instr\n"); + uint32_t _num_instr = READ_32_UNALIGNED(pos); + UNUSED(_num_instr); + pos += 4; + + CHECK_FREE_SPACE(4, "Error reading Line chunk: num_refs\n"); + uint32_t num_refs = READ_32_UNALIGNED(pos); + pos += 4; + + CHECK_FREE_SPACE(4, "Error reading Line chunk: num_filenames\n"); + uint32_t num_filenames = READ_32_UNALIGNED(pos); + pos += 4; + + *line_refs = parse_line_refs(&pos, num_refs, len - (pos - data)); + if (IS_NULL_PTR(*line_refs)) { + return; + } + + *filenames = parse_filename_table(&pos, num_filenames, len - (pos - data)); + if (IS_NULL_PTR(*filenames)) { + free(*line_refs); + return; + } +} + +void module_insert_line_ref_offset(Module *mod, int line_ref, int offset) +{ + if (IS_NULL_PTR(mod->line_refs) || line_ref == 0) { + return; + } + struct LineRefOffset *ref_offset = malloc(sizeof(struct LineRefOffset)); + if (IS_NULL_PTR(ref_offset)) { + fprintf(stderr, "Warning: Unable to allocate space for line ref offset. Line information in stacktraces may be missing\n"); + return; + } + ref_offset->line_ref = line_ref; + ref_offset->offset = offset; + list_append(&mod->line_ref_offsets, &ref_offset->head); +} + +int module_find_line(Module *mod, unsigned int offset) +{ + int i = 0; + struct LineRefOffset *head = GET_LIST_ENTRY(&mod->line_ref_offsets, struct LineRefOffset, head); + struct ListHead *item; + LIST_FOR_EACH (item, &mod->line_ref_offsets) { + struct LineRefOffset *ref_offset = GET_LIST_ENTRY(item, struct LineRefOffset, head); + + if (offset == ref_offset->offset) { + return mod->line_refs[ref_offset->line_ref]; + } else if (i == 0 && offset < ref_offset->offset) { + return -1; + } else { + + struct LineRefOffset *prev_ref_offset = GET_LIST_ENTRY(ref_offset->head.prev, struct LineRefOffset, head); + if (prev_ref_offset->offset <= offset && offset < ref_offset->offset) { + return mod->line_refs[prev_ref_offset->line_ref]; + } + + struct LineRefOffset *next_ref_offset = GET_LIST_ENTRY(ref_offset->head.next, struct LineRefOffset, head); + if (next_ref_offset == head && ref_offset->offset <= offset) { + return mod->line_refs[ref_offset->line_ref]; + } + } + + ++i; + } + // should never occur, but return is needed to squelch compiler warnings + AVM_ABORT(); + return -1; +} diff --git a/src/libAtomVM/module.h b/src/libAtomVM/module.h index ac4e3c9d97..8819946936 100644 --- a/src/libAtomVM/module.h +++ b/src/libAtomVM/module.h @@ -32,9 +32,11 @@ extern "C" { #endif +#include #include #include "atom.h" +#include "atomshashtable.h" #include "context.h" #include "globalcontext.h" #include "valueshashtable.h" @@ -60,6 +62,19 @@ struct LiteralEntry void const *data; }; +struct ModuleFilename +{ + uint8_t *data; + size_t len; +}; + +struct LineRefOffset +{ + struct ListHead head; + unsigned int offset; + uint16_t line_ref; +}; + struct Module { GlobalContext *global; @@ -70,11 +85,16 @@ struct Module CodeChunk *code; void *export_table; + void *local_table; void *atom_table; void *fun_table; void *str_table; size_t str_table_len; + uint16_t *line_refs; + struct ModuleFilename *filenames; + struct ListHead line_ref_offsets; + union imported_func *imported_funcs; void **labels; @@ -262,6 +282,57 @@ static inline const uint8_t *module_get_str(Module *mod, size_t offset, size_t * return ((const uint8_t *) mod->str_table) + 8 + offset; } +/* + * @brief Get the function name and arity of a function from a label. + * + * @details This function is used to get the function name and arity from a + * given label inside the function. It is based the property of labels are + * being allocated monotonously within functions. + * @param mod the module + * @param label the current label used to look up the function/arity + * @param function_name (output) the function name, as an AtomString. + * @param arity (output) the function arity + */ +bool module_get_function_from_label(Module *this_module, int label, AtomString *function_name, int *arity); + +/* + * @brief Insert the instruction offset for a given module at a line reference instruction. + * + * @details This function is used when loading a module. When a line instruction is + * scanned, this function is used to record the instruction offset at which the line + * instruction occurred. + * + * Note that if the module (BEAM file) does not contain a Line chunk, then this function + * is a no-op. + * + * @param mod the module + * @param line_ref the line reference (index) + * @param offset the instruction offset at which the line instruction occurred. + */ +void module_insert_line_ref_offset(Module *mod, int line_ref, int offset); + +/* + * @brief Find the latest line reference (index) before or at which the instruction offset + * occurs. + * + * @details This function searches for the most recent line instruction scanned during + * code loading at or before the specified instruction offset. This function is used to + * locate the line number from a continuation pointer in a stack trace. + * + * @param mod the module + * @param offset + * @return the line reference + */ +int module_find_line(Module *mod, unsigned int offset); + +/** + * @return true if the module has line information, false, otherwise. + */ +static inline bool module_has_line_chunk(Module *mod) +{ + return mod->line_refs != NULL; +} + #ifdef __cplusplus } #endif diff --git a/src/libAtomVM/opcodes.h b/src/libAtomVM/opcodes.h index f60e307e3c..f6d4bbbbcf 100644 --- a/src/libAtomVM/opcodes.h +++ b/src/libAtomVM/opcodes.h @@ -134,6 +134,7 @@ #define OP_HAS_MAP_FIELDS 157 #define OP_GET_MAP_ELEMENTS 158 #define OP_IS_TAGGED_TUPLE 159 +#define OP_BUILD_STACKTRACE 160 #define OP_GET_HD 162 #define OP_GET_TL 163 #define OP_PUT_TUPLE2 164 diff --git a/src/libAtomVM/opcodesswitch.h b/src/libAtomVM/opcodesswitch.h index 17ddf65578..6ad5b897a4 100644 --- a/src/libAtomVM/opcodesswitch.h +++ b/src/libAtomVM/opcodesswitch.h @@ -35,6 +35,7 @@ #ifdef IMPL_EXECUTE_LOOP #include "bitstring.h" #include "mailbox.h" + #include "stacktrace.h" #endif #define ENABLE_OTP21 @@ -83,9 +84,10 @@ typedef union } dreg_type_t; #ifdef IMPL_EXECUTE_LOOP -#define RAISE_ERROR(error_type_atom) \ - ctx->x[0] = ERROR_ATOM; \ - ctx->x[1] = error_type_atom; \ +#define RAISE_ERROR(error_type_atom) \ + ctx->x[0] = ERROR_ATOM; \ + ctx->x[1] = error_type_atom; \ + ctx->x[2] = stacktrace_create_raw(ctx, mod, i); \ goto handle_error; #define VM_ABORT() \ @@ -274,7 +276,7 @@ typedef union AVM_ABORT(); \ } \ val = 0; \ - for (int vi = 0; vi < sz; vi++) { \ + for (uint8_t vi = 0; vi < sz; vi++) { \ val <<= 8; \ val |= code[(base_index) + (off) + 1 + vi]; \ } \ @@ -307,7 +309,7 @@ typedef union AVM_ABORT(); \ } \ val = 0; \ - for (int vi = 0; vi < sz; vi++) { \ + for (uint8_t vi = 0; vi < sz; vi++) { \ val <<= 8; \ val |= code[(base_index) + (off) + 1 + vi]; \ } \ @@ -382,7 +384,7 @@ typedef union DECODE_LITERAL(list_size, code_chunk, base_index, off); \ uint32_t allocator_tag; \ uint32_t allocator_size; \ - for (int j = 0; j < list_size; j++) { \ + for (uint32_t j = 0; j < list_size; j++) { \ DECODE_LITERAL(allocator_tag, code_chunk, base_index, off); \ DECODE_LITERAL(allocator_size, code_chunk, base_index, off); \ if (allocator_tag == COMPACT_EXTENDED_ALLOCATOR_LIST_TAG_FLOATS) { \ @@ -402,7 +404,7 @@ typedef union DECODE_LITERAL(list_size, code_chunk, base_index, off); \ uint32_t allocator_tag; \ uint32_t allocator_size; \ - for (int j = 0; j < list_size; j++) { \ + for (uint32_t j = 0; j < list_size; j++) { \ DECODE_LITERAL(allocator_tag, code_chunk, base_index, off); \ DECODE_LITERAL(allocator_size, code_chunk, base_index, off); \ if (allocator_size > 0 && allocator_tag == COMPACT_EXTENDED_ALLOCATOR_LIST_TAG_FLOATS) { \ @@ -626,7 +628,7 @@ typedef union case 3: { \ uint8_t sz = (first_byte >> 5) + 2; \ val = 0; \ - for (int vi = 0; vi < sz; vi++) { \ + for (uint8_t vi = 0; vi < sz; vi++) { \ val <<= 8; \ val |= code_chunk[(base_index) + (off) + 1 + vi]; \ } \ @@ -668,7 +670,7 @@ typedef union DECODE_LITERAL(list_size, code_chunk, base_index, off); \ uint32_t allocator_tag; \ uint32_t allocator_size; \ - for (int j = 0; j < list_size; j++) { \ + for (uint32_t j = 0; j < list_size; j++) { \ DECODE_LITERAL(allocator_tag, code_chunk, base_index, off); \ DECODE_LITERAL(allocator_size, code_chunk, base_index, off); \ if (allocator_tag == COMPACT_EXTENDED_ALLOCATOR_LIST_TAG_FLOATS) { \ @@ -688,7 +690,7 @@ typedef union DECODE_LITERAL(list_size, code_chunk, base_index, off); \ uint32_t allocator_tag; \ uint32_t allocator_size; \ - for (int j = 0; j < list_size; j++) { \ + for (uint32_t j = 0; j < list_size; j++) { \ DECODE_LITERAL(allocator_tag, code_chunk, base_index, off); \ DECODE_LITERAL(allocator_size, code_chunk, base_index, off); \ need += allocator_size; \ @@ -743,7 +745,8 @@ typedef union #define POINTER_TO_II(instruction_pointer) \ (((uint8_t *) (instruction_pointer)) - code) -#define HANDLE_ERROR() \ +#define HANDLE_ERROR() \ + ctx->x[2] = stacktrace_create_raw(ctx, mod, i); \ goto handle_error; #define VERIFY_IS_INTEGER(t, opcode_name) \ @@ -896,6 +899,10 @@ COLD_FUNC static void dump(Context *ctx) term_display(stderr, term_from_local_process_id(ctx->process_id), ctx); fprintf(stderr, "\n"); + fprintf(stderr, "\nStacktrace:\n"); + term_display(stderr, stacktrace_build(ctx, &ctx->x[2]), ctx); + fprintf(stderr, "\n\n"); + { Module *cp_mod; int label; @@ -909,6 +916,8 @@ COLD_FUNC static void dump(Context *ctx) term_display(stderr, ctx->x[0], ctx); fprintf(stderr, "\nx[1]: "); term_display(stderr, ctx->x[1], ctx); + fprintf(stderr, "\nx[2]: "); + term_display(stderr, ctx->x[2], ctx); fprintf(stderr, "\n\nStack \n------\n\n"); term *ct = ctx->e; @@ -1457,6 +1466,8 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f continue; } + // save instruction offset in case of error + int orig_i = i; NEXT_INSTRUCTION(next_off); TRACE_CALL_EXT(ctx, mod, "call_ext", index, arity); @@ -1476,6 +1487,7 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f const struct Nif *nif = EXPORTED_FUNCTION_TO_NIF(func); term return_value = nif->nif_ptr(ctx, arity, ctx->x); if (UNLIKELY(term_is_invalid_term(return_value))) { + i = orig_i; HANDLE_ERROR(); } ctx->x[0] = return_value; @@ -1768,7 +1780,7 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f } ctx->e -= stack_need + 1; - for (int s = 0; s < stack_need; s++) { + for (uint32_t s = 0; s < stack_need; s++) { ctx->e[s] = term_nil(); } ctx->e[stack_need] = ctx->cp; @@ -1807,7 +1819,7 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f } } ctx->e -= stack_need + 1; - for (int s = 0; s < stack_need; s++) { + for (uint32_t s = 0; s < stack_need; s++) { ctx->e[s] = term_nil(); } ctx->e[stack_need] = ctx->cp; @@ -2616,7 +2628,7 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f #ifdef IMPL_EXECUTE_LOOP TRACE("test_arity/2, label=%i, arg1=%lx\n", label, arg1); - if (term_is_tuple(arg1) && term_get_tuple_arity(arg1) == arity) { + if (term_is_tuple(arg1) && (uint32_t) term_get_tuple_arity(arg1) == arity) { NEXT_INSTRUCTION(next_off); } else { i = (uint8_t *) mod->labels[label] - code; @@ -2655,7 +2667,7 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f void *jump_to_address = NULL; #endif - for (int j = 0; j < size / 2; j++) { + for (uint32_t j = 0; j < size / 2; j++) { term cmp_value; DECODE_COMPACT_TERM(cmp_value, code, i, next_off) uint32_t jmp_label; @@ -2714,7 +2726,7 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f int arity = term_get_tuple_arity(src_value); #endif - for (int j = 0; j < size / 2; j++) { + for (uint32_t j = 0; j < size / 2; j++) { uint32_t cmp_value; DECODE_LITERAL(cmp_value, code, i, next_off) uint32_t jmp_label; @@ -2726,7 +2738,7 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f #ifdef IMPL_EXECUTE_LOOP //TODO: check if src_value is a tuple - if (!jump_to_address && (arity == cmp_value)) { + if (!jump_to_address && ((uint32_t) arity == cmp_value)) { jump_to_address = mod->labels[jmp_label]; } #endif @@ -2841,7 +2853,7 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f USED_BY_TRACE(element); #ifdef IMPL_EXECUTE_LOOP - if (UNLIKELY(!term_is_tuple(src_value) || (element >= term_get_tuple_arity(src_value)))) { + if (UNLIKELY(!term_is_tuple(src_value) || (element >= (uint32_t) term_get_tuple_arity(src_value)))) { AVM_ABORT(); } @@ -2869,7 +2881,7 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f TRACE("set_tuple_element/2\n"); #ifdef IMPL_EXECUTE_LOOP - if (UNLIKELY(!term_is_tuple(tuple) || (position >= term_get_tuple_arity(tuple)))) { + if (UNLIKELY(!term_is_tuple(tuple) || (position >= (uint32_t) term_get_tuple_arity(tuple)))) { AVM_ABORT(); } @@ -2932,7 +2944,7 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f WRITE_REGISTER(dreg_type, dreg, t); #endif - for (int j = 0; j < size; j++) { + for (uint32_t j = 0; j < size; j++) { if (code[i + next_off] != OP_PUT) { fprintf(stderr, "Expected put, got opcode: %i\n", code[i + next_off]); AVM_ABORT(); @@ -3397,13 +3409,14 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f break; case ERROR_ATOM_INDEX: { + ctx->x[2] = stacktrace_build(ctx, &ctx->x[2]); + if (UNLIKELY(memory_ensure_free(ctx, 6) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } term reason_tuple = term_alloc_tuple(2, ctx); term_put_tuple_element(reason_tuple, 0, ctx->x[1]); - // TODO add stacktrace - term_put_tuple_element(reason_tuple, 1, UNDEFINED_ATOM); + term_put_tuple_element(reason_tuple, 1, ctx->x[2]); term exit_tuple = term_alloc_tuple(2, ctx); term_put_tuple_element(exit_tuple, 0, EXIT_ATOM); term_put_tuple_element(exit_tuple, 1, reason_tuple); @@ -4382,6 +4395,9 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f SCHEDULE_NEXT(mod, INSTRUCTION_POINTER()); continue; } + + // save instruction offset in case of error + int orig_i = i; NEXT_INSTRUCTION(next_off); if (UNLIKELY(!term_is_atom(module) || !term_is_atom(function))) { @@ -4396,6 +4412,7 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f term native_return; if (maybe_call_native(ctx, module_name, function_name, arity, &native_return)) { if (UNLIKELY(term_is_invalid_term(native_return))) { + i = orig_i; HANDLE_ERROR(); } ctx->x[0] = native_return; @@ -4403,10 +4420,12 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f } else { Module *target_module = globalcontext_get_module(ctx->global, module_name); if (IS_NULL_PTR(target_module)) { + i = orig_i; HANDLE_ERROR(); } int target_label = module_search_exported_function(target_module, function_name, arity); if (target_label == 0) { + i = orig_i; HANDLE_ERROR(); } ctx->cp = module_address(mod->module_index, i); @@ -4781,6 +4800,10 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f TRACE("line/1: %i\n", line_number); + #ifdef IMPL_CODE_LOADER + module_insert_line_ref_offset(mod, line_number, i); + #endif + NEXT_INSTRUCTION(next_off); break; } @@ -4808,14 +4831,14 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f #ifdef IMPL_EXECUTE_LOOP int list_off = next_off; #endif - int num_elements = list_len / 2; + uint32_t num_elements = list_len / 2; // // Count how many of the entries in list(...) are not already in src // #ifdef IMPL_EXECUTE_LOOP unsigned new_entries = 0; #endif - for (int j = 0; j < num_elements; ++j) { + for (uint32_t j = 0; j < num_elements; ++j) { term key, value; DECODE_COMPACT_TERM(key, code, i, next_off); DECODE_COMPACT_TERM(value, code, i, next_off); @@ -4846,7 +4869,7 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f if (IS_NULL_PTR(kv)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - for (int j = 0; j < num_elements; j++) { + for (uint32_t j = 0; j < num_elements; j++) { term key, value; DECODE_COMPACT_TERM(key, code, i, list_off); DECODE_COMPACT_TERM(value, code, i, list_off); @@ -4860,8 +4883,8 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f // term map = term_alloc_map_maybe_shared(ctx, new_map_size, is_shared ? term_get_map_keys(src) : term_invalid_term()); size_t src_pos = 0; - int kv_pos = 0; - for (int j = 0; j < new_map_size; j++) { + uint32_t kv_pos = 0; + for (size_t j = 0; j < new_map_size; j++) { if (src_pos >= src_size) { term new_key = kv[kv_pos].key; term new_value = kv[kv_pos].value; @@ -4924,11 +4947,11 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f #ifdef IMPL_EXECUTE_LOOP int list_off = next_off; #endif - int num_elements = list_len / 2; + uint32_t num_elements = list_len / 2; // // Make sure every key from list is in src // - for (int j = 0; j < num_elements; ++j) { + for (uint32_t j = 0; j < num_elements; ++j) { term key, value; DECODE_COMPACT_TERM(key, code, i, next_off); DECODE_COMPACT_TERM(value, code, i, next_off); @@ -4953,13 +4976,13 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f // Create a new map of the same size as src and populate with entries from src // term map = term_alloc_map_maybe_shared(ctx, src_size, term_get_map_keys(src)); - for (int j = 0; j < src_size; ++j) { + for (size_t j = 0; j < src_size; ++j) { term_set_map_assoc(map, j, term_get_map_key(src, j), term_get_map_value(src, j)); } // // Copy the new terms into the new map, in situ only // - for (int j = 0; j < num_elements; ++j) { + for (uint32_t j = 0; j < num_elements; ++j) { term key, value; DECODE_COMPACT_TERM(key, code, i, list_off); DECODE_COMPACT_TERM(value, code, i, list_off); @@ -5014,7 +5037,7 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f uint32_t list_len; DECODE_LITERAL(list_len, code, i, next_off); int fail = 0; - for (int j = 0; j < list_len && !fail; ++j) { + for (uint32_t j = 0; j < list_len && !fail; ++j) { term key; DECODE_COMPACT_TERM(key, code, i, next_off); @@ -5043,9 +5066,9 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f DECODE_EXTENDED_LIST_TAG(code, i, next_off); uint32_t list_len; DECODE_LITERAL(list_len, code, i, next_off); - int num_elements = list_len / 2; - int fail = 0; - for (int j = 0; j < num_elements && !fail; ++j) { + uint32_t num_elements = list_len / 2; + uint32_t fail = 0; + for (uint32_t j = 0; j < num_elements && !fail; ++j) { term key; DECODE_COMPACT_TERM(key, code, i, next_off); dreg_t dreg; @@ -5083,7 +5106,7 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f #ifdef IMPL_EXECUTE_LOOP TRACE("is_tagged_tuple/2, label=%u, arg1=%p, arity=%u, atom_id=%p\n", (unsigned) label, (void *) arg1, (unsigned) arity, (void *) tag_atom); - if (term_is_tuple(arg1) && (term_get_tuple_arity(arg1) == arity) && (term_get_tuple_element(arg1, 0) == tag_atom)) { + if (term_is_tuple(arg1) && ((uint32_t) term_get_tuple_arity(arg1) == arity) && (term_get_tuple_element(arg1, 0) == tag_atom)) { NEXT_INSTRUCTION(next_off); } else { i = POINTER_TO_II(mod->labels[label]); @@ -5366,6 +5389,22 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f } #endif + case OP_BUILD_STACKTRACE: { + int next_off = 1; + + TRACE("build_stacktrace/0\n"); + + #ifdef IMPL_EXECUTE_LOOP + + ctx->x[0] = stacktrace_build(ctx, &ctx->x[0]); + + #endif + + NEXT_INSTRUCTION(next_off); + + break; + } + #ifdef ENABLE_OTP21 case OP_GET_HD: { int next_off = 1; @@ -5439,7 +5478,7 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f term t = term_alloc_tuple(size, ctx); #endif - for (int j = 0; j < size; j++) { + for (uint32_t j = 0; j < size; j++) { term element; DECODE_COMPACT_TERM(element, code, i, next_off) @@ -5554,7 +5593,7 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f boxed_func[2] = term_from_int(fun_index); #endif - for (int j = 0; j < size; j++) { + for (uint32_t j = 0; j < size; j++) { term arg; DECODE_COMPACT_TERM(arg, code, i, next_off); #ifdef IMPL_EXECUTE_LOOP @@ -5575,7 +5614,7 @@ static bool maybe_call_native(Context *ctx, AtomString module_name, AtomString f DECODE_EXTENDED_LIST_TAG(code, i, next_off); uint32_t size; DECODE_LITERAL(size, code, i, next_off); - for (int j = 0; j < size; j++) { + for (uint32_t j = 0; j < size; j++) { uint32_t target; DECODE_YREG(target, code, i, next_off); #ifdef IMPL_EXECUTE_LOOP diff --git a/src/libAtomVM/stacktrace.c b/src/libAtomVM/stacktrace.c new file mode 100644 index 0000000000..4d8287cd49 --- /dev/null +++ b/src/libAtomVM/stacktrace.c @@ -0,0 +1,337 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2022 Fred Dushin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#include "stacktrace.h" +#include "defaultatoms.h" + +#ifndef AVM_CREATE_STACKTRACES + +term stacktrace_create_raw(Context *ctx, Module *mod, int current_offset) +{ + UNUSED(ctx); + UNUSED(mod); + UNUSED(current_offset); + return UNDEFINED_ATOM; +} + +term stacktrace_build(Context *ctx, term *stack_info) +{ + UNUSED(ctx); + UNUSED(stack_info); + return UNDEFINED_ATOM; +} + +#else + +static void cp_to_mod_lbl_off(term cp, Context *ctx, Module **cp_mod, int *label, int *l_off, long *mod_offset) +{ + Module *mod = ctx->global->modules_by_index[cp >> 24]; + *mod_offset = (cp & 0xFFFFFF) >> 2; + + *cp_mod = mod; + + uint8_t *code = &mod->code->code[0]; + int labels_count = ENDIAN_SWAP_32(mod->code->labels); + + int i = 1; + uint8_t *l = mod->labels[1]; + while (*mod_offset > l - code) { + i++; + if (i >= labels_count) { + // last label + 1 is reserved for end of module. + *label = i; + *l_off = 0; + return; + } + l = mod->labels[i]; + } + + *label = i - 1; + *l_off = *mod_offset - ((uint8_t *) mod->labels[*label] - code); +} + +static bool is_module_member(Module *mod, Module **mods, unsigned long len) +{ + for (unsigned long i = 0; i < len; ++i) { + if (mods[i] == mod) { + return true; + } + } + return false; +} + +term stacktrace_create_raw(Context *ctx, Module *mod, int current_offset) +{ + unsigned int num_frames = 0; + unsigned int num_aux_terms = 0; + unsigned int filename_lens = 0; + Module *prev_mod = NULL; + long prev_mod_offset = -1; + term *ct = ctx->e; + + unsigned long stack_size = context_stack_size(ctx); + Module **modules = malloc(stack_size * sizeof(Module *)); + if (IS_NULL_PTR(modules)) { + fprintf(stderr, "Unable to allocate space for modules list. No stacktrace will be created\n"); + return UNDEFINED_ATOM; + } + + size_t num_mods = 0; + + while (ct != ctx->stack_base) { + if (term_is_cp(*ct)) { + + Module *cp_mod; + int label; + int offset; + long mod_offset; + + cp_to_mod_lbl_off(*ct, ctx, &cp_mod, &label, &offset, &mod_offset); + if (mod_offset != cp_mod->end_instruction_ii && !(prev_mod == cp_mod && mod_offset == prev_mod_offset)) { + ++num_frames; + prev_mod = cp_mod; + prev_mod_offset = mod_offset; + if (module_has_line_chunk(cp_mod)) { + if (!is_module_member(cp_mod, modules, num_mods)) { + modules[num_mods] = cp_mod; + filename_lens += cp_mod->filenames[0].len; + num_mods++; + } + num_aux_terms++; + } + } + } else if (term_is_catch_label(*ct)) { + int module_index; + int label = term_to_catch_label_and_module(*ct, &module_index); + + Module *cl_mod = ctx->global->modules_by_index[module_index]; + uint8_t *code = &cl_mod->code->code[0]; + int mod_offset = ((uint8_t *) cl_mod->labels[label] - code); + + if (!(prev_mod == cl_mod && mod_offset == prev_mod_offset)) { + ++num_frames; + prev_mod = cl_mod; + prev_mod_offset = mod_offset; + if (module_has_line_chunk(cl_mod)) { + if (!is_module_member(cl_mod, modules, num_mods)) { + modules[num_mods] = cl_mod; + filename_lens += cl_mod->filenames[0].len; + num_mods++; + } + num_aux_terms++; + } + } + } + ct++; + } + + num_frames++; + if (module_has_line_chunk(mod)) { + if (!is_module_member(mod, modules, num_mods)) { + filename_lens += mod->filenames[0].len; + num_mods++; + } + num_aux_terms++; + } + + free(modules); + + // {num_frames, num_aux_terms, filename_lens, num_mods, [{module, offset}, ...]} + size_t requested_size = TUPLE_SIZE(5) + num_frames * (2 + TUPLE_SIZE(2)); + if (UNLIKELY(memory_ensure_free(ctx, requested_size) != MEMORY_GC_OK)) { + fprintf(stderr, "WARNING: Unable to allocate heap space for raw stacktrace\n"); + return OUT_OF_MEMORY_ATOM; + } + + term raw_stacktrace = term_nil(); + + term frame_info = term_alloc_tuple(2, ctx); + term_put_tuple_element(frame_info, 0, term_from_int(mod->module_index)); + term_put_tuple_element(frame_info, 1, term_from_int(current_offset)); + raw_stacktrace = term_list_prepend(frame_info, raw_stacktrace, ctx); + + prev_mod = NULL; + prev_mod_offset = -1; + ct = ctx->e; + while (ct != ctx->stack_base) { + if (term_is_cp(*ct)) { + Module *cp_mod; + int label; + int offset; + long mod_offset; + + cp_to_mod_lbl_off(*ct, ctx, &cp_mod, &label, &offset, &mod_offset); + if (mod_offset != cp_mod->end_instruction_ii && !(prev_mod == cp_mod && mod_offset == prev_mod_offset)) { + + prev_mod = cp_mod; + prev_mod_offset = mod_offset; + + term frame_info = term_alloc_tuple(2, ctx); + term_put_tuple_element(frame_info, 0, term_from_int(cp_mod->module_index)); + term_put_tuple_element(frame_info, 1, term_from_int(mod_offset)); + + raw_stacktrace = term_list_prepend(frame_info, raw_stacktrace, ctx); + } + } else if (term_is_catch_label(*ct)) { + + int module_index; + int label = term_to_catch_label_and_module(*ct, &module_index); + Module *cl_mod = ctx->global->modules_by_index[module_index]; + uint8_t *code = &cl_mod->code->code[0]; + int mod_offset = ((uint8_t *) cl_mod->labels[label] - code); + + if (!(prev_mod == cl_mod && mod_offset == prev_mod_offset)) { + + prev_mod = cl_mod; + prev_mod_offset = mod_offset; + + term frame_info = term_alloc_tuple(2, ctx); + term_put_tuple_element(frame_info, 0, term_from_int(module_index)); + term_put_tuple_element(frame_info, 1, term_from_int(mod_offset)); + + raw_stacktrace = term_list_prepend(frame_info, raw_stacktrace, ctx); + } + } + ct++; + } + + term stack_info = term_alloc_tuple(5, ctx); + term_put_tuple_element(stack_info, 0, term_from_int(num_frames)); + term_put_tuple_element(stack_info, 1, term_from_int(num_aux_terms)); + term_put_tuple_element(stack_info, 2, term_from_int(filename_lens)); + term_put_tuple_element(stack_info, 3, term_from_int(num_mods)); + term_put_tuple_element(stack_info, 4, raw_stacktrace); + + return stack_info; +} + +struct ModulePathPair +{ + term module; + term path; +}; + +static term find_path_created(term module_name, struct ModulePathPair *module_paths, int len) +{ + for (int i = 0; i < len; ++i) { + if (module_paths[i].module == module_name) { + return module_paths[i].path; + } + } + return term_invalid_term(); +} + +term stacktrace_build(Context *ctx, term *stack_info) +{ + if (*stack_info == OUT_OF_MEMORY_ATOM) { + return *stack_info; + } + if (!term_is_tuple(*stack_info)) { + return UNDEFINED_ATOM; + } + + int num_frames = term_to_int(term_get_tuple_element(*stack_info, 0)); + int num_aux_terms = term_to_int(term_get_tuple_element(*stack_info, 1)); + int filename_lens = term_to_int(term_get_tuple_element(*stack_info, 2)); + int num_mods = term_to_int(term_get_tuple_element(*stack_info, 3)); + + struct ModulePathPair *module_paths = malloc(num_mods * sizeof(struct ModulePathPair)); + if (IS_NULL_PTR(module_paths)) { + fprintf(stderr, "Unable to allocate space for module paths. Returning raw stacktrace.\n"); + return *stack_info; + } + + // + // [{module, function, arity, [{file, string()}, {line, int}]}, ...] + // + size_t requested_size = (TUPLE_SIZE(4) + 2) * num_frames + num_aux_terms * (2 + 2 * TUPLE_SIZE(2)) + 2 * filename_lens; + if (UNLIKELY(memory_ensure_free(ctx, requested_size) != MEMORY_GC_OK)) { + free(module_paths); + return OUT_OF_MEMORY_ATOM; + } + + // Note. Safe to get stacktrace after GC when stack_info comes from x[0] + term raw_stacktrace = term_get_tuple_element(*stack_info, 4); + + term stacktrace = term_nil(); + term el = raw_stacktrace; + int module_path_idx = 0; + while (!term_is_nil(el)) { + term mod_index_tuple = term_get_list_head(el); + term cp = module_address( + term_to_int(term_get_tuple_element(mod_index_tuple, 0)), + term_to_int(term_get_tuple_element(mod_index_tuple, 1))); + + Module *cp_mod; + int label; + int offset; + long mod_offset; + cp_to_mod_lbl_off(cp, ctx, &cp_mod, &label, &offset, &mod_offset); + + term module_name = module_get_name(cp_mod); + + term frame_i = term_alloc_tuple(4, ctx); + term_put_tuple_element(frame_i, 0, module_name); + + term aux_data = term_nil(); + if (module_has_line_chunk(cp_mod)) { + + term line_tuple = term_alloc_tuple(2, ctx); + term_put_tuple_element(line_tuple, 0, context_make_atom(ctx, ATOM_STR("\x4", "line"))); + int line = module_find_line(cp_mod, (unsigned int) mod_offset); + term_put_tuple_element(line_tuple, 1, line == -1 ? UNDEFINED_ATOM : term_from_int(line)); + aux_data = term_list_prepend(line_tuple, aux_data, ctx); + + term file_tuple = term_alloc_tuple(2, ctx); + term_put_tuple_element(file_tuple, 0, context_make_atom(ctx, ATOM_STR("\x4", "file"))); + + term path = find_path_created(module_name, module_paths, module_path_idx); + if (term_is_invalid_term(path)) { + path = term_from_string((const uint8_t *) cp_mod->filenames[0].data, cp_mod->filenames[0].len, ctx); + module_paths[module_path_idx].module = module_name; + module_paths[module_path_idx].path = path; + module_path_idx++; + } + term_put_tuple_element(file_tuple, 1, path); + aux_data = term_list_prepend(file_tuple, aux_data, ctx); + } + term_put_tuple_element(frame_i, 3, aux_data); + + AtomString function_name = NULL; + int arity = 0; + bool result = module_get_function_from_label(cp_mod, label, &function_name, &arity); + + if (LIKELY(result)) { + term_put_tuple_element(frame_i, 1, context_make_atom(ctx, function_name)); + term_put_tuple_element(frame_i, 2, term_from_int(arity)); + } else { + term_put_tuple_element(frame_i, 1, UNDEFINED_ATOM); + term_put_tuple_element(frame_i, 2, term_from_int(0)); + } + stacktrace = term_list_prepend(frame_i, stacktrace, ctx); + + el = term_get_list_tail(el); + } + free(module_paths); + + return stacktrace; +} + +#endif diff --git a/src/libAtomVM/stacktrace.h b/src/libAtomVM/stacktrace.h new file mode 100644 index 0000000000..24dc6d559d --- /dev/null +++ b/src/libAtomVM/stacktrace.h @@ -0,0 +1,39 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2022 Fred Dushin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#ifndef _STACKTRACE_H_ +#define _STACKTRACE_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include "context.h" +#include "module.h" +#include "term.h" + +term stacktrace_create_raw(Context *ctx, Module *mod, int current_offset); +term stacktrace_build(Context *ctx, term *stack_info); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/platforms/esp32/CMakeLists.txt b/src/platforms/esp32/CMakeLists.txt index ed6f41415b..a2f1018759 100644 --- a/src/platforms/esp32/CMakeLists.txt +++ b/src/platforms/esp32/CMakeLists.txt @@ -31,5 +31,6 @@ idf_build_set_property(C_COMPILE_OPTIONS ${c_compile_options}) option(AVM_DISABLE_FP "Disable floating point support." OFF) option(AVM_USE_32BIT_FLOAT "Use 32 bit floats." OFF) option(AVM_VERBOSE_ABORT "Print module and line number on VM abort" OFF) +option(AVM_CREATE_STACKTRACES "Create stacktraces" ON) add_subdirectory(tools) diff --git a/src/platforms/stm32/CMakeLists.txt b/src/platforms/stm32/CMakeLists.txt index 20b1f5d36f..ee59b08a8d 100644 --- a/src/platforms/stm32/CMakeLists.txt +++ b/src/platforms/stm32/CMakeLists.txt @@ -27,6 +27,7 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) option(AVM_DISABLE_FP "Disable floating point support." OFF) option(AVM_USE_32BIT_FLOAT "Use 32 bit floats." OFF) option(AVM_VERBOSE_ABORT "Print module and line number on VM abort" OFF) +option(AVM_CREATE_STACKTRACES "Create stacktraces" ON) # Include an error in case the user forgets to specify ARM as a toolchain if (NOT CMAKE_TOOLCHAIN_FILE) diff --git a/tests/erlang_tests/CMakeLists.txt b/tests/erlang_tests/CMakeLists.txt index 857b7a10fe..e3533f5533 100644 --- a/tests/erlang_tests/CMakeLists.txt +++ b/tests/erlang_tests/CMakeLists.txt @@ -436,6 +436,8 @@ compile_erlang(link_throw) compile_erlang(unlink_error) compile_erlang(trap_exit_flag) +compile_erlang(test_stacktrace) + add_custom_target(erlang_test_modules DEPENDS add.beam fact.beam @@ -845,4 +847,6 @@ add_custom_target(erlang_test_modules DEPENDS link_throw.beam unlink_error.beam trap_exit_flag.beam + + test_stacktrace.beam ) diff --git a/tests/erlang_tests/test_stacktrace.erl b/tests/erlang_tests/test_stacktrace.erl new file mode 100644 index 0000000000..8655f4a711 --- /dev/null +++ b/tests/erlang_tests/test_stacktrace.erl @@ -0,0 +1,324 @@ +% +% This file is part of AtomVM. +% +% Copyright 2022 Fred Dushin +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% +-module(test_stacktrace). + +-export([start/0, maybe_crash/1]). + +start() -> + ok = test_local_throw(), + ok = test_local_error(), + ok = test_badmatch(), + ok = test_apply(), + ok = test_fun(), + ok = test_remote_throw(), + ok = test_tail_recursive_throw(), + ok = test_body_recursive_throw(), + ok = test_spawned_throw(), + ok = test_catch(), + ok = maybe_test_filelineno(), + 0. + +test_local_throw() -> + ok = + try + maybe_crash(throw_me), + fail + catch + throw:throw_me:Stacktrace -> + expect_stacktrace( + Stacktrace, + [ + {?MODULE, maybe_crash, 1}, + {?MODULE, test_local_throw, 0}, + {?MODULE, start, 0} + ] + ) + end. + +test_local_error() -> + ok = + try + maybe_crash(error_me), + fail + catch + error:error_me:Stacktrace -> + expect_stacktrace( + Stacktrace, + [ + {?MODULE, maybe_crash, 1}, + {?MODULE, test_local_error, 0}, + {?MODULE, start, 0} + ] + ) + end. + +test_badmatch() -> + ok = + try + maybe_badmatch(crash_me), + fail + catch + error:{badmatch, crash_me}:Stacktrace -> + expect_stacktrace( + Stacktrace, + [ + {?MODULE, maybe_badmatch, 1}, + {?MODULE, test_badmatch, 0}, + {?MODULE, start, 0} + ] + ) + end. + +test_remote_throw() -> + ok = + try + ?MODULE:maybe_crash(throw_me), + fail + catch + throw:throw_me:Stacktrace -> + expect_stacktrace( + Stacktrace, + [ + {?MODULE, maybe_crash, 1}, + {?MODULE, test_remote_throw, 0}, + {?MODULE, start, 0} + ] + ) + end. + +test_apply() -> + ok = + try + erlang:apply(?MODULE, maybe_crash, [throw_me]), + fail + catch + throw:throw_me:Stacktrace -> + expect_stacktrace( + Stacktrace, + [ + {?MODULE, maybe_crash, 1}, + {?MODULE, test_apply, 0}, + {?MODULE, start, 0} + ] + ) + end. + +test_fun() -> + ok = + try + F = fun() -> maybe_crash(throw_me) end, + F(), + fail + catch + throw:throw_me:Stacktrace -> + expect_stacktrace( + Stacktrace, + [ + {?MODULE, maybe_crash, 1}, + {?MODULE, test_fun, 0}, + {?MODULE, start, 0} + ] + ) + end. + +a_tail_recursive_function(0, Msg) -> + maybe_crash(Msg); +a_tail_recursive_function(I, Msg) -> + a_tail_recursive_function(I - 1, Msg). + +test_tail_recursive_throw() -> + ok = + try + a_tail_recursive_function(5, throw_me), + fail + catch + throw:throw_me:Stacktrace -> + expect_stacktrace( + Stacktrace, + [ + {?MODULE, maybe_crash, 1}, + {?MODULE, test_tail_recursive_throw, 0}, + {?MODULE, start, 0} + ] + ) + end. + +a_body_recursive_function(0, _Msg) -> + ok; +a_body_recursive_function(I, Msg) -> + case a_body_recursive_function(I - 1, Msg) of + ok -> + maybe_crash(Msg); + _ -> + error + end. + +test_body_recursive_throw() -> + ok = + try + a_body_recursive_function(5, throw_me), + fail + catch + throw:throw_me:Stacktrace -> + expect_stacktrace( + Stacktrace, + [ + {?MODULE, maybe_crash, 1}, + {?MODULE, a_body_recursive_function, 2}, + {?MODULE, test_body_recursive_throw, 0}, + {?MODULE, start, 0} + ] + ) + end. + +test_spawned_throw() -> + Self = self(), + spawn( + fun() -> + try + do_some_stuff(blah), + a_tail_recursive_function(5, throw_me), + do_some_stuff(blah), + Self ! fail + catch + throw:throw_me:Stacktrace -> + Result = expect_stacktrace( + Stacktrace, + [ + {?MODULE, maybe_crash, 1}, + {?MODULE, '-test_spawned_throw/0-fun-0-', 1} + ] + ), + Self ! Result + end + end + ), + receive + Result -> + erlang:display(Result), + Result + end. + +test_catch() -> + {'EXIT', {error_me, Stacktrace}} = (catch maybe_crash(error_me)), + Result = expect_stacktrace( + Stacktrace, + [ + {?MODULE, maybe_crash, 1}, + {?MODULE, test_catch, 0}, + {?MODULE, start, 0} + ] + ), + do_some_stuff(Result), + Result. + +maybe_test_filelineno() -> + ok = + try + throw_with_file_and_line(), + fail + catch + throw:{File, Line}:Stacktrace -> + [Frame | _] = Stacktrace, + {?MODULE, throw_with_file_and_line, 0, AuxData} = Frame, + case {get_value(file, AuxData), get_value(line, AuxData)} of + {undefined, undefined} -> + ok; + {F, L} -> + Ef = + case is_binary(F) of + true -> + erlang:binary_to_list(F); + _ -> + F + end, + case File == Ef andalso Line == L of + true -> + ok; + _ -> + {unexpected_file_line, F, L} + end + end + end. + +get_value(_Key, []) -> + undefined; +get_value(Key, [{Key, Value} | _]) -> + Value; +get_value(Key, [_ | T]) -> + get_value(Key, T). + +throw_with_file_and_line() -> + throw({?FILE, ?LINE}). + +maybe_crash(Term) -> + case Term of + ok -> + ok; + throw_me -> + throw(Term); + error_me -> + error(Term) + end. + +maybe_badmatch(Term) -> + ok = Term. + +do_some_stuff(_) -> + ok. + +expect_stacktrace(Stacktrace, Expect) -> + % erlang:display({stacktrace, [{M, F, A} || {M, F, A, _} <- Stacktrace]}), + % erlang:display({stacktrace, Stacktrace}), + % erlang:display({expect, Expect}), + MFAStacktrace = [{M, F, A} || {M, F, A, _} <- Stacktrace], + case contains_in_order(Expect, remove_duplicates(MFAStacktrace, [])) of + true -> + ok; + _ -> + error(shit) + end. + +remove_duplicates([], Accum) -> + reverse(Accum, []); +remove_duplicates([H, H | T], Accum) -> + remove_duplicates([H | T], Accum); +remove_duplicates([H | T], Accum) -> + remove_duplicates(T, [H | Accum]). + +reverse([], Accum) -> Accum; +reverse([H | T], Accum) -> reverse(T, [H | Accum]). + +contains_in_order([], _) -> + true; +contains_in_order([H | T], E) -> + case find(H, E) of + not_found -> + false; + E2 -> + contains_in_order(T, E2) + end. + +find(_H, []) -> + not_found; +find(H, [H | T]) -> + T; +find(H, [_ | T]) -> + find(H, T). diff --git a/tests/test.c b/tests/test.c index 01fca37635..8f6eb21ccd 100644 --- a/tests/test.c +++ b/tests/test.c @@ -71,6 +71,12 @@ struct Test #define SKIP_FP false #endif +#ifndef AVM_CREATE_STACKTRACES +#define SKIP_STACKTRACES true +#else +#define SKIP_STACKTRACES false +#endif + struct Test tests[] = { TEST_CASE_EXPECTED(add, 17), TEST_CASE_EXPECTED(fact, 120), @@ -474,6 +480,7 @@ struct Test tests[] = { TEST_CASE_EXPECTED(link_throw, 1), TEST_CASE_EXPECTED(unlink_error, 1), TEST_CASE_EXPECTED(trap_exit_flag, 1), + TEST_CASE_COND(test_stacktrace, 0, SKIP_STACKTRACES), // TEST CRASHES HERE: TEST_CASE(memlimit), diff --git a/tools/packbeam/packbeam.c b/tools/packbeam/packbeam.c index eff55bd95c..ba7fcf185d 100644 --- a/tools/packbeam/packbeam.c +++ b/tools/packbeam/packbeam.c @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -48,9 +49,9 @@ typedef struct FileData { static void pad_and_align(FILE *f); static void *uncompress_literals(const uint8_t *litT, int size, size_t *uncompressedSize); static void add_module_header(FILE *f, const char *module_name, uint32_t flags); -static void pack_beam_file(FILE *pack, const uint8_t *data, size_t size, const char *filename, int is_entrypoint); +static void pack_beam_file(FILE *pack, const uint8_t *data, size_t size, const char *filename, int is_entrypoint, bool include_lines); -static int do_pack(int argc, char **argv, int is_archive); +static int do_pack(int argc, char **argv, int is_archive, bool include_lines); static int do_list(int argc, char **argv); static void usage3(FILE *out, const char *program, const char *msg) { @@ -59,6 +60,7 @@ static void usage3(FILE *out, const char *program, const char *msg) { } fprintf(out, "Usage: %s [-h] [-l] []\n", program); fprintf(out, " -h Print this help menu.\n"); + fprintf(out, " -i Include file and line information.\n"); fprintf(out, " -l List the contents of an AVM file.\n"); fprintf(out, " [-a] + Create an AVM file (archive if -a specified).\n" ); @@ -76,7 +78,8 @@ int main(int argc, char **argv) const char *action = "pack"; int is_archive = 0; - while ((opt = getopt(argc, argv, "hal")) != -1) { + bool include_lines = false; + while ((opt = getopt(argc, argv, "hail")) != -1) { switch(opt) { case 'h': usage(argv[0]); @@ -84,6 +87,9 @@ int main(int argc, char **argv) case 'a': is_archive = 1; break; + case 'i': + include_lines = true; + break; case 'l': action = "list"; break; @@ -109,7 +115,7 @@ int main(int argc, char **argv) usage3(stderr, argv[0], "Missing options for pack\n"); return EXIT_FAILURE; } - return do_pack(new_argc, new_argv, is_archive); + return do_pack(new_argc, new_argv, is_archive, include_lines); } else { return do_list(new_argc, new_argv); } @@ -213,7 +219,7 @@ static void validate_pack_options(int argc, char ** argv) } } -static int do_pack(int argc, char **argv, int is_archive) +static int do_pack(int argc, char **argv, int is_archive, bool include_lines) { validate_pack_options(argc, argv); @@ -263,7 +269,7 @@ static int do_pack(int argc, char **argv, int is_archive) } } else { char *filename = basename(argv[i]); - pack_beam_file(pack, file_data, file_size, filename, !is_archive && i == 1); + pack_beam_file(pack, file_data, file_size, filename, !is_archive && i == 1, include_lines); } } @@ -273,7 +279,7 @@ static int do_pack(int argc, char **argv, int is_archive) return EXIT_SUCCESS; } -static void pack_beam_file(FILE *pack, const uint8_t *data, size_t size, const char *section_name, int is_entrypoint) +static void pack_beam_file(FILE *pack, const uint8_t *data, size_t size, const char *section_name, int is_entrypoint, bool include_lines) { size_t zero_pos = ftell(pack); @@ -328,6 +334,10 @@ static void pack_beam_file(FILE *pack, const uint8_t *data, size_t size, const c assert_fwrite(data + offsets[STRT], sizes[STRT] + IFF_SECTION_HEADER_SIZE, pack); pad_and_align(pack); } + if (offsets[LINT] && include_lines) { + assert_fwrite(data + offsets[LINT], sizes[LINT] + IFF_SECTION_HEADER_SIZE, pack); + pad_and_align(pack); + } if (offsets[LITT]) { size_t u_size;