I seem to recall this came up a little while back.
Really, there is nothing that can be done on the architecture. Within the constraints of the underlying instruction set, if you call a function you cause a few things to happen. One is that the stack space is consumed by the return address, and the other is that auto variables (local variables) are also allocated stack space. So for a moment forgetting about dynamic memory allocation, the processor has no way of knowing if a function call is going to hit the "top end" of the statically allocated variables (and if you use dynamic memory, the dynamically allocated variables AKA the heap).
Some sort of check when doing a malloc doesn't solve the problem, because the problem can rear its head a moment later when you do a function call.
You could try allocating a "safety region" of X bytes, but when you only start with 2K bytes, the amount you have over for this purpose would be small.
Even then, what do you do? Stop working? Well it will probably do that anyway of its own accord. Do a "blue screen of death"? Hardly. Put a message out the serial port? What if the port is connected to a motor?
Plus, the memory (program and RAM) you consume implementing such a scheme may itself push the program over the edge. In other words, the debugging stuff might actually cause the problem it is trying to solve.
Is there any practical way to find out how much stack is available now, and the lowest stack availability (i.e. when stack use was at its peak) during the current execution?
This seems like the sort of thing that would be very easy to do within the runtime library and much more difficult to do outside it.
No, because no runtime library calls are involved in invoking a function.
I really think the best you can do is good design. For example, you leave yourself a margin. Put things into program memory (eg. string constants) where possible. Be aware of how much your variables take. Be aware of the likely depth of function calls. Be aware that local variables consume stack space, and go easy on their use. For example, none of this:
void foo ()
char buf ;
If you use dynamic memory be aware of fragmentation issues. Make sure you free things no longer needed.
It is also difficult for a developer to predict exactly how much memory a given sketch will use and also very difficult to tell whether the sketch has, in fact, run out of memory at any point.
The "free memory" function, or whatever it is called, used judicially during development, should give you an idea of how much you have used up.