Module 6 Exercises — Memory Without a Heap
Back to the Course 2 syllabus. Read first: Module 6 lessons (Seacord 6, Rust Book 15, and the heapless/static_cell crate docs remain available as optional deep-dives).
Work in the labs repo’s course2/ folder — c/host, c/mcu, rust/host, rust/qemu, rust/mcu, rust/linux as each exercise names — and record everything in m6/notes.md. Everything in this module runs on the Mac: host builds under ASan/UBSan and cargo test, Cortex-M layout probes by cross-compiling with clang --target=thumbv7em-none-eabihf and cargo check --target thumbv7em-none-eabihf (a layout assertion that fails is the observation), and bare-metal execution in QEMU. Only Exercise 6.7’s last rung leaves the Mac. Predicted cells are filled in before building; observed cells at the machine.
Exercises
Exercise 6.1 — Layout tables, two ABIs, two languages. Declare the same three structs in C (c/host/src/ex-6-1/, c/mcu/src/ex-6-1/) and Rust (rust/host/src/bin/ex-6-1.rs, rust/mcu/src/bin/ex-6-1.rs): (a) { uint8_t a; uint32_t b; uint16_t c; }, (b) the same members reordered by descending alignment, and (c) { uint8_t tag; void *payload; uint16_t len; }. Predict sizeof, alignof, and every offsetof on LP64 and ILP32 by hand. In C, verify with _Static_assert + offsetof probes that either compile or do not, on the host and on thumbv7em. In Rust, declare each struct twice — default repr(Rust) and #[repr(C)] — and verify with const _: () = assert!(size_of::<T>() == …) and offset_of!, on the host and with cargo check --target thumbv7em-none-eabihf.
| Struct | Field | Predicted offset / size, LP64 | Observed, host | Predicted, ILP32 | Observed, thumbv7em |
|---|---|---|---|---|---|
| (a) C | a, b, c, total |
… | … | … | … |
| (b) C | b, c, a, total |
… | … | … | … |
| (c) C | tag, payload, len, total |
… | … | … | … |
(a) Rust repr(C) |
total | … | … | … | … |
(a) Rust repr(Rust) |
total | … | … | … | … |
(c) Rust repr(C) |
total | … | … | … | … |
Deliverable: the table, the two sets of assertion probes, and a notes.md paragraph on which rows differ between the ABIs and which differ between repr(Rust) and repr(C) — and therefore which structs in a real driver must carry #[repr(C)].
Exercise 6.2 — A fixed-block pool, twice. Implement the lessons §3.2 pool in C (c/host/src/ex-6-2/: a static array of 64-byte blocks, free list threaded through the blocks via a union, pool_alloc/pool_free, an _Static_assert that a block can hold a link, and a count of blocks in use) and in Rust (rust/host/src/bin/ex-6-2.rs: [MaybeUninit<Block>; N] in a static behind a safe API that returns a handle type — not a raw pointer — such that double-free and use-after-free cannot be written in safe code; then compare with heapless::pool). Write the same test sequence against both: allocate all N, attempt one more, free the middle block, allocate again, free all. Build the C version under ASan and the Rust version under cargo miri test.
| Property | C pool — predicted / observed | Rust pool — predicted / observed |
|---|---|---|
| Bytes of static storage for N = 16 | … / … | … / … |
| Result of the (N+1)-th allocation | … / … | … / … |
| Which block the post-free allocation returns | … / … | … / … |
| Double-free: what happens | … / … | … / … (should be: cannot be expressed) |
| Use-after-free: who catches it | … / … | … / … |
Deliverable: both implementations, the table, and one paragraph on what the Rust handle type had to look like to make the last two rows true — and what it cost in API ergonomics.
Exercise 6.3 — DMA buffer ownership. Model the lessons §7 protocol without a DMA engine: a “transfer” is a function that takes a buffer and completes later. In C (c/mcu/src/ex-6-3/, freestanding, read as disassembly): a static double buffer, a volatile ownership flag, a fake dma_start(buf) and dma_done(); show in the -O2 listing where the compiler hoists or does not hoist reads of the buffer around the flag, and where a __DSB()/atomic_signal_fence changes the listing. In Rust (rust/qemu/src/bin/ex-6-3.rs, run in QEMU): StaticCell<[u16; 256]> handed out as &'static mut, a struct Transfer<'a>(&'a mut [u16; 256]) that holds the borrow until .finish() returns the buffer. Then, predicting first, try to (a) call init a second time, (b) read the buffer while a Transfer holds it, (c) drop the Transfer without finishing and read the buffer.
| Attempt | Predicted outcome (compile error / panic / runs) | Observed |
|---|---|---|
(a) second init |
… | … |
| (b) read while transferred | … | … |
| (c) drop unfinished, then read | … | … |
Deliverable: the annotated C listing, the Rust program, the table, and a notes.md paragraph on which of the three attempts the type system catches and which one only a Drop implementation or a hardware barrier can — the boundary between what ownership proves and what the DMA engine does.
Exercise 6.4 — The stack budget, predicted and read back. In c/mcu/src/ex-6-4/ write a small call chain (main → process → parse → checksum) where parse has a 512-byte local array; compile with -fstack-usage -O0 and -O2 for thumbv7em (and -Wstack-usage=256 to see it fire). Before reading the .su files, predict each function’s frame size from its locals and the AAPCS callee-saved rule. Add one VLA and one recursive function and record what -fstack-usage reports for them. Then, in Rust (rust/host/src/bin/ex-6-4.rs), write the same chain with a [u8; 512] local and, if cargo call-stack supports the host target on your toolchain, compare its worst-case path against your hand sum; otherwise compare -O0 vs -O2 frame sizes from cargo objdump prologues (sub sp, sp, #…).
| Function | Predicted frame, -O0 |
Observed .su, -O0 |
Predicted, -O2 |
Observed, -O2 |
.su qualifier |
|---|---|---|---|---|---|
main |
… | … | … | … | … |
process |
… | … | … | … | … |
parse |
… | … | … | … | … |
checksum |
… | … | … | … | … |
| the VLA function | … | … | … | … | … (dynamic?) |
| the recursive function | … | … | … | … | … |
Deliverable: the table, the worst-case path sum for the whole chain plus the M4F’s exception-entry FP stacking, and a notes.md line stating the stack size you would put in memory.x/the linker script for this program and why.
Exercise 6.5 — heapless in QEMU. In rust/qemu/src/bin/ex-6-5.rs: a heapless::Vec<i16, 8> filled past capacity (record the ninth push’s return value, not a panic), a heapless::String<32> written with core::fmt::Write past its capacity (record what write! returns), and a heapless::spsc::Queue<u16, 8> split into producer and consumer, with enqueues until full — predict the number of elements it holds and confirm against the crate’s documentation for the capacity semantics of your pinned version. Read the .bss/.data sizes with cargo size -- -A before and after adding each static collection and predict each delta from the type.
| Collection | Predicted capacity behavior | Observed | Predicted .bss delta |
Observed |
|---|---|---|---|---|
Vec<i16, 8> (static) |
9th push → … | … | … | … |
String<32> (static) |
overflowing write! → … |
… | … | … |
spsc::Queue<u16, 8> (static) |
holds … elements | … | … | … |
Deliverable: the program, the table, and a notes.md line on what heapless::Vec::push returning Err(value) means for the caller compared with alloc::vec::Vec::push — who owns the value that did not fit.
Exercise 6.6 — What a heap costs. Create a second qemu binary (rust/qemu/src/bin/ex-6-6.rs) that enables extern crate alloc, installs embedded-alloc over an 8 KB static region, and pushes to an alloc::vec::Vec<u16>. Predict, before building, the direction and rough magnitude (qualitative: tens of bytes / hundreds / kilobytes) of the .text and .bss deltas versus Exercise 6.5’s heapless program, then read them with cargo size. Then allocate past 8 KB and record what happens in QEMU. In C (c/host/src/ex-6-6/), write the flexible-array-member frame_t of lessons §2.1 with malloc, and its static-pool twin (a pool of fixed-maximum frames); build under ASan and deliberately commit each of the three memory-state violations — read before write, use after free, double free — recording ASan’s report line for each.
| Measurement | Predicted | Observed |
|---|---|---|
.text delta, alloc vs. heapless |
… | … |
.bss delta |
… | … |
| Allocation past the heap region | … | … |
| ASan: read-before-write | … | … |
| ASan: use-after-free | … | … |
| ASan: double free | … | … |
Deliverable: the table and the notes.md verdict: for a Cortex-M4 with 96 KB of RAM, what the allocator bought and what it cost, in your own numbers.
Exercise 6.7 — Page faults in a real-time loop (Linux tier). Write a 1 kHz loop that fills a 1 MB buffer once per iteration, in C (c/linux/src/ex-6-7/) and Rust (rust/linux/src/bin/ex-6-7.rs), and count minor page faults with getrusage (ru_minflt) across the loop’s first 100 iterations and its next 100, in three configurations: (a) as written; (b) with the buffer pre-touched before the loop; (c) with mlockall(MCL_CURRENT | MCL_FUTURE) plus the mallopt settings of lessons §8 (Rust: nix::sys::mman::mlockall and the same mallopt calls through libc). On the Mac, build and run the C version with the mallopt lines under #ifdef __linux__ and the Rust version with the equivalent cfg — macOS has mlock but not mlockall — and predict which configurations still fault. Optional rung: build and run both on the Jetson (cargo build --release on the board) and fill the real column.
| Configuration | Predicted faults, first 100 / next 100 | Observed (Mac) | Observed (Jetson) |
|---|---|---|---|
| (a) as written, C | … / … | … | … |
| (b) pre-touched, C | … / … | … | … |
(c) locked + mallopt, C |
… / … | … | … |
| (a) as written, Rust | … / … | … | … |
| (b) pre-touched, Rust | … / … | … | … |
| (c) locked, Rust | … / … | … | … |
Deliverable: both programs, the table, and a notes.md paragraph stating the steady-state fault count a real-time loop must show and what std::hint::black_box (or a volatile sink in C) was needed for to make the pre-touch survive -O2/--release.