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))