Where the top of the stack is on x86 (2011)
eli.thegreenplace.net
eli.thegreenplace.net
It doesn't seem to be an standard and there are almost the same number of diagrams with memory from 0 to F than from F to 0. Some of them are more useful in specific contexts, but most are simply drawn the way the author is used to.
Personally I prefer increasing numbers go down, like lines in a file, numbers on a numbered list, timestamp on a log, etc. Having the highest memory value 0xFF on the top seems...odd.
btw my understanding is that the heap came first in the logical development of C and other systems programming. so it made sense to have .text and other program data at the lowest virtual memory addresses and then have the heap grow towards higher memory addresses.
then when the stack became a thing it had to grow down...
In fact, I think the only time I see diagrams with F at the top and 0 at the bottom is in explanations of the stack, or in explanations of how "the stack is so weird it grows the wrong way!".
When describing, e.g., a memory map for a custom ASIC, you would always put the low addresses first and the high addresses later. That's just how numbers work. The "stack grows the wrong way" issue seems to be an invented problem.
Just like, back when graphics bitplanes were a thing, it was much less confusing if the diagram just showed (virtual) memory interleaved, the way you'd actually have to deal with it as a programmer, rather than showing the "clean" physical-memory diagrams for what ends up on the "even" and "odd" chips.
(x86 does indeed have the bt family of instructions that sort of address bits. I would argue that the existence of memory-operand bit accessors like this that accept larger-than-bits-per-word offsets is bizarre, but the instructions are not backwards. On a big endian system, these instructions would have different behavior depending on word size, which would be odd.)
But, even without the ability to address the memory at the bit level, bits-within-bytes ordering still matters on any architecture where bits are striped between memory channels, RAID-0 wise. (Just like those graphics bitplanes!) The processor being LSB-first vs. MSB-first on how it interprets the data bus, determines which sockets you put the LoROM and HiROM firmware chips into your Apple II :)
If you want that 12-bit value to work nice, it's probably going to be all the bits of byte 0 and the lower half of byte 1. (Or the upper half of byte 0 and all of byte 1). That looks best if you write bits and bytes right to left from zero. That 12-bit value uses bits 0-11 and you can mask it from your 32-bit read with 0xfff. The 4-bit value you can mask with 0xf00 and then shift (or shift first, whatever)
We actually dealt with this by adding support to a general purpose open source parsing tool we maintain [0]; where we had to add a feature to the standard specifically to accommodate this [1].
Even though least-significant-bit-first makes sense with littleEndian actually working with it is a giant pain, because approximately no tooling actually thinks that way.
Dunno ... the highest being on top feels very normal ... . Isn't that more or less the definition of "highest" and "top"?
We have conflicting conventions in all kinds situations:
Numbers increase towards the top in a class Cartesian coordinate system, in the numbering of floors in buildings, on calculator keypads, ...
Numbers increase towards the bottom in coordinate systems in many computer graphics contexts, in numbered lists, on telephone keypads, ...
Sometimes these conventions meet and clash and there is no one right way to handle that.
> You may try to fix the confusion by viewing memory with its low addresses at the top and high addresses at the bottom. While this would indeed make stack movement more natural, it would also mean that increasing some memory address would take it down in the graphical representation, which is probably even more counter-intuitive.
Ultimately, my approach is to avoid directional terms such as 'top' and 'bottom' or 'high' and 'low' in the first place; they only cause confusion. Prefer 'greater' or 'smaller' addresses.
(Similarly 'left' and 'right' when applied to bits, which gets especially confusing if endianness is involved; prefer 'more significant' and 'less significant'.)
I think it's better to just stop drawing memory diagrams vertically, and show it horizontally instead. At least most people seem to agree that left is lower and right is higher.
Most operating systems and standard libraries have checks and countermeasures to make this a lot harder nowadays, but still.
I actually don't know where it came from. It was true in original PDP-11 Unix for sure: you had the program text followed by a grows-up heap, with the grows-down stack placed at the top of the program segment. Interestingly PDP-11 addressing was general enough to have implemented a grows-up stack, so this is clearly a mistake Unix could have corrected. I just don't know if this was the original use of the convention or if it inherited it from elsewhere.
The 8080 had 64KB address space. I bet the rationale was to partition memory so that classic memory usage goes upwards from 0000 to FFFF and the stack goes downwards from FFFF to 0000. This removes the need to define a boundary between the two beforehand.
Of course this is totally speculation on my part, I might be super wrong.
Independently the 6502 also had a downward stack. I think the only modern machine with am upward-growing stack is the HP PA-RISC.
[1] https://www.unibw.de/patch/papers/usenixsecurity20-wasm.pdf
"The x86 architecture is the weirdo" --Raymond Chen
https://devblogs.microsoft.com/oldnewthing/20040914-00/?p=37...
https://devblogs.microsoft.com/oldnewthing/20130320-00/?p=48...
[1] EDIT: This comment mentions PA-RISC: https://news.ycombinator.com/item?id=27079832
In coordination with the kernel, yes. Different OSes do this differently, but many will set up the stack pointer for you. Linux historically has not, the new thread keeps the parent stack pointer and has to implement careful entry code to switch it without messing up the parent thread.
> Couldn't it decide to set up the stack starting from the bottom of memory and put heap allocations at the top?
In theory, but as you mention the architecture makes some clear demands here. On x86 CALL/RET and PUSH/POP both require a SP/ESP/RSP register with free space below it. Likewise x86 interrupts are handled on the stack and so you need to keep the stack pointer initialized for them (modern CPUs will automatically switch the stack for you from whatever user code is running, but they still need to switch it to a grows-down area you initialized for them).
Broadly, sure, you can come up with a software abstraction that acted as a "stack", but it would have to be in addition to the CPU stack you already need. In effect you'd have to burn a register for this extra stack pointer, which has performance implications.
[1] vs. the clone() Linux syscall, which isn't really useful to regular code because of the complexity.
Well, for calls to fork(2) and clone(2), sure, but the kernel will setup a stack for you on exec(2). It has to have somewhere to stick the command line args, env, and some other extra bits of information like a random seed.
It is particularly the case in a language like Forth, which has two stacks that can be manipulated directly by the user (actually the user has to if they want to get anything done...) : one for the parameters/arguments, and one for the return addresses. This deviates from the usual single hardware stack processors. Forth processors (there are still some in operation) fully support those two stacks.
When you implement a Forth interpreter in assembler, you generally use the hardware stack for either the parameters or for the return addresses, while the other is managed manually (usually using another addressing-capable register such as EBP, ESI or EDI on x86).
If for instance you dedicate one segment (either in the x86 sense or in the common meaning), you typically use the push down "hardware" stack that you initialize at the top of memory, while the return addresses stack is a "push up" software-managed stack and initialized at the bottom of the memory.
If your kernel is re-entrant, you need to keep platform stack conventions in the kernel, or an interrupt (or exception) during the kernel will overwrite your backwards stack.
I don't know for sure, but I think aignal handing in user processes runs on the user stack too (but I could easily be wrong).
If you really wanted, you could use the platform stack for call/ret only and have a separate data stack with whatever conventions you like.
It's worth mentioning what fully descending means here. I'm surprised the article didn't mention it.
"Fully" means that the stack pointer points to the last entry on the stack (the top). "Empty" means it points to the next entry just after the last (top) entry. That is, it points to where the next push would be written to.
Anyway, you can have whatever calling convention you want, really. All you need is a way to pass arguments, return values, and to know where to return to. You could have a linked list of frames allocated by malloc() with the head pointer in EBP, for example. Say the return address and caller frame pointer is stored in the start of the frame. Then you would return by mov ebx, [ebp]; mov ebp, [ebp+4]; jmp ebx. Or something lile that. This is a pretty ridiculous calling convention though, but it'd work.
TBH, dunno where the difficulty is.
On anything resembling a recent ARM core though, using the stack pointer register with anything other than a descending stack has you falling off the perf wagon as it's backed by a hardware stack engine.
Why? Because of Fortran. There are (were?) cases where a Fortran app would reach back on the stack to retrieve values from before the last call or some such. Rather than find some other way of making that work for those folks, the separate-return-stack idea was shelved.
And we all live with this debacle for decades.
I would guess it’s more because early systems had small address spaces. If your heap grows up from the bottom of memory, and one stack grows down from top of your address space, how do you know where to place that second stack, especially in a system with, say, a 64kB address space?
And the stack could go in a page not addressable by general purpose instructions, save call and return instructions (and perhaps some kernel debug).
Some limited-RAM embedded devices have dedicated stack RAM. But my point was about Intel x86
Another nice side effect aside from security is that it simplifies the return predictor.
https://en.wikipedia.org/wiki/Shadow_stack
But it seems a shadow stack is a slightly different concept which requires less changes in software.
2. How would coroutines/threads handle this? Switch two stack pointers?
2. Yes, they'd have to switch two stack pointers. I think a CPU could have a special instruction for this case.
https://fitzgeraldnick.com/2019/11/01/always-bump-downwards....
Apparently on PA-RISC (hppa) the stack grows the other way, so it is arbitrary.
Though I would agree that for the topmost value specifically, rsp+0 feels more natural than rsp-4 or rsp-8.
For one thing, you can have your code, static memory, and then heap at the beginning of the address space, and the stack at the end of the address space, and not have to worry about explicitly sizing the stack. On some architectures you could even just zero-initialize the stack pointer, and it will roll over to the end of memory on the first push.
You can also use the same machine code instructions that make array indexing efficient to access variables on the stack (load/store with literal offset,) without having to support signed offsets which would have effectively wasted a precious bit in a very common opcode.
When you overrun a buffer, it is more likely to cause your program to crash by corrupting the stack. This is bad in modern times for obvious security reasons, but pragmatically it would have been a useful "feature" in the early days of programming.
It pairs nicely with the semantics of a semaphore, where you decrement to acquire and increment to release.
The stack pointer conceptually points to valid memory, rather than pointing to invalid memory adjacent to valid memory. This likely has some favorable traits when it comes to how an architecture implements function calls. For example, RET could load the value at the stack pointer into the program counter, then increment the stack pointer. Loading from memory often takes multiple cycles compared to incrementing a register, and this starts the load a cycle earlier than otherwise for the same amount of combinatorial logic.
Nova had write PC to memory address and jump to the next address. But that is not CALL. And subroutine could not be bigger than 255, because there was no RET but short jump via the stored address.