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,