A stack is a LIFO data structure (last in, first out, meaning last entry you push on to the stack is the first one you get back when you pop). It is typically used to hold stack frames (bits of the stack that belong to the current function).
This may include, but is not limited to:
- the return address.
- a place for a return value.
- passed parameters.
- local variables.
You push items onto the stack and pop them off. In a microprocessor, the stack can be used for both user data (such as local variables and passed parameters) and CPU data (such as return addresses when calling subroutines).
The actual implementation of a stack depends on the microprocessor architecture. It can grow up or down in memory and can move either before or after the push/pop operations.
Operation which typically affect the stack are:
- subroutine calls and returns.
- interrupt calls and returns.
- code explicitly pushing and popping entries.
- direct manipulation of the stack pointer register,
sp
.
Consider the following program in my (fictional) assembly language:
Addr Opcodes Instructions ; Comments
---- -------- -------------- ----------
; 1: pc<-0000, sp<-8000
0000 01 00 07 load r0,7 ; 2: pc<-0003, r0<-7
0003 02 00 push r0 ; 3: pc<-0005, sp<-7ffe, (sp:7ffe)<-0007
0005 03 00 00 call 000b ; 4: pc<-000b, sp<-7ffc, (sp:7ffc)<-0008
0008 04 00 pop r0 ; 7: pc<-000a, r0<-(sp:7ffe[0007]), sp<-8000
000a 05 halt ; 8: pc<-000a
000b 06 01 02 load r1,[sp+2] ; 5: pc<-000e, r1<-(sp+2:7ffe[0007])
000e 07 ret ; 6: pc<-(sp:7ffc[0008]), sp<-7ffe
Now let’s follow the execution, describing the steps shown in the comments above:
-
This is the starting condition where
pc
(the program counter) is0
andsp
is8000
(all these numbers are hexadecimal). -
This simply loads register
r0
with the immediate value7
and movespc
to the next instruction (I’ll assume that you understand the default behavior will be to move to the next instruction unless otherwise specified). -
This pushes
r0
onto the stack by reducingsp
by two then storing the value of the register to that location. -
This calls a subroutine. What would have been
pc
in the next step is pushed on to the stack in a similar fashion tor0
in the previous step, thenpc
is set to its new value. This is no different to a user-level push other than the fact it’s done more as a system-level thing. -
This loads
r1
from a memory location calculated from the stack pointer – it shows a way to pass parameters to functions. -
The return statement extracts the value from where
sp
points and loads it intopc
, adjustingsp
up at the same time. This is like a system-levelpop
instruction (see next step). -
Popping
r0
off the stack involves extracting the value from wheresp
currently points, then adjustingsp
up. -
The
halt
instruction simply leavespc
where it is, an infinite loop of sorts.
Hopefully from that description, it will become clear. Bottom line is: a stack is useful for storing state in a LIFO way and this is generally ideal for the way most microprocessors do subroutine calls.
Unless you’re a SPARC of course, in which case you use a circular buffer for your stack 🙂
Update: Just to clarify the steps taken when pushing and popping values in the above example (whether explicitly or by call/return), see the following examples:
LOAD R0,7
PUSH R0
Adjust sp Store val
sp-> +--------+ +--------+ +--------+
| xxxx | sp->| xxxx | sp->| 0007 |
| | | | | |
| | | | | |
| | | | | |
+--------+ +--------+ +--------+
POP R0
Get value Adjust sp
+--------+ +--------+ sp->+--------+
sp-> | 0007 | sp->| 0007 | | 0007 |
| | | | | |
| | | | | |
| | | | | |
+--------+ +--------+ +--------+