06_Linux_Toolchain_DevOps: interview questions and model answers¶
38 questions, ordered from screening level to senior level. Answer out loud, then compare. A good answer is 2 to 6 sentences, uses the precise term, and ends with a practical consequence. Where a module covers the topic in depth, it is named in brackets.
Part 1. Building C and C++ on Linux¶
1. What are the stages of building a C++ program, and what does each produce? [02]
Preprocessing (cpp, part of g++ -E) expands #include and macros into a translation unit.
Compilation (cc1plus) turns it into assembly; assembly (as) turns that into a relocatable
object file (.o, ELF) with a symbol table and relocations. Linking (ld, driven by g++)
combines objects and libraries, resolves every undefined symbol to a definition, applies
relocations and writes an executable or shared library. -E, -S and -c stop after each of
the first three stages.
2. What does the linker actually do? [02]
It reads the symbol tables of all inputs, matches every undefined reference (U in nm) with
exactly one definition, lays out the sections into segments, assigns final addresses, and patches
each relocation with the resolved address. For static libraries it pulls in only the archive
members that resolve a currently undefined symbol, scanning left to right, which is why library
order on the command line matters with GNU ld. For shared libraries it records a NEEDED entry and
leaves the final binding to the dynamic loader at run time.
3. Static library vs shared library: differences and trade-offs? [02]
A static library (.a) is an archive of object files; the needed members are copied into the
executable at link time, so the binary is self-contained and has no run-time lookup cost, but every
binary carries its own copy and a security fix requires relinking everything. A shared library
(.so) is loaded once by ld.so and mapped into every process that uses it: smaller binaries,
shared memory pages, updates without relinking, at the cost of run-time resolution, version
compatibility (ABI, sonames) and "cannot open shared object file" failures in deployment.
4. You get "undefined reference to foo". What are the possible causes? [02]
Three families: the definition is not in the link at all (a missing .cpp, a missing -lfoo, or
a library listed before the object that needs it); the names do not match (C vs C++ linkage, a
missing extern "C", a different signature or namespace, so the mangled names differ); or the
symbol exists but is not visible (hidden visibility, a template never instantiated, an inline
or static function defined only in another file). nm -C on the objects and libraries tells
you which: absent, present under another mangled name, or present but local (t instead of T).
5. What is the One Definition Rule, and why can violations go undetected? [02]
The ODR says every non-inline function and variable has exactly one definition in the program, and
every class, inline function or template defined in several translation units must be defined
identically (token for token, with the same meaning). Duplicate strong symbols give "multiple
definition" link errors, but differing class definitions with the same name (two struct Config
in two .cpp files, or a header compiled with different macros) produce no error, because the
linker only sees symbol names, not layouts. The program then reads the wrong offsets at run time;
-flto -Wodr and ASan's ODR checks can catch some cases.
6. What is -fPIC and why do shared libraries need it? [02]
Position-independent code addresses its global data and external functions through the GOT and PLT
(and PC-relative addressing) instead of absolute addresses, so the same code pages work wherever the
loader maps the library and can be shared between processes. Without it, the loader would have to
patch text relocations in the code itself, which makes the pages private per process and is
refused on x86-64 for most relocation types. Executables are PIE (position-independent) by default
on Ubuntu for ASLR.
7. How does the dynamic loader find a shared library at run time? [02]
ld.so reads the executable's NEEDED entries and searches, in order: DT_RPATH (if no
RUNPATH), LD_LIBRARY_PATH, DT_RUNPATH, the /etc/ld.so.cache built by ldconfig, then the
default directories /lib and /usr/lib (and their multiarch subdirectories). ldd shows the
result and LD_DEBUG=libs traces the search. In production, prefer installing to a standard path
with ldconfig or an $ORIGIN-relative rpath over LD_LIBRARY_PATH.
8. What do -O0, -O1, -O2, -O3, -Os and -Og mean? Which do you ship? [03]
-O0 does no optimisation (fast compile, debuggable, slow); -O1 basic optimisations; -O2 nearly
all optimisations that do not trade size for speed, the usual release level; -O3 adds aggressive
inlining, loop transformations and more vectorisation, which may or may not be faster; -Os
optimises for size; -Og optimises without hurting debugging. Ship -O2 -g (CMake's
RelWithDebInfo) with symbols archived, and adopt -O3 or LTO only when a benchmark shows a gain.
9. Why build with -g in release builds? [03, 07]
Debug info does not change the generated code; it only adds .debug_* sections that are never
loaded at run time. Shipping a stripped binary and archiving the unstripped one (or a split
.debug file matched by build-id) lets you symbolise production cores and perf profiles with
exact file and line numbers. Without it, a release-build crash gives you raw addresses.
10. What are sanitizers, and which ones would you run in CI? [03, 05]
Compiler instrumentation that detects bugs at run time: AddressSanitizer (out-of-bounds,
use-after-free, double free, leaks via LeakSanitizer; about 2x slowdown, typical), UndefinedBehaviorSanitizer
(signed overflow, invalid shifts, misaligned access, null dereference), ThreadSanitizer (data races,
lock-order inversions; 5 to 15x slowdown, typical) and MemorySanitizer (uninitialised reads,
Clang only). ASan and TSan cannot be combined, so CI runs an ASan+UBSan job and a separate TSan
job over the whole test suite, with halt_on_error so findings fail the build.
11. What does -Wall -Wextra -Werror give you, and what is the risk of -Werror? [03]
-Wall and -Wextra enable the warnings that usually indicate bugs (unused variables, sign
comparisons, missing returns, implicit fallthrough); -Werror makes the build fail on any of them,
so warnings cannot accumulate. The risk is that a new compiler version adds warnings and breaks the
build for packagers and users; the standard answer is -Werror on in CI and for developers, and
an option to turn it off (tcpkv's TCPKV_WERROR, the no-werror preset).
Part 2. CMake and testing¶
12. What do PUBLIC, PRIVATE and INTERFACE mean in target_link_libraries and friends? [04]
They define which usage requirements (include directories, compile definitions, options, linked
libraries) apply to the target itself and which propagate to targets that link to it. PRIVATE:
used to build this target only. INTERFACE: not used for this target, only for consumers (header-only
libraries). PUBLIC: both. The rule: if a dependency appears in your public headers it is PUBLIC; if
only in your .cpp files it is PRIVATE. Wrong visibility either leaks dependencies to every
consumer or breaks their compilation with missing headers.
13. Why "modern CMake" with targets instead of setting global variables like CMAKE_CXX_FLAGS and include_directories? [04]
Global settings apply to every target in the directory and below, cannot express which consumer
needs what, and do not travel with an installed package. Target-based commands attach requirements
to the target and propagate them through the dependency graph, so target_link_libraries(app
PRIVATE tcpkv::core) brings the right include paths, definitions, C++ standard and -pthread
automatically, both in the build tree and through find_package after installation.
14. What are CMake presets and why use them? [04, 09]
CMakePresets.json names complete configurations (generator, build directory, build type, cache
variables such as sanitizers) and matching build and test presets, checked into the repository.
cmake --preset asan && cmake --build --preset asan && ctest --preset asan is then the same command
for every developer, every IDE and CI. Personal tweaks go in the gitignored CMakeUserPresets.json.
15. How do you structure tests for a networked server so most of them are fast and deterministic? [05]
Keep the logic in pure, I/O-free units (the protocol parser, the store) and test them with
GoogleTest directly: fixtures for setup, parameterised tests for input tables, death tests for
aborts, gMock for dependencies behind interfaces. Run them under sanitizers. Keep a smaller set of
integration tests that start the real binary on a free port and drive it through a client,
labelled separately (ctest -L integration) so the unit suite stays under a few seconds.
Part 3. Git¶
16. Merge vs rebase: what is the difference and when do you use each? [06]
Merge creates a new commit with two parents that joins the histories, preserving exactly what
happened. Rebase replays your commits one by one on top of another base, creating new commits with
new hashes and a linear history. Rebase your own unpublished feature branch onto main to keep it
current and clean; never rebase commits others have based work on; integrate into main by merge
or squash-merge according to the team's convention.
17. You ran git reset --hard and lost commits. How do you get them back? [06]
git reflog lists every position HEAD (and each branch) has had, including commits no branch
points to any more. Find the entry before the reset, then git reset --hard HEAD@{n} or git branch
rescue <sha>. Unreachable commits survive until garbage collection prunes them (reflog entries
expire after 90 days by default, 30 for unreachable ones), so recovery is almost always possible
for recent mistakes.
18. How does git bisect work, and how do you automate it? [06]
It binary-searches the history between a known good and a known bad commit: Git checks out the
midpoint, you mark it good or bad, and it halves the range, finding the first bad commit in about
log2(N) steps. git bisect run ./script.sh automates it: exit 0 means good, 1 to 124 or 126 to 127
bad, 125 skip (does not build). The script should build in a separate directory and use timeout
so a hang counts as bad.
19. What is a Git commit, physically? [06]
An object in .git/objects addressed by the hash of its content: it records a tree (a snapshot of
the directory, itself made of trees and blobs), zero or more parent commit hashes, author,
committer and message. Branches and tags are just references to commit hashes. Because the hash
covers the parents, changing any old commit changes every hash after it, which is why rebasing
rewrites history.
Part 4. Debugging, profiling and scripting¶
20. What do strace and perf each tell you, and when do you reach for which? [07]
strace intercepts system calls with ptrace, showing every syscall, its arguments, result and
(with -T) time spent: the tool for "what is it doing with files, sockets and processes" and
"why does it fail with EACCES or hang in futex". It slows the target heavily. perf samples
the CPU (stack traces at a frequency) with low overhead and shows where CPU time goes, per
function, as a report or flame graph. Hang or error: strace first. Slow while busy: perf. Slow while
idle (waiting): strace -c, off-CPU analysis, or gdb stacks.
21. A process is at 100 % CPU in production. What do you do? [07]
Find the thread: top -H -p PID (or ps -L). Sample it: perf top -p PID or perf record -g -p
PID -- sleep 30 then perf report, which names the hot functions. If it is a spin or an infinite
loop, gdb -p PID (or a batch stack dump) on that thread shows where. Capture evidence before
restarting, compare with a known-good build, and reproduce under the same load with a profiler
attached.
22. What does set -euo pipefail do in a bash script? [10]
-e exits on the first failing command, -u makes expanding an unset variable an error, and
-o pipefail makes a pipeline fail if any stage fails instead of only the last. Together they turn
silent failures into immediate exits, for example ctest | tee log passing after failed tests, or
rm -r "$DIR/" running with an empty variable. It has holes (commands in if, ||, &&, local
x=$(cmd)), so critical steps still need explicit checks, and a trap cleanup EXIT handles cleanup.
Part 5. Linux systems programming¶
23. What is the difference between a system call and a library call? [11]
A system call switches the CPU to kernel mode to request a service only the kernel can provide
(I/O, memory mapping, process creation); a library call runs in user space and may make zero or
several syscalls. printf buffers and calls write occasionally; malloc serves most requests
from its arenas and calls brk or mmap only to grow. Failures are reported through a return
value plus errno, which must be read immediately after the failing call.
24. What happens when you send SIGTERM to a process, and how should a server handle it? [11, 08]
The kernel marks the signal pending; on the next return to user mode it runs the installed handler
or applies the default action (terminate). A server should install a handler with sigaction that
only sets a volatile sig_atomic_t flag (or writes to a self-pipe, or use signalfd), let the
main loop notice it, stop accepting, drain in-flight requests within a bounded time and exit 0.
systemd and Docker send SIGTERM and follow with SIGKILL after a timeout (TimeoutStopSec, 10 s
for docker stop), so the drain must finish within that window.
25. Why can you not call printf or malloc in a signal handler? [11]
The handler can interrupt the program in the middle of any function, including malloc or
printf while they hold internal locks or have data structures half-updated. Calling them again
from the handler can deadlock on the same lock or corrupt the heap. Only async-signal-safe
functions listed in signal-safety(7) (such as write, _exit, sigaction) are allowed, and the
handler must save and restore errno.
26. Explain fork and exec and the danger of fork in a multithreaded program. [11]
fork creates a child that is a copy of the parent with copy-on-write memory and duplicated file
descriptors; exec replaces the process image with a new program. The split lets the child set up
redirections and privileges before exec. In a multithreaded parent, only the forking thread
exists in the child, and locks held by other threads at that moment stay locked forever, so the
child may deadlock in malloc or a logger; between fork and exec it must call only
async-signal-safe functions, or the program should use posix_spawn.
27. Does write() guarantee the data is on disk? How do you make a write durable? [11]
No. write returns once the data is in the kernel's page cache; writeback happens later. Durability
requires fsync (or fdatasync) on the file, plus fsync on the directory after creating or
renaming a file. For atomic replacement: write a temp file, fsync it, rename over the target,
fsync the directory. Treat an fsync failure as fatal for that data: after a writeback error the
kernel may have discarded the dirty pages, so retrying can report success falsely.
28. Why is epoll more scalable than select/poll for many connections? [11]
select and poll pass the full list of fds on every call and the kernel scans all of them, so
each wait costs O(n). epoll registers interest once in a kernel object; readiness callbacks put fds
on a ready list, and epoll_wait returns only those, so the cost is proportional to activity. It
needs non-blocking fds; level-triggered mode re-reports while data remains, edge-triggered mode
reports transitions and requires reading until EAGAIN.
29. A service keeps disappearing with exit code 137 and nothing in its logs. What is happening? [08, 11]
137 is 128 + 9: SIGKILL, which cannot be caught, so the program never logs. The usual sender is the
OOM killer, system-wide or because the service's cgroup hit its memory limit (MemoryMax=, Docker
--memory), or a stop timeout escalating from SIGTERM. Confirm with journalctl -k | grep -i oom,
docker inspect (OOMKilled: true) or systemctl status; then find the memory growth with a heap
profiler instead of raising the limit.
30. How do you run a C++ server as a production service on Linux with systemd? [11]
Write a unit with ExecStart running the binary in the foreground (Type=exec or notify), a
dedicated unprivileged User=, Restart=on-failure with RestartSec and a start-limit burst,
TimeoutStopSec matched to the graceful drain, LimitNOFILE sized for connections, MemoryMax
for a cgroup cap, and sandboxing (NoNewPrivileges, ProtectSystem=strict, PrivateTmp). Logs go
to stdout/stderr and are read with journalctl -u. systemctl daemon-reload after edits,
enable --now to start it and at boot.
Part 6. Docker and CI/CD¶
31. What is a Docker image layer, and how does layer caching affect your Dockerfile? [08]
Each filesystem-changing instruction (RUN, COPY, ADD) produces a read-only layer, a tarball
of the changes, stacked with an overlay filesystem; the container adds one writable layer on top.
The builder reuses a cached layer as long as the instruction and its inputs (for COPY, the file
checksums) are unchanged, and rebuilds every layer after the first change. So order instructions
from least to most frequently changing: system packages, then build files and dependency fetches,
then sources. Files deleted in a later layer still occupy space in the earlier one.
32. What is a multi-stage build and why use it for C++? [08]
A Dockerfile with several FROM stages, where later stages copy only selected artefacts from
earlier ones with COPY --from=. For C++, the build stage has the compiler, CMake and headers and
runs the tests; the runtime stage starts from a small base, installs only runtime libraries and
copies the stripped binary. The result is an image without compilers or sources: smaller, faster
to pull, smaller attack surface, and the tests ran in the exact environment of the shipped binary.
33. Why does PID 1 matter inside a container? [08, 11]
The process with PID 1 in a namespace gets no default signal actions from the kernel: a SIGTERM
without an installed handler is ignored, so docker stop waits 10 seconds and sends SIGKILL. PID 1
also inherits orphaned processes and must reap them, or zombies accumulate. Either the server
installs SIGTERM handling and never forks, or a tiny init such as tini (or docker run --init)
runs as PID 1, forwards signals and reaps children.
34. How would you design a CI pipeline for a C++ service? [09]
Trigger on every PR and on main and tags; cancel superseded runs. A matrix of compilers (GCC,
Clang) and build types with -Werror, running the full test suite with JUnit output; separate
sanitizer jobs (ASan+UBSan, TSan) using the same CMake presets developers use; formatting and
clang-tidy checks; ccache and dependency caches keyed by content hash. Then build the Docker image,
smoke-test that exact image, and push it tagged with the commit SHA only from main or tags.
Archive debug symbols per build, require all checks via branch protection, keep PR feedback under
about ten minutes and move slow suites (Valgrind, long fuzzing) to nightly jobs.
35. How do you handle secrets in CI and in containers? [08, 09]
Store them in the CI system's encrypted secrets (repository, organisation, or a vault such as Key
Vault), expose them only to the steps that need them, and rely on them being masked and unavailable
to fork PRs. Prefer short-lived credentials: the per-run GITHUB_TOKEN, OIDC federation to cloud
providers. Never put secrets in the repository, in Dockerfile ARG/ENV (they persist in image
history) or in compose files; inject them at run time through environment files, Docker secrets or
the orchestrator.
Part 7. Senior-level and cross-platform¶
36. Compare the Windows and Linux toolchains for C++ in two minutes. [12]
MSVC cl/link with MSBuild versus GCC or Clang with Make or Ninja, both drivable by CMake.
/W4 /WX maps to -Wall -Wextra -Werror; /O2 /Zi to -O2 -g. Windows has PE images, .lib and
.dll with import libraries and nothing exported by default, separate PDBs matched by GUID, and a
per-module CRT choice (/MD vs /MT); Linux has ELF, .a and .so with everything exported by
default, DWARF matched by build-id, and one libc per process. Debugging is WinDbg or Visual Studio
on dumps versus GDB on cores; tracing is ETW/Process Monitor versus perf/strace; services are SCM
versus systemd; Windows commits memory up front while Linux overcommits and has an OOM killer.
37. A release build crashes in production but the debug build never does. How do you approach it? [03, 05, 07]
Optimisation exposes undefined behaviour the debug build hides (uninitialised variables, signed
overflow, out-of-bounds, data races, strict-aliasing violations) or changes timing. Get the core
dump and the matching unstripped binary, read the stack and registers in GDB; rebuild the release
configuration with -g and run the tests and a load test under ASan+UBSan and TSan; compile with
more warnings (-Wall -Wextra -Wconversion) and with both GCC and Clang. If it reproduces only in
one place, git bisect with the release preset narrows it to a commit.
38. Where do the "it works on my machine" failures come from, and how does your setup prevent them? [04, 08, 09]
From differences in compiler version and flags, library versions, environment variables, file
system behaviour (case, line endings), and undeclared dependencies installed on the developer
machine. The prevention is to define everything in the repository: CMake presets for flags,
pinned dependencies (FetchContent tags, vcpkg baselines), .gitattributes for line endings, a
Docker build stage that is the reference environment, and CI that builds from a clean checkout on
every commit and tests the exact image that ships.