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.