51

When compiling C code and looking at assembly, it all has the stack grow backwards like this:

_main:
    pushq   %rbp
    movl    $5, -4(%rbp)
     popq    %rbp
    ret

-4(%rbp) - does this mean the base pointer or the stack pointer are actually moving down the memory addresses instead of going up? Why is that?

I changed $5, -4(%rbp) to $5, +4(%rbp), compiled and ran the code and there were no errors. So why do we have to still go backwards on the memory stack?

alex
  • 509

4 Answers4

93

Does this mean the base pointer or the stack pointer are actually moving down the memory addresses instead of going up? Why is that?

Yes, the push instructions decrement the stack pointer and write to the stack, while the pop do the reverse, read from the stack and increment the stack pointer.

This is somewhat historical in that for machines with limited memory, the stack was placed high and grown downwards, while the heap was placed low and grown upwards.  There is only one gap of "free memory" — between the heap & stack, and this gap is shared, either one can grow into the gap as individually needed.  Thus, the program only runs out of memory when the stack and heap collide leaving no free memory. 

If the stack and heap both grow in the same direction, then there are two gaps, and the stack cannot really grow into the heap's gap (the vice versa is also problematic).

Originally, processors had no dedicated stack handling instructions.  However, as stack support was added to the hardware, it took on this pattern of growing downward, and processors still follow this pattern today.

One could argue that on a 64-bit machine there is sufficient address space to allow multiple gaps — and as evidence, multiple gaps are necessarily the case when a process has multiple threads.  Though this is not sufficient motivation to change things around, since with multiple gap systems, the growth direction is arguably arbitrary, so tradition/compatibility tips the scale.


You'd have to change the CPU stack handling instructions in order to change the direction of the stack, or else give up on use of the dedicated pushing & popping instructions (e.g. push, pop, call, ret, others).

Note that the MIPS instruction set architecture does not have dedicated push & pop, so it is practical to grow the stack in either direction — you still might want a one-gap memory layout for a single thread process, but could grow the stack upwards and the heap downwards.  If you did that, however, some C varargs code might require adjustment in source or in under-the-hood parameter passing.

(In fact, since there is no dedicated stack handling on MIPS, we could use pre or post increment or pre or post decrement for pushing onto the stack as long as we used the exact reverse for popping off the stack, and also assuming that the operating system respects the chosen stack usage model.  Indeed, in some embedded systems and some educational systems, the MIPS stack is grown upwards.)


We refer to multi-byte items by the lowest address among them — i.e. by their first byte aka the beginning.  Another advantage of growing the stack downward is that, after pushing, the stack pointer refers to the item recently pushed onto the stack, no matter its size.  Growing the stack in the reverse direction means pointing to the logical end of the last item pushed.

Erik Eidt
  • 34,819
8

In your specific system the stack starts from high memory address and "grow" downwards to low memory addresses. (the symmetric case from low to high also exists)

And since you changed from -4 and +4 and it ran it doesn't mean that it's correct. The memory layout of a running program is more complex and dependent on many other factors that may contributed to the fact that you didn't instantly crashed on this extremely simple program.

nadir
  • 773
2

The stack pointer points at the boundary between allocated and unallocated stack memory. Growing it downwards means that it points at the start of the first structure in allocated stack space, with other allocated items following at larger addresses. Having pointers point to the start of allocated structures is much more common than the other way round.

Now on many systems these days, there is a separate register for stack frames which can be somewhat reliably be unwound in order to figure out the call chain, with local variable storage interspersed. The way this stack frame register is set up on some architectures means that it ends up pointing behind the local variable storage as opposed to the stack pointer before it. So using this stack frame register then requires negative indexing.

Note that stack frames and their indexing are a side aspect of compiled computer languages so it is the compiler's code generator that has to deal with the "unnaturalness" rather than a poor assembly language programmer.

So while there were good historical reasons for choosing stacks to grow downward (and some of them are retained if you program in assembly language and don't bother setting up a proper stack frame), they have become less visible.

0

It's like driving a car: You can drive on the left side of the road, or on the right side, it doesn't really make a difference, except that everyone in a large area needs to agree.

It's the same with the direction the stack grows: It doesn't really matter. Of course the processor hardware will make some assumptions, so changing the direction would mean new hardware, so we are bound by that. But apart from the hardware's use of the stack, changing the direction wouldn't give us any advantage. So why are we still driving on the left side of the road? Because changing it would be pointless.

And personally, giving memory locations names like "top" or "bottom", "forward" or "backward", "up" or "down", "left" or "right" is rather pointless to me.

gnasher729
  • 49,096