Module 4 Exercises — Functions, the Stack, and the C Boundary
Back to the Course 3 syllabus. Read first: Smith 6, 7, 9; Apple’s Writing ARM64 code for Apple platforms (the ABI divergences section).
Work in course3/m4/ of the labs repo. Mixed C/assembly builds: clang harness.c routine.s -o test — the driver handles both.
- x18 is reserved. Never use it, even scratch.
- Variadic functions take all variadic arguments on the stack, 8-byte slots — unlike Linux, where the first eight still ride in registers. This is the #1 “printf from assembly works on the Pi but not the Mac” gotcha, exercised below.
- SP must be 16-byte aligned at every external call (hardware-enforced on access via SP).
- No stable raw-syscall ABI — the libc boundary (
_write,_exit, …) is the OS interface.
Exercises
Exercise 4.1 — Recursion with real frames. Recursive factorial: full prologue/epilogue (stp x29, x30, frame pointer chained), argument preserved across the recursive call in a callee-saved register or on the stack. In lldb, break at maximum depth for fact(6) and: (a) run bt — confirm 6 frames; (b) dump the raw stack with memory read and identify each frame’s saved x29/x30 pair by hand. Deliverable: the annotated stack dump in notes.md, each frame boundary marked.
Exercise 4.2 — Assembly under a C harness. Implement size_t my_strlen(const char *) in assembly, AAPCS64-clean. Drive it from a C harness that checks correctness against strlen over a test set, then benchmarks both (Module 2 discipline) on a ~1 MB string. Record the ratio — then note in notes.md why libc wins (it vectorizes; Module 5 closes the gap).
| Implementation | ns per 1 MB scan | Ratio |
|---|---|---|
my_strlen (byte loop) |
… | … |
libc strlen |
… | 1.0× |
Exercise 4.3 — Calling back into C. Write an assembly routine apply(long *arr, size_t n, long (*f)(long)) that maps f over an array in place, calling through BLR with correct save/restore of its own state around each call. Drive it from C with two different callbacks. Single-step one BLR round trip in lldb and watch lr/x30 change hands.
Exercise 4.4 — The variadic gotcha, demonstrated. Call printf("%s %ld\n", s, n) from assembly twice: (a) the Linux way (arguments in x0–x2), (b) the Apple way (x0 = format, variadic arguments in 8-byte stack slots). Run both on the Mac; record what (a) actually prints and explain it via the callout above. This exercise is the reason Smith’s Linux examples “almost” work here.
| Variant | Output | Why |
|---|---|---|
| Register-passed variadics | … | … |
| Stack-passed variadics | … | … |
Exercise 4.5 — Walking the frame chain. With Exercise 4.1’s factorial paused mid-recursion, reconstruct the backtrace manually: start at x29, follow the saved-x29 chain, and list each return address; symbolicate with image lookup -a. Confirm your hand-walked list matches bt. Deliverable: the walk transcript — this is exactly what a debugger, a profiler, and a crash reporter do, and what Course 2’s hard-fault handler will do on the Cortex-M.
Exercise 4.6 — Leaf vs. non-leaf cost. Take Exercise 3.6’s min/max scan (a leaf) and wrap it in a non-leaf variant that spills/reloads a full callee-saved set on every call. Benchmark both called 10M times on a tiny array. The measured delta is the AAPCS’s price tag — worth knowing when Course 2 asks “why is my per-sample ISR budget gone?”