4

I am new to MIPS programming and have been struggling to understand the MIPS program and how it flows. How can I understand it? Below is the code.

My doubt is in RTN function where $ra is returned where the execution supposed to return after jr $ra:

  • What will be stored after sw $ra, 8($sp) to stack because when this statement first executes what will be the value of $ra?
  • Is it some garbage value or do we need to assume some value in order to understand the program?
f: 
    addi $sp, $sp, -12
    sw $ra, 8($sp)
    sw $s0, 4($sp)
    sw $a0, 0($sp)
    bgt $a0, $0, L1
    add $v0, $0, $0
    j RTN

L1: 
    addi $t0, $0, 1
    bne $t0, $a0, L2
    add $v0, $0, $t0
    j RTN
    
L2: 
    subi $a0, $a0,1
    jal f
    add $s0, $v0, $0
    sub $a0, $a0,1
    jal f
    add $v0, $v0, $s0

RTN: 
    lw $a0, 0($sp)
    lw $s0, 4($sp)
    lw $ra, 8($sp)
    addi $sp, $sp, 12
    jr $ra
halfer
  • 19,824
  • 17
  • 99
  • 186
user2206366
  • 461
  • 3
  • 6
  • 17
  • 1
    _"when this statement first executes what will be the value of $ra?"_ You can find out easily by stepping through the code in a simulator like SPIM or MARS. – Michael Sep 23 '15 at 07:19
  • 1
    Yes I did run the code in MARS. For the first go suppose $a0=0 then the code goes to RTN and tries to retun some invalid address(because $ra=0 initially) at jr $ra and ends with error. So my questions was in ideal scenario where the code should return after jr $ra? Is it the next line where RTN has been called? which is L1: addi $t0, $0, 1 – user2206366 Sep 23 '15 at 20:17

2 Answers2

5

On function entry, ra holds the return address where our caller wants us to jump when we're done.

The function preamble saves it to the stack because the function body uses jal to make function calls. jal overwrites ra so we need to save/restore our own return address around that.

f: addi $sp, $sp, -12    ; adjust stack pointer to accommodate stack frame (subtract 12 bytes)
sw $ra, 8($sp)           ; save a copy of our return address
...

When the function is complete we can restore the things we saved, then use the standard jr $ra to return, setting PC = $ra.

...
lw $ra, 8($sp)           ; restore saved return address
addi $sp, $sp, 12        ; restore stack pointer (dispose of stack frame)
jr $ra                   ; jump to return address

(Some phrasing and code formatting copied from @PaulR's answer which seems to explain it incorrectly, so I felt all its text needed a rewrite outside of the code blocks.)

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
2

I am new to MIPS programming and have been struggling to understand the MIPS program and how it flows.

Preface

If you want to learn much more deeply about this material and how computer processors work (how they retrieve data, use it with the ALU, and store the data), I advise taking a Computer Organization course (prerequisite being Digital Design which covers basic transistor, logic gate, and single-bit memory components).

I am not very knowledgeable about floating point details in relation to MIPS. We glazed over it in my Computer Organization class.

Binary Review

If you aren't familiar with how to do binary addition, it is similar to regular addition - binary addition uses carry digits (but in binary they are called 'carry' for short). It is crucial to realize that only bits OF THE SAME PLACE VALUE can be added together. Just like lining up decimal points to add numbers in scientific notation, or how adding 33dec+7dec isn't the same as adding 33dec+700dec , 11bin+110bin isn't the same as 11bin+11bin.

Note that 000001bin, for positive integers (negative integers are treated differently in Two's Complement), is the exact same as 1bin. This is similar to how 876dec is the same as 0000000000876dec.

'Carry' is a fancy way of saying 'the next highest place value'; carry is never displayed except sometimes as either the most significant bit of the final result or in bug-testing each place value's full adder. Adding two bits (where both have the same place value and...) where they are both '1' gives an output of 0 for the same place value as both inputs, but a 'carry' (next higher place value) of 1 (which together is 10bin , which is 2dec): 1+1=2, or in binary, 1+1=10bin. Here is a chart of what I mean:

 1         0    1        0 
+1        +1   +0       +0
--        --   --       --
10         1    1        0
^         ^    ^        ^
carry=1   carry=0       carry=0


These 4 examples above show the basic rules of binary addition and hold
  true for each and every place value.
If you include a carry value into each of the additions, remember that
  you can split one addition into multiple operations: Instead of trying
  to compute 1+10+111, try (1+10)+111; this way you'll only ever deal
  with a maximum of two bits at a time, meaning you can use the above
  rules.
Note that all the bits being added (at least, looking at any single
  example of the four examples) are of the same place value.

If you want more-complicated rules, the result's place value
  (as long as it is the same place value as the input bits') will be an
  XOR of all the inputs' same place values. Example (where each input
  bit has the same place value and the result bit has that same place value):
 1
 1
+1                                          1 XOR 1 XOR 1
--                                          -------------
X1 (the X is a 'don't care'/irrelevant)                 1

I don't know how to explain the more-complicated carry rule in words so I will leave you to examine Full Adder (and as a prerequisite - Half Adder) circuits and figure that out for yourself.

The rightmost bit is always place value 1, the bit to the left of that being 2, the bit to the left of place value 2 being place value 4, then 8, then 16, then 32, etc. You can figure out further place values by powers: 2n+...+23+22+21+20 = 2n+...+8+4+2+1. The entire binary number itself (e.g. 11001)(not its place values) tells you which place values to add together and which to ignore (which place values to not add).

Two's Complement is used to convert between positive and negative numbers using only binary instead of having +- signs in the final binary representation. E.g. 11bin instead of -1bin. Numbers that use 2's Complement are called signed numbers. Numbers that are always positive are called unsigned numbers.

Overview of MIPS

Anything with a '$' in front of numbers or letters is a register, which is a group of consecutive bits. In MIPS, which is a 32-bit system, each and every register is made of 32 consecutive bits, even including the weird lo and hi registers designed for use with multiplication. In a 32-bit system, you can think of each register as a 32-bit array or as a house with 32 family members, each of whom has their own place in the house.

Adding two registers (e.g. $a0 + $a1) really means adding ('adding' in the grade school sense, no extra fanciness) the two values in each register. If register $a0 has bits representing the number -5 and register $a1 has bits representing the number 9, then $a0+$a1 = (-5)+(9) = 4. In MIPS, the literal (face-value number) '4' would be 'inserted' into a destination register where it can be stored and not just disappear after calculation.

The stack is a subsection of memory. The stack, as well as nearly every other type of memory, isn't made of registers. Registers are very fast. Accessing non-register memory is in the top two slowest instructions that can be done (division, retrieving from memory [i.e. reading] are the slowest).

stackPointer = addressOf"Top"OfStack. Top is in double quotes because the stack grows downward in memory when adding to the stack, making both adding to and removing from the stack confusing at first. This is discussed in code comments below.

The stack is used when you have to overwrite registers that already have important values you don't want to lose.

framePointer = addressOf1stWordOfFrame, where a frame is nearly all the info a single function will need. In a program with nested functions, there will be more than one frame. A single frame could contain all the argument registers and saved variable registers along with a return address, or it could simply contain a return address and nothing else.

Remember that memory is always byte-addressable in MIPS. This means that you can access every single individual byte in memory (and anything larger), but you won't be able to access an exact bit without accessing everything else in its 'housing' byte (the byte in which it exists).

In MIPS, size ('width') of a single register = 1 word = 32 bits = 4 bytes

  • The definitions of a 'bit' and a 'byte' never change, but the definition of a 'word' depends on your computer architecture. A 64-bit Operating System will have a word be 64-bits long ('wide').

$0 (specifically for MIPS, not guaranteed in other ISAs, where an ISA is a specific way of interpreting and running machine code, 1s and 0s, on hardware) is ALWAYS guaranteed to have the value 0 (all 32 bits are 0s). Even if you try to write to this register and somehow succeed, the register is electrically connected to 'ground', so register zero will always be hardware 0 unless you have MAJOR issues in your computer that permit nothing else to work. Logic gates don't work correctly when 'ground' retains too much voltage.

$ra = return address register. Think of this as the 'pick up where I left off' or 'temporary home address to return to' register.

$v0 = special function register. Typically, the result of a just-completed function are placed in this specific register. If the result is a longer value like a 64-bit multiplication result, use $v1 as well. When a function 'returns', the function's output is expected to be in this register (or 'these two registers' if the result is 64 bits instead of 32 bits).

  • When 'syscall' (giving the Operating System control) happens IMMEDIATELY AFTER $v0, $v0 is checked for having a value between 1 and 16. Each number from 1-16 has a specific purpose: Code 1 is 'print int to console', 2 is 'print float to console', 3 is 'print double to console' , 4 is 'print string', 5 is 'read int', 6 is ..., 9 is 'sbrk/allocate memory', 10 is 'exit program', 11 is 'print char', 12 is 'read char', 13 is 'open file', 14 is 'read file', 15 is 'write to file', and 16 is 'close file'.
  • What happens when syscall happens after $v0 when $v0 DOESN'T have a number between 1 and 16? "Syscall [X] not supported" from the assembler. Idk what else happens.

With the exception of store instructions, MIPS uses the first operand of an instruction as a destination register (where the result is stored - the final step).

Though registers ARE a form of memory, people nearly always mean "non-register memory" when they use the word "memory". "Memory" includes data in the stack.

Common Instructions

sw = store word (INTO memory) = store 4 consecutive bytes into memory. The 1st operand is the data to store INTO memory.

lw = load word (FROM memory) = load 4 consecutive bytes from memoryAddress to memoryAddress+3. The 1st operand is the recipient register for whatever data is selected from memory.

sh = store halfword = store lower half (lower 2 consecutive bytes) of a register into memory (memoryAddress to memoryAddress+1). MemoryAddressByte => Lowest byte in register, memoryAddress+1 => 2ndLowestByteInRegister. Preserves upper half of the word in memory. Same as sw but for the LOWER half of the register's data.

lh = load halfword = load memoryAddressByte and byte above it into lower half of register. (lowest byte in register => stored in memory address byte, 2ndLowestByteInRegister => memoryAddress+1). Preserves upper half of the word in the register.

  • There are many other instructions that use halfwords ("h" in the instruction name) and bytes ("b" in the instruction name). They also use lower halves and lower quarters respectively.

la = load (memory) address (of label). 1st operand is recipient register for the memory address of the label (the address being wherever the assembler decided to put it). 2nd operand is the label. E.g. la $t0, FUNC1

  • If you know the exact memory address for something (or you have it in a register), there is no need to use this instruction (la).

add = add values from two registers (operands 2 and 3), store result in a possibly different register (operand 1). E.g. add $s0, $s1, $s2 in C code is $s0 = $s1+$s2;

addi = add immediate (value) = add data from a register (2nd operand) together with a literal value (3rd operand), then store it into a register (1st operand).

subi = subtract immediate (value) = subtract data from a register (2nd operand) together with a literal value (3rd operand), then store it into a register (1st operand). Software instruction, not hardware - called a pseudoinstruction. Pseudoinstruction for addi with a negative immediate value.

li = load immediate (value) = Copies your literal value (2nd operand) into a register (1st operand). Pseudoinstruction for either addi $dest, $0, $input or addi $dest, $input, $0 where you choose destination and input.

bne = branch on not equal = if(1stTwoOperandsAreNotEqual){BranchToThirdOperand;}

bgt = branch on greater than = if(1stOperand > 2ndOperand){BranchToThirdOperand;}. Pseudoinstruction (I can't explain how the hardware on this works, but Peter Cordes in the comments probably can).

j = jump (to the provided label)

jal = jump and link = store the next instruction's address into $ra, then jump to the provided label

jr = jump (to 32-bit address provided by) register. MUCH wider jump range than branching and even other jumping instructions.

BE AWARE OF signed vs unsigned instructions like using add instead of addu. Unsigned instructions (e.g. addu) do NOT throw exceptions (i.e. they don't halt the entire program and tell the Operating System 'something's wrong because a number overflowed'). Afaik, programming languages ALWAYS use unsigned instructions (when converted down to assembly) because it's the programmer's responsibility to determine when there's overflow. Why is it left up to the programmer? Because there are enough cases where overflow is actually a feature and not a bug, like using Two's Complement to switch between positive and negative numbers in binary.

Here is a 2-page summary of instructions that I found extremely useful.

Another thing to be aware of that I don't see/hear many people talk of is that MIPS instructions (e.g. addi $sp, $sp, -12) must be word-aligned. Note that this rule doesn't necessarily exist in other ISAs that have much-more-complicated instructions (called CISC ISAs). Remember how I said memory is byte-addressed? A word is 4x bigger than a byte, meaning you don't necessarily have to store/load a word at some byte address that is a multiple of 4. Word-alignment is having imaginary blocks of 4 bytes per block, starting at byte address 0-3, classified as a word. As soon as you cross over into a different 4-byte "block", you occupy more than one word, even if you aren't technically using all 8 bytes. Ensuring ALL instructions are word-aligned lets the computer allow for less accounting for weird jumps (jump exactly +4 bytes. no more. no less.), and hence, faster switching from the current instruction to the next instruction.

  • As an example of mandatory word-alignment, I could do "store[orLoad]Word byteAddress1", which would store 4 bytes of data into byte addresses 1, 2, 3, and 4. Afaik, this is fine (there should be a warning) AS LONG AS that data won't be used for an instruction. Proper word-alignment would be "store[orLoad]Word byteAddress0" (stores into 0,1,2,3) or "store[orLoad]Word byteAddress4" (stores into 4,5,6,7).
    addi $sp, $sp, -12 #stackPointer = stackPointer-12;
                       #12 is # of bytes.
                       #-12 indicates adding 12 bytes of space onto the stack.
                       #+12 indicates removing ("popping") 12 bytes from the stack.
    sw $ra, 8($sp)   #Memory[stackPointer+8] = returnAddress;
                     #Store 4 bytes (i.e. 1 word) of $ra into
                     #  memoryAddressOfItemOn'Top'OfStack (bytes 8-11 away from $sp)
    sw $s0, 4($sp)     #Memory[stackPointer+4] = saved0;
                       #Store 4 bytes (i.e. 1 word) of $s0
                       #  into memoryAddressBytes4To7OfStack
    sw $a0, 0($sp)   #Memory[stackPointer+0] = argument0;
                     #Store 4 bytes (i.e. 1 word) of $a0
                     #  into memoryAddressBytes0To3OfStack
    bgt $a0, $0, L1    #if($a0 > 0){goto labelL1;} else{GoToNextInstructionBelow();}
    add $v0, $0, $0  #$v0 = 0+0;
    j RTN              #jump to instruction at label RTN

L1:                            #Label L1
    addi $t0, $0, 1      #$t0 = 0+1;
                         #The '1' above is an immediate value (face-value value)
                         #, not data in a register.
    bne $t0, $a0, L2  #if($t0 != $a0){goto labelL2;}
    add $v0, $0, $t0    #$v0 = 0+$t0;
                        #Copies value in $t0 INTO $v0,
                        #  overwriting whatever $v0 originally had.
                        #$v0 is a special function register that
                        #  the Operating System uses when combined
                        #  with a 'syscall' afterward.
    j RTN             #jump to instruction at label RTN
    
L2: 
    subi $a0, $a0,1     #$a0 = $a0 - +1;
                        #'+1' is an immediate value.
    jal f         #Store the address of the instruction below this
                  #  (the next instruction disregarding jumping) into
                  #  $ra, then jump to instruction at label f
    add $s0, $v0, $0      #$s0 = $v0+0;
    sub $a0, $a0,1     #$a0 = $a0-1;
                       #I don't think this is legal since '1' is an immediate value...
                       #However, '$1' would be legal since it's register 1.
    jal f         #Store the address of the instruction below this
                  #  (the next instruction disregarding jumping) into
                  #  $ra, then jump to instruction at label f
    add $v0, $v0, $s0    #$v0 = $v0 + $s0;

RTN: 
    lw $a0, 0($sp)      #$a0 = Memory[stackPointer+0bytes];
                        #Copy data FROM the stackPointer address TO register $a0
    lw $s0, 4($sp)      #$s0 = Memory[stackPointer+4bytes];
    lw $ra, 8($sp)      #$ra = Memory[stackPointer+8bytes];
    addi $sp, $sp, 12   #$sp = $sp+12;
                        #Deallocate (remove) 12 Bytes from stack.
                        #Doesn't actually erase data in memory.
                        #$sp is just a marker of available memory for
                        #  the stack to use.
    jr $ra    #Jump to instruction at (address that's stored in $ra)
              #Since $ra was most recently modified by being loaded from memory,
              #  that's the instruction address that is jumped to.
              #Okay it was loaded from memory but what's that value?
              #  The value was stored at the beginning of label 'f'.
              #In which function iteration was it stored and with what value?
              #  You need to know the original values in the registers.
                #NOTE: $ra can be modified by either the jal or lw instructions.
                #I THINK $ra is a protected register, meaning you can't simply
                #add or subtract or multiply etc to it.
halfer
  • 19,824
  • 17
  • 99
  • 186
Stev
  • 65
  • 1
  • 7
  • 1
    *You can think of each register as a 32-bit array.* looks like it should be part of the previous paragraph. Thinking of a register as a bit-array doesn't seem to me like it would be helpful in understanding binary integer addition. `add` and `addu` treat registers as single binary integers. – Peter Cordes Jan 16 '23 at 14:48
  • 1
    `bgt` is also a pseudo-instruction: `slt` into a temp register + `beq $zero`. If you're going to mention it for `subi`, it makes sense to mention for `bgt`. (OTOH, some assemblers don't accept `subi` at all, requiring you to use a negative constant for `addi`, but all accept `bgt` if they accept any pseudo-instructions at all.) – Peter Cordes Jan 16 '23 at 14:51
  • That's an impressive amount of info about MIPS and computer architecture. Usually on Stack Overflow we treat questions as more focused; people say they're beginners all the time, and we don't try to answer that part with a complete tutorial or ground-up intro, because it takes a lot of space in the answer, making it harder for future readers to find the answer to the specific question. And it takes a long time to write, and some obscure question with a title like this isn't where most beginners will look who could benefit from this. – Peter Cordes Jan 18 '23 at 05:46
  • 1
    OTOH, it's potentially useful to have a writeup like this *somewhere* that we can link in comments on future questions. But each individual point (e.g. about registers being groups of bits) is buried as a small part of a big answer, so in other cases it's not as useful as a few specific answers on different questions. (That's one downside to SO's format, that info gets scattered around; it's not great if answers assume you know everything except the one thing the question is asking about. But OTOH it's not sustainable to write every answer like this. Answerers would burn out.) – Peter Cordes Jan 18 '23 at 05:49
  • 1
    Anyway, nice work, just wanted to let you know that writing this much is not typical. And/or maybe there's a better or more general MIPS beginner Q&A where you could post most of this. – Peter Cordes Jan 18 '23 at 05:52
  • 1
    Also, going "into the weeds" about stuff like how `$zero` electrically works is a delicate balance: more text fills up the answer with stuff only tangentially related to the main point, which might distract some readers. Sometimes it's relevant enough for a footnote, or something you think is interesting enough for the detour; I do that in my answers. In this case; "wired to ground" is only half the story: the read lines will be wired to ground, but the write lines can't be; that [would short circuit](https://stackoverflow.com/a/52412306/224132) in any bits written with a 1. – Peter Cordes Jan 18 '23 at 06:09
  • I added stuff I would have liked to have known when I was learning the basics of MIPS, and many sources that I utilized to learn skipped out on most of the details I provided in my answer. How $0 works bugged me to death when learning it was ALWAYS 0. I do not know of a better place to put much of this conglomerated yet very relevant info (I would provide a link to it on my answer), but I would like for somebody more experienced on Stack Overflow to tell me how I should put this info out there in an easily-accessible manner. Do I create a question and answer it? That doesn't feel okay to do. – Stev Jan 19 '23 at 08:01
  • A self-answered Q&A is actually encouraged, but it generally has to be a specific-enough question that it would be on-topic if posted without a self-answer. It sounds like you want to write an "intro to MIPS basics" article, and I'm not sure what kind of question would support that. Something like "what should I know as a MIPS beginner" is not specific enough for Stack Overflow. So it's hard to fit what you want to write into one SO Q&A that fits the Q&A format. Maybe ask on https://meta.stackexchange.com/ - link this answer and ask for suggestions on how to put this info somewhere useful – Peter Cordes Jan 19 '23 at 09:27