Build Systems & Toolchain
Level 3 — Production (from-scratch derivations, code-from-memory, explain-out-loud) Time limit: 45 minutes Total marks: 60
Question 1 — The Compilation Pipeline (10 marks)
A single file main.c includes <stdio.h> and calls printf. From memory, describe the four canonical stages GCC runs from source to executable.
(a) For each stage, name the tool/phase, its input file extension, and its output file extension. (4)
(b) At which stage is the #include directive expanded, and at which stage is the address of printf finally fixed? Explain why these happen at different stages. (4)
(c) Give the GCC flag that stops the pipeline after producing an assembly file, and the flag that stops after producing an object file. (2)
Question 2 — Symbol Resolution & Link Order (12 marks)
You have three files:
util.c defines helper(), calls nothing external
app.c defines main(), calls helper() and calls libm's sin()
You build static archive libutil.a from util.o.
(a) Explain, from memory, the single-pass left-to-right rule the traditional Unix linker uses to resolve symbols from static archives. (3)
(b) One of the two commands below links successfully, the other fails with an undefined reference to helper. State which fails and give the precise reason in terms of the pending-undefined-symbol set. (4)
(A) gcc app.o -lutil -lm -L.
(B) gcc -lutil app.o -lm -L.
(c) Write the correct, portable link command and explain why placing -lm after app.o matters. (3)
(d) Static vs shared: state one runtime difference and one distribution difference between linking libutil.a vs libutil.so. (2)
Question 3 — Makefile From Scratch (12 marks)
Write, from memory, a complete Makefile that builds an executable calc from main.c, parse.c, eval.c. Requirements:
- Use a
CCandCFLAGSvariable (CFLAGS includes-Wall -Wextra -O2). - Use a pattern rule
%.o : %.cwith the correct automatic variables for the target and the first prerequisite. (4) - Link all objects into
calc, using the automatic variable for all prerequisites. (3) - Provide
.PHONYtargetsclean(removes objects and executable) andall(default). (3) - Add explicit header dependencies so that editing
defs.hrebuilds every object. (2)
Question 4 — CMake + Build Types (10 marks)
(a) Write a minimal CMakeLists.txt (from memory) that: sets a minimum version and project name, builds a static library mathx from mathx.c, builds executable app from main.c, and links mathx into app using the modern target-based command. (6)
(b) Name the three standard CMAKE_BUILD_TYPE values in the question title, and for each state the typical optimization/debug-info flag combination it implies. (3)
(c) Give the command-line invocation to configure a Release build in a build/ directory. (1)
Question 5 — Sanitizers & Debugging (10 marks)
(a) A program reads arr[10] from a 10-element stack array. Name the sanitizer that catches this, the compile flags to enable it, and explain why it detects the error at runtime rather than compile time. (4)
(b) Match each sanitizer — ASan / UBSan / TSan — to the single defect it is designed to catch: (i) a data race between two threads, (ii) signed integer overflow, (iii) heap-buffer-overflow. (3)
(c) In GDB, you suspect a global counter is being corrupted. Give the exact command to stop execution whenever its value changes, and name one command to print the call stack at that point. (3)
Question 6 — Disassembly & PIC (6 marks)
(a) Give the objdump command (with the flag) that produces an annotated disassembly with source interleaved of a.out. (2)
(b) Explain from memory what PIC stands for and why shared libraries must be compiled with it. Name the GCC flag used. (4)
Answer keyMark scheme & solutions
Question 1 (10 marks)
(a) (4 — 1 per stage)
| Stage | Tool/phase | Input | Output |
|---|---|---|---|
| Preprocessing | cpp (preprocessor) |
.c |
.i |
| Compilation | compiler proper (cc1) |
.i |
.s |
| Assembly | assembler (as) |
.s |
.o |
| Linking | linker (ld) |
.o (+ libs) |
executable (a.out) |
Why: each stage transforms one representation to the next lower level.
(b) (4)
#includeis expanded during preprocessing (2) — it is a textual substitution done bycppbefore any C is compiled.- The address of
printfis fixed during linking (2) —printflives in libc; the compiler only emits an unresolved symbol reference / relocation entry, and only the linker (or dynamic loader) knows the final address. They differ because header expansion is purely textual, while address assignment requires knowing where all object code lands in memory.
(c) (2) — -S stops after assembly file; -c stops after object file. (1 each)
Question 2 (12 marks)
(a) (3) The linker processes inputs left to right in one pass. It maintains a set of currently-unresolved (undefined) symbols. When it reaches an archive, it pulls in only those member .o files that satisfy a currently pending undefined symbol; it does not revisit an archive later. Objects listed on the command line are always fully included.
(b) (4) Command (B) fails. When -lutil is seen first, the pending-undefined set is empty (no helper reference has appeared yet, because app.o comes later), so the linker pulls nothing from libutil.a and discards it. When app.o is then processed, helper becomes undefined but the archive has already been passed → undefined reference to helper. (2 for identifying B, 2 for the empty-pending-set reasoning.)
(c) (3)
gcc app.o -L. -lutil -lm
app.o first registers helper and sin as undefined; -lutil then resolves helper, -lm resolves sin. Libraries must come after the objects that use them so the referencing symbols are already pending. (2 command, 1 explanation.)
(d) (2)
- Runtime: static → code is copied into the executable, no library needed at run time; shared → the
.somust be present and loaded by the dynamic linker at run time. (1) - Distribution: static → larger self-contained binary, no external dependency; shared → smaller binary but you must ship/have the
.so, and can update it independently. (1)
Question 3 (12 marks)
CC = gcc
CFLAGS = -Wall -Wextra -O2
OBJS = main.o parse.o eval.o
all: calc # default target (1)
calc: $(OBJS) # link
$(CC) $(CFLAGS) $^ -o $@ # $^ = all prereqs, $@ = target (3)
%.o: %.c # pattern rule (2)
$(CC) $(CFLAGS) -c $< -o $@ # $< = first prereq, $@ = target (2)
$(OBJS): defs.h # header dependency (2)
.PHONY: all clean # (1)
clean:
rm -f $(OBJS) calc # (1)Mark breakdown: pattern rule + $</$@ (4); link with $^ (3); .PHONY all+clean (3); defs.h dependency (2).
Question 4 (10 marks)
(a) (6)
cmake_minimum_required(VERSION 3.10) # (1)
project(demo C) # (1)
add_library(mathx STATIC mathx.c) # (2)
add_executable(app main.c) # (1)
target_link_libraries(app PRIVATE mathx) # (1)(b) (3 — 1 each)
- Debug:
-O0 -g(no optimization, full debug info). - Release:
-O3 -DNDEBUG(max optimization, asserts off, no debug info). - RelWithDebInfo:
-O2 -g -DNDEBUG(optimized but with debug symbols).
(c) (1)
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
(then cmake --build build).
Question 5 (10 marks)
(a) (4) AddressSanitizer (ASan). Flags: -fsanitize=address -g (also link with it). It works at runtime because the out-of-bounds index may be computed dynamically; ASan instruments memory with redzones/shadow memory around allocations and checks each access at execution time — a compiler cannot in general prove the index is out of range statically. (1 name, 1 flag, 2 reasoning.)
(b) (3) (i) data race → TSan; (ii) signed integer overflow → UBSan; (iii) heap-buffer-overflow → ASan. (1 each.)
(c) (3)
watch counter # stops when value changes (2)
backtrace (or bt) # print call stack (1)
Question 6 (6 marks)
(a) (2)
objdump -S a.out # -S = disassemble with source interleaved (needs -g)
(Accept objdump -d for plain disassembly but full marks require -S.)
(b) (4) PIC = Position-Independent Code (1). Shared libraries can be loaded at different virtual addresses in different processes, so their code must not contain hard-coded absolute addresses; PIC uses relative addressing (via the GOT/PLT) so the same .so mapping works regardless of load address, allowing it to be shared across processes (2). Flag: -fPIC (1).
[
{"claim":"Q1c: -S stops after assembly (.s), -c stops after object (.o) — distinct flags",
"code":"S_flag='.s'; c_flag='.o'; result = (S_flag=='.s' and c_flag=='.o' and S_flag!=c_flag)"},
{"claim":"Q2b: link fails when archive precedes referencing object (empty pending set)",
"code":"def links(order):\n pending=set()\n for tok in order:\n if tok=='app.o': pending|={'helper','sin'}\n elif tok=='-lutil':\n if 'helper' in pending: pending.discard('helper')\n elif tok=='-lm':\n if 'sin' in pending: pending.discard('sin')\n return len(pending)==0\nB=links(['-lutil','app.o','-lm'])\nC=links(['app.o','-lutil','-lm'])\nresult = (B==False and C==True)"},
{"claim":"Q1a: four canonical compilation stages",
"code":"stages=['preprocess','compile','assemble','link']; result = len(stages)==4"},
{"claim":"Q4b: build type flag mapping distinct optimization levels",
"code":"bt={'Debug':'-O0','Release':'-O3','RelWithDebInfo':'-O2'}; result = len(set(bt.values()))==3 and bt['Debug']=='-O0'"}
]