diff --git a/core/.changelog.d/1557.added b/core/.changelog.d/1557.added
new file mode 100644
index 000000000..e195a6d6f
--- /dev/null
+++ b/core/.changelog.d/1557.added
@@ -0,0 +1 @@
+[emulator] Added option to dump detailed Micropython memory layout
diff --git a/core/embed/extmod/modtrezorui/modtrezorui.c b/core/embed/extmod/modtrezorui/modtrezorui.c
index b1193a629..924b58861 100644
--- a/core/embed/extmod/modtrezorui/modtrezorui.c
+++ b/core/embed/extmod/modtrezorui/modtrezorui.c
@@ -25,6 +25,7 @@
#if MICROPY_PY_TREZORUI
+#include "embed/extmod/trezorobj.h"
#include "modtrezorui-display.h"
STATIC const mp_rom_map_elem_t mp_module_trezorui_globals_table[] = {
diff --git a/core/embed/extmod/modtrezorutils/modtrezorutils-meminfo.h b/core/embed/extmod/modtrezorutils/modtrezorutils-meminfo.h
new file mode 100644
index 000000000..7eedd6026
--- /dev/null
+++ b/core/embed/extmod/modtrezorutils/modtrezorutils-meminfo.h
@@ -0,0 +1,728 @@
+/*
+ * This file is part of the TREZOR project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#if !TREZOR_EMULATOR || PYOPT
+#define MEMINFO_DICT_ENTRIES /* empty */
+
+#else
+
+#include "py/bc.h"
+#include "py/gc.h"
+#include "py/nlr.h"
+#include "py/objarray.h"
+#include "py/objfun.h"
+#include "py/objgenerator.h"
+#include "py/objlist.h"
+#include "py/objstr.h"
+#include "py/objtype.h"
+
+#include "embed/extmod/trezorobj.h"
+#include "embed/trezorhal/usb.h"
+
+#include
+
+#define WORDS_PER_BLOCK ((MICROPY_BYTES_PER_GC_BLOCK) / BYTES_PER_WORD)
+#define BYTES_PER_BLOCK (MICROPY_BYTES_PER_GC_BLOCK)
+
+// ATB = allocation table byte
+// 0b00 = FREE -- free block
+// 0b01 = HEAD -- head of a chain of blocks
+// 0b10 = TAIL -- in the tail of a chain of blocks
+// 0b11 = MARK -- marked head block
+
+#define AT_FREE (0)
+#define AT_HEAD (1)
+#define AT_TAIL (2)
+#define AT_MARK (3)
+
+#define BLOCKS_PER_ATB (4)
+#define ATB_MASK_0 (0x03)
+#define ATB_MASK_1 (0x0c)
+#define ATB_MASK_2 (0x30)
+#define ATB_MASK_3 (0xc0)
+
+#define ATB_0_IS_FREE(a) (((a)&ATB_MASK_0) == 0)
+#define ATB_1_IS_FREE(a) (((a)&ATB_MASK_1) == 0)
+#define ATB_2_IS_FREE(a) (((a)&ATB_MASK_2) == 0)
+#define ATB_3_IS_FREE(a) (((a)&ATB_MASK_3) == 0)
+
+#define BLOCK_SHIFT(block) (2 * ((block) & (BLOCKS_PER_ATB - 1)))
+#define ATB_GET_KIND(block) \
+ ((MP_STATE_MEM(gc_alloc_table_start)[(block) / BLOCKS_PER_ATB] >> \
+ BLOCK_SHIFT(block)) & \
+ 3)
+#define ATB_ANY_TO_FREE(block) \
+ do { \
+ MP_STATE_MEM(gc_alloc_table_start) \
+ [(block) / BLOCKS_PER_ATB] &= (~(AT_MARK << BLOCK_SHIFT(block))); \
+ } while (0)
+#define ATB_FREE_TO_HEAD(block) \
+ do { \
+ MP_STATE_MEM(gc_alloc_table_start) \
+ [(block) / BLOCKS_PER_ATB] |= (AT_HEAD << BLOCK_SHIFT(block)); \
+ } while (0)
+#define ATB_FREE_TO_TAIL(block) \
+ do { \
+ MP_STATE_MEM(gc_alloc_table_start) \
+ [(block) / BLOCKS_PER_ATB] |= (AT_TAIL << BLOCK_SHIFT(block)); \
+ } while (0)
+#define ATB_HEAD_TO_MARK(block) \
+ do { \
+ MP_STATE_MEM(gc_alloc_table_start) \
+ [(block) / BLOCKS_PER_ATB] |= (AT_MARK << BLOCK_SHIFT(block)); \
+ } while (0)
+#define ATB_MARK_TO_HEAD(block) \
+ do { \
+ MP_STATE_MEM(gc_alloc_table_start) \
+ [(block) / BLOCKS_PER_ATB] &= (~(AT_TAIL << BLOCK_SHIFT(block))); \
+ } while (0)
+
+#define BLOCK_FROM_PTR(ptr) \
+ (((byte *)(ptr)-MP_STATE_MEM(gc_pool_start)) / BYTES_PER_BLOCK)
+#define PTR_FROM_BLOCK(block) \
+ (((block)*BYTES_PER_BLOCK + (uintptr_t)MP_STATE_MEM(gc_pool_start)))
+#define ATB_FROM_BLOCK(bl) ((bl) / BLOCKS_PER_ATB)
+
+// ptr should be of type void*
+#define VERIFY_PTR(ptr) \
+ (((uintptr_t)(ptr) & (BYTES_PER_BLOCK - 1)) == \
+ 0 /* must be aligned on a block */ \
+ && ptr >= (void *)MP_STATE_MEM( \
+ gc_pool_start) /* must be above start of pool */ \
+ && ptr < (void *)MP_STATE_MEM(gc_pool_end) /* must be below end of pool */ \
+ )
+
+#define Q_GET_DATA(q) \
+ ((q) + MICROPY_QSTR_BYTES_IN_HASH + MICROPY_QSTR_BYTES_IN_LEN)
+extern const qstr_pool_t mp_qstr_const_pool;
+
+size_t find_allocated_size(void const *const ptr) {
+ if (!ptr) {
+ return 0;
+ }
+
+ if (!VERIFY_PTR(ptr)) {
+ // printf("failed to verify ptr: %p\n", ptr);
+ return 0;
+ }
+
+ size_t block = BLOCK_FROM_PTR(ptr);
+ if (ATB_GET_KIND(block) == AT_TAIL) {
+ return 0;
+ }
+ size_t n = 0;
+ do {
+ ++n;
+ } while (ATB_GET_KIND(block + n) == AT_TAIL);
+ return n;
+}
+
+void dump_value(FILE *out, mp_const_obj_t value);
+
+void mark(void const *const ptr) {
+ if (!VERIFY_PTR(ptr)) return;
+ size_t block = BLOCK_FROM_PTR(ptr);
+ if (ATB_GET_KIND(block) == AT_HEAD) {
+ ATB_HEAD_TO_MARK(block);
+ }
+}
+
+bool is_short(mp_const_obj_t value) {
+ return value == NULL || value == MP_OBJ_NULL || mp_obj_is_qstr(value) ||
+ mp_obj_is_small_int(value) || !VERIFY_PTR(value);
+}
+
+static void print_type(FILE *out, const char *typename, const char *shortval,
+ const void *ptr, bool end) {
+ static char unescaped[1000];
+ size_t size = 0;
+ if (!is_short(ptr)) {
+ size = find_allocated_size(ptr);
+ }
+ fprintf(out, "{\"type\": \"%s\", \"alloc\": %ld, \"ptr\": \"%p\"", typename,
+ size, ptr);
+ if (shortval) {
+ assert(strlen(shortval) < 1000);
+ char *c = unescaped;
+ while (*shortval) {
+ if (*shortval == '\\' || *shortval == '"') *c++ = '\\';
+ *c++ = *shortval++;
+ }
+ *c = 0;
+ fprintf(out, ", \"shortval\": \"%s\"", unescaped);
+ } else {
+ fprintf(out, ", \"shortval\": null");
+ }
+ if (end) fprintf(out, "}");
+}
+
+static void print_repr(FILE *out, const char *strbuf, size_t buflen) {
+ fprintf(out, "\"");
+ for (size_t i = 0; i < buflen; ++i) {
+ if (strbuf[i] == '\\')
+ fprintf(out, "\\\\");
+ else if (strbuf[i] == '"')
+ fprintf(out, "\\\"");
+ else if (strbuf[i] >= 0x20 && strbuf[i] <= 0x7e)
+ fprintf(out, "%c", strbuf[i]);
+ else
+ fprintf(out, "\\\\x%02x", (unsigned char)strbuf[i]);
+ }
+ fprintf(out, "\"");
+}
+
+void dump_short(FILE *out, mp_const_obj_t value) {
+ fflush(out);
+ if (value == NULL || value == MP_OBJ_NULL) {
+ fprintf(out, "null");
+
+ } else if (mp_obj_is_qstr(value)) {
+ mp_int_t q = MP_OBJ_QSTR_VALUE(value);
+ print_type(out, "qstr", qstr_str(q), NULL, true);
+
+ } else if (mp_obj_is_small_int(value)) {
+ static char num_buf[100];
+ snprintf(num_buf, 100, "%ld", MP_OBJ_SMALL_INT_VALUE(value));
+ print_type(out, "smallint", num_buf, NULL, true);
+
+ } else if (!VERIFY_PTR(value)) {
+ print_type(out, "romdata", NULL, value, true);
+ }
+}
+
+void dump_short_or_ptr(FILE *out, mp_const_obj_t value) {
+ if (is_short(value))
+ dump_short(out, value);
+ else
+ fprintf(out, "\"%p\"", value);
+}
+
+void dump_map_as_children(FILE *out, const mp_map_t *map) {
+ fprintf(out, ", \"children\": [");
+ bool first = true;
+ for (size_t i = 0; i < map->alloc; ++i) {
+ if (!mp_map_slot_is_filled(map, i)) continue;
+ if (!first) fprintf(out, ",\n");
+ first = false;
+ fprintf(out, "{\"key\": ");
+ dump_short_or_ptr(out, map->table[i].key);
+ fprintf(out, ",\n\"value\": ");
+ dump_short_or_ptr(out, map->table[i].value);
+ fprintf(out, "}");
+ }
+ fprintf(out, "]");
+}
+
+void dump_map_as_values(FILE *out, const void *const owner,
+ const mp_map_t *map) {
+ print_type(out, "mapitems", NULL, map->table, false);
+ fprintf(out, ",\n\"owner\": \"%p\"", owner);
+ fprintf(out, "},\n");
+
+ for (size_t i = 0; i < map->alloc; ++i) {
+ if (!mp_map_slot_is_filled(map, i)) continue;
+ dump_value(out, map->table[i].key);
+ dump_value(out, map->table[i].value);
+ }
+}
+
+void dump_dict_inner(FILE *out, const mp_obj_dict_t *dict) {
+ print_type(out, "dict", NULL, dict, false);
+ dump_map_as_children(out, &dict->map);
+ fprintf(out, "},\n");
+ dump_map_as_values(out, dict, &dict->map);
+}
+
+void dump_function(FILE *out, const mp_obj_fun_bc_t *func) {
+ print_type(out, "function", NULL, func, false);
+ fprintf(out, ",\n\"globals\": \"%p\"", func->globals);
+ fprintf(out, ",\n\"code_alloc\": %ld", find_allocated_size(func->bytecode));
+ fprintf(out, ",\n\"code_ptr\": \"%p\"", func->bytecode);
+ fprintf(out, ",\n\"const_table_alloc\": %ld",
+ find_allocated_size(func->const_table));
+ fprintf(out, ",\n\"const_table_ptr\": \"%p\"", func->const_table);
+ mark(func->bytecode);
+ mark(func->const_table);
+ fprintf(out, "},\n");
+
+ dump_value(out, func->globals);
+}
+
+typedef struct _mp_obj_bound_meth_t {
+ mp_obj_base_t base;
+ mp_obj_t meth;
+ mp_obj_t self;
+} mp_obj_bound_meth_t;
+
+typedef struct _mp_obj_closure_t {
+ mp_obj_base_t base;
+ mp_obj_t fun;
+ size_t n_closed;
+ mp_obj_t closed[];
+} mp_obj_closure_t;
+
+extern const mp_obj_type_t mp_type_bound_meth;
+extern const mp_obj_type_t closure_type;
+extern const mp_obj_type_t mp_type_cell;
+extern const mp_obj_type_t mod_trezorio_WebUSB_type;
+extern const mp_obj_type_t mod_trezorio_USB_type;
+extern const mp_obj_type_t mod_trezorio_VCP_type;
+extern const mp_obj_type_t mod_trezorio_HID_type;
+extern const mp_obj_type_t mod_trezorui_Display_type;
+
+typedef struct _mp_obj_WebUSB_t {
+ mp_obj_base_t base;
+ usb_webusb_info_t info;
+} mp_obj_WebUSB_t;
+
+typedef struct _mp_obj_VCP_t {
+ mp_obj_base_t base;
+ usb_vcp_info_t info;
+} mp_obj_VCP_t;
+
+typedef struct _mp_obj_HID_t {
+ mp_obj_base_t base;
+ usb_hid_info_t info;
+} mp_obj_HID_t;
+
+void dump_bound_method(FILE *out, const mp_obj_bound_meth_t *meth) {
+ print_type(out, "method", NULL, meth, false);
+
+ fprintf(out, ",\n\"self\": \"%p\"", meth->self);
+ fprintf(out, ",\n\"body\": \"%p\"", meth->meth);
+ fprintf(out, "},");
+
+ dump_value(out, meth->self);
+ dump_value(out, meth->meth);
+}
+
+void dump_static_method(FILE *out, const mp_obj_static_class_method_t *meth) {
+ print_type(out, "staticmethod", NULL, meth, false);
+ fprintf(out, ",\n\"body\": \"%p\"", meth->fun);
+ fprintf(out, "},");
+ dump_value(out, meth->fun);
+}
+
+void dump_closure(FILE *out, const mp_obj_closure_t *closure) {
+ size_t size = find_allocated_size(closure);
+ for (size_t i = 0; i < closure->n_closed; ++i) {
+ // XXX this is unimportant to track properly, hopefully
+ size += find_allocated_size(closure->closed[i]);
+ assert(mp_obj_is_type(closure->closed[i], &mp_type_cell));
+ }
+ print_type(out, "closure", NULL, closure, false);
+
+ fprintf(out, ",\n\"function\": \"%p\"", closure->fun);
+ fprintf(out, ",\n\"closed\": [\n");
+ bool first = true;
+ for (size_t i = 0; i < closure->n_closed; ++i) {
+ if (!first) fprintf(out, ",\n");
+ first = false;
+ dump_short_or_ptr(out, mp_obj_cell_get(closure->closed[i]));
+ }
+ fprintf(out, "]},");
+
+ dump_value(out, closure->fun);
+ for (size_t i = 0; i < closure->n_closed; ++i) {
+ dump_value(out, mp_obj_cell_get(closure->closed[i]));
+ }
+}
+
+typedef struct _mp_obj_gen_instance_t {
+ mp_obj_base_t base;
+ // mp_const_none: Not-running, no exception.
+ // MP_OBJ_NULL: Running, no exception.
+ // other: Not running, pending exception.
+ mp_obj_t pend_exc;
+ mp_code_state_t code_state;
+} mp_obj_gen_instance_t;
+
+void dump_generator(FILE *out, const mp_obj_gen_instance_t *gen) {
+ print_type(out, "generator", NULL, gen, false);
+
+ fprintf(out, ",\n\"pending_exception\": \"%p\"", gen->pend_exc);
+ fprintf(out, ",\n\"function\": \"%p\"", gen->code_state.fun_bc);
+ fprintf(out, ",\n\"old_globals\": \"%p\"", gen->code_state.old_globals);
+ fprintf(out, ",\n\"state\": [\n");
+ bool first = true;
+ for (size_t i = 0; i < gen->code_state.n_state; ++i) {
+ if (!first) fprintf(out, ",\n");
+ first = false;
+ dump_short_or_ptr(out, gen->code_state.state[i]);
+ }
+
+ fprintf(out, "]},\n");
+ dump_value(out, gen->pend_exc);
+ dump_value(out, gen->code_state.fun_bc);
+ dump_value(out, gen->code_state.old_globals);
+ for (size_t i = 0; i < gen->code_state.n_state; ++i) {
+ dump_value(out, gen->code_state.state[i]);
+ }
+}
+
+void dump_instance(FILE *out, const mp_obj_instance_t *obj) {
+ print_type(out, "instance", NULL, obj, false);
+ fprintf(out, ",\n\"base\": \"%p\"", obj->base.type);
+ dump_map_as_children(out, &obj->members);
+ fprintf(out, "},\n");
+
+ dump_value(out, obj->base.type);
+ dump_map_as_values(out, obj, &obj->members);
+}
+
+void dump_type(FILE *out, const mp_obj_type_t *type) {
+ print_type(out, "type", qstr_str(type->name), type, false);
+ fprintf(out, ",\n\"locals\": \"%p\"", type->locals_dict);
+ fprintf(out, ",\n\"parent\": \"%p\"},\n", type->parent);
+
+ dump_value(out, type->parent);
+ dump_value(out, type->locals_dict);
+}
+
+void dump_list(FILE *out, const mp_obj_list_t *list) {
+ print_type(out, "list", NULL, list, false);
+ fprintf(out, ",\n\"items\": [\n");
+ bool first = true;
+ for (size_t i = 0; i < list->len; ++i) {
+ if (!first) fprintf(out, ",\n");
+ first = false;
+ dump_short_or_ptr(out, list->items[i]);
+ }
+ fprintf(out, "]},\n");
+
+ print_type(out, "listitems", NULL, list->items, false);
+ fprintf(out, ",\n\"owner\": \"%p\"},\n", list);
+ for (size_t i = 0; i < list->len; ++i) {
+ dump_value(out, list->items[i]);
+ }
+}
+
+void dump_tuple(FILE *out, const mp_obj_tuple_t *tuple) {
+ print_type(out, "tuple", NULL, tuple, false);
+ fprintf(out, ",\n\"items\": [\n");
+ bool first = true;
+ for (size_t i = 0; i < tuple->len; ++i) {
+ if (!first) fprintf(out, ",\n");
+ first = false;
+ dump_short_or_ptr(out, tuple->items[i]);
+ }
+ fprintf(out, "]},\n");
+
+ for (size_t i = 0; i < tuple->len; ++i) {
+ dump_value(out, tuple->items[i]);
+ }
+}
+
+typedef struct _mp_obj_set_t {
+ mp_obj_base_t base;
+ mp_set_t set;
+} mp_obj_set_t;
+
+bool is_set_or_frozenset(mp_const_obj_t o);
+
+void dump_set(FILE *out, const mp_obj_set_t *set) {
+ print_type(out, "set", NULL, set, false);
+ fprintf(out, ",\n\"items\": [\n");
+ bool first = true;
+ for (size_t i = 0; i < set->set.alloc; ++i) {
+ if (!mp_set_slot_is_filled(&set->set, i)) continue;
+ if (!first) fprintf(out, ",\n");
+ first = false;
+ dump_short_or_ptr(out, set->set.table[i]);
+ }
+ fprintf(out, "]},\n");
+
+ print_type(out, "setitems", NULL, set->set.table, false);
+ fprintf(out, ",\n\"owner\": \"%p\"},\n", set);
+
+ for (size_t i = 0; i < set->set.alloc; ++i) {
+ if (!mp_set_slot_is_filled(&set->set, i)) continue;
+ dump_value(out, set->set.table[i]);
+ }
+}
+
+void dump_trezor_hid(FILE *out, const mp_obj_HID_t *hid) {
+ print_type(out, "trezor-hid", NULL, hid, false);
+ fprintf(out, ",\n\"rx_buffer\": \"%p\"},\n", hid->info.rx_buffer);
+ print_type(out, "rawbuffer", NULL, hid->info.rx_buffer, true);
+ fprintf(out, ",\n");
+}
+
+void dump_trezor_webusb(FILE *out, const mp_obj_WebUSB_t *webusb) {
+ print_type(out, "trezor-webusb", NULL, webusb, false);
+ fprintf(out, ",\n\"rx_buffer\": \"%p\"},\n", webusb->info.rx_buffer);
+ print_type(out, "rawbuffer", NULL, webusb->info.rx_buffer, true);
+ fprintf(out, ",\n");
+}
+
+void dump_trezor_vcp(FILE *out, const mp_obj_VCP_t *vcp) {
+ print_type(out, "trezor-vcp", NULL, vcp, false);
+ fprintf(out, ",\n\"tx_packet\": \"%p\"", vcp->info.tx_packet);
+ fprintf(out, ",\n\"tx_buffer\": \"%p\"", vcp->info.tx_buffer);
+ fprintf(out, ",\n\"rx_packet\": \"%p\"", vcp->info.rx_packet);
+ fprintf(out, ",\n\"rx_buffer\": \"%p\"},\n", vcp->info.rx_buffer);
+ print_type(out, "rawbuffer", NULL, vcp->info.tx_packet, true);
+ fprintf(out, ",\n");
+ print_type(out, "rawbuffer", NULL, vcp->info.tx_buffer, true);
+ fprintf(out, ",\n");
+ print_type(out, "rawbuffer", NULL, vcp->info.rx_packet, true);
+ fprintf(out, ",\n");
+ print_type(out, "rawbuffer", NULL, vcp->info.rx_buffer, true);
+ fprintf(out, ",\n");
+}
+
+void dump_value_opt(FILE *out, mp_const_obj_t value, bool eval_short) {
+ if (!eval_short && is_short(value)) return;
+
+ if (!eval_short || VERIFY_PTR(value)) {
+ size_t block = BLOCK_FROM_PTR(value);
+ switch (ATB_GET_KIND(block)) {
+ case AT_HEAD:
+ // all is ok
+ ATB_HEAD_TO_MARK(block);
+ break;
+ case AT_TAIL:
+ printf("===== pointer to tail???\n");
+ break;
+ case AT_MARK:
+ // print_type(out, "already_dumped", 0, NULL, value);
+ return;
+ }
+ }
+
+ if (mp_obj_is_str_or_bytes(value)) {
+ const mp_obj_str_t *strvalue = (mp_obj_str_t *)value;
+ print_type(out, "anystr", NULL, value, false);
+ fprintf(out, ", \"val\": ");
+ print_repr(out, (const char *)strvalue->data, strvalue->len);
+ fprintf(out, ", \"data\": \"%p\"", strvalue->data);
+ fprintf(out, "},\n");
+ print_type(out, "strdata", NULL, strvalue->data, true);
+ fprintf(out, ",\n");
+ }
+
+ else if (mp_obj_is_type(value, &mp_type_bytearray)) {
+ const mp_obj_array_t *array = (mp_obj_array_t *)value;
+ print_type(out, "array", NULL, array, true);
+ fprintf(out, ",\n");
+ print_type(out, "arrayitems", NULL, array->items, false);
+ fprintf(out, ", \"owner\": \"%p\"}", array);
+ fprintf(out, ",\n");
+ }
+
+ else if (mp_obj_is_type(value, &mp_type_dict)) {
+ dump_dict_inner(out, value);
+ }
+
+ else if (mp_obj_is_type(value, &mp_type_module)) {
+ print_type(out, "module", NULL, value, false);
+ mp_obj_module_t *module = MP_OBJ_TO_PTR(value);
+ fprintf(out, ", \"globals\": \"%p\"", module->globals);
+ fprintf(out, "},\n");
+ dump_value(out, module->globals);
+ }
+
+ else if (mp_obj_is_type(value, &mp_type_fun_bc) ||
+ mp_obj_is_type(value, &mp_type_gen_wrap)) {
+ dump_function(out, value);
+ }
+
+ else if (mp_obj_is_type(value, &mp_type_bound_meth)) {
+ dump_bound_method(out, value);
+ }
+
+ else if (mp_obj_is_type(value, &closure_type)) {
+ dump_closure(out, value);
+ }
+
+ else if (mp_obj_is_type(value, &mp_type_staticmethod) ||
+ mp_obj_is_type(value, &mp_type_classmethod)) {
+ dump_static_method(out, value);
+ }
+
+ else if (mp_obj_is_instance_type(mp_obj_get_type(value))) {
+ dump_instance(out, value);
+ }
+
+ else if (mp_obj_is_type(value, &mp_type_object)) {
+ print_type(out, "object", NULL, value, true);
+ fprintf(out, ",\n");
+ }
+
+ else if (mp_obj_is_type(value, &mp_type_type)) {
+ dump_type(out, value);
+ }
+
+ else if (mp_obj_is_type(value, &mp_type_list)) {
+ dump_list(out, value);
+ }
+
+ else if (mp_obj_is_type(value, &mp_type_tuple)) {
+ dump_tuple(out, value);
+ }
+
+ else if (is_set_or_frozenset(value)) {
+ dump_set(out, value);
+ }
+
+ else if (mp_obj_is_type(value, &mp_type_gen_instance)) {
+ dump_generator(out, value);
+ }
+
+ else if (mp_obj_is_type(value, &mod_trezorio_WebUSB_type)) {
+ dump_trezor_webusb(out, value);
+ }
+
+ else if (mp_obj_is_type(value, &mod_trezorio_VCP_type)) {
+ dump_trezor_vcp(out, value);
+ }
+
+ else if (mp_obj_is_type(value, &mod_trezorio_HID_type)) {
+ dump_trezor_hid(out, value);
+ }
+
+ else if (mp_obj_is_type(value, &mod_trezorio_USB_type) ||
+ mp_obj_is_type(value, &mod_trezorui_Display_type)) {
+ print_type(out, "trezor", NULL, value, true);
+ fprintf(out, ",\n");
+ }
+
+ else {
+ print_type(out, "unknown", NULL, value, true);
+ fprintf(out, ",\n");
+ }
+
+ fflush(out);
+}
+
+void dump_value(FILE *out, mp_const_obj_t value) {
+ dump_value_opt(out, value, false);
+}
+
+void dump_qstr_pool(FILE *out, qstr_pool_t *pool) {
+ print_type(out, "qstrpool", NULL, pool, false);
+ fprintf(out, ", \"qstrs\": [\n");
+ for (const byte **q = pool->qstrs, **q_top = pool->qstrs + pool->len;
+ q < q_top; q++) {
+ if (q < (q_top - 1))
+ fprintf(out, "\"%s\",\n", Q_GET_DATA(*q));
+ else
+ fprintf(out, "\"%s\"]\n", Q_GET_DATA(*q));
+ }
+ fprintf(out, "},\n");
+ for (const byte **q = pool->qstrs, **q_top = pool->qstrs + pool->len;
+ q < q_top; q++) {
+ print_type(out, "qstrdata", NULL, *q, false);
+ fprintf(out, ", \"pool\": \"%p\"},\n", pool);
+ }
+}
+
+void dump_qstrdata(FILE *out) {
+ qstr_pool_t *pool = MP_STATE_VM(last_pool);
+ while (pool != NULL) {
+ for (const byte **q = pool->qstrs, **q_top = pool->qstrs + pool->len;
+ q < q_top; q++) {
+ if ((void *)*q > (void *)mp_state_ctx.mem.gc_pool_start) {
+ print_type(out, "qstrdata", NULL, q, false);
+ fprintf(out, ", \"pool\": \"%p\"},\n", pool);
+ }
+ }
+ pool = pool->prev;
+ }
+}
+
+/// def meminfo(filename: str) -> None:
+/// """Dumps map of micropython GC arena to a file.
+/// The JSON file can be decoded by analyze.py
+/// Only available in the emulator.
+/// """
+STATIC mp_obj_t mod_trezorutils_meminfo(mp_obj_t filename) {
+ size_t fn_len;
+ FILE *out = fopen(mp_obj_str_get_data(filename, &fn_len), "w");
+ fprintf(out, "[");
+
+ // void **ptrs = (void **)(void *)&mp_state_ctx;
+ // size_t root_start = offsetof(mp_state_ctx_t, thread.dict_locals);
+ // size_t root_end = offsetof(mp_state_ctx_t, vm.qstr_last_chunk);
+
+ // for (size_t i = root_start; i < root_end; i++) {
+ // void *ptr = ptrs[i];
+ // if (i == 55) continue; // mp_loaded_modules_dict
+ // if (i == 62) continue; // dict_main
+ // if (i == 66) continue; // mp_sys_path_obj
+ // if (i == 70) continue; // mp_sys_argv_obj
+ // if (i == 123) continue; // qstr_last_chunk
+ // if (VERIFY_PTR(ptr)) {
+ // size_t block = BLOCK_FROM_PTR(ptr);
+ // if (ATB_GET_KIND(block) == AT_HEAD) {
+ // fprintf(out, "\"root_ofs: %ld\",\n", i);
+ // dump_value(out, ptr);
+ // // // An unmarked head: mark it, and mark all its children
+ // // TRACE_MARK(block, ptr);
+ // // ATB_HEAD_TO_MARK(block);
+ // // gc_mark_subtree(block);
+ // }
+ // }
+ // }
+
+ fprintf(out, "\"dict_locals\",\n");
+ dump_value(out, MP_STATE_THREAD(dict_locals));
+
+ fprintf(out, "\"mp_loaded_modules_dict\",\n");
+ dump_value_opt(out, &MP_STATE_VM(mp_loaded_modules_dict), true);
+
+ fprintf(out, "\"dict_main\",\n");
+ dump_value_opt(out, &MP_STATE_VM(dict_main), true);
+
+ fprintf(out, "\"mp_sys_path_obj\",\n");
+ dump_value_opt(out, &MP_STATE_VM(mp_sys_path_obj), true);
+
+ fprintf(out, "\"mp_sys_argv_obj\",\n");
+ dump_value_opt(out, &MP_STATE_VM(mp_sys_argv_obj), true);
+
+ fprintf(out, "\"ui_wait_callback\",\n");
+ dump_value(out, MP_STATE_VM(trezorconfig_ui_wait_callback));
+
+ fprintf(out, "\"qstr_pools\",\n");
+ qstr_pool_t *pool = MP_STATE_VM(last_pool);
+ while (VERIFY_PTR((void *)pool)) {
+ dump_qstr_pool(out, pool);
+ pool = pool->prev;
+ }
+
+ fprintf(out, "null]\n");
+ fclose(out);
+ for (size_t block = 0;
+ block < MP_STATE_MEM(gc_alloc_table_byte_len) * BLOCKS_PER_ATB;
+ block++) {
+ if (ATB_GET_KIND(block) == AT_MARK) {
+ ATB_MARK_TO_HEAD(block);
+ }
+ }
+
+ gc_dump_alloc_table();
+ return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorutils_meminfo_obj,
+ mod_trezorutils_meminfo);
+
+#define MEMINFO_DICT_ENTRIES \
+ {MP_ROM_QSTR(MP_QSTR_meminfo), MP_ROM_PTR(&mod_trezorutils_meminfo_obj)},
+
+#endif
diff --git a/core/embed/extmod/modtrezorutils/modtrezorutils.c b/core/embed/extmod/modtrezorutils/modtrezorutils.c
index c76165709..5c456fc9c 100644
--- a/core/embed/extmod/modtrezorutils/modtrezorutils.c
+++ b/core/embed/extmod/modtrezorutils/modtrezorutils.c
@@ -24,6 +24,7 @@
#if MICROPY_PY_TREZORUTILS
+#include "embed/extmod/modtrezorutils/modtrezorutils-meminfo.h"
#include "embed/extmod/trezorobj.h"
#include
@@ -144,6 +145,7 @@ STATIC const mp_rom_map_elem_t mp_module_trezorutils_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR_MODEL), MP_ROM_QSTR(MP_QSTR(TREZOR_MODEL))},
#ifdef TREZOR_EMULATOR
{MP_ROM_QSTR(MP_QSTR_EMULATOR), mp_const_true},
+ MEMINFO_DICT_ENTRIES
#else
{MP_ROM_QSTR(MP_QSTR_EMULATOR), mp_const_false},
#endif
diff --git a/core/mocks/generated/trezorutils.pyi b/core/mocks/generated/trezorutils.pyi
index cce8392e3..c3692da13 100644
--- a/core/mocks/generated/trezorutils.pyi
+++ b/core/mocks/generated/trezorutils.pyi
@@ -1,6 +1,14 @@
from typing import *
+# extmod/modtrezorutils/modtrezorutils-meminfo.h
+def meminfo(filename: str) -> None:
+ """Dumps map of micropython GC arena to a file.
+ The JSON file can be decoded by analyze.py
+ Only available in the emulator.
+ """
+
+
# extmod/modtrezorutils/modtrezorutils.c
def consteq(sec: bytes, pub: bytes) -> bool:
"""
diff --git a/core/mocks/micropython.pyi b/core/mocks/micropython.pyi
index 661204442..22238a298 100644
--- a/core/mocks/micropython.pyi
+++ b/core/mocks/micropython.pyi
@@ -3,7 +3,7 @@ from typing import TypeVar
C = TypeVar("C", bound=int)
def const(c: C) -> C: ...
-def mem_info() -> None: ...
+def mem_info(verbose: bool | None = None) -> None: ...
def mem_current() -> int: ...
def mem_total() -> int: ...
def mem_peak() -> int: ...
diff --git a/core/src/trezor/utils.py b/core/src/trezor/utils.py
index e15e2de37..7bc6d95ac 100644
--- a/core/src/trezor/utils.py
+++ b/core/src/trezor/utils.py
@@ -96,6 +96,24 @@ def presize_module(modname: str, size: int) -> None:
delattr(module, "___PRESIZE_MODULE_%d" % i)
+if __debug__:
+
+ def mem_dump(filename: str) -> None:
+ from micropython import mem_info
+
+ print("### sysmodules (%d):" % len(sys.modules))
+ for mod in sys.modules:
+ print("*", mod)
+ if EMULATOR:
+ from trezorutils import meminfo
+
+ print("### dumping to", filename)
+ meminfo(filename)
+ mem_info()
+ else:
+ mem_info(True)
+
+
def ensure(cond: bool, msg: str | None = None) -> None:
if not cond:
if msg is None:
diff --git a/core/tools/analyze-memory-dump.py b/core/tools/analyze-memory-dump.py
new file mode 100755
index 000000000..d6cfb2c4a
--- /dev/null
+++ b/core/tools/analyze-memory-dump.py
@@ -0,0 +1,364 @@
+#!/usr/bin/env python3
+import json
+import sys
+
+
+if len(sys.argv) < 2:
+ print("""\
+USAGE: ./analyze-memory-dump.py somefile.json
+
+Where "somefile.json" was produced by using `trezor.utils.mem_dump("somefile.json")`
+somewhere in emulator source code.
+
+Outputs a memory dump similar to `micropython.mem_info(True)`, except with complete
+(hopefully :) ) understanding of concrete objects at given memory addresses. Also
+outputs `memorymap.html`, which is a clickable memory dump with cross-references
+between the objects, for detailed examination of what is going on.
+
+The "inferred name" feature works by looking up the closest dict containing the object
+as a value, with a string key. This sometimes works amazingly and sometimes not so much.
+
+Certain kinds of objects have a separately allocated storage space. Such storage space
+is given an "owner" backreference that will point you back to the object that you
+actually care about.
+
+Modules are nothing but a link to a globals dict. The dict must be examined separately.
+
+Generators and closures are painful :(
+""")
+
+
+
+with open(sys.argv[1]) as f:
+ MEMMAP = json.load(f)
+
+
+# filter out notices and comments
+MEMMAP = [m for m in MEMMAP if isinstance(m, dict)]
+
+MEMORY = {}
+
+
+def is_ptr(maybe_ptr):
+ return isinstance(maybe_ptr, str) and maybe_ptr.startswith("0x")
+
+
+def is_gc_ptr(maybe_ptr):
+ return is_ptr(maybe_ptr) and maybe_ptr.startswith("0x7f")
+
+
+def ptr_or_shortval(maybe_ptr):
+ if is_ptr(maybe_ptr):
+ return maybe_ptr
+ else:
+ assert isinstance(maybe_ptr, dict), f"maybe_ptr is {type(maybe_ptr)}"
+ assert "shortval" in maybe_ptr, f"maybe_ptr does not have shortval: {maybe_ptr}"
+ return maybe_ptr["shortval"]
+
+
+def deref_or_shortval(maybe_ptr):
+ if is_ptr(maybe_ptr) and maybe_ptr in MEMORY:
+ return MEMORY[maybe_ptr]
+ else:
+ return ptr_or_shortval(maybe_ptr)
+
+
+class Item:
+ def __init__(self, item):
+ self.item = item
+ self.backlinks = []
+ self.dict = {}
+ self.visited = False
+ self.type = item["type"]
+ self.ptr = item["ptr"]
+
+ def backlinkify(self):
+ if "children" in self.item:
+ for child in self.item["children"]:
+ key_str = ptr_or_shortval(child["key"])
+ value_deref = deref_or_shortval(child["value"])
+ self.dict[key_str] = value_deref
+
+ for ptr in self.find_pointers():
+ if ptr not in MEMORY:
+ continue
+ MEMORY[ptr].backlinks.append(self)
+
+ def find_pointers(self):
+ if "children" in self.item:
+ for child in self.item["children"]:
+ if is_ptr(child["key"]):
+ yield child["key"]
+ if is_ptr(child["value"]):
+ yield child["value"]
+
+ for k, v in self.item.items():
+ if k in ("ptr", "owner", "children"):
+ continue
+ if not v:
+ continue
+ if isinstance(v, list):
+ yield from (p for p in v if is_ptr(p))
+ if is_ptr(v):
+ yield v
+
+ def __getattr__(self, key):
+ if key not in self.item:
+ raise AttributeError
+ return self.item[key]
+
+ def find_modules(self):
+ return [it for it in self.backlinks if it.type == "module"]
+
+ def name(self):
+ if "__name__" in self.dict:
+ return self.dict["__name__"]
+
+ if "__qualname__" in self.dict:
+ return self.dict["__module__"] + "::" + self.dict["__qualname__"]
+
+ if self.type == "type":
+ return MEMORY[self.item["locals"]].name()
+
+ if self.type == "instance":
+ return MEMORY[self.item["base"]].name() + "()"
+
+ if self.type == "module":
+ return MEMORY[self.item["globals"]].name()
+
+ if self.type == "generator":
+ return MEMORY[self.item["function"]].name()
+
+ for item in self.backlinks:
+ if item.type == "dict":
+ for k, v in item.dict.items():
+ if v == self:
+ return k
+
+ return None
+
+ def ptrval(self):
+ return int(self.ptr[2:], 16)
+
+
+for item_data in MEMMAP:
+ item = Item(item_data)
+ MEMORY[item.ptr] = item
+
+for item in MEMORY.values():
+ item.backlinkify()
+
+
+allobjs = list(MEMORY.values())
+allobjs.sort(key=lambda x: x.ptr)
+min_ptr = min(
+ item.ptrval()
+ for item in allobjs
+ if item.ptr != "(nil)" and not item.ptr.startswith("0x5")
+)
+max_ptr = max(item.ptrval() for item in allobjs if item.ptr != "(nil)")
+
+
+types = {
+ "anystr": "S",
+ "strdata": "s",
+ "array": "A",
+ "arrayitems": "a",
+ "closure": "c",
+ "dict": "D",
+ "function": "B",
+ "generator": "G",
+ "instance": "I",
+ "list": "L",
+ "listitems": "l",
+ "mapitems": "m",
+ "method": "C",
+ "module": "M",
+ "object": "o",
+ "set": "E",
+ "setitems": "e",
+ "staticmethod": "C",
+ "trezor": "t",
+ "tuple": "T",
+ "type": "y",
+ "unknown": "h",
+ "trezor-webusb": "t",
+ "trezor-vcp": "t",
+ "trezor-hid": "t",
+ "rawbuffer": "R",
+ "qstrpool": "Q",
+ "qstrdata": "q",
+}
+
+pixels_per_line = len(
+ "................................................................"
+)
+pixelsize = 0x800 // pixels_per_line
+maxline = ((max_ptr - min_ptr) & ~0x7FF) + (0x800 * 2)
+pixelmap = [None] * (maxline // pixelsize)
+
+
+def pixel_index(ptrval):
+ ptridx = ptrval - min_ptr
+ # assert ptridx >= 0
+ return ptridx // pixelsize
+
+
+for item in MEMORY.values():
+ if item.alloc == 0:
+ continue
+ if item.ptr.startswith("0x5"):
+ continue
+ ptridx = pixel_index(item.ptrval())
+ assert ptridx >= 0, item.item
+ for i in range(ptridx, ptridx + item.alloc):
+ pixelmap[i] = item
+
+for item in MEMORY.values():
+ if item.alloc > 0:
+ continue
+ if item.ptr == "(nil)" or item.ptr.startswith("0x5"):
+ continue
+ ptridx = pixel_index(item.ptrval())
+ if ptridx < 0:
+ continue
+ for i in range(ptridx, ptridx + item.alloc):
+ pixelmap[i] = item
+
+ctr = 0
+newline = True
+previtem = None
+for pixel in pixelmap:
+ if ctr % pixels_per_line == 0:
+ print()
+ print(f"{ctr * pixelsize:05x}: ", end="")
+ if pixel is None:
+ c = "."
+ elif pixel is previtem:
+ c = "="
+ else:
+ c = types[pixel.type]
+ print(c, end="")
+ ctr += 1
+ previtem = pixel
+print()
+
+
+import dominate
+import dominate.tags as t
+
+doc = dominate.document(title="memory map")
+with doc.head:
+ t.meta(charset="utf-8")
+ t.style(
+ """\
+span, a {
+ font-family: monospace;
+ color: black;
+ text-decoration: none;
+ margin: 0;
+}
+
+a {
+ color: darkblue;
+}
+
+.entry a:target {
+ display: block;
+ background-color: navy;
+}
+
+#memorymap a:target {
+ color: red;
+}
+
+span.leadin {
+ margin-right: 1rem;
+}
+
+dl { border-left: 1px solid grey; padding-left: 0.4rem; }
+dt { font-weight: bold }
+
+div.
+"""
+ )
+
+ctr = 0
+newline = True
+previtem = None
+container = t.div(id="memorymap")
+doc.add(container)
+line = t.div()
+for pixel in pixelmap:
+ if ctr % pixels_per_line == 0:
+ container.add(line)
+ line = t.div()
+ line.add(t.span(f"{ctr * pixelsize:05x}: ", cls="leadin"))
+ if pixel is None:
+ line.add(t.span("."))
+ elif pixel is previtem:
+ line.add(t.a("=", href=f"#{pixel.ptr}"))
+ else:
+ c = types[pixel.type]
+ line.add(t.a(c, href=f"#{pixel.ptr}", name=f"mapentry-{pixel.ptr}"))
+ ctr += 1
+ previtem = pixel
+
+
+def text_or_ptr(s):
+ if s.startswith("0x7"):
+ sp = t.span()
+ sp.add(t.a(s, href=f"#{s}"))
+ sp.add(" (")
+ sp.add(t.a("M", href=f"#mapentry-{s}"))
+ sp.add(")")
+ return sp
+ else:
+ return t.span(s)
+
+
+def dump_single_val(value):
+ if isinstance(value, str):
+ return text_or_ptr(value)
+ elif isinstance(value, dict):
+ if value.get("shortval"):
+ return value["shortval"]
+ elif value.get("type") == "romdata":
+ return "romdata"
+ sdl = t.dl()
+ dump_dict(sdl, value)
+ return sdl
+ elif isinstance(value, list):
+ ul = t.ul()
+ for subval in value:
+ ul.add(t.li(dump_single_val(subval)))
+ return ul
+ else:
+ return str(value)
+
+
+def dump_dict(dl, d):
+ for key, value in d.items():
+ dl.add(t.dt(key))
+ dl.add(t.dd(dump_single_val(value)))
+
+
+for item in allobjs:
+ div = t.div(cls="entry")
+ div.add(t.a("{", name=item.ptr))
+ dl = t.dl()
+ dl.add(t.dt("Inferred name:"))
+ dl.add(t.dd(str(item.name())))
+ dl.add(t.dt("Backrefs:"))
+ refs = t.dd()
+ for backref in item.backlinks:
+ refs.add(text_or_ptr(backref.ptr))
+ refs.add(", ")
+ dl.add(refs)
+ dump_dict(dl, item.item)
+ div.add(dl)
+ doc.add(div)
+
+print("Writing to memorymap.html...")
+with open("memorymap.html", "w") as f:
+ f.write(doc.render(pretty=False))