Please read this page about taking my exams!

## Exam format

**When/where**- During class, here, like normal
- 75 minutes (4:00-5:15 PM)
- it is not going to be “too long to finish”
**no calculator**

**Closed-note**- You may not have any notes, cheat sheets etc. to take the exam
- The open-note thing was just for when we were remote

**Length**- 3 sheets of paper, double-sided
- there are A Number of Questions and I cannot tell you how many because it is not a useful thing to tell you because they are all different kinds and sizes.
- But I will say that I tend to give
*many, small questions*instead of a few huge ones.

- But I will say that I tend to give

**Topic point distribution**- More credit for earlier topics (e.g. numerical representation, memory addresses, arrays, MIPS programming)
- Less credit for more recent ones (e.g. overflow)
- More credit for
**things I expect you to know because of your experience**(labs, project) **VERY ROUGHLY:**- ~15% “pick
*n*” and fill-in-the-blank - ~20% short answer (just writing stuff)
- ~20% math (bases, ranges, +/-)
- ~20% MIPS ASM (tracing, interpreting, filling in blanks)
- ~15% understanding memory
- ~10% bitfields/bitsets
*(though probably less)*

- ~15% “pick

**Kinds of questions****No multiple choice***A few***“pick**(but not many)*n*“- Some
**fill in the blanks**- mostly for
**vocabulary** - or things that I want you to be able to recognize, even if you don’t know the details

- mostly for
**Application**questions about numbers and arithmetic (i.e.*math problems, basically*)- Base conversion
- Interpreting patterns of bits in different ways (signed, unsigned, bitfields, floats etc)
- Unsigned and signed (2’s complement) addition

- Several
**short answer**questions- again, read that page above about answering short answer questions!!

**No writing code from scratch,***but:***tracing**(reading code and saying what it does)**debugging**(spot the mistake)- interpreting
**asm as HLL**code (identifying common asm patterns) **fill in the blanks**(e.g. picking right registers, right branch instructions)**identifying loads and stores**in HLL code

## Things people asked about in the reviews

This is a list of *what people asked about.* The exam may have other topics not listed, and some of these topics may not appear on the exam.

**CISC vs RISC****CISC:***Complex*Instruction Set Computer- made for
*humans*to write programs directly in assembly

- made for
**RISC:***Reduced*Instruction Set Computer- made for
*compilers*to produce assembly/machine code from high-level languages

- made for
- The differences are really in the name:
**CISC**has*complex,*flexible, multi-step instructions that are great for humans (do more stuff with fewer instructions!) but terrible for performance- x86 is really the only CISC still in widespread use

**RISC**has*reduced,*simple, single-step instructions that are great for compilers (so easy to write algorithms to write RISC code!) but more awkward for humans to write- MIPS and Berkeley RISC were the first mainstream RISC architectures (and where the name RISC came from)
- most architectures designed after MIPS works like MIPS (e.g. ARM)

**Conversion from decimal to binary**- I presented one way on the slides, the “long division” method:
**You have to know the binary place values**- From MSB to LSB (left to right):
- If the place value fits into the remainder, put a
`1`

and subtract it off the remainder - Otherwise put a
`0`

- If the place value fits into the remainder, put a

- There’s another method that involves repeatedly dividing by 2 until you get a quotient of 0, and you
**write every remainder even if it’s a 0,**and the binary representation is the remainders read from top to bottom.- Try doing both methods to see what I mean, if you’re curious.

- I presented one way on the slides, the “long division” method:
**Conversion between hex and binary****4 bits = 1 hex digit (nybble)**- The table is simple - count up in binary from
`0000`

to`1111`

, and count up in hex from`0`

to`F`

next to it. - When going from binary to hexadecimal,
**group the bits into 4***starting from the right (LSB)*- add 0s to the
*left*side as needed to make a group of 4 bits - then each group of 4 bits is 1 hex digit

- add 0s to the

**Unsigned integers**- There are
**no negatives.**It’s in the name: unsigned = NO SIGN. - To convert to decimal, add up the place values for each 1 bit.
**You, the programmer,**decide when an integer is unsigned. It’s then up to you to use the appropriate unsigned versions of things like`addu`

,`bltu`

,`lbu/lhu`

, “print unsigned integer” syscall, etc.

- There are
**Sign-magnitude**- Is
**NOT**used for integers, it’s used for*floats* - Is also how we write numbers on paper. +123 and -123: same digits, different sign.
- The MSB is the sign, 0 for positive, 1 for negative, and is
*totally separate*from the rest of the bits **Downsides:**two “versions” of 0 (+0 and -0); arithmetic is more complicated (special cases)**To negate:**just flip the sign bit. The rest of the digits are unchanged.

- Is
**2’s complement integers**- The
**one and only**system used to represent signed integers on computers today - It works by making the MSB the
**negative version**of its place value- The MSB also represents the sign - 0 for positive, 1 for negative

- This representation is great because it makes
**arithmetic**super simple, no special cases- You can add any two numbers of any signs and it will Just Work (unless there’s overflow lol)

**Downside:**there is one more negative number than positives, and it is A Bit Weird (it has no positive counterpart, so if you negate it, you get the same value back out).**To convert to decimal,**you still just add the place values up. e.g.`1001`

is -8 + 1 =**-7.****To negate:**`-x == ~x + 1`

, or, “flip the bits, then add 1.”- The negative of a number is also called its “2’s complement.”

- The
**Addition and subtraction**- Binary addition works just like in base 10, but you carry at 2 instead of 10.
- The same addition algorithm is used for both unsigned and signed integers.
- Remember, when adding 2’s complement numbers,
**nothing special happens.**You just add the bits and you will get the correct value/sign at the end. **Subtraction**is defined in terms of addition:`x - y == x + (-y)`

… and because of how 2’s complement works…`x + (-y) == x + (~y + 1)`

**amazingly, this works for signed**the 2’s complement of*and*unsigned subtraction!`x`

(`~x + 1`

) “behaves like” its negative, even in a number system that has no negative numbers. remember: two ways around the number circle.

**Extension**- Going from a
**smaller number of bits**to a**bigger number of bits**while**preserving the value**- e.g. the number 5 can be represented as
`0101`

binary, or as`0000 0101`

binary - same value, but the second one has more bits

- e.g. the number 5 can be represented as
- There are
**two flavors of extension:****Zero-extension**is for**unsigned**numbers and puts`0`

bits on the left side of the number**Sign-extension**is for**signed**numbers and puts*copies of the sign bit (MSB)*on the left side of the number

- Going from a
**Truncation**- Going from a
**larger number of bits**to a**smaller number**- You can also look at it as “erasing bits on the left side of the number”

- There is only one kind of truncation, doesn’t matter if it’s signed or unsigned.
- However, truncating
*too far*can change the value- e.g. If you have
`0001 0010`

(decimal 18), and truncate it to 5 bits, you get`1 0010`

(still decimal 18)… but if you keep going and truncate to 4 bits, you get`0010`

(decimal 2)! - This is actually
**performing modulo:**truncating to*n*bits gives you the value modulo 2^{n}. If the number is less than that, it’ll be preserved; if it’s bigger, it’ll be changed.

- e.g. If you have

- Going from a
**Control flow**- Conditional branches
**go to the label if their condition is true (satisfied)**- e.g.
`beq t0, 10, _label`

says “if`t0 == 10`

, then go to`_label`

”

- e.g.
- So there is a mismatch between the way we write conditions in
`if`

s in Java vs. how they work in asm- When writing
`if`

s in asm, you usually have to**invert the condition** - Because in asm, the condition is really testing “when do we
*skip*the contents of the`if`

”

- When writing
- However, you don’t
*always*invert the condition. E.g.`do-while`

, “cheesy”`for`

loops- These test the condition at the
*end*of the loop, so we*do*want to go backwards when the condition is true

- These test the condition at the

- Conditional branches
**MEMORY****Alignment**- The address of an
*n*-byte value must be a multiple of*n*.- e.g. words are 4 bytes. so, their addresses are multiples of 4 (
`0x00, 0x04, 0x08, 0x0C, 0x10, 0x14, 0x18, 0x1C,...`

)

- e.g. words are 4 bytes. so, their addresses are multiples of 4 (
- There are underlying hardware design reasons for this, but some architectures (like MIPS) will
**crash your program**if you don’t respect this rule.

- The address of an
**Zero/sign extension**- when you load a value < 32 bits (byte, half) into a 32-bit register, have to
**extend**it- want to
**preserve the same value,**just represent it with more bits

- want to
`lb/lh`

does sign extension (**copies sign bit (0 OR 1)**to left)`lbu/lhu`

does zero extension (fills extra bits with**0s**)- No extension happens with
`lw`

because you’re loading a 32-bit value into a 32-bit register - same size

- when you load a value < 32 bits (byte, half) into a 32-bit register, have to
**Truncation**- When you store into a half/byte variable,
**only the least significant bits (rightmost bits) of the register are stored** - The rest are truncated (cut off)
`sb`

stores the 8 least significant bits of the register in memory and leaves 24 behind`sh`

stores the 16 least significant bits of the register in memory and leaves 16 behind

- When you store into a half/byte variable,
**Endianness,**discussed below**Does the CPU crash if you**`lw`

a byte or`lb`

a word?**NO!**the only thing it will crash for is address misalignment.- Otherwise it assumes you know what you’re doing and does exactly what you tell it to do.

**Endianness**- it is a rule which is used to decide the order of BYTES
- when going from things bigger than a byte to bytes
- or vice versa.

- it comes up in…
**memory**(cause it’s an array of**bytes**)**files**(also arrays of bytes)- networking

**big endian**stores the big end (**most significant byte**) first.- “read it in order”
`0xDEADBEEF`

is stored in memory as`0xDE, 0xAD, 0xBE, 0xEF`

**little endian**does the opposite, stores the**least significant byte**first.- “swap the order”
`0xDEADBEEF`

is stored in memory as`0xEF, 0xBE, 0xAD, 0xDE`

- Notice that we don’t swap the
*hex digits*or the*bits,*we swap the order of entire**bytes**

- but
**1-byte values**and**arrays of 1-byte values**are immune to endianness- because they aren’t chopped up when loading or storing

- it is a rule which is used to decide the order of BYTES
**Accessing arrays in MIPS**- An array is multiple variables of the
**same type and size, equidistantly spaced apart in memory**- E.g.
`arr: .word 1, 2, 3`

is 3 words/12 bytes of memory; each item of the array is 4 bytes apart because a word is 4 bytes.

- E.g.
- The address of
`A[i]`

is`A + S×i`

where:`A`

is the address of the array (in asm, the label*is*the address)`S`

is the size of one item in bytes (so for`.word`

it’s 4,`.byte`

it’s 1, etc)`i`

is the index you want to access (in asm, typically a register)

- The “long form” of array access looks like:

`# ASSUMING that s0 is the index (maybe we're in a for loop and s0 is the loop counter): la t0, arr # t0 = address of arr mul t1, s0, 4 # t1 = s0 * 4 add t0, t0, t1 # t0 = address of arr + (s0 * 4) # Now you can load/store using (t0) as the address lw a0, (t0) # a0 = arr[s0]`

- The “short form” folds the
`la`

and`add`

into the`lw`

instruction, but you still have to multiply the index:

`mul t1, s0, 4 # t1 = s0 * 4 lw a0, arr(t1) # a0 = arr[s0]`

- The stupid
`arr(t1)`

syntax means “add the address of`arr`

and`t1`

together, and use that as the address to load from”

- An array is multiple variables of the
**ATV Rule**- Any function is allowed to change the A, T, V registers at any time for any reason! :))))))
- but the consequence is that a caller cannot assume that the
`a`

,`t`

, or`v`

registers have the same values*after*a`jal`

as they did*before*it. - So every time you
`jal`

, on the line after`jal`

, you have no clue what is in any of the`a`

,`t`

, or`v`

registers - only the`s`

registers’ values will be the same as they were before the call.- you just can’t
*read*the value out of those registers anymore. they’re not like, poisoned or something. you can use the same register before and after a jal for*different purposes.*

- you just can’t
- This is a scary-sounding rule, but it
**gives you the freedom**to:- Use any
`a`

,`t`

, or`v`

register at**any time**for**any purpose**- yes! go ahead and use
`t0`

everywhere! everyone is allowed to use it! :DDDDDD

- yes! go ahead and use
- Use the
`a`

,`t`

, and`v`

registers**without having to “ask permission” or “put them back the way they were” by pushing and popping them**- you never have to push or pop any of them.

- Use any

**Function call mechanism in MIPS**`jal func`

does two things:- sets
`ra = pc + 4`

(`pc`

is pointing at the`jal`

, so`pc + 4`

is the instruction after it) - sets
`pc = func`

(whatever its address is)

- sets
`jr ra`

does one thing:- sets
`pc = ra`

(where`ra`

was the address of the instruction after the`jal`

that`jal`

set up for us)

- sets
- this is all these instructions do!
- there is also only
**one**`ra`

register. what this means is:**you can only go one function call deep.**- if
`main`

calls`fork`

, and`fork`

calls`knife`

… - then the
`jal knife`

*overwrites the value that was in*`ra`

- meaning we will be able to get back to
`fork`

, but we will get**stuck in an infinite loop when we try to return from**`fork`

- if
- to solve this, we make every function
`push ra`

at the beginning, and`pop ra`

at the end- this way, every function’s return address goes on the
*stack,*where it’s safe (because there are lots of stack slots)

- this way, every function’s return address goes on the

**The stack**- A region of memory that contains
**information about function calls** *Pushing*puts a value on top of the stack,*popping*removes a value from the top of the stack- A stack is a perfect match for the way function calls work:
- Whenever a function is called, it
*pushes*its**activation record (AR)**-saved registers, local variables in HLLs - Whenever a function is about to return, it
*pops*the AR - ARs are removed in the opposite order from when they are created, so stack is exactly what is needed
- In-progress functions’ data is safe on the stack (in memory)

- Whenever a function is called, it
- The stack is necessary to make
**recursive functions**work:- Every time a recursive function calls itself, a
**new copy**of its local variables is pushed - So there can be
**multiple activation records**for the**same function**on the stack at the**same time,**each with different values for the local variables

- Every time a recursive function calls itself, a

- A region of memory that contains
**Calling Convention**- Honor system used to let
**multiple functions work together**- Remember that
**all functions share the registers**so this is important!

- Remember that
- Makes them agree on:
- How arguments are passed from caller to callee
- How values are returned from callee to caller
- How control flows from caller to callee, and then back again
- What goes on the stack
- Who is allowed to use which registers, and for what purposes
- Which registers must be preserved across calls, and which can be trashed

- In MIPS, part of this is the
`s`

register contract- If you want to use some
`s`

register`sx`

, you:`push sx`

at the beginning of the function that wants to use it`pop sx`

at the end of the function that wants to use it

- By following this protocol, it’s as if every function gets its
**own**`s`

registers- But
*everyone*has to follow the protocol, or the guarantee is gone!

- But
- If you look back at your labs 3 and 4, and in some functions you
*used*`s0`

but didn’t`push s0`

at the beginning and`pop s0`

at the end, but it still worked…**you just got lucky.**- you were actually messing up the caller’s copy of
`s0`

, but the caller never used`s0`

for anything, so you never noticed it.

- If you want to use some

- Honor system used to let
**Bitwise AND and its uses**- Two main uses:
**masking:**isolating the lowest*n*bits of a number by ANDing with 2^{n}- 1- doing
**fast modulo**by 2^{n}by ANDing with 2^{n}- 1

- Notice both of those are the same operation, just different interpretations
**Do not confuse bitwise AND (**`&`

, works on ints) with logical AND (`&&`

, works on booleans, is lazy)!

- Two main uses:
**Bit shifting**- Shifting
*left*by*n*places is like multiplying by 2^{n}- Shifting left writes 0s on the right side of the number and then
**erases bits on the left side**, which means it has a truncation “built in” - Truncation can give you weird results if you lose meaningful bits!

- Shifting left writes 0s on the right side of the number and then
- Shifting
*right*by*n*places is like dividing by 2^{n}- Shifting right
**erases bits on the right side of the number,**which forces you to*add bits*on the*left side,*which means it has an extension “built in” - Because of that, there are
**two flavors of right-shift:****Logical (unsigned) right shift**puts 0s to the left of the number`>>>`

**Arithmetic (signed) right shift**puts`>>`

*copies of the sign bit*to the left of the number

- Shifting right

- Shifting
**Bitsets**- Simplification of
*bitfields*where each field is 1 bit (0 or 1) - Bits are numbered starting with bit 0 on the
**right side**and**increasing to the left**- (this is because bit numbers are the
*powers of 2*that they represent)

- (this is because bit numbers are the
**To turn on bit***n*:`sets |= (1 << n)`

**To turn off bit***n*:`sets &= ~(1 << n)`

- note the`~`

in there**To test if bit***n*is 1:`if((sets & (1 << n)) != 0)`

- do NOT use`~`

in there!

- Simplification of
**Bitfields**- Given the specification for a bitfield, you can determine these for each field:
**Position:**the low bit number (the one on the right)- this indicates how far to shift left/right for encoding/decoding that field

**Size:**high bit + 1 - low bit- this is how many bits the field is

**Mask:**`2^size - 1`

(where`size`

is calculated in the previous point)- another way of thinking of it is writing
`size`

1 bits in binary - and then turn that into hex
- e.g. if size = 6, in binary that’s
`11 1111`

(6 1s in a row) - turn that to hex, it’s
`0x3F`

- another way of thinking of it is writing

- Then, to
**decode**(get a field OUT of an encoded bitfield):- shift value right by position and AND with mask
- so,
`field = (encoded >> FIELD_POSITION) & FIELD_MASK`

- e.g. with a position of 7 and a mask of
`0x3F`

,`field = (encoded >> 7) & 0x3F`

- Finally, to
**encode**(put fields together into an encoded bitfield):- shift each field
*left*by position, and*or*them all together - e.g. with 3 fields it might look like
`encoded = (A << 9) | (B << 7) | (C << 0)`

- shift each field

- Given the specification for a bitfield, you can determine these for each field:
**Floats**- IEEE 754 standard is the
**only way**floating-point numbers are encoded and manipulated on modern computers - Based on
**binary scientific notation,**e.g. +1.10101 x 2^{6} - Represented in
**sign-magnitude,****not 2’s complement** - Three parts of a number:
**sign, fraction,**and**exponent****Sign**is the MSB and follows same rule as ints (0 = positive, 1 = negative)**Fraction**is just the bits*after*the binary point, left-aligned- e.g. if significand is
`1.001`

, then fraction is`00100000....`

(many 0s after it)

- e.g. if significand is
**Exponent:**if you have 2^{n},*n*is encoded as an unsigned number*n + k*, where*k*is the**bias constant**- The bias constant is
**given to you**, e.g. for single-precision floats,*k*= 127. - So for a
`float`

, an exponent of +6 is encoded as 127 + 6 =**133**, as an unsigned integer.

- The bias constant is

- IEEE 754 standard is the
**When should you***not*use floats?**NEVER EVER EVER USE FLOATS TO REPRESENT CURRENCY/MONEY/FINANCIAL TRANSACTIONS.**This is because floats use**binary (base-2)**fractions, and 1/10, 1/100, 1/1000 etc. are**infinitely repeating fractions in binary.**- it is
*not*a matter of “not having enough precision” or “the numbers get rounded off.” it’s that they are*infinite in size and computers are incapable of representing infinitely sized values, it’s just a mathematical impossibility*

- it is
- If you need to represent money, do it in
*base 10*- e.g. in Java, there is the`BigDecimal`

class built in to do so.

**HOW TO DETECT OVERFLOW****AN OVERFLOW OCCURRED IF:**

**Addition****Subtraction****Unsigned**MSB carry out is **1**MSB carry out is **0**(i.e. there is no carry out)**Signed**same sign inputs, different sign output same as addition, but *after*negating second input- For signed addition: you get an overflow
**only**if you add two numbers of the same sign and get the opposite sign out (e.g. add two positives, get a negative)- it’s totally possible to add two numbers of the same sign and
*not*have overflow - also if the inputs are opposite signs, then overflow is impossible.

- it’s totally possible to add two numbers of the same sign and
- Remember that detecting overflow is only the first step.
- Once it has been detected, you can respond to it in 3 ways:
**store, ignore,**fall on the**floor**(crash)- in MIPS,
`add/sub`

**crash**on*signed*overflow, and`addu/subu`

**ignore**all overflow. - not all architectures are this limited.

- in MIPS,

- Once it has been detected, you can respond to it in 3 ways:

**Responding to overflow**- After detecting an overflow occurred (see above), there are three possible ways in which the addition or subtraction instructions can respond:
**Store**the extra bit into a special 1-bit*carry register*- This can be checked after the addition or subtraction to see what happened
- Or it can be used as an input to a subsequent addition or subtraction to perform
**arbitrary-precision arithmetic,**which lets you add or subtract numbers of*any*number of bits

**Ignore**that an overflow occurred, and use the result truncated back to*n*bits- This sucks and is the most popular way to respond because it’s Easy

**Fall on the floor (crash the program)**instantly- This lets the programmer/user know right away that something went wrong
- But in some high-reliability environments (e.g. aerospace) this might be a Bad Idea
- It really depends

- After detecting an overflow occurred (see above), there are three possible ways in which the addition or subtraction instructions can respond: