






Study with the several resources on Docsity
Earn points by helping other students or get them with a premium plan
Prepare for your exams
Study with the several resources on Docsity
Earn points to download
Earn points by helping other students or get them with a premium plan
Community
Ask the community for help and clear up your study doubts
Discover the best universities in your country according to Docsity users
Free resources
Download our free guides on studying techniques, anxiety management strategies, and thesis advice from Docsity tutors
The concept of two's complement representation in n-bit unsigned and signed numbers, with a focus on 16-bit representation. It also covers the differences between stack and heap memory allocation in mips and provides examples of valid and invalid c code. Additionally, it discusses the floating point representation and the importance of maintaining a list of free blocks.
Typology: Exams
1 / 10
This page cannot be seen from the preview
Don't miss anything!
CS 61C Fall 2003 Midterm 1 solutions
Most of the short-answer questions were worth 1 point or 1/2 point, with no possibility of partial credit. Scoring information is given below only for the problems where it's not obvious from the above. We carried half points to the front of the exam, but after adding the points we truncated the result to an integer.
1a. Negation.
23 = 16+7 = 16+4+2+1 = 00010111 in 8-bit binary.
The sign-magnitude representation of -23 just turns on the leftmost bit, so 00010111 -> 10010111.
The ones-complement representation reverses all the bits, so 00010111 ->
The twos-complement representation is the ones-complement representation plus 1, so 00010111 -> 11101001.
So the answers in order are B, C, A, D.
1b. twos complement range
With N bits you can represent 2^N values, but when you are representing signed integers about half those values are negative, so the largest magnitude is around 2^(N-1). For N=8, 2^N=256 and 2^(N-1)=128, so the answer has to be close to -128.
More precisely, in N-bit twos complement, the most negative number is -(2^(N-1)), which is represented as 100...00. (The most positive number is (2^(N-1))-1, which is 011...11.
For eight bits, the range is from -(2^7) to (2^7)-1, or -128 to 127. So the exact answer is indeed -128.
1c. unsigned range
In N-bit unsigned representation, the smallest number is 0, and the largest number is (2^N)-1, represented as 111...11.
For eight bits, the range is 0 to 255, so the answer is 255.
1d. hex addition
0xFA25 + 0xB705. The easiest way to do this is the same way you'd do addition of decimal integers: add from right to left, carrying when the result is 16 or more.
5+5 = 10 decimal = A hex, no carry. 0+2 = 2, no carry. A+7 = 10 decimal + 7 = 17 = 16+1 = 1, carry 1. F+B+1 = 15 + 11 + 1 decimal = 27 = 16+11 = B, carry 1.
So the result is 0x1B12A. But we only have 16 bits, which is 4 hex digits, so the leftmost digit is lost and the result is 0xB12A.
If you didn't know the addition table for hex, another way to solve this problem is to convert to binary and add the binary values:
carry 1 1111 11 1 1 first number 1111 1010 0010 0101 second number 1011 0111 0000 0101
1 1011 0001 0010 1010
Dropping the leftmost bit and converting back to hex gives 0xB12A.
If we'd allowed calculators, you could have converted the numbers to decimal and added them. Since they're signed numbers and both have the leftmost bit on, both are negative, so we should take their twos complements to get their absolute value.
We get the ones complement by subtracting each hex digit from 15 decimal, then we add 1 (to the entire number, not to each digit) to get the twos complement:
Given number Ones complement Twos complement
FA25 05DA 05DB = 516^2 + 1316 + 11 = 1499 B705 48FA 48FB = 416^3 + 816^2 + 15*16 + 11 = 18683
So our problem is (-1499)+(-18683) = -20182 = -4ED6 hex
0x4ED6's ones complement is 0xB129; its twos complement is 0xB12A.
1e. Overflow?
No, there is no overflow. In unsigned addition, a carry out from the leftmost bit is always an overflow, but in twos complement addition, overflow occurs when the carry out from the leftmost bit is unequal to the carry into the leftmost bit. In this case, both carries are 1. Another way to see this is that the range of representable numbers in 16-bit twos complement is -2^15 to (2^15)-1, or -32768 to 32767. So -20182, which is the correct answer, is representable, so there will be no overflow.
2a1. static char str[] = "thing";
This allocates a six-byte character array (including a byte for the null at the end) in global data space. Every call to set() refers to this same array. So each of the three calls changes one character: thing -> thong -> whong -> wrong. So the result that's printed is "wrong".
2a2. char str[] = "thing";
This allocates a new six-byte array on the stack for each call, then returns the address of that stack array, but the stack frame containing it is deallocated when set() returns. So what will be printed is whatever the call to printf() puts at that address on the stack! The result is therefore undefined, or a runtime error.
2a3. char *str = malloc(6); strcpy(str, "thing");
This heap-allocates a new six-byte array for each call. Each array has the initial value "thing" and then one character is changed. So the first two calls have essentially no effect; the third call changes thing -> tring, and "tring" is printed.
2b. What's legal C?
offset and uses it in the machine instruction that it generates.
BNE has opcode 5, which is 000101 in six bits.
$t0 = $8 = 01000 in five bits.
$zero = $0 = 00000.
LOOP is the instruction before the branch. The offset would be zero to branch to the instruction after the branch, -1 to branch to the branch instruction itself, and -2 to branch to the previous instruction. -2 is 1111111111111110 as 16 bits.
000101 01000 00000 1111111111111110
Regrouping in four-bit chunks gives
0001 0101 0000 0000 1111 1111 1111 1110
which is 0x1500fffe.
A popular wrong answer was 0x1500fff8, which would be correct if branch offsets were measured in bytes, like load/store offsets, rather than in words as they actually are.
3b. Why stack pointer in register?
Note that this is not the same as question 2c, which was about stack vs. heap allocation.
The main reason to keep pointers in registers is that all MIPS memory references use I-format (register+offset) addressing. So in order to get to things on the stack, we need a register that contains an address near the thing we want. The most straightforward way to do this is to keep the address of the current stack frame in a register. This is answer 2.
Answer 1 is wrong because (unlike some other architectures) there is nothing specifically about stacks in the MIPS architecture. (Other architectures, for example, have instructions named PUSH that allocate one stack word and add an item at the new stack address, and POP for the reverse.)
Answer 3 is nonsense; stack frames point to variables, not to procedures (unless a local variable happens to be of type pointer-to-procedure). And 4 is clearly wrong.
3c. MAL pseudo-instruction to real MIPS
The reason why the given ADDI instruction is not an actual machine instruction is that its operand doesn't fit in 16 bits. So we have to get it into a register, namely $at, the one reserved for use by the assembler.
lui $at, 0x7f ori $at, $at, 0xf add $s0, $s1, $at
It is incorrect to use ADDI or ADDIU instead of ORI in the second instruction. These instructions both sign-extend their immediate operand, so you would be adding 0xfffff333, not 0x0000f333, to the result of the LUI. The sum would be 0x7ef333, not 0x7ff333. Since this is the main point of the question, this error got no credit.
We gave 1/2 point credit for a translation that used a register other than $at (or the equivalent $1) to hold the temporary result. The assembler is not allowed to use other registers, such as $t0, for this purpose, because your program might need the value in $t0. (But it's okay, in this case, to use $s0 instead of $at, since $s0 is the register whose value we're trying to change here. We gave full credit for $s0.)
3d. Structs and unions.
The key point to notice here is that fields i and d share memory. Each of the unions (x and y) are therefore 8 bytes long (the larger of sizeof(int) and sizeof(double)). sizeof(struct point) is therefore 16.
la $8, p # $8 points to p[0] addi $9, $8, 160 # $9 points to p[10] ($8 + 10*16) L1: bge $8, $9, L sw $0, 0($8) # store 0 ($0) into x.i (offset 0) sw $0, 8($8) # store 0 into y.i (offset 8) addi $8, $8, 16 # add sizeof(struct point) to pointer b L L2:
Note: Given that x and y are unions of different-sized alternatives, it's not obvious whether &x.i is the same as &x.d or whether it comes 4 bytes later. But the code we gave you settles that question, because we used an offset of 0 for x.i in the first SW instruction. This is the correct answer, according to K&R page 213: "A union may be thought of as a structure all of whose members begin at offset 0..."
There was some confusion about the second instruction, which produces a pointer to a nonexistent array element. The array has 10 elements, namely p[0] through p[9]. So why do we make a pointer past the end of the array? Because we aren't going to dereference this pointer; we use it only for the test for the end of the loop! When $8 reaches the value in $9, we've gone past the end of the array, so we stop looping.
Scoring: We counted the two uses of $0 as a single answer. Thus there are four answers here: 160, $0, 8, and 16. Each of these was worth 1/2 point.
4a. Floating point representation
Many people had trouble with this question, partly because you didn't read, or didn't believe, the part about "the same as IEEE." So, during the exam, we got questions like "is the exponent biased?" and "does this include denorms?" We answered by saying "the same as IEEE" but of course that implies "yes" to both questions.
What is the exponent bias? In IEEE single precision, with an 8-bit exponent field, the bias is 127, which is (2^7)-1. For our format, with a 3-bit exponent field, the bias will be (2^2)-1, which is 3. An all-zero exponent field is used for zero and denorms; an all-one exponent field is used for infinity and NaN, so the range representing normalized numbers is 001-110, which after bias conversion means -2 to 3.
There are four significand bits, plus (except for denorms) an implicit one before the binary point.
That means the largest representable number is 1.1111 * 2^3 = 1111.1 = 15 1/2 = 15.5 decimal.
instruction program program microsecond
That gives 3 * X = 15 * 500, because MHz means cycles/microsecond.
Some people asked about the "micro" prefix. You should know these!
pico trillionths p nano billionths n micro millionths mu milli thousandths m
kilo thousands k mega millions M giga billions G tera trillions T
So MHz = Megahertz = millions of Hertz = millions of cycles/second = cycles/microsecond
Generally, capital letters abbreviate more-than-one multipliers, and lower case letters abbreviate less-than-one. The exceptions are k for kilo and the Greek letter mu for micro.
5b. The weighted average is (.3 * 2) + (.6 * 1) + (.1 * 10) = .6 + .6 + 1 = 2.
If all blocks are the same size, we should maintain a free list that contains only blocks of that exact size. So we're never in the situation in which we allocate less than an entire free block, so there's no fragmentation. Similarly, there's no need to coalesce small free blocks to make a big one, since the size we want is exactly the size we have.
On the other hand, we do still need to maintain a list of free blocks, because over time they'll be scattered among other (same size) blocks that are in use. And it's still a good idea to keep the freeing of memory out of the hands of human beings by using a garbage collection system. (Think about a Lisp system, in which all pairs are the same size, and they're garbage collected!)
So the answers are No, Yes, No, Yes.
Because this procedure calls other procedures (including itself), we have to save $ra, and we have to save our one argument $a0. We also have to save the result from the first recursive call while we're working on the second recursive call. That's three things to save, so we need three words of stack frame. (Alternatively, we can save $s0 and $s1, and use those for the list argument and for the intermediate result.)
mergesort: addi $sp, -12 # prologue: sw $ra, 0($sp) # we accept either order of saving sw $a0, 4($sp) # the two things we know at start
add $v0, $a0, $zero # body: beqz $a0, epi # end test: list == 0
lw $t0, 4($a0) # list->next beqz $t0, epi # end test: list->next == 0 jal evens # evens(list) add $a0, $v0, $zero # ret val becomes arg jal mergesort # mergesort(evens(list)) sw $v0, 8($sp) # save the result lw $a0, 4($sp) # list (my arg) jal odds # odds(list) add $a0, $v0, $zero # ret val becomes arg jal mergesort # mergesort(odds(list)) add $a1, $v0, $zero # ret val becomes 2nd arg lw $a0, 8($sp) # saved result becomes 1st arg jal merge # call merge
epi: lw $ra, 0($sp) # epilogue: addi $sp, 12 # restore $ra and stack jr $ra # return
This is a completely straightforward translation, in standard prologue-body- epilogue form. It's possible to shave off a few instructions by being clever at the cost of clarity; for example, by putting the end tests before the prologue we could avoid allocating and deallocating stack space in the base case. [Extra for experts:] More interestingly, 61A alumni should recognize the call to merge() as a tail call -- when it returns, we return. We could take advantage of that by doing the epilogue things before calling merge, and then turning the JAL instruction into a plain jump:
... add $a1, $v0, $zero # ret val becomes 2nd arg lw $a0, 8($sp) # saved result becomes 1st arg epi: lw $ra, 0($sp) # epilogue: addi $sp, 12 # restore $ra and stack j merge # goto merge
In this version our stack frame and merge's stack frame aren't both on the stack at the same time, so the memory required to run the program doesn't grow because of the call to merge. This is exactly what a Scheme interpreter or compiler does to implement tail call elimination.
But you shouldn't try to be clever when taking an exam. Just do the straightforward thing, as above.
The scoring of this problem was less straightforward than the others. We wanted a solution with several minor errors, but still basically having the right structure and many correct details, to get some credit. So we divided all the errors we found into three categories:
Minor errors: One point off per error, up to three errors. So a solution with a lot of minor errors but no others got 2 points. Note: "Minor" doesn't mean "unimportant"! Many of these errors showed important misunderstandings.
Major errors: These seemed to reflect a serious misunderstanding of the way the MIPS hardware works or the whole idea of procedure calling. Two points off per error, up to two errors.
Disasters: These suggested a complete lack of understanding, and got zero points.
There were too many minor errors to list them all, but here's a representative sample:
If you believe our solutions are incorrect, we'll be happy to discuss it, but you're probably wrong!