Show all solutions
Rules
The exam is open book, open note, open computer. You may access the book and your own notes. You may also use computers or electronic devices to access your own class materials and public class materials. Specifically:
- You may access a browser and a PDF reader.
- You may access your own notes and problem set code electronically.
- You may access Internet sites on which your own notes and problem set code are stored.
- You may access the course site.
- You may access pages directly linked from the course site, including our lecture notes, exercises, and section notes.
- You may access our lecture, problem set, and section code.
- You may run a C or C++ compiler, including an assembler and linker.
- You may access manual pages and common utilities, including calculators and a Python interpreter.
However:
- You may not access class discussion forums, such as the Edboard, or other question forums, such as Stack Overflow.
- You may not access an on-line disassembler or decompiler.
- You may not access solutions from any previous exam, by paper or computer, except for those on the course site.
- You may not broadly search the Internet for answers to questions or access other courses’ materials. Stick to our course site.
- You absolutely may not contact other humans or AI assistants with questions about the exam—whether in person, via IM, via GitHub Copilot, or by posting questions to forums. The single exception is that you may contact course staff.
Any violations of this policy are breaches of academic honesty and will be treated accordingly. Please appreciate our flexibility and behave honestly and honorably.
Students are taking the exam at different times. Do not post publicly about the exam until given permission to do so by course staff. This includes general comments about exam difficulty.
Questions
If you have questions during the exam, write us email at
cs61-staff@g.harvard.edu
. Don’t use the Edboard. We may not notice
Edboard posts, and you aren’t supposed to access the Edboard anyway.
Completing your exam
You have 3 hours to complete the exam starting from the time you press Start. Different students may have different exams. Enter your answers directly in this form. A green checkmark appears when an answer is saved. (Click outside a text box to save the answer.)
Students with extra time accommodations: If your extra time is not reflected next to the start button, contact course staff and wait for confirmation before pressing it.
You should not need to attach a drawing or other longer notes, but you may do
so by adding them to a midterm
subdirectory in your cs61-f24-psets
repository. Make sure you push your changes to GitHub before time is up, and
explain in this form where we should look at your notes.
There is no need to explicitly “submit” the exam. Your changes are automatically saved with timestamps. We’ll grade the latest version saved within your exam window.
Notes
Assume a Linux operating system running on the x86-64 architecture unless otherwise stated. If you get stuck, move on to the next question. If you’re confused, explain your thinking briefly for potential partial credit.
1. Basics (15 points)
QUESTION 1A. How many bits are in a long
?
QUESTION 1B. What is the address of register %rax
?
QUESTION 1C. Why might traversing a linked list be slower than traversing an array? List all that apply.
- A node in a linked list is larger than an element of an array, because the linked list node must also have pointers to its neighbors.
- Nodes in a linked list might be arranged in random order in memory, rather than sequentially, and sequential access is usually faster than random access.
- Traversing a linked list requires more instructions than traversing an array, and more instructions are always slower.
- Traversing a linked list just requires accessing memory, whereas traversing an array always require multiplication instructions to access elements by index, and multiplication is faster than memory access.
- None of the above.
QUESTION 1D. List the typical segments of memory in increasing order by address on x86-64 Linux.
QUESTION 1E. Give three different examples of types of undefined behavior.
QUESTION 1F. Assuming that the following code does not execute undefined
behavior, what is the minimum value for N
(the number of elements in
array a
)?
int a[N];
int* x = a + 100;
x += 10;
int* y = &x[-5];
printf("%d value, %zd difference\n", *y, x - y);
2. Validity (10 points)
You are helping Emily St. John Mandel debug her implementation of pset1. Her code includes the following global variables:
// Key: pointer to start of allocation
// Value: padded size of allocation in bytes
std::map<void*, size_t> active_allocs;
std::map<void*, size_t> freed_allocs;
const int MAGIC_FOUR_BYTE_VALUE = 0xABBACAFE; // For wild write detection
QUESTION 2A. Emily’s code produces a segmentation fault in one of the tests.
A sanitizer run crashes on line 19 of an m61_insert_and_coalesce
helper
function:
void m61_insert_and_coalesce(void* ptr, size_t sz) {
auto it = freed_allocs.lower_bound(ptr);
if (it != freed_allocs.begin()) {
--it;
}
if (it != freed_allocs.end() && (char*) it->first + it->second == (char*) ptr) {
// Coalesce down
it->second += sz;
} else {
// Can’t coalesce down, so insert
freed_allocs[ptr] = sz;
it = freed_allocs.find(ptr);
}
// Try coalescing up.
auto next = it;
++next;
if ((char*) it->first + it->second == next->first) {
it->second += next->second;
freed_allocs.erase(next);
}
}
What is the problem with Emily’s code? Describe how to fix it.
QUESTION 2B. Having fixed that problem, she sees different sanitizer error
messages, on line 11 of m61_malloc
.
void* m61_malloc(size_t sz, const char* file, int line) {
size_t padded_size = add_padding(sz);
assert(padded_size >= sz + 4 && padded_size % alignof(std::max_align_t) == 0);
void* ptr = m61_find_free_space(padded_size);
if (!ptr) {
return nullptr;
}
// Place magic_word after sz bytes to detect wild writes.
char* magic_ptr = (char*) ptr + sz;
*((int*) magic_ptr) = MAGIC_FOUR_BYTE_VALUE;
active_allocs[ptr] = padded_size;
return ptr;
}
What is the problem with Emily’s code? Describe how to fix it.
QUESTION 2C. After resolving that problem, Emily observes that sometimes a
call to m61_free
will corrupt a nearby allocation. For example:
size_t n = ...; /* some number > 0 */
void* a = m61_malloc(n);
memset(a, 'a', n);
void* b = m61_malloc(11);
memset(b, 'b', 11);
m61_free(a);
assert(memcmp(b, "bbbbbbbbbbb", 11) == 0); // Fails!
Here is her m61_free
code. We’ve left out the code to detect invalid and
double frees, which is not relevant here.
void m61_free(void* ptr) {
// ... detect & handle `nullptr` and invalid and double frees ...
auto sz = active_allocs[ptr];
auto max_alignment = alignof(std::max_align_t);
size_t sz_plus_magic = sz + sizeof(int);
size_t padded_size = sz_plus_magic + max_alignment - (sz_plus_magic % max_alignment);
// Scribble over data to discourage use-after-free
memset(ptr, '!', padded_size);
active_allocs.erase(ptr);
freed_allocs.insert({ptr, padded_size});
}
Give a possible problem with Emily’s code, and describe how to fix it.
3. Internal metadata (15 points)
The following internal metadata implementation places a header
struct at the
start of every memory allocation. The payload (the space requested by the
user) immediately follows the header. MAGIC_ALLOC
and MAGIC_FREE
are
8-byte constants defined elsewhere.
struct header {
size_t magic; // MAGIC_ALLOC or MAGIC_FREE
size_t size; // Size of allocation, including padding and header
size_t prev_size; // Size of *previous* allocation; 0 if no previous allocation
size_t payload_size; // Size of payload (user-requested size)
char* file;
int line;
};
QUESTION 3A. What is the alignment of the header
struct?
QUESTION 3B. What is the size of the header
struct?
QUESTION 3C. How many bytes of padding does the header
struct contain, and
where?
QUESTION 3D. Can the members of the header
struct be rearranged at all to
make the struct smaller?
QUESTION 3E. Assuming that the first header
struct appears at the beginning
of default_buffer.buffer
, how many bytes of padding are required after a
header
struct to maintain correct alignment for user payloads?
QUESTION 3F. Complete this function, which returns the payload pointer corresponding to a given header pointer.
void* header_to_payload(header* hdr) {
}
QUESTION 3G. Complete this function, which returns the header pointer corresponding to a given payload pointer.
void* payload_to_header(void* payload) {
}
QUESTION 3H. Complete this function, which given a header pointer hdr
,
returns a pointer to the next allocation in default_buffer
, or nullptr
if hdr
is the last allocation in the buffer.
header* next_header(header* hdr) {
}
QUESTION 3I. Complete this function, which given a header pointer hdr
,
returns a pointer to the previous allocation in default_buffer
, or
nullptr
if hdr
is the first allocation in the buffer.
header* previous_header(header* hdr) {
}
4. Invariants and allocators (20 points)
Louise Bogan’s problem set 1 implementation uses external metadata. She has also written several data representation invariant checkers. Here are her data structures and checker functions.
struct allocation {
size_t size; // size requested by user
unsigned padding; // additional padding beyond `size`, e.g.,
// for boundary write detection and/or alignment
bool free; // true iff allocation is free
};
std::map<char*, allocation> allocs; // address -> allocation
void check_addresses_in_buffer() {
char* buf_first = default_buffer.buffer;
char* buf_last = buf_first + default_buffer.size;
for (auto& a : allocs) {
char* ptr = a.first;
assert(ptr != nullptr);
assert(ptr >= buf_first);
assert(ptr <= buf_last);
assert(ptr + a.second.size + a.second.padding <= buf_last);
}
}
void check_addresses_aligned() {
for (auto& a : allocs) {
uintptr_t addr = (uintptr_t) a.first;
assert(addr % alignof(std::max_align_t) == 0);
}
}
void check_sizes_valid() {
for (auto& a : allocs) {
assert(a.second.size + a.second.padding <= default_buffer.size);
}
}
void check_non_overlapping() {
char* last = default_buffer.buffer;
for (auto& a : allocs) {
char* ptr = a.first;
assert(ptr >= last);
last = ptr + a.second.size + a.second.padding;
}
}
void check_contiguous() {
char* last = default_buffer.buffer;
for (auto& a : allocs) {
char* ptr = (char*) a.first;
assert(ptr == last);
last = ptr + a.second.size + a.second.padding;
}
}
QUESTION 4A. Some of these checker functions are redundant. (A checker
F
is redundant when another checker, G
, checks everything that F
checks
and more.) List the redundant checker(s) and say which checkers they are
redundant with.
QUESTION 4B. Add an assertion to the check_contiguous
function that also
checks that the allocs
map is complete, meaning that the entire space of
the default_buffer
is included in allocs
. Give the line number where the
assertion should go.
QUESTION 4C. Write an assertion that would hold if Louise implements boundary
write detection, but would not necessarily hold if she did not. This
assertion could go in check_sizes_valid
. Give the line number where the
assertion should go.
QUESTION 4D. Louise’s m61_free
, shown below, is meant to check for
invalid or double frees. Write an expression that evaluates to true
on line
83 for most invalid or double frees (and never evaluates to true
for
valid frees). The expression should use only the structures Louise already
defined.
void m61_free(void* ptr) {
if (ptr != nullptr) {
auto it = allocs.find((char*) ptr);
// ...Check for invalid or double free...
it->second.free = true;
}
}
QUESTION 4E. Louise is currently using this function to make an allocation.
static char* find_allocation(size_t sz, size_t required_padding) {
assert((sz + required_padding) % alignof(std::max_align_t) == 0);
check_all_invariants(); // calls all checker functions
for (auto it = allocs.begin(); it != allocs.end(); ++it) {
// Is this allocation free & does it fit the space we need?
if (it->second.free && sz <= it->second.size) {
// Find this allocation’s boundaries
char* ptr = it->first;
char* end_ptr = ptr + it->second.size + it->second.padding;
// Split off the remaining space
char* split_ptr = ptr + sz + required_padding;
allocs.insert(it, {split_ptr, {end_ptr - split_ptr, 0, true}});
// Adjust current allocation
it->second.size = sz;
it->second.padding = required_padding;
it->second.free = false;
// Check invariants and return
check_all_invariants();
return ptr;
}
}
return nullptr;
}
She observes that when using this function, none of her invariants ever fail, but her allocator seems to grow slower and slower over time. For instance:
for (int i = 0; i != 1000000; ++i) {
void* ptr = m61_malloc(10);
m61_free(ptr);
}
void* ptr = m61_malloc(20); // This takes far longer than expected!
Why is this? Explain briefly, referring to specific lines in Louise’s code.
QUESTION 4F. Suggest an invariant that would have caught this slowing-down problem and write a C++ assertion that would eventually fail with Louise’s problematic code. You can write a new assertion function or give a line number from the above assertions where the new assertion would go.
QUESTION 4G. Louise also observes that when using her find_allocation
function, m61_malloc
sometimes returns nullptr
even when there should be
enough space available. For instance:
void* ptr1 = m61_malloc(1);
void* ptr2 = m61_malloc(default_buffer.size - 16);
assert(ptr1 && ptr2);
// The whole buffer is occupied, so any allocation will fail.
void* ptr3 = m61_malloc(2);
assert(!ptr3);
// After we free an allocation, though, there should be space—but there’s not!
m61_free(ptr1);
ptr3 = m61_malloc(2);
assert(ptr3); // Assertion fails!
Why is this? Explain briefly, referring to specific lines in Louise’s code.
QUESTION 4H. Louise’s find_allocation
has a “time bomb” in it—a problem
that doesn’t occur the way it is currently used, but that could occur if it
were called with valid, but unusual, arguments. Specifically,
find_allocation(1, 31)
would cause a problem in some situations. Why is
this? Explain briefly, referring to specific lines in Louise’s code.
QUESTION 4I. Write a checker function that checks that Louise’s allocations are completely coalesced, meaning that there are no adjacent free allocations.
5. Assembly special cases (15 points)
All the questions in this section concern function assembly generated by C++ compilers for a conventional Linux x86-64 executable.
QUESTION 5A. List at least two different reasons that assembly for a
function might not refer to the %rdi
register.
QUESTION 5B. List at least two different reasons that
assembly for a function that takes zero arguments might refer to the %rdi
register.
QUESTION 5C. List at least two different reasons that assembly for a
function might not include a ret
instruction.
QUESTION 5D. Any function that refers to the %rbx
register always
explicitly saves %rbx
’s initial value and restores it to that value before
returning. Why is this? List all that apply; a fully correct response will
list at least two answers.
- The
%rbx
register must always have a fixed value when a function returns. - The
%rbx
register is callee-saved. - The
%rbx
register is caller-saved. - The calling convention does not assign the
%rbx
register any meaning at function entry, so its initial value is meaningless in the context of the function. - The
%rbx
register is the base pointer, and is used by debuggers to implement backtraces. - Other (describe briefly).
QUESTION 5E. Some functions that refer to the %rsp
register do not
explicitly save and restore its initial value. Why is this? List all that
apply.
- Some functions’ local variables can fit entirely in registers.
- Some function arguments may be stored on the stack by the caller.
- Some functions never return.
- Some functions use a different calling convention.
- Some functions’ local variables can fit in the “red zone.”
- None of the above.
QUESTION 5F. How have the compilers we’ve seen taken advantage of undefined behavior? List the best answers; explain briefly if unsure.
- If a compiler detects undefined behavior, it can refuse to compile the function.
- If a compiler can prove that an argument value causes undefined behavior within a function, it can assume that the argument will never have that value.
- If a compiler believes that undefined behavior might occur, it can perform fewer optimizations.
- After a pointer is dereferenced, the compiler can assume that the pointer
value is not
nullptr
. - After a unsigned addition, the compiler can assume there was no integer overflow.
- None of the above.
QUESTION 5G. In the remaining parts, we give function assembly generated by different buggy C++ compilers. Your job is to briefly explain how you can tell from the assembly that the relevant compiler is buggy.
How can you tell that this compiler is buggy?
_Z1fv:
movq $1, %rdi
callq _Z1gl
ret
QUESTION 5H. How can you tell that this compiler is buggy?
_Z1fv:
movq $1, %rdi
pushq %rax
callq _Z1gl
cmp %rax, %rdi
popq %rdi
je .L2
movq $1, %rax
retq
.L2:
xorl %eax, %eax
retq
QUESTION 5I. How can you tell that this compiler is buggy?
_Z1fv:
movq $1, %rdi
pushq %rdi
callq _Z1gl
movq (%rsp), %rdi
cmp %rax, %rdi
je .L2
movq $1, %rax
retq
.L2:
xorl %eax, %eax
retq
QUESTION 5J. The remaining three compilers are generating assembly for this source code:
int calc(int a, int b) {
if (a > b) {
return a + b;
} else {
return a + b + 1;
}
}
How can you tell that this compiler is buggy?
_Z4calcii:
cmpl %esi, %edi
movl (%rdi,%rsi), %eax
jg .L2
addl $1, %eax
.L2:
ret
QUESTION 5K. (Same source code as above) How can you tell that this compiler is buggy?
_Z4calcii:
cmpl %esi, %edi
leal (%rdi,%rsi), %eax
ja .L2
addl $1, %eax
.L2:
ret
QUESTION 5L. (Same source code as above) How can you tell that this compiler is buggy?
_Z4calcii:
movq %rdi, %rax
cmpl %esi, %eax
addl %esi, %eax
jg .L2
addl $1, %eax
.L2:
ret
6. Data representation inference (10 points)
For each question, write one to three assembly instructions that might imply that the containing function has the given property. Explain briefly if unsure.
QUESTION 6A. The first argument to the function has type int
or unsigned int
(but not long
, unsigned long
, or a pointer type).
QUESTION 6B. The first argument to the function has type short*
(not
unsigned short*
).
QUESTION 6C. The first argument to the function is a pointer to an array of
point
structures, where a point
is defined as:
struct point {
int x;
int y;
};
QUESTION 6D. The first argument to the function has type char*
or unsigned char*
.
QUESTION 6E. The first argument to the function has type int**
.
QUESTION 6F. The first argument to the function has type long
or unsigned long
(but not a pointer type).
QUESTION 6G. The first argument to the function has type unsigned long
(but
likely not long
or pointer type).
QUESTION 6H. The return value for the function has integral type (but likely not pointer type).
QUESTION 6I. The return value for the function has size greater than 8.
7. Addition is magic (15 points)
For each question, complete the given assembly so that it correctly implements this C++ function:
long add(long a, long b) {
return a + b;
}
The completed assembly function must produce the correct answer according to
the abstract machine. For full credit, you must write one instruction for
each numbered blank. Your instructions must not include ret
😉, and you
should not reuse any instruction line. (You can reuse an instruction
opcode, like addl
, more than once; just don’t repeat an instruction
line, which includes both opcode and instruction arguments.)
QUESTION 7A.
_Z3addll:
endbr64
____1____
ret
QUESTION 7B.
_Z3addll:
endbr64
movq %rdi, %rax
xorl %edi, %edi
____1____
ret
QUESTION 7C.
_Z3addll:
endbr64
testq %rsi, %rsi
je .L2
.L3:
incq %rdi
____1____
____2____
.L2:
____3____
ret
QUESTION 7D.
_Z3addll:
endbr64
movq $1, %rdx
.L2:
testq %rdx, %rsi
____1____
addq %rdx, %rdi
.L3:
____2____
jnc .L2
movq %rdi, %rax
ret
QUESTION 7E.
_Z3addll:
endbr64
____1____
____2____
orq %rdi, %rsi
____3____
movq %rsi, %rax
ret
QUESTION 7F.
_Z3addll:
endbr64
____1____
____2____
____3____ # must be a branch instruction
ret
.L9:
pushq %rax
movq %rsi, %rdx
movq %rdi, %rsi
leaq .Lubsan_data0(%rip), %rdi
call __ubsan_handle_add_overflow_abort@PLT
8. The end (5 points)
QUESTION 8A. What improvements would you suggest to the class? Any answer but no answer will receive full credit.
QUESTION 8B. What topics do you wish we had spent more time on? Any answer but no answer will receive full credit.