Module 7 Exercises — Modern C After K&R
Back to the Course 3 syllabus. Read first: MC 1–38; Seacord 2–4 as assigned.
Work in course3/m7/ of the labs repo. Everything here runs on the Mac: host-side experiments compile with clang, and every STM32-flavored question is answered by cross-compiling with arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb and reading the disassembly — same convention as Module 6, no board required. Predicted cells are filled in before compiling; observed cells at the machine.
Exercises
Exercise 7.1 — The dialect ladder. Write one small source file that deliberately uses the Module 7 feature set: a // comment, a mid-block declaration, a for-scope counter, a designated initializer, a compound literal, a static inline helper, a variadic macro, and a _Static_assert. Compile it under -std=c90, -std=c99, -std=c11, -std=c17, and -std=gnu17, each with the MC 49 warning set (-Wall -Wextra -Wpedantic -Wconversion -Wshadow -Wstrict-prototypes), on both clang (host) and arm-none-eabi-gcc. Complete the acceptance table:
| Feature | c90 | c99 | c11 | c17 | gnu17 |
|---|---|---|---|---|---|
// comment |
… | … | … | … | … |
| Mid-block declaration | … | … | … | … | … |
| Designated initializer | … | … | … | … | … |
| Compound literal | … | … | … | … | … |
static inline |
… | … | … | … | … |
| Variadic macro | … | … | … | … | … |
_Static_assert |
… | … | … | … | … |
In notes.md: which mode the Course 2 labs repo toolchain files actually pin, and why gnu17 rather than strict c17 is the practical choice for code that includes CMSIS headers (MC 2).
Exercise 7.2 — Modernize a K&R-era module. Write (or resurrect) a small module in deliberately 1989-vintage style: all declarations at the top, plain int/long everywhere, memset for clearing state, function-like macros for small helpers, positional struct initializers. Then port it to the Module 7 idiom: <stdint.h>/<stdbool.h> types, declarations at first use, {0} and designated initializers, static inline replacing the macros, _Static_assert guarding every layout or width assumption the old code made silently. Diff the -O2 disassembly of both versions on both toolchains. Deliverable: the before/after source pair plus a notes.md verdict on the codegen diff — the modernization should be free (identical or better code), and the note says where and why.
Exercise 7.3 — Widths and promotions, LP64 vs. ILP32. The same C integer expressions mean subtly different things on the Mac (LP64) and the STM32 (ILP32). First fill in the sizes table by prediction, then verify — on the host by running, on the cross side with _Static_assert(sizeof(...) == ...) probes that either compile or don’t:
| Type | Apple Silicon (LP64) | Cortex-M4 (ILP32) |
|---|---|---|
int |
… | … |
long |
… | … |
size_t |
… | … |
uintptr_t |
… | … |
long long |
… | … |
Then predict the value and type of a battery of promotion traps from MC 6–10 and Seacord 3 — uint8_t * uint8_t overflowing into int, uint16_t shifted left 20, int8_t right-shifted, unsigned wraparound, a signed/unsigned comparison, a 32×32→64 product with and without the cast-before-multiply — and verify on the host with <inttypes.h> PRI macros. Record every case where your prediction missed and which promotion rule explains it.
Exercise 7.4 — Designated initializers and the compound-literal lifetime. Build a CMSIS-style configuration struct (model it on GPIO_InitTypeDef) and a sparse lookup table using [index] = designators. Verify in lldb that unnamed members and gaps really are zero-filled (MC 12, 14). Then demonstrate the MC 13 pitfall deliberately: a function that returns (or stores) a pointer to a block-scope compound literal. Predict what -Wall -Wextra catches at compile time and what only AddressSanitizer catches at runtime; verify both. Deliverable: the two-column “caught by warnings / caught by ASan” table with observed compiler output.
Exercise 7.5 — Compile-time layout contracts. Invent a small “datasheet” for a fake peripheral: a register block with fixed offsets. Write the C struct overlay for it and pin the contract down with _Static_assert + offsetof for every register, a width assert for every field type, and _Alignas(32) on a DMA-style buffer whose alignment you then confirm via _Alignof, the linker map, and an lldb address check (MC 25–27). Break one assertion deliberately (reorder two members) and record the exact compile error — the point of the exercise is that the build fails, not the bench session. Cross-compile the same file to confirm the contract holds identically under arm-none-eabi-gcc.
Exercise 7.6 — A type-safe clamp with _Generic. Implement clamp_i16, clamp_i32, and clamp_f32 as static inline functions and dispatch them through one clamp(x, lo, hi) generic selection (MC 30). Contrast with the classic macro version: pass an argument with a side effect (counter++) through both and show — by predicted-then-observed output — that the _Generic/static inline version evaluates once while the macro double-evaluates. Then pass an unhandled type and record the diagnostic. In notes.md: one paragraph on where _Generic earns its keep in firmware (type-safe register/DSP helpers) and where it’s over-engineering (MC 30’s caveat).
Exercise 7.7 — restrict on the course dot product. Take Module 5’s naive C dot product and produce the four-way codegen comparison: with and without restrict-qualified pointers, at clang -O2 on A64 and arm-none-eabi-gcc -O2 on the M4 (MC 18):
A64, no restrict |
A64, restrict |
M4, no restrict |
M4, restrict |
|
|---|---|---|---|---|
| Inner-loop instruction count | … | … | … | … |
| Vectorized? | … | … | … | … |
| Loads per iteration | … | … | … | … |
Deliverable: the table plus the API-contract paragraph — when restrict is an honest promise the caller can actually keep, what happens when it lies (UB, not a wrong answer you can test for), and why the CMSIS-DSP sources you read in Exercise 6.5 use it where they do.
Exercise 7.8 — Atomics without an OS. Probe what <stdatomic.h> really costs on each machine (MC 33–35). Write an atomic_uint event counter incremented from a “handler” and drained from a main loop, plus the same with a plain volatile uint32_t. Cross-compile at -O2 and annotate the disassembly: the atomic version’s LDREX/STREX retry loop vs. the volatile version’s plain (and racy) read-modify-write. Then fill the lock-free table by prediction and verify with atomic_is_lock_free on the host and the ATOMIC_*_LOCK_FREE macros plus emitted calls on the cross side:
| Object size | A64 lock-free? | Cortex-M4 lock-free? |
|---|---|---|
| 1 byte | … | … |
| 2 bytes | … | … |
| 4 bytes | … | … |
| 8 bytes | … | … |
Close with the fence distinction (MC 35): where atomic_signal_fence compiles to nothing but still matters, and what atomic_thread_fence emits on each ISA. This exercise is the language-level floor under Module 8’s ISR patterns and Course 2’s Module 7 RTOS work.