Please read this page about taking my exams!
Exam format
- When/where
- During class, here, like normal
- 75 minutes
- 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.
- 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
- Kinds of questions
- No multiple choice
- A few “pick n“ (but not many)
- 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
- 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
- RISC: Reduced Instruction Set Computer
- made for compilers to produce assembly/machine code from high-level languages
- 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)
- CISC has complex, flexible, multi-step instructions that are great for humans (do more stuff with fewer instructions!) but terrible for performance
- CISC: Complex Instruction Set Computer
- 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
to1111
, and count up in hex from0
toF
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
- 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.
- 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.
- 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.”
- 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 and unsigned subtraction! the 2’s complement of
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 as0000 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
- Zero-extension is for unsigned numbers and puts
- Going from a smaller number of bits to a bigger number of bits while preserving the value
- 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 get1 0010
(still decimal 18)… but if you keep going and truncate to 4 bits, you get0010
(decimal 2)! - This is actually performing modulo: truncating to n bits gives you the value modulo 2n. 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 larger number of bits to a smaller number
- Control flow
- Conditional branches go to the label if their condition is true (satisfied)
- e.g.
beq t0, 10, _label
says “ift0 == 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
- Conditional branches go to the label if their condition is true (satisfied)
- 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 n-byte value must be a multiple of n.
- 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
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 extend it
- 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 behindsh
stores the 16 least significant bits of the register in memory and leaves 16 behind
- Endianness, discussed below
- Does the CPU crash if you
lw
a byte orlb
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.
- Alignment
- 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 as0xDE, 0xAD, 0xBE, 0xEF
- little endian does the opposite, stores the least significant byte first.
- “swap the order”
0xDEADBEEF
is stored in memory as0xEF, 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]
isA + 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
andadd
into thelw
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 ofarr
andt1
together, and use that as the address to load from”
- An array is multiple variables of the same type and size, equidistantly spaced apart in memory
- 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
, orv
registers have the same values after ajal
as they did before it. - So every time you
jal
, on the line afterjal
, you have no clue what is in any of thea
,t
, orv
registers - only thes
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.
- This is a scary-sounding rule, but it gives you the freedom to:
- Use any
a
,t
, orv
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
, andv
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 thejal
, sopc + 4
is the instruction after it) - sets
pc = func
(whatever its address is)
- sets
jr ra
does one thing:- sets
pc = ra
(wherera
was the address of the instruction after thejal
thatjal
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
callsfork
, andfork
callsknife
… - then the
jal knife
overwrites the value that was inra
- meaning we will be able to get back to
fork
, but we will get stuck in an infinite loop when we try to return fromfork
- if
- to solve this, we make every function
push ra
at the beginning, andpop 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)
- 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)
- 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
- Calling Convention
- Honor system used to let multiple functions work together
- Remember that all functions share the registers so this is important!
- 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
registersx
, you:push sx
at the beginning of the function that wants to use itpop 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!
- If you look back at your labs 3 and 4, and in some functions you used
s0
but didn’tpush s0
at the beginning andpop 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 useds0
for anything, so you never noticed it.
- If you want to use some
- Honor system used to let multiple functions work together
- Bitwise AND and its uses
- Two main uses:
- masking: isolating the lowest n bits of a number by ANDing with 2n - 1
- doing fast modulo by 2n by ANDing with 2n - 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 2n
- 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 right by n places is like dividing by 2n
- 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
- Logical (unsigned) right shift
- Shifting left by n places is like multiplying by 2n
- 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)
- 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!
- 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
(wheresize
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
- Position: the low bit number (the one on the right)
- 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)
- 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 26
- 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 is00100000....
(many 0s after it)
- e.g. if significand is
- Exponent: if you have 2n, 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.
- Fixed-point numbers
- A fixed-point number is an integer where we decide to interpret the place values differently, and we consider some places to be fractional.
- We decide in advance how many fractional places there are.
- e.g. if we have a 32-bit value, we might say we have 10 fractional places, giving us a “22.10” fixed-point number - 22 bits of whole number, then the binary point, then 10 bits of fraction.
- Note that to make some bits fractional, you always have to sacrifice an equal number of whole number bits
- That means that you can have either precision (tiny fractions) or range (big numbers), but never both
- Since fixed-point numbers are just integers, then they are just as fast as integers (compared to floats, which can be 2-4x slower on average)
- Fixed-point numbers can also be used in situations when floats are unavailable (e.g. small/cheap CPUs inside appliances; or when writing code for the operating system)
- However there are some special considerations:
- After multiplying, you have to shift the product right to get the correct answer
- Before dividing, you have to shift the dividend (first number) left to get the correct answer
- You need to write your own functions to print them out properly
- What should you use: floats,
BigDecimal
, or fixed-point numbers?if(you need to represent fractions like 1/10, 1/100, 1/1000) { use BigDecimal; } else if(floats are not available || floats are too slow) { use fixedPointNumbers; } else { 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
- 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.
- 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.
- 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, andaddu/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: store, ignore, fall on the floor (crash)
- 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
- Store the extra bit into a special 1-bit carry register
- After detecting an overflow occurred (see above), there are three possible ways in which the addition or subtraction instructions can respond: