The stack and sp

RISC-V has no push and no pop: the stack is memory, sp is an ordinary register, and a push is a subtraction and a store. Saving several registers with one adjustment, local room, and giving every byte of it back.

The stack lecture of Assembly basics pushed by hand, moving a stack pointer and then writing at it. That is what RISC-V does, because RISC-V has no push instruction and no pop instruction: sp is x2, one of the 32 ordinary registers, and everything about the stack is a convention plus two instructions you already know.

A push is a subtraction and a store

sp starts at 0x7FFFEFFC, near the top of the address space, and the stack grows downwards. So:

  • a push is addi sp, sp, -4 and then sw at 0(sp),
  • a pop is lw from 0(sp) and then addi sp, sp, 4.

The 4 is the size of what you are putting there, and it has to keep sp a multiple of 4, because a sw at an address that is not ends the run.

Step through it and watch sp in the registers panel. Before the first push the stack is empty and sp holds 7FFFEFFC (🟢 is the stack pointer, and untouched memory on this machine reads zero):

addressvalue
0x7FFFEFF400000000
0x7FFFEFF800000000
0x7FFFEFFC🟢 00000000

The addi drops sp to 0x7FFFEFF8 and the sw writes there:

addressvalue
0x7FFFEFF400000000
0x7FFFEFF8🟢 11111111
0x7FFFEFFC00000000

The second pair drops it another 4 and writes underneath:

addressvalue
0x7FFFEFF4🟢 22222222
0x7FFFEFF811111111
0x7FFFEFFC00000000

Then the two pops read them back in the other order, so t2 gets 22222222 and t3 gets 11111111, and sp climbs back to 7FFFEFFC. Last in, first out, and both values are still in memory afterwards: popping moves the pointer and erases nothing.

Type 7FFFEFF4 in the memory panel's address box after running and both words are still there. The Stack tab of the memory panel is already looking at that region.

One adjustment, several stores

Nothing says a push has to be one word. Move sp once by as much as you need and reach the room with different offsets, which costs one instruction instead of one per value.

All three registers end at the values they started with. While they are saved, sp is at 0x7FFFEFF0 and the stack holds:

addressvaluereached asregister
0x7FFFEFF0🟢 000000010(sp)s0
0x7FFFEFF4000000024(sp)s1
0x7FFFEFF8000000038(sp)s2

The offsets are yours to choose and the only rule is that the same one is used to save and to restore. This is exactly what a subroutine does on entry and exit, and "jal, ret and the calling convention" writes that out.

The RISC-V ABI asks for more than the alignment the instructions need: it says sp should be a multiple of 16 whenever one subroutine calls another, so that a value of any size can be put on the stack without a fault. This simulator only enforces the multiple of 4 that sw needs, so the -12 above runs, and a compiler would have written -16 and left four bytes unused.

Room of your own

The other use of the stack is space. addi sp, sp, -16 takes sixteen bytes, which you then reach as 0(sp), 4(sp), 8(sp) and 12(sp), and addi sp, sp, 16 gives them back. Nothing allocates it and nothing checks it: the stack is memory, and sp is the only record of which part of it is yours.

t5 comes out at 10 and t6 at 13. While the loop is running the four words are at 0x7FFFEFEC to 0x7FFFEFF8, and after the last addi they are still there and no longer yours: the next thing that takes room gets the same addresses and writes over them.

That is what "the stack is memory and the pointer is the only record" means in practice. Reading below sp reads whatever the code that used that room before you left behind.

Give every byte back

A subroutine that moves sp and does not move it back leaves the caller's stack pointer somewhere else, and every offset the caller had into the stack is wrong from then on. Worse, the return address of a call is on the stack too, which the next lecture is about, so a mismatched adjustment makes a ret jump to a number that was never an address.

The rule is one line: whatever a piece of code subtracts from sp, it adds back before it hands control on. Write the two addi instructions at the same time, before you fill in what goes between them.

There is nothing to stop you breaking it. sp is x2 and add sp, sp, t0 assembles as happily as any other addition. The Stack tab of the memory panel and the sp row of the registers panel are how you check.

Your turn

The test starts t0 at 0x11111111 and t1 at 0x22222222, and wants them exchanged. Do it through the stack, without a third register.

Show solution

The second one starts s0, s1 and s2 at 1, 2 and 3, and the three li lines in the middle are not yours to change. Save the three registers before them and put them back afterwards, moving sp once each way.

Show solution