Heap render debugging facility (#377) It's occasionally useful while debugging to be able to look at the heap's layout and internal state machine. Co-authored-by: Nathaniel Wesley Filardo <nfilardo@microsoft.com> Co-authored-by: Robert Norton-Wright <robert.norton@scisemi.com>
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1e29fe1..833a291 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml
@@ -17,7 +17,7 @@ include: - sonata: false - build-type: debug - build-flags: --debug-loader=y --debug-scheduler=y --debug-allocator=y -m debug + build-flags: --debug-loader=y --debug-scheduler=y --debug-allocator=y --allocator-rendering=y -m debug - build-type: release build-flags: --debug-loader=n --debug-scheduler=n --debug-allocator=n -m release --stack-usage-check-allocator=y --stack-usage-check-scheduler=y - board: sonata-simulator
diff --git a/sdk/core/allocator/alloc.h b/sdk/core/allocator/alloc.h index 9560be0..85f8c6e 100644 --- a/sdk/core/allocator/alloc.h +++ b/sdk/core/allocator/alloc.h
@@ -2811,4 +2811,163 @@ { ABORT(); } + +#if HEAP_RENDER + public: + /** + * "Render" the heap for debugging. + */ + template<bool Asserts = true, bool Chatty = true> + void render() + { + using RenderDebug = ConditionalDebug<Chatty, "Allocator heap">; + + size_t measuredAllocated = 0, measuredFree = 0, measuredQuarantined = 0; + + auto toAddr = [](void *p) { + return static_cast<ptraddr_t>(CHERI::Capability{p}.address()); + }; + + auto header = + static_cast<MChunkHeader *>(static_cast<void *>(heapStart)); + + RenderDebug::log("Dumping MState={} start={} end={}", + toAddr(this), + toAddr(heapStart), + heapStart.top()); + + while (toAddr(header) != heapStart.top()) + { + RenderDebug::log(" header {}: size={} inuse={} pinuse={}", + toAddr(header), + header->size_get(), + header->isCurrInUse, + header->isPrevInUse); + + if (!header->is_in_use()) + { + measuredFree += header->size_get(); + + auto chunk = MChunk::from_header(header); + if (!ds::linked_list::is_singleton(&chunk->ring)) + { + RenderDebug::log( + " free ring <{},{}>", + toAddr(MChunk::from_ring(chunk->ring.cell_prev())), + toAddr(MChunk::from_ring(chunk->ring.cell_next()))); + } + else + { + RenderDebug::log(" free ring empty"); + } + + if (!is_small(header->size_get())) + { + auto t = TChunk::from_mchunk(chunk); + + if (t->is_tree_ring()) + { + RenderDebug::log(" tree ring, index={}", t->index); + } + else + { + RenderDebug::log( + " tree, index={} parent={} children=[{},{}]", + t->index, + toAddr(t->parent), + toAddr(t->child[0]), + toAddr(t->child[1])); + } + } + } + else + { + measuredAllocated += header->size_get(); + } + + header = header->cell_next(); + } + + auto showQuarantineRing = [&](ChunkFreeLink *&p) { + auto header = MChunkHeader::from_body(MChunk::from_ring(p)); + RenderDebug::log( + " quarantined {} size={}", toAddr(header), header->size_get()); + measuredQuarantined += header->size_get(); + if constexpr (Asserts) + { + RenderDebug::invariant(ds::linked_list::is_well_formed(p), + "Quarantine list node malformed"); + } + return false; + }; + + { + decltype(quarantinePendingRing)::Ix head = 0, tail = 0; + + quarantinePendingRing.head_get(head); + quarantinePendingRing.tail_get(tail); + RenderDebug::log(" Quarantine status: empty={} head={} tail={}", + quarantinePendingRing.is_empty(), + head, + tail); + } + + if (quarantineFinishedSentinel.is_empty()) + { + RenderDebug::log(" finished ring empty"); + } + else + { + RenderDebug::log(" finished ring <{},{}>:", + toAddr(quarantineFinishedSentinel.last()), + toAddr(quarantineFinishedSentinel.first())); + quarantine_finished_get()->search(showQuarantineRing); + } + + for (size_t ix = 0; ix < QuarantineRings; ix++) + { + if (quarantinePendingChunks[ix].is_empty()) + { + RenderDebug::log( + " index={} epoch={} empty", ix, quarantinePendingEpoch[ix]); + } + else + { + RenderDebug::log( + " index={} epoch={}:", ix, quarantinePendingEpoch[ix]); + quarantine_pending_get(ix)->search(showQuarantineRing); + } + } + + auto measuredTotal = measuredAllocated + measuredFree; + + RenderDebug::log( + "Sizes: alloc={} free={} (expect {}) quar={} (expect {}) " + "total={} (expect {})", + measuredAllocated, + measuredFree, + heapFreeSize, + measuredQuarantined, + heapQuarantineSize, + measuredTotal, + heapTotalSize); + + if constexpr (Asserts) + { + RenderDebug::invariant(measuredFree == heapFreeSize, + "Bad accounting in free size: {} {}", + std::make_tuple(measuredFree, heapFreeSize)); + + RenderDebug::invariant( + measuredQuarantined == heapQuarantineSize, + "Bad accounting in quarantine size: {} {}", + std::make_tuple(measuredQuarantined, heapQuarantineSize)); + + RenderDebug::invariant( + measuredTotal == heapTotalSize, + "Bad accounting in total size: {} {}", + std::make_tuple(measuredTotal, heapTotalSize)); + } + } +#endif };
diff --git a/sdk/core/allocator/main.cc b/sdk/core/allocator/main.cc index bb21962..8db5936 100644 --- a/sdk/core/allocator/main.cc +++ b/sdk/core/allocator/main.cc
@@ -1268,3 +1268,11 @@ { return gm->heapFreeSize; } + +[[cheri::interrupt_state(disabled)]] int heap_render() +{ +#if HEAP_RENDER + gm->render(); +#endif + return 0; +}
diff --git a/sdk/include/debug.hh b/sdk/include/debug.hh index af0732e..9cfaa56 100644 --- a/sdk/include/debug.hh +++ b/sdk/include/debug.hh
@@ -715,6 +715,27 @@ Invariant(T, const char *, Ts &&...) -> Invariant<Ts...>; /** + * Overt wrapper function around Invariant. Sometimes template + * deduction guides just don't cut it. At the cost of a + * std::make_tuple at the call site, we can still take advantage + * of much of the machinery here. + */ + template<typename T, typename... Args> + __always_inline static void + invariant(T condition, + const char *fmt, + std::tuple<Args...> args = std::make_tuple(), + SourceLocation loc = SourceLocation::current()) + { + std::apply( + [&](Args... iargs) { + Invariant<Args...>( + condition, fmt, std::forward<Args>(iargs)..., loc); + }, + args); + } + + /** * Deduction guide, allows `Assert` to be used as if it were a * function. */ @@ -727,6 +748,27 @@ */ template<typename... Ts> Assert(auto, const char *, Ts &&...) -> Assert<Ts...>; + + /** + * Overt wrapper function around Assert. Sometimes template + * deduction guides just don't cut it. At the cost of a + * std::make_tuple at the call site, we can still take advantage + * of much of the machinery here. + */ + template<typename T, typename... Args> + __always_inline static void + assertion(T condition, + const char *fmt, + std::tuple<Args...> args = std::make_tuple(), + SourceLocation loc = SourceLocation::current()) + { + std::apply( + [&](Args... iargs) { + Assert<Args...>( + condition, fmt, std::forward<Args>(iargs)..., loc); + }, + args); + } }; enum class StackCheckMode
diff --git a/sdk/include/stdlib.h b/sdk/include/stdlib.h index 54a9dc6..2dca318 100644 --- a/sdk/include/stdlib.h +++ b/sdk/include/stdlib.h
@@ -307,6 +307,15 @@ return (address >= heap_start) && (address < heap_end); } +/** + * Dump a textual rendering of the heap's structure to the debug console. + * + * If the RTOS is not built with --allocator-rendering=y, this is a no-op. + * + * Returns zero on success, non-zero on error (e.g. compartment call failure). + */ +int __cheri_compartment("alloc") heap_render(); + static inline void __dead2 abort() { panic();
diff --git a/sdk/xmake.lua b/sdk/xmake.lua index 07a37c5..ae74917 100644 --- a/sdk/xmake.lua +++ b/sdk/xmake.lua
@@ -25,6 +25,11 @@ set_description("Track per-thread cycle counts in the scheduler"); set_showmenu(true) +option("allocator-rendering") + set_default(false) + set_description("Include heap_render() functionality in the allocator") + set_showmenu(true) + function debugOption(name) option("debug-" .. name) set_default(false) @@ -229,6 +234,7 @@ on_load(function (target) target:set("cheriot.compartment", "alloc") target:set('cheriot.debug-name', "allocator") + target:add('defines', "HEAP_RENDER=" .. tostring(get_config("allocator-rendering"))) end) target("cheriot.token_library")
diff --git a/tests/allocator-test.cc b/tests/allocator-test.cc index 05a4ac8..bbce8d3 100644 --- a/tests/allocator-test.cc +++ b/tests/allocator-test.cc
@@ -154,6 +154,8 @@ } } TEST(memoryExhausted, "Failed to exhaust memory"); + debug_log("Calling heap_render"); + TEST_EQUAL(heap_render(), 0, "heap_render returned non-zero"); debug_log("Trying a non-blocking allocation"); // nullptr check because we explicitly want to check for OOM TEST(heap_allocate(&noWait, MALLOC_CAPABILITY, BigAllocSize) == nullptr,