Machine Architecture and Organization (sec 010)
index.php

## CSci 2021 (010) Spring 2020 Midterm 2 solutions notes

### Question “0” (shows up as 1)

These identifying questions did not affect your grade, even if Canvas marked them as “wrong”.

### Question “1” (shows up as 2-9)

Each of these loops does something like adding together all the numbers in a range of integers.

These eight different loops differed in three different aspects:

• Whether the loop was a “while” loop or a “do-while” loop. This makes a difference if the range would have otherwise been empty, because a while loop will return 0 for an empty range, while a do-while loop will always include a in the total even if the range should be empty.
• Whether the integers are signed or unsigned. This doesn’t affect the compilation of either “i++” or “total += i”, because addition is the same operation on signed or unsigned integers. But it does affect the meaning of the comparison between i and b.
• Whether the upper-bound b is included as part of the range or not.

The naming of the C functions represented the above differences:

• First digit: 0 for a while loop, 1 for a do-while loop
• Second digit: 0 for unsigned, 1 for signed
• Third digit: 0 for b included in range, 1 for b not included

Here’s how the three differences showed up in the assembly language functions:

• A do-while loop has a simpler structure with only one label and conditional jump. The code before the loop falls into the loop body, and then after the loop body there is a conditional jump that jumps back to the beginning for the loop (L2) if the loop is not finished yet. For a while loop the condition has to be executed before the first execution of the body, and in these examples this was always done by entering the loop for the first time via an unconditional jump to the condition (L2); in the while loop versions, the label for the start of the body was L3.
• The comparison instructions that use signed arithmetic use “l” and “g” for “less than” and “greater than”. The comparison instructions that use unsigned arithmetic are different and use the letters “b” and “a” for “below” and “above”.
• In all of these cases the ending condition was compiled directly the way it appeared in the C source, with a comparison between i in %edi and b in %esi (written in the reverse order according to AT&T syntax). Therefore, the versions where b was not included in the range had the condition be false when i == b, while the versions where b was included in the range had the condition be true in this case. This difference shows up in whether “e”, short for “or equal”, appears in the conditional jump instruction.

### Question “2a” (10)

Field p is at offset 0, because the first field of a structure is always at offset 0. Field p is 8 bytes long because it is a pointer, so the first place where field i can go is at offset 8, and that’s the correct location for it because it is properly aligned (8 is a multiple of 4). The first available offset after the end of field i would be 8+4=12, but field q can’t go at offset 12. Field q is an 8-byte pointer that needs to be 8-byte aligned, so its location is the first offset starting from  12 that is a multiple of 8, namely 16.

### Question “2b” (11)

This function implements search in a binary search tree: similar to a function in project 1, but comparing integers instead of string prefixes. We intentionally used generic variable names, but the “i” field is the number stored in each binary search tree node, while “p” and “q” represent the left and right subtree pointers. Among the function parameters, “n” is the pointer to the root of a subtree being searched, while “x” is the number we’re looking for, and the function returns a pointer to the node containing the number if it is found, otherwise a null pointer. Here are two examples of correct C code that could have compiled to the assembly:

struct node *func(struct node *n, long x) {

if (n == NULL) {

return NULL;

}

long y = n->i;

if (y > x) {

return func(n->p, x);

} else if (y != x) {

return func(n->q, x);

} else {

return n;

}

}

struct node *func(struct node *n, long x) {

if (!n) {

return 0;

}

long y = n->i;

if (x < y) {

return func(n->p, x);

} else if (x == y) {

return n;

} else {

return func(n->q, x);

}

}

### Question “3” (12-21)

Here are the complete instruction descriptions once all the sticky notes were put back. These aren’t necessarily the only way these instructions could be implemented, just what one team came up with.

Common for all instructions:

icode:ifun <- M1[PC]

rA:rB <- M1[PC + 1]

• rrxchgq rA, rB. Interchange (swap) the values in registers rA and rB. The value that used to be in rA is moved to rB, and vice versa. (question 19)
• valP <- PC + 2
• valA <- R[rA]
• valB <- R[rB]
• R[rA] <- valB
• R[rB] <- valA
• PC <- valP
• rjmp *rA. Like the x86-64 "jmp *%rax", jump to the address held in register rA. (Might be used for a switch statement.) (question 20)
• valP <- PC + 2
• valA <- R[rA]
• PC <- valA
• rcall *rA. Like the x86-64 "call *%rax", call a function whose address is held in register rA. (Might be used for a function pointer.) (question 12)
• valP <- PC + 2
• valA <- R[rA]
• valB <- R[%rsp]
• valE <- valB + -8
• M8[valE] <- valP
• R[%rsp] <- valE
• PC <- valA
• incq rB. Add 1 to the value in register rB. rB = rB + 1. (question 16)
• valP <- PC + 2
• valB <- R[rB]
• valE <- valB + 1
• R[rB] <- valE
• PC <- valP
• decq rB. Subtract 1 from the value in register rB. rB = rB - 1. (question 15)
• valP <- PC + 2
• valB <- R[rB]
• valE <- valB + -1
• R[rB] <- valE
• PC <- valP
• negq rB. Unary negation: replace the value in rB with its negative. rB = -rB. (question 18)
• valP <- PC + 2
• valB <- R[rB]
• valE <- 0 - valB
• R[rB] <- valE
• PC <- valP
• leaq D(rB), rA. Load effective address. Compute D + rB, and store the result in rA. (question 13). This instruction is similar to iaddq from the lab, except that it has a separate destination register.
• valP <- PC + 10
• valC <- M8[PC + 2]
• valB <- R[rB]
• valE <- valC + valB
• R[rB] <- valE
• PC <- valP
• shlq rB. Left shift by 1. rB = rB << 1. (question 14). The Y86-64 ALU doesn’t have a shifting operation, but shifting left by one is the same as doubling or adding a number to itself.
• valP <- PC + 2
• valB <- R[rB]
• valE <- valB + valB
• R[rB] <- valE
• PC <- valP
• immovq C, (rB). Move immediate value to memory. Copy the 64-bit constant value C into the memory location pointed to by rB. (question 17)
• valP <- PC + 10
• valC <- M8[PC + 2]
• valB <- R[rB]
• valE <- valB + 0
• M[valE] <- valC
• PC <- valP
• setCCq rB. Set based on condition. If the condition is true, set rB to 1, otherwise set it to 0. (question 21)
• valP <- PC + 2
• valE <- 0 + (Cnd ? 1 : 0)
• R[rB] <- valE
• PC <- valP

Notice that “PC <- valA” is used in both rjmp and in rcall. We considered “rjmp” to be the best answer for question 20 because question 12 could only have been rcall (since call but not jump pushes a value on the stack), and the instructions asked you to use each instruction once. But we gave full credit for rcall as an answer for question 20 as well.

### Question “4” (22)

The “essay” question at the end didn’t affect your grade, but we did read all of your comments. We will avoid test designs that require so much scrolling in the future.