The C Stack
What is a stack?
A stack is a data structure that follows a LIFO (last in, first out) element adding order.
- This means that when you remove an element from the stack, the most recently added one will be removed first.
- Likewise, the first added element, is the last one to go.
How is a stack used?
There are many different uses for the stack data structure, but the one focused on here is the hardware stack.
This is what your computer uses for memory that is allocated towards applications.
There is also the heap, which is similar but has different uses in the software context.
How is memory in the hardware stack allocated?
That depends!
Lets take an example from C.
|
|
What’s happening here?
Well, it’s simple, each time a function is called, (let’s start with main()
), a stack frame is created.
This is a chunk of memory that contains everything required in the function, within this stack frame is every variable in the function.
Entering the main function, here is what a simplified view of the stack would look like:
Stack |
---|
int y; |
main - Stack Frame |
Next we go into printf
which calls function()
, and the stack would look like:
Stack |
---|
int x; |
function - Stack frame |
int y; |
main - Stack frame |
What does this matter?
Knowing how the stack works is very important when working in embedded, and low-level development.
It’s important that you know exactly how your computer is doing things in the background, so you can code accordingly, and diagnose issues when they come up.
What else?
Well, there are two different ways that a processor will handle a stack.
- Growing upwards
- Growing downards
What does it mean if a stack is growing upwards?
- Well it’s simple.
- This means that the address of the first element (or the lowest element) in the stack is the lowest possible memory address.
- For this example, lets say that the address is
0x0000
- Anything else that will be added to the stack will increase the memory address.
- The second element would be at
0x0001
, the third would be at0x0002
, etc. - This is a gross oversimplification, however it is a good representation.
What about downwards?
- Similar to growing upwards, however the address of the first (or lowest) element in the stack is at the highest possible memory addrses.
- Let’s say the address of the first element is
0xFFFF
. - Anything else that will be added to the stack will lower the memory address.
- The second element would be
0xFFFE
, the third would be at0xFFFD
, etc.
Now that we know this. Let’s find out whether my system’s stack grows up down.
The code
|
|
Download: code
How does this code work?
You might be asking, why don’t you just declare two variables and check their addresses in a single function.
- Firstly, you would need to specify those variables with
volatile
so signify to the compiler that these variables shouldn’t be optimized by the compiler.- GCC (or any C compiler) will optimize everything to the best of its ability, which includes the positions of variables on the stack.
- Secondly, the addresses and position of these variables on the stack are not guaranteed.
- By using a recursive function, we create a new stack frame which guarantees a push onto the stack.
This code looks a little complex, so let’s dissect what’s happening here.
In main()
we have a simple ternary expression that evaluates to “Up” if upordown
returns true
, and “Down”, if it returns false
.
We’re passing NULL
to upordown
because it expects a pointer.
As we move into upordown
, we can see we check if other
is NULL
. Since it is, we call it recursively, while passing the address of x
along with it.
Now we compare &x > other
, and it returns false. Which means the stack grows downwards.
Why does this work?
The first call to upordown
generates a stack frame with int x
in it.
Down:
Stack | Memory Address |
---|---|
x | 0xFFFD |
upordown | 0xFFFE |
main | 0xFFFF |
Up:
Stack | Memory Address |
---|---|
x | 0x0002 |
upordown | 0x0001 |
main | 0x0000 |
Then we recursively call upordown
and pass in &x
. So now we have ANOTHER stack frame, with a new x
, and the address of the last x
.
Down:
Stack | Memory Address |
---|---|
x | 0xFFFB |
upordown | 0xFFFC |
x | 0xFFFD |
upordown | 0xFFFE |
main | 0xFFFF |
Up:
Stack | Memory Address |
---|---|
x | 0x0004 |
upordown | 0x0003 |
x | 0x0002 |
upordown | 0x0001 |
main | 0x0000 |
If the stack grows down:
- Let’s say that the first stackframe is at address
0xFFFE
, and thex
is at address0xFFFD
. - In the second call, the stackframe is at address
0xFFFC
, and thex
is at address0XFFFB
. - The comparison evalutes out to
0xFFFB > 0xFFFD
. This is false, so we know that the stack grows downwards.
If the stack grows up:
- The first stackframe is at address
0x0001
, and thex
is at address0x0002
. - In the second call, the stackframe is at address
0x0003
, and thex
is at address0x0004
. - The comparison evaluates out to
0x0004 > 0x0002
. This is true, so we know that the stack grows upwards.
In my case (and really for everyone’s case), this will come out to show that the stack in fact, grows downwards.