We need to distinguish here between the "stack," which is a data
structure in the imagination of the programmer, and what the CPU does
with stack registers and the memory pointed to by them, which is a
mechanical process. When the CPU pushes a value on the stack it simply
changes the values in the stack pointer register and certain memory
locations; whether this action changes what the programmer decided was
the "stack" or some other area of memory depends on the program.
Later processors such as the 80826 offered hardware memory management
that allowed the programmer to tell the CPU what the top and bottom
addresses of his stack are and the hardware would generate an
exception if an instruction using the stack pointer tried to write to
memory outside this range. The progammer thus reserved memory for the
stack by use of these settings.
But earlier processors offered no such thing; a programmer "reserves"
an area for the stack only in his mind, and then attempts to make sure
that his code and any other code called during the course of running
his code (such as interrupt routines) won't write outside that area.
The stack pointer wrapping around may or may not be an issue
depending on how the programmer set up the stack (though it usually
would be). If the first free location on the stack is 0000
and you
push two bytes, these bytes will be stored in location 0000
and
FFFF
and the next free stack location will be FFFE
. Whether this
is a problem depends on if FFFF
was being used for something else.
E.g.:
- If the programmer had started the stack at
FFFF
, the write
toFFFF
would be overwriting information earlier in the stack and
you will have a problem if this is used at some point later on.
- If the programmer had started the stack at
7FFF
and the memory
above that was being used for other purposes, whatever was using
that memory location pointed to by SS:FFFF
(even though they may
not have been using the Stack Segment register to access that
location) will have its data changed, again likely causing problems.
- If the programmer had started the stack at
7FFF
, but nothing was
using the location FFFF
32K above that until after that value was
popped off the stack, the programmer's expectations might be
violated but no harm would come from this since it was "free" memory
anyway.
- If the programmer had started the stack at
7FFF
, but considered
the entire 64K stack segment to be "stack" anyway, things would be
just fine. The "top half" of the stack would be 0000-7FFF
and the
"bottom half" would be 8000-FFFF
.
Programmers always had to have some awareness of what interrupt code
was running in the system and what its stack usage requirements were
likely to be. For example, normally the 8254 system timer calls
INT 08h
on a regular basis, and the default handler in the
BIOS will need a little bit of stack space. A handler similar to this
one from the Bochs emulator would use four bytes of stack
when it pushes AX
and DS
, and an additional six bytes would have
been used by CPU automatically pushing the flags, instruction pointer
and code segement register. That handler also invokes INT 1Ch,
which
by default points to just an IRET
instruction and so that
would use an additional six bytes of stack, for a total of 16.
But this INT 1Ch
handler could be, and often was, replaced by
something else that wanted to be called on a regular basis, such as
with a TSR (terminate and stay resident) utility. Multiple TSRs might
stack on each other, each one replacing the value for INT 1Ch
with its
own value and then doing both its own thing and invoking the previous
handler when called.
There were also typically several other interrupts regularly happening
in the system (e.g., for disk drives, serial ports, and the like),
some of which might even be called when the system was already handling
an interrupt.
The combination of non-determinancy in the "standard" set of hardware
interrupts along with some random and unknown collection of TSRs and
similar software being loaded by the user before running a program
meant that it's nearly impossible to know how much spare stack space
you really needed. And if you overflowed your stack space, the program
or user might not even know about it for some time, depending on how
the stack was set up and what else was using areas of memory that
might be referenced by the stack pointer.
So the basic approach you'd take is:
- Leave as much extra space on the stack, beyond your program's
requirements, as reasonably possible. If you get close to using 64K
in your own stack, consider using multiple stack segments to keep a
reasonable amount of free space in every one.
- Try to ensure that any interrupt handler routines you write use the
minimum possible amount of the interrupted program's stack.
Consider allocating your own stack area and switch the stack to
that shortly after entering the handler if you use more than a few
dozen bytes of stack space.
- Hope for the best.
Early MS-DOS didn't offer any special facilities for switching to a
different stack, though of course there's nothing stopping anybody
writing an interrupt handler from just saving the old SS:SP
pair and
loading new values pointing to an area of memory they've allocated.
But starting in 3.2 MS-DOS could be configured to allocate additional
stack areas and switch to them after receiving a hardware interrupt
but before calling its handler. See Eric Towers' answer for
details.
Multiple Stack Registers and ARM FIQ
This isn't really related to the core of your question, but since you
mention these, I'll briefly elucidate on them.
As you mention, MC6809 had two "stack" registers, the system stack
pointer S and the user stack pointer U. Only the system stack pointer
was used by the CPU interrupt system. Whether U was even a separate
stack depended on how the program decided to use it; it could also be
used as another pointer into the main stack, as described in this
answer to the question you linked, or it could even be used as
just another index pointer, without using its push and pull operations
at all.
The ARM implmentation of faster interrupts had a separate set
of hardware registers that replaced r8 through r14 during an FIQ,
meaning that you don't need to push them on the stack at all, saving
the time it takes to do that. (It's actually a little more complex
than this, due to user vs. system mode and other things, but none of
that is really relevant to this question.)