Contents:

Printing strings and characters

Strings are many bytes long and cannot fit in registers. So, we put them in the data segment, and then refer to them by their address (location in memory).

For example, print_string("Hello!\n") would look like:

# you can switch to .data anywhere you want, not just at the top of the file
.data
str_hello: .asciiz "Hello!\n"
.text
	la a0, str_hello # get the string's address
	li v0, 4         # v0 = 4 for print_string
	syscall          # print it!

Single characters do fit in a register, and you can use li to put them there.

For example, print_char('\n') will print a newline, and it would look like:

	li a0, '\n' # characters are integers, so this is fine
	li v0, 11   # v0 = 11 for print_char
	syscall     # print it!

Using (Global) Variables

Globals are stored in memory. First, you need to declare the variable in the data segment, like this:

.data
	# this is like: int my_var = 10;
	my_var: .word 10

# don't forget to switch back to the text segment
.text

To access this variable, you must use load instructions to get its value, and store instructions to set its value.

For example: the equivalent of print_int(my_var) would look like:

	lw a0, my_var # get the variable's value
	li v0, 1      # v0 = 1 for print_int
	syscall       # run the syscall

You can change its value with a store instruction. my_var = 20 would look like:

	li t0, 20     # must put the value in a register
	sw t0, my_var # copy that 20 into my_var

Read-modify-write operations, like incrementing, require a load and a store. my_var++ would look like:

	lw  t0, my_var # get the value
	add t0, t0, 1  # add 1 to it
	sw  t0, my_var # and put the incremented value back into it

Choosing registers

It’s way less complicated than you are thinking.

  1. If you are about to call a function, its arguments go into a registers.
    • Start at a0, then a1 etc
  2. If you are about to return a value from a function, the return value goes into v0.
    • Also, if you are about to do a syscall, the syscall number goes into v0. idk why.
  3. Otherwise ask yourself this question: do I need to access this value again after a jal?
    • No? Use a t register.
      • Most values only have to be in a register for a brief time.
      • It’s common to use these for calculations, variable access, intermediate values etc.
      • Reuse the registers. You don’t have to do t0, t1, t2, t3, t4, t5, t6 etc. I rarely need to use beyond t2 because I rarely need to be using more than 3 “things” at once.
    • Yes? use an s register.
      • It’s common to use these as loop counters and other “local variable” tasks.
      • Be sure to push and pop them as described in “writing functions” below.

If that question in 3 doesn’t make sense, think of it in Java terms:

void func() {
    // i is a local variable. I should probably use an s register to represent it.
    for(int i = 0; i < 10; i++) {
        // the address of this array is only used once, within this line, and never again.
        // I should probably use a t register to load its address.

        // also, since the value from the array is the first argument to print_int,
        // I should load that value into a0.
        print_int(my_array[i]);
    }
}

Doing Arithmetic

The CPU can only do one operation at a time. So, write out your algebraic expression, and use order-of-operations to determine the sequence of operations you need to perform.

Examples:

	# x = y + z
	# we just need to do one 'add' here.

	lw  t0, y       # t0 = y
	lw  t1, z       # t1 = z
	add t0, t0, t1  # t0 = y + z
	sw  t0, x       # store y + z into x

	# ---------------------------------------
	# x = x / 2 + 3 * y - z
	# 1. divide, 2. multiply, 3. add, 4. subtract.
	# it's helpful to make notes of what is in which registers as you go.
	# notice how the value kind of "builds up" in t0. like a snowball.

	lw  t0, x      # t0 = x
	div t0, t0, 2  # t0 = x / 2
	lw  t1, y      # t1 = y
	mul t1, t1, 3  # t1 = y * 3
	add t0, t0, t1 # t0 = x / 2 + 3 * y
	lw  t1, z      # t1 = z
	sub t0, t0, t1 # t0 = x / 2 + 3 * y - z
	sw  t0, x      # store the final result into x!

Writing functions

For a foolproof way to write functions, do this:

  1. Start with this skeleton code:

     function_name:
         push ra
         # --------------------------------
    
         # --------------------------------
     function_name_end:
         pop ra
         jr ra
    
  2. Write code between the ---- lines above. The pushes and pops are like the {} on a function.
    • Arguments are already in the a registers.
    • To return early, use j function_name_end. DO NOT just throw a jr ra in the middle of the function!!!!!!
    • To return a value, put a value in v0 before going to function_name_end.
  3. If you need to use any s registers, push them at the beginning, and pop them in reverse order at the end:

     function_name:
         push ra
         push s0
         push s1
         # --------------------------------
    
         li s0, 0
         li s1, 1
         # ... blah blah more code...
    
         # --------------------------------
     function_name_end:
         # all pops in REVERSE order.
         pop s1
         pop s0
         pop ra
         jr ra