Assembly (x86-64, AArch64, RISC-V) Module 11
Reading compiler output interview questions
Interview questions on reading compiler output: short warm-ups, then what each harder question tests, the first step of a good answer and its follow-ups.
Warm-up questions
Short questions with short answers, each answered in full.
-
Fresher to mid
Why do engineers usually read -O2 output rather than -O0 output when they check what a compiler did with their code?
Answered in the lesson Reading compiler output: -O0 versus -O2
-
Fresher to mid
Does adding -g to a release build change the machine code or slow the program down?
Answered in the lesson Debug info: symbols and source mapping
-
Fresher to mid
Why does a C++ compiler mangle names at all?
Answered in the lesson C++ and Rust in assembly: names, vtables, checks
-
Fresher to mid
What does -O2 turn on that -O0 leaves off?
Answered in the lesson Optimisation and undefined behaviour in assembly
-
Fresher to mid
Why does a compiler write xor eax, eax rather than mov eax, 0?
Questions with model answers
What each question tests, the first step of a good answer and the follow-ups to expect.
-
Mid to senior
A code review states that a summing loop like sum() makes exactly one memory access per element in the release build, on x86-64 and on AArch64. How do you check the claim from compiler output, and what in the listings confirms or refutes it?
- What it tests
- Producing listings with the build's own flags, filtering them, finding the loop by its branch back, and counting memory operands on two ISAs, including x86-64 arithmetic that reads memory directly.
- First step
- Compile the file for each target with the release flags plus -S, filter out directives and comments but keep the labels, find the block that the backward branch returns to, and list every instruction in it that reads or writes memory.
- Follow-ups
-
- Which x86-64 instruction in that loop reads memory without being a mov or a load?
- What does the same count give for an -O0 build, and why?
- How does vectorisation change what you count?
- Why must the flags match the release build exactly?
From the lesson Reading compiler output: -O0 versus -O2
Model answer, Pro. Model answers open with any pass; prices are in the notice on this page.Opening the model answers with your pass…
Model answers open with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so the model answers can’t be opened at the moment. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to staff
One C function compiles to 10 instructions on one x86-64 Linux machine and to 17 on another, with the same GCC version at -O2. The extra ones are endbr64, two reads of fs:[40], a store and a compare of a copy, a jne and a call of __stack_chk_fail. Explain them and how you would find the cause.
- What it tests
- Knowing what -fcf-protection and -fstack-protector-strong emit, why endbr64 is safe on older processors, where the canary and its copy live, and how to find a compiler's default options instead of guessing.
- First step
- Name the two features from their instructions, then ask each compiler which of them it turns on by default with gcc -Q --help=common, and compare that with the flags each build passes.
- Follow-ups
-
- Why is the frame of the longer version larger?
- Does the longer version still run on a processor without CET?
- Which functions does -fstack-protector-strong protect, and which does it leave alone?
- Which default should a team rely on?
From the lesson Reading compiler output: -O0 versus -O2
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Fresher to senior
Stepping through an -O2 -g build of a summing loop, the debugger's current line goes 3, 4, 3, 4, 3, some steps land on no line at all, and printing the loop counter shows "optimized out". A teammate says the debug information is broken. What is happening, and how do you show it?
- What it tests
- Reading the line table of optimised code: rows interleaved by scheduling, line 0 rows, is_stmt flags, and variables that live in a register for part of a function or not at all; choosing a build for stepping.
- First step
- Dump the line table with llvm-dwarfdump --debug-line and read it in address order beside llvm-objdump -S: if the rows alternate between lines 3 and 4 exactly where the loop's instructions alternate, the table is right and the debugger is reporting optimised code faithfully.
- Follow-ups
-
- What does a row with line 0 mean, and why does the debugger not stop on it?
- What does the is_stmt flag tell a debugger?
- Why can the loop counter not be printed, and what shows where a variable lives?
- Which build would you use to step through this function, and which to debug a crash in the shipped code?
From the lesson Debug info: symbols and source mapping
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A crash in a service gives a backtrace that is correct up to a hand-written assembly routine and then stops or shows garbage frames above it, even though the binary was built with -g. What is the likely cause and how do you confirm and fix it?
- What it tests
- Knowing that unwinding needs call-frame information per function, that -g on C does not supply it for hand-written assembly, how to read an FDE, and how to add correct .cfi directives.
- First step
- Check whether the assembly routine on the boundary has CFI: dump the binary's unwind section and look for an FDE covering its address range; a routine with none is where an unwinder loses the stack.
- Follow-ups
-
- Why does -g on the C files not fix the assembly routine?
- Which .cfi directives does a routine that saves the frame pointer and link register need?
- How do you verify the fix without waiting for another crash?
- What symptoms other than a truncated backtrace does missing CFI cause?
From the lesson Debug info: symbols and source mapping
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to staff
You own a native binary shipped to many machines and must be able to turn a crash report into a function and line. How do you build, ship and symbolise it so the shipped file stays small and crashes are still readable?
- What it tests
- Splitting debug info, archiving it by build id, stripping the shipped binary, accounting for the load slide of a PIE, and matching the right debug file to a build.
- First step
- Build with -O2 -g, split the debug info into a dSYM or .debug file, ship the stripped binary, and archive the debug file under the build's UUID so a later crash can be symbolised against it.
- Follow-ups
-
- Why keep the debug file per build rather than one for all?
- What must the crash reporter do with a PIE's addresses before symbolising?
- What breaks if the archived debug file does not match the shipped build?
- When is shipping fully stripped with no archive acceptable?
From the lesson Debug info: symbols and source mapping
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A stripped x86-64 Linux build of a C++ service crashes in a function whose listing is mov rax, qword ptr [rdi] then call qword ptr [rax + 0x28]. There are no symbol names. How do you work out which virtual method that call reaches, and for which class?
- What it tests
- Reading a virtual call's slot offset back to a method, knowing that the object's vtable pointer decides the class at run time, and recovering a class name from a binary without symbols.
- First step
- Separate the two questions. The slot offset 0x28 is fixed at compile time and names one method of the pointer's static type; the class comes from the object at run time. So read the offset against the hierarchy's declarations, then take the object's vtable pointer from the core dump.
- Follow-ups
-
- Which method sits at 0x28 when the base class declares its virtual destructor first?
- How can the vtable lead you to the class name when the symbols are stripped?
- What would make that route fail, and what does a failure tell you?
- Why might the same offset name a different method in a 64-bit Windows build?
From the lesson C++ and Rust in assembly: names, vtables, checks
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A profile of a Rust image filter puts most of its time in a loop that computes out[i] = a[i] + b[i] for i in 0..width over three slices. A teammate proposes get_unchecked everywhere to remove the bounds checks. What do you check first, and what would you try before unsafe code?
- What it tests
- Reading where a release build keeps bounds checks in a loop and whether the hot part is vectorised, and knowing the safe rewrites, reslicing to the bound or zipping iterators, that let the compiler drop the checks.
- First step
- Compile the function with the release profile's flags to assembly and find the loop by its backward branch. Note whether the body uses vector registers, and count the compares against slice lengths inside it. Only compares that run on every iteration of the hot path are worth removing.
- Follow-ups
-
- Why can three slices of possibly different lengths keep compares in the loop?
- What does reslicing each slice to width before the loop give the compiler?
- How does zipping the three slices change the listing and the behaviour?
- When is get_unchecked justified, and what must come with it?
From the lesson C++ and Rust in assembly: names, vtables, checks
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Senior to staff
A C++ simulation calls body->step() through a base-class pointer for millions of objects per frame, and the profile shows the indirect call among the top costs. Which ways can make that call direct or cheaper, what does each need, and how do you confirm the result in the listing?
- What it tests
- Knowing which facts let a compiler prove the target of a virtual call, what each costs or requires, why an indirect call is expensive in a hot loop, and confirming from the listing that the lookup is gone.
- First step
- Read the call in the listing of the real build: a load from the object, a load from the vtable and an indirect call mean the compiler could not prove the target. Then list what it would need to know about the object's class, and which of those facts the code or the build can supply.
- Follow-ups
-
- Which language features let the compiler prove the target, and what do they cost in flexibility?
- What does link-time optimisation add, and what must hold for it to help?
- What does a speculative direct call look like in a listing, and when does it pay?
- When does changing the data layout beat every compiler-side fix?
From the lesson C++ and Rust in assembly: names, vtables, checks
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A C function rejects bad input with if (a + b < a) return -1; for int a and a non-negative int b. Its unit test of an overflowing pair passes in the debug build and fails in the -O2 release build. Explain what happened, how you would show it, and how you would fix it for every build.
- What it tests
- Knowing that signed overflow is undefined, reading the deleted comparison in a listing, using a sanitizer to show the overflow, and replacing the test with one that cannot overflow or with checked arithmetic.
- First step
- Compile the function with the release flags to assembly and look for the comparison: if the listing tests only the sign of b, the compiler removed the overflow case because it may assume signed addition never overflows.
- Follow-ups
-
- Why is the -O0 result not the correct one either?
- How would a sanitizer show the problem in CI?
- What are the trade-offs of building with -fwrapv instead?
- Which rewrite is portable C, and what does it compile to?
From the lesson Optimisation and undefined behaviour in assembly
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A library's README says its hot summing loop is vectorised at -O2. Your product builds it with Clang on one platform and GCC on another. How do you confirm the claim for both builds, and what do you check if one of them is not vectorised?
- What it tests
- Reading vector registers and leftover loops in a listing, asking each compiler for its vectoriser remarks, knowing that cost models and levels differ between compilers, and the usual blockers such as aliasing.
- First step
- Build each platform's object with its real flags and compiler, ask each compiler to report what its vectoriser did with the loop, and read the loop in each listing for vector registers.
- Follow-ups
-
- What does a vectorised loop look like apart from its main body?
- Why might GCC at -O2 keep a loop scalar that Clang vectorises?
- What in the source most often blocks vectorisation, and how do you tell from the report?
- How do you keep the claim true after later changes?
From the lesson Optimisation and undefined behaviour in assembly
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Senior to staff
A teammate rewrote a summing loop with NEON intrinsics and expects it to beat the compiler's code. How do you decide whether the rewrite helps, and why could it be slower than what the compiler produced from plain C?
- What it tests
- Comparing the listings of both versions, counting the work each loop iteration does, measuring the two fairly, and weighing what intrinsics cost to keep and to port.
- First step
- Before timing anything, compile the plain loop with the release flags and read its listing: if the compiler already vectorised it, compare the two loop bodies instruction by instruction, counting elements per iteration and accumulators.
- Follow-ups
-
- What in the two loop bodies would you count, and why?
- How would you measure the two versions so the result means something?
- When are intrinsics the right call?
- What does the rewrite cost when the code must also build for x86-64?
From the lesson Optimisation and undefined behaviour in assembly
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A stripped x86-64 binary has a function of one unsigned int: mov ecx, edi; mov eax, 2863311531; imul rax, rcx; shr rax, 33; ret. It contains no divide. What does it compute, how would you show that your reading holds for every input, and what would change for a signed int?
- What it tests
- Recognising reciprocal multiplication, recovering the divisor from the constant and the shift, proving the result exact for all inputs rather than sampling, and the correction signed division needs for negative numbers.
- First step
- Treat it as a reciprocal multiplication: the shift gives the power of two, and dividing that power by the constant gives the divisor. Then prove that the rounding of the constant is too small to change any 32-bit result, rather than testing a handful of values.
- Follow-ups
-
- How do you show it is exact for all 2^32 inputs without running them all?
- Why does a divisor such as 7 need a different sequence?
- What does the signed version of the same division look like?
- Why does the compiler avoid the divide instruction, and when does it keep it?
From the lesson Compiler idioms you will meet
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A teammate wants every int index in a hot C loop changed to long, because int indexes cost a sign extension on 64-bit machines. Another says the compiler removes that cost anyway. Using the code compilers emit for x86-64, AArch64 and RISC-V, when is each of them right, and how would you settle it for one loop?
- What it tests
- Knowing why a 32-bit signed index needs widening before it addresses memory, which ISAs fold or avoid the extension, when a compiler can remove it from a loop, and checking the listing rather than guessing.
- First step
- Compile the loop with the real flags for each target and look for the extension: movsxd on x86-64, sxtw on AArch64, and on RISC-V the W forms such as addw that extend as they compute. Then check whether each one sits inside the loop body or runs once before it.
- Follow-ups
-
- Why must a 32-bit signed index be extended before it can address memory?
- Why can a compiler often move the extension out of a counted loop?
- Would unsigned indexes be cheaper, and on which ISA?
- What else changes when you switch the type of an index?
From the lesson Compiler idioms you will meet
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Senior to staff
The boot firmware of a 64-bit RISC-V board must fit in 128 KB of on-chip memory and has grown to 150 KB. It is built with -O2 -march=rv64g. What would you try, in what order, and how would you measure what each step saves?
- What it tests
- Knowing what the compressed extension saves and that the core must implement it, the size levels -Os and -Oz and what they cost in speed, removing unused code at link time, and measuring sizes per section and per function.
- First step
- Measure before changing anything: the size of every section with llvm-size, and the largest functions with llvm-nm --size-sort. Then confirm that the board's core implements the C extension, because rebuilding for rv64gc changes no source and the RISC-V manual puts its typical saving at 25 to 30 %.
- Follow-ups
-
- How much does the C extension typically save, and how do you check the core supports it?
- What do -Os and -Oz give up, and where would you see it in a listing?
- How do you find which functions and sections to work on first?
- What would you do if the image is still too large after these steps?
From the lesson Compiler idioms you will meet
Model answer, Pro.In the answer:
- Model answer
- Common mistakes
- Rubric
- Variants
- about 3 minutes
All Assembly (x86-64, AArch64, RISC-V) interview questions · The Assembly (x86-64, AArch64, RISC-V) track