Level 4 — ApplicationBuild Systems & Toolchain

Build Systems & Toolchain

60 minutes60 marksprintable — key stays hidden on paper

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.


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/%.c into build/%.o with -Iinclude -Wall,
  • links all objects into bin/calc,
  • uses a variable OBJ derived from the source list,
  • has a .PHONY clean target that removes build/ and bin/ 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 time libutil.a is scanned, no symbol is yet unresolved (app.o not seen yet), so util.o is not extracted; then app.o references helper, but the archive is already past → undefined reference. (2)
  • In gcc app.o libutil.a: app.o is processed first, leaving helper unresolved; then libutil.a is scanned and util.o is 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 .so can satisfy references from objects placed before or after it on the command line in typical configurations. (2)
  • Reason: with --as-needed behaviour a .so may still need to appear before use to be recorded, but fundamentally an archive is a collection of separable .o files pulled selectively, whereas a .so is 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:

  • OBJ derived from source list via patsubst/wildcard — 2
  • Pattern rule build/%.o: src/%.c with -c compile — 2
  • Correct link rule producing bin/calc from $(OBJ)2
  • Use of automatic variables $<, $@, $^ correctly — 1
  • .PHONY: clean with a removal recipe — 2

(b) [3]

  • $< → the first prerequisite (here src/%.c, the single source file). (1)
  • $@ → the target name (e.g. build/parse.o or bin/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 -MP to CFLAGS and -include $(OBJ:.o=.d) in the Makefile (auto-generated .d files list header dependencies). (2) (Also acceptable: manually listing headers as prerequisites.)

Question 3 (12 marks)

(a) [6] — 2 marks each:

  • Line 4 (a[4]=7 on a 4-element malloc): ASan — heap buffer overflow (out-of-bounds write). (2)
  • Line 5 (1 << 31 on int): UBSan — signed integer overflow / left-shift into sign bit (undefined behaviour). (2)
  • Line 7 (return a[0] after free(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 -fsanitize list). (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), then run/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 + project2
  • add_library(mathx STATIC mathx.c)2
  • add_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, defines NDEBUG — 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) and CMAKE_SYSTEM_PROCESSOR (e.g. aarch64); plus CMAKE_C_COMPILER set 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 (or objdump --disassemble --source hot.o); -S interleaves source with assembly. (3) (requires -g at compile for source interleaving.)

(b) [4] Amdahl's law:

  • Fraction sped up p=0.8p = 0.8, speedup factor s=4s = 4. (1)
  • Overall speedup =1(1p)+p/s=10.2+0.8/4=10.2+0.2=10.4= \dfrac{1}{(1-p) + p/s} = \dfrac{1}{0.2 + 0.8/4} = \dfrac{1}{0.2 + 0.2} = \dfrac{1}{0.4}. (2)
  • =2.5×= 2.5\times. (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)"}
]