MIPS unlike other archs doesn't have a push or pop register/immediate instruction. So you rely on managing the stack yourself. This is actually noted in most of the arch outside of mul/div where your registers don't have a specific use, just a suggested way to use it. Now if you used it however you wanted, you would break something if you tried to integrate with C for example.
In order to push something to the stack, you need to use a store instruction. These are sb, sh, sw, swl, swr
. byte, half, word, word left, word right respectively.
addiu $sp, $sp, -4 # push stack 1 word
sw $t0, 0($sp) # place item on newly pushed space
In order to pop something from the stack, you just need to deincrement it with addiu too. However, you may want to load the data from it using lb, lh, lw, lwl, lwr
.
lw $t0, 0($sp)
addiu $sp, $sp, 4 # pop stack 1 word
Here is an example of using it with two word push.
addiu $sp, $sp, -8 # allocate two words
sw $t0, 0($sp) # push two registers t0 t1
sw $t1, 4($sp)
lw $t1, 4($sp) # pop two registers t0 t1
lw $t0, 0($sp)
addiu $sp, $sp, 8 # deallocate two words
Here is an example of using it for return addresses so calls to non-leaf functions don't mess you up.
# grab us a quick string
.data
example_str: .asciiz "hello world :^)"
# grab us a function
.text
.globl example
.type test, @function
test:
addiu $sp, $sp, -4 # push stack for 1 word
sw $ra, 0($sp) # save return address
la $a0, example_str # call puts and give it a string
jal puts
nop
lw $ra, 0($sp) # load return address
addiu $sp, $sp, 4 # pop stack for 1 word
jr $ra # return from function to caller
nop
Here is an example of pushing multiple elements in a row. Popping is the reverse of course.
.data
example_arr: .word 0, 0, 0, 0
.text
addiu $sp, $sp, -16
la $t0, example_arr
lw $t1, 0($t0)
sw $t1, 0($sp)
lw $t1, 0($t0)
sw $t1, 4($sp)
lw $t1, 0($t0)
sw $t1, 8($sp)
sw $t1, 12($sp)
Here is an example of using malloc/calloc thereof.
# grab us a function
.text
.globl example
.type test, @function
test:
addiu $sp, $sp, -4 # push stack for 1 word
sw $ra, 0($sp) # save return address
li $a0, 4 # allocate 4*4 bytes (16)
li $a1, 4
jal calloc
nop
addiu $sp, $sp, -4 # push stack for 1 word
sw $v0, 0($sp) # save calloc'd buffer
move $t0, $v0 # get the buffer into a temp
li $t1, 1 # fill some temps with numbers
li $t2, 2
li $t3, 3
li $t4, 4
sw $t1, 0($t0) # save some temps to buffer
sw $t2, 4($t0)
sw $t3, 8($t0)
sw $t4, 12($t0)
... do stuff with the buffer ...
lw $a0, 0($sp) # pop buffer from stack
jal free # run it through free
nop
addiu $sp, $sp, 4 # don't forget to decrement
lw $ra, 0($sp) # load return address
addiu $sp, $sp, 4 # pop stack for 1 word
jr $ra # return from function to caller
nop
Like I mentioned earlier, nothing has a hard defined specific use, so you can also use your own stack and forget about using $sp if you want. I shown examples where I was using $t* as $s*. This works in the case of forcing each function to have its own stack for instance or some other usecase you can think of. As one instance, Lua (https://lua.org) does this to some extent. However, not MIPS. Multiple stacks are lovely especially when dealing with multiple objectives.
Edit:
I realized that I omitted the stack frame pointer. Be aware of that if your code is linked with something written in C, that you properly handle the stack frame pointer.