Another strategy that I've been thinking about: Place the initial stack pointer at the end of .bss and let it grow _upwards_. Instead of a stack overflow mangling your .bss/.data segments, the processor will fault when de-referencing the $sp, instead of your code subtly veering off course. Debugging a stack overflow can be very difficult unless you know what you are looking for. This strategy assumes that you're not using the heap and are running on bare-metal. It even works without an MMU!
You can't generally change the direction the stack moves, at least on targets like Cortex-M. Maybe in ASM it'd be feasible to work around, but getting LLVM to do this isn't anything I've ever seen.
The "walk off the edge" technique is what is described here [0], in embedded rust we have a tool that does this automatically (from the author of the post) called `flip-link` [1]. It requires a second linker/re-linking pass to do this sizing automatically (you could also do the same manually with a linker script in one pass, if you hardcode certain sizes).
It's a matter of the ABI, and most importantly the calling conventions. Plus, depending on the architecture, you'll not be able to use some of the builtin push/pop/call/return-like instructions anymore. This can be a minor caveat or a major performance penalty, depending on your program and target architecture.
And I'm not even talking about compiler-builtins or libc stuff that will wreak havoc unless you rewrite all of that for your new ABI.
Also: The program still crashes at "random" spots instead of behaving unpredictable. But for that you could add some assembly at the beginning of each function, asserting that the sp is in the pre-defined stack range. If your hardware supports it, you could also mark the "overflow area" behind the stack as read-only and let it crash that way.
Due to a past job I'm biased to static program analysis for this task, something the post author completly neglects (doesn't know about?). At least if sound boundaries matter (e.g. safety critical stuff).
At least in Rust, we have `cargo-call-stack` [0] for static analysis, but "worst case stack usage" can be challenging to measure, if you have interrupts (including nested ones at different prios so you have to add them together!), function pointers (including dyn dispatch), or recursion.
There are ways to annotate and work around this! I'm also from a safety critical background. But there aren't many (any?) OSS tools that do this completely. cargo-call-stack is the closest I've seen.
What makes you think the processor will fault? Many small µc architectures don't fault at any point.
If you have an MPU or an MMU (which is generally true for 32-bit microcontrollers), you can just set a range to fault a standard downwards growing stack – pretty standard stuff.
Some mc have a statically defined memory map with regions that are read only or unmapped etc, so you can find a boundary where the CPU will fault when you cross it.
It may not be possible to dynamically redefine those though
Is there an easier way to measure this in normal programs? For example, is it possible to keep track of the pages the kernel allocates for the stack? If the program touches those pages, we know the stack's high water mark has reached that page. Would be useful for limiting the native stack usage via resource limits.