Build Systems & Toolchain
Level 4 — Application (novel problems, no hints) Time limit: 60 minutes Total marks: 60
Answer all questions. Show reasoning. Assume a GNU/Linux x86-64 toolchain (gcc, ld, make, cmake) unless stated otherwise.
Question 1 — Symbol Resolution & Link Order (12 marks)
You have four files:
/* util.c */
int helper(void) { return 42; }
/* app.c */
int helper(void);
int main(void) { return helper(); }You build a static library and link:
gcc -c util.c -o util.o
ar rcs libutil.a util.o
gcc -c app.c -o app.o
(a) The command gcc libutil.a app.o -o prog fails with an undefined reference to helper, but gcc app.o libutil.a -o prog succeeds. Explain precisely why, referencing how the linker processes archives left-to-right. (5)
(b) Now suppose libutil.a and libmath.a are mutually dependent (each uses a symbol from the other). Give a single gcc command line that resolves this cyclic dependency without duplicating each -l flag three times, and name the linker option used. (3)
(c) A colleague replaces the static library with a shared library libutil.so. The link now succeeds regardless of the position of -lutil relative to app.o in some setups but not others. State the general rule for when archive order matters versus shared-object order, and give the reason. (4)
Question 2 — Makefile Construction (14 marks)
A project has sources main.c, parse.c, eval.c in src/, headers in include/, and must build object files into build/ and a final binary bin/calc.
(a) Write a Makefile using a pattern rule and automatic variables that:
- compiles each
src/%.cintobuild/%.owith-Iinclude -Wall, - links all objects into
bin/calc, - uses a variable
OBJderived from the source list, - has a
.PHONYcleantarget that removesbuild/andbin/contents. (9)
(b) Explain what $<, $@, and $^ each expand to inside your compile and link recipes. (3)
(c) Your build does not recompile when a header in include/ changes. State one concrete mechanism to make object files depend on headers automatically. (2)
Question 3 — Sanitizers & Debugging Diagnosis (12 marks)
Consider:
#include <stdlib.h>
int main(void) {
int *a = malloc(4 * sizeof(int));
a[4] = 7; /* line 4 */
int x = 1 << 31; /* line 5 */
free(a);
return a[0]; /* line 7 */
}(a) For each of lines 4, 5, and 7, name the specific sanitizer (ASan, UBSan, or TSan) that would report it and the category of error each reports. (6)
(b) Give the exact compile flag(s) needed to enable both ASan and UBSan together in one build, and explain why you generally cannot combine ASan and TSan in the same binary. (3)
(c) Describe how you would use GDB to stop exactly at the moment a[0]'s value changes, naming the specific GDB feature and one command. (3)
Question 4 — CMake & Cross-Compilation (12 marks)
(a) Write a minimal CMakeLists.txt that builds a static library mathx from mathx.c and an executable demo from main.c that links against it. Include the cmake_minimum_required and project lines. (6)
(b) Explain the difference in generated compiler flags between the Debug and Release build types, and give the CMake command-line syntax to configure a RelWithDebInfo build. (3)
(c) You must cross-compile the above for aarch64 from an x86-64 host. Name the two CMake variables that specify the target system and the cross-compiler, and state the role of the sysroot in this process. (3)
Question 5 — Disassembly & Profiling (10 marks)
You compile hot.c at -O2 and profile it. perf reports that 80% of runtime is in one function dot.
(a) Give the objdump command (with the flag that interleaves source) to disassemble hot.o and view the assembly of dot. (3)
(b) Using Amdahl's law, if dot takes 80% of runtime and you make dot 4× faster (leaving the rest unchanged), compute the overall speedup of the whole program. Show the calculation. (4)
(c) Contrast what gprof measures versus what valgrind --tool=callgrind measures, in one sentence each, noting one accuracy trade-off. (3)
Answer keyMark scheme & solutions
Question 1 (12 marks)
(a) [5]
- The linker processes inputs left to right, maintaining a set of unresolved symbols. (1)
- When it encounters an archive (
.a), it only pulls in member object files that resolve a currently unresolved symbol. (1) - In
gcc libutil.a app.o: at the timelibutil.ais scanned, no symbol is yet unresolved (app.onot seen yet), soutil.ois not extracted; thenapp.oreferenceshelper, but the archive is already past → undefined reference. (2) - In
gcc app.o libutil.a:app.ois processed first, leavinghelperunresolved; thenlibutil.ais scanned andutil.ois pulled in to satisfy it → success. (1)
(b) [3]
- Use grouping:
gcc app.o -Wl,--start-group -lutil -lmath -Wl,--end-group -o prog(2) - The linker option is
--start-group/--end-group(-(/-)), which repeatedly rescans the enclosed archives until no new symbols are resolved. (1)
(c) [4]
- For static archives, order matters because members are extracted only on demand as unresolved symbols exist. (1)
- For shared objects, the entire library's symbol table is made available; the dynamic symbols are recorded and resolved as a whole, so a
.socan satisfy references from objects placed before or after it on the command line in typical configurations. (2) - Reason: with
--as-neededbehaviour a.somay still need to appear before use to be recorded, but fundamentally an archive is a collection of separable .o files pulled selectively, whereas a.sois a single indivisible unit with a complete symbol table. (1)
Question 2 (14 marks)
(a) [9] Example acceptable Makefile:
CC := gcc
CFLAGS := -Iinclude -Wall
SRC := $(wildcard src/*.c)
OBJ := $(patsubst src/%.c,build/%.o,$(SRC))
bin/calc: $(OBJ)
@mkdir -p bin
$(CC) $^ -o $@
build/%.o: src/%.c
@mkdir -p build
$(CC) $(CFLAGS) -c $< -o $@
.PHONY: clean
clean:
rm -rf build/* bin/*Mark scheme:
OBJderived from source list viapatsubst/wildcard — 2- Pattern rule
build/%.o: src/%.cwith-ccompile — 2 - Correct link rule producing
bin/calcfrom$(OBJ)— 2 - Use of automatic variables
$<,$@,$^correctly — 1 .PHONY: cleanwith a removal recipe — 2
(b) [3]
$<→ the first prerequisite (heresrc/%.c, the single source file). (1)$@→ the target name (e.g.build/parse.oorbin/calc). (1)$^→ all prerequisites, space-separated, duplicates removed (the full object list in the link rule). (1)
(c) [2]
- Generate dependency files with the compiler and include them, e.g. add
-MMD -MPtoCFLAGSand-include $(OBJ:.o=.d)in the Makefile (auto-generated.dfiles list header dependencies). (2) (Also acceptable: manually listing headers as prerequisites.)
Question 3 (12 marks)
(a) [6] — 2 marks each:
- Line 4 (
a[4]=7on a 4-element malloc): ASan — heap buffer overflow (out-of-bounds write). (2) - Line 5 (
1 << 31onint): UBSan — signed integer overflow / left-shift into sign bit (undefined behaviour). (2) - Line 7 (
return a[0]afterfree(a)): ASan — heap-use-after-free. (2) (TSan reports data races; none of these are data races.)
(b) [3]
- Compile with
-fsanitize=address,undefined -g(both in one-fsanitizelist). (2) - ASan and TSan cannot coexist because both instrument memory access with incompatible, conflicting runtime memory layouts / shadow-memory schemes; they are mutually exclusive in one binary. (1)
(c) [3]
- Use a watchpoint — GDB stops when a memory location's value changes. (1)
- Command:
watch a[0](a data/hardware watchpoint), thenrun/continue. (2)
Question 4 (12 marks)
(a) [6] Example:
cmake_minimum_required(VERSION 3.10)
project(demo_project C)
add_library(mathx STATIC mathx.c)
add_executable(demo main.c)
target_link_libraries(demo PRIVATE mathx)Mark scheme:
cmake_minimum_required+project— 2add_library(mathx STATIC mathx.c)— 2add_executable(demo main.c)+target_link_libraries(demo ... mathx)— 2
(b) [3]
Debug: no/low optimization + debug info, roughly-g -O0(defines nothing special). (1)Release: high optimization, no debug info, definesNDEBUG— roughly-O3 -DNDEBUG. (1)- Configure RelWithDebInfo:
cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo ..(yields-O2 -g -DNDEBUG). (1)
(c) [3]
CMAKE_SYSTEM_NAME(target system, e.g.Linux) andCMAKE_SYSTEM_PROCESSOR(e.g.aarch64); plusCMAKE_C_COMPILERset to the cross-compiler (e.g.aarch64-linux-gnu-gcc). Accept naming the compiler variable + system name variable. (2)- The sysroot (
CMAKE_SYSROOT) provides the target's headers and libraries so the cross-compiler links against target-appropriate (not host) system libraries. (1)
Question 5 (10 marks)
(a) [3]
objdump -d -S hot.o(orobjdump --disassemble --source hot.o);-Sinterleaves source with assembly. (3) (requires-gat compile for source interleaving.)
(b) [4] Amdahl's law:
- Fraction sped up , speedup factor . (1)
- Overall speedup . (2)
- . (1)
(c) [3]
gprof: sampling-based statistical profiler + call-graph via instrumentation (-pg); low overhead but approximate/statistical timing. (1.5)valgrind --tool=callgrind: exact instruction/call counts via full binary instrumentation; very accurate counts but large slowdown (10–50×) and simulated, not wall-clock, timing. (1.5)
[
{"claim":"Amdahl overall speedup with p=0.8, s=4 is 2.5", "code":"p=Rational(8,10); s=4; result = (1/((1-p)+p/s) == Rational(5,2))"},
{"claim":"Denominator (1-p)+p/s equals 0.4", "code":"p=Rational(8,10); s=4; result = ((1-p)+p/s == Rational(2,5))"},
{"claim":"1<<31 exceeds max signed 32-bit int (2**31-1)", "code":"result = ((1<<31) > (2**31 - 1))"},
{"claim":"malloc(4*sizeof int) valid indices are 0..3 so index 4 is out of bounds", "code":"n=4; result = (4 >= n)"}
]