optional · CSS study notes
II.7 Source to Exe File Conversion Process (2016)
⬆️ P-I.II. Programming Fundamentals · Question Bank · Chapter II (2016–2026)
🧠
Remember: P–C–A–L builds; the loader runs.
Start here — what you should be able to do
- Draw and explain each build stage and its output.
- Distinguish object file, executable and running process.
- Compare static and dynamic linking without absolute claims.
Reading order: foundation checks → full notes and examples → exam traps → self-test without notes → past-paper answers. All sections below remain part of the topic; code examples are kept in full.
Foundation checks
Pipeline at a glance
| Stage | Typical result | Key job |
|---|---|---|
| Preprocess | Expanded source | Includes, macros, conditional compilation |
| Compile | Assembly or other target representation | Parse, check, optimise and generate code |
| Assemble | Object file | Encode instructions plus symbols/relocations |
| Link | Executable or library | Combine objects and resolve/bind dependencies |
| Load/run | Process | Map program/dependencies and transfer control |
GCC checkpoints: -E stops after preprocessing; -S after compilation; -c after assembly. Actual toolchains can combine stages.
Source: GCC build-stage documentation.
1. The Full Pipeline: Source Code → Executable
- Source code (.c / .cpp) — human-written code
- Preprocessing — preprocessor directives resolved (
#include,#define, macro expansion) → produces expanded source - Compilation — compiler translates preprocessed source into assembly code
- Assembly — assembler converts assembly code into object code (machine code,
.obj/.o), but not yet linked - Linking — linker combines object code with required libraries (standard library, other object files) and resolves external references → produces the final executable (.exe)
- Loading — at run time, the OS loader loads the executable into memory and transfers control to start execution
2. Diagram (textual)
Source (.c) → Preprocessor → Expanded Source → Compiler → Assembly (.s) → Assembler → Object Code (.o) → Linker (+ libraries) → Executable (.exe) → Loader → Running Program
3. Key Distinctions
- Object code is machine code for just one file — may reference functions from other files/libraries it doesn't yet have addresses for
- Linker resolves those references, producing one complete, runnable executable
- Static linking embeds library code directly into the executable; dynamic linking links to shared libraries (.dll/.so) at load/run time
4. Worked Example
Given a tiny C program:
#include <stdio.h>
#define PI 3.14
int main() {
printf("%f", PI);
return 0;
}
- Preprocessing:
#include <stdio.h>is replaced with the contents of the stdio header;PIis textually replaced with3.14everywhere it appears - Compilation: the expanded source is translated into assembly instructions for
printf("%f", 3.14); return 0; - Assembly: the assembler turns those assembly instructions into object code (
hello.o) — machine code, butprintfis only referenced, not yet resolved - Linking: the linker pulls in the actual
printfimplementation from the C standard library and resolves the reference, producinghello.exe - Loading: when you run
hello.exe, the OS loader copies it into memory and starts executing it
5. 💻 Code Examples: C & C++
Running every stage of the pipeline by hand, using the compiler's own flags to stop at each intermediate file:
C:
// pi.c
#include <stdio.h>
#define PI 3.14
int main(void) {
printf("%f", PI);
return 0;
}
gcc -E pi.c -o pi.i # 1. Preprocessing -> expanded source (pi.i)
gcc -S pi.i -o pi.s # 2. Compilation -> assembly code (pi.s)
gcc -c pi.s -o pi.o # 3. Assembly -> object code (pi.o)
gcc pi.o -o pi # 4. Linking -> executable (pi)
./pi # 5. Loading + Execution
C++:
// pi.cpp
#include <iostream>
#define PI 3.14
int main() {
std::cout << PI;
return 0;
}
g++ -E pi.cpp -o pi.i # 1. Preprocessing -> #define PI 3.14 is textually expanded
g++ -S pi.i -o pi.s # 2. Compilation -> assembly code
g++ -c pi.s -o pi.o # 3. Assembly -> object code (references cout, unresolved)
g++ pi.o -o pi # 4. Linking -> pulls in the iostream library, resolves cout
./pi # 5. Loading + Execution
- Running
gcc/g++with no flags does all five stages silently in one command — the-E/-S/-cflags exist specifically to inspect each intermediate file (.i,.s,.o) individually
6. MCQ Traps
- The preprocessor runs before the compiler, not after
- Object code ≠ executable — object code still needs linking
- Static linking produces a larger but self-contained executable; dynamic linking produces a smaller executable that depends on shared libraries at runtime
7. ✅ Self-Test Questions (with Answers)
Q1. List, in order, the stages from source code to a running executable.
Answer — key points
- The conversion of a
.c/.cppfile into a running program passes through six stages, each with its own tool and its own intermediate file:
- Source code (.c / .cpp) — the human-written program
- Preprocessing — the preprocessor resolves
#include, expands#definemacros and applies conditional compilation → expanded source (.i) - Compilation — the compiler checks syntax and semantics and translates the expanded source → assembly code (.s)
- Assembly — the assembler converts mnemonics into binary → object code (.o / .obj), still holding unresolved references
- Linking: combine objects, resolve static symbols and arrange dynamic dependencies → executable/library.
- Loading — the OS loader copies the executable into memory, sets up the stack and heap, and transfers control → running program
Pipeline: Source (.c) → Preprocessor → Expanded Source (.i) → Compiler → Assembly (.s) → Assembler → Object Code (.o) → Linker (+ libraries) → Executable (.exe) → Loader → Running Program
gcc -E pi.c -o pi.i # 2. Preprocessing -> expanded source
gcc -S pi.i -o pi.s # 3. Compilation -> assembly
gcc -c pi.s -o pi.o # 4. Assembly -> object code
gcc pi.o -o pi # 5. Linking -> executable
./pi # 6. Loading + Execution
# A plain "gcc pi.c -o pi" performs stages 2-5 silently in one command.
#include <cstdio> // stage 2 pastes in the whole header
#define PI 3.14 // stage 2 replaces every PI with 3.14
int main() {
printf("%f", PI); // printf resolved at stage 5, not before
return 0;
}
MCQ trap: the preprocessor runs before the compiler, never after, and linking precedes loading. Note that stages 2–5 happen before the program runs, while stage 6 happens each time it is launched — which is why the executable can be run repeatedly without recompiling.
Q2. What is the difference between object code and an executable?
Answer
- Object file: compiled instructions/data plus symbol and relocation information, usually for a translation unit.
- Why not normally standalone: unresolved references or required executable metadata still need linking.
- Executable: linked output with the entry and format information required by its platform.
- Dynamic dependency: an executable may still require shared libraries at load time/runtime.
- Example:
main.o+helper.o+ required libraries → executable. - Caution: not every address is simply “relative”, and not every external binding must be resolved at static link time.
| Aspect | Object Code (.o / .obj) | Executable (.exe / a.out) |
|---|---|---|
| Produced by | Assembler | Linker |
| Scope covered | One source file only | All files plus libraries |
| External references | Unresolved | All resolved |
| Addresses | Relative / relocatable | Final, ready for the loader |
| Can it be run? | No | Yes |
| Entry point | Absent | Present (startup code to main) |
g++ -c main.cpp # -> main.o (object code: "cout" still undefined)
g++ -c helper.cpp # -> helper.o
g++ main.o helper.o -o app # LINKER -> app (executable)
./app # runs; ./main.o would not
- MCQ trap: object code is not an executable — it still needs linking.
- Remember the pipeline order: source → preprocessor → compiler → assembler → object code → LINKER → executable → loader → running program, and note that a single-file program still passes through the linker, because it must be joined to the standard library and the C run-time startup code.
Q3. Differentiate static linking from dynamic linking.
Answer
- Static linking: incorporate needed library object code into the output at build time.
- Benefit: fewer dependencies on those separate shared libraries.
- Trade-off: larger binaries; library fixes usually require relinking and redistributing.
- Dynamic linking: use shared libraries such as DLL, SO or dylib files.
- Benefit: smaller binaries and potentially shared code pages; compatible libraries can be updated separately.
- Trade-off: deployment/version compatibility matters; binding can occur at load time or runtime.
- Qualification: static linking does not remove OS/platform dependencies, and startup-speed comparisons are workload-dependent.
| Aspect | Static Linking | Dynamic Linking |
|---|---|---|
| When binding occurs | At link (build) time | At load time or run time |
| Library code location | Copied into the executable | Stays in a separate .dll / .so file |
| Executable size | Large | Small |
| Memory when many programs run | Each has its own copy | One shared copy for all |
| Library update | Requires relinking the program | Replace the .so/.dll only |
| Dependency at run time | None; fully self-contained | Library must be present |
| Startup speed | Faster | Slightly slower (symbol resolution) |
| File extension | .a / .lib | .so / .dll |
g++ main.cpp -static -o app_static # ~2 MB, runs anywhere, no deps
g++ main.cpp -o app_dynamic # ~20 KB, needs libstdc++.so at run time
ldd app_dynamic # lists the shared libraries required
- MCQ trap: static → bigger file but no run-time dependency; dynamic → smaller file but the library must be present.
- The infamous "DLL Hell" — a program breaking because a shared library was replaced with an incompatible version — is a dynamic-linking problem that static linking cannot suffer from.
Q4. What does the preprocessor do with a #define macro before compilation even begins?
Answer
- Macro expansion: substitutes preprocessing-token sequences before later translation stages.
- Example: eligible uses of
PIexpand to3.14after#define PI 3.14. - Other jobs: header inclusion and conditional compilation.
- Not ordinary blind search: macro names inside string literals are not expanded; preprocessing has its own token rules.
- Types: the macro itself has no C++ variable type, but the expanded expression is type-checked later.
- Storage: a macro is not a runtime variable; its expansion may still create objects or code that consume memory.
- Pitfall: parenthesise function-like macro arguments and beware of repeated evaluation; prefer suitable constants/functions where possible.
#define PI 3.14
#define SQUARE(x) ((x) * (x))
double area = PI * SQUARE(3);
// After preprocessing the compiler literally sees:
// double area = 3.14 * ((3) * (3)); -> 28.26
gcc -E pi.c -o pi.i # stop after preprocessing; open pi.i and PI is gone
The classic macro trap — why those inner parentheses matter:
#define BAD(x) x * x
#define GOOD(x) ((x) * (x))
cout << BAD(2 + 3); // expands to 2 + 3 * 2 + 3 -> 11 (WRONG)
cout << GOOD(2 + 3); // expands to ((2+3)*(2+3)) -> 25 (correct)
| Aspect | #define macro | const variable |
|---|---|---|
| Handled by | Preprocessor (before compiling) | Compiler |
| Type checking | None | Full |
| Memory used | None; text is substituted | Occupies storage |
| Respects scope | No, file-wide from that point | Yes |
MCQ trap: the preprocessor runs before the compiler, not after, and a #define line takes no semicolon — writing #define PI 3.14; pastes the stray semicolon into every use site. Modern C++ prefers const double PI = 3.14; precisely because it is typed and scoped.
Q5. Why can't the assembler alone produce a runnable executable when a program calls library functions like printf()?
Answer
- Assembly converts a translation unit's generated instructions into object-code form.
- External calls can remain symbolic references with relocation information.
- The compiler used header declarations for type checking; the assembler does not read the C header to discover a function signature.
- Linking resolves or arranges bindings to external definitions.
- Dynamic linking can defer some binding to loading or runtime.
- Remember: an object file is input to linking, not an executable to run directly.
gcc -c pi.c -o pi.o # Assembler stage: succeeds, BUT printf is unresolved
nm pi.o # Shows: U printf <- "U" = Undefined symbol
gcc pi.o -o pi # LINKER: binds printf from libc -> runnable executable
./pi # Works only after linking
#include <cstdio>
int main() {
printf("%d", 42); // declaration gives the signature only;
return 0; // the BODY of printf is in libc, not in this file
}
// gcc -c -> compiles fine
// running pi.o directly -> impossible; it is not an executable
- The declaration supplies what to call and how (types, argument order); the library supplies what it actually does
- The same applies to your own multi-file projects:
helper.omust be linked withmain.o, or you get "undefined reference to ..."
MCQ trap: object code is not an executable. .o/.obj files contain machine code but retain unresolved references and relative addresses; the missing step is linking, and an "undefined reference" message is a linker error, not a compiler or assembler error.
8. 📝 Exam Practice: Fibonacci Series (CSS-2020)
Q: Write a C/C++ program which inputs a number from a user and prints the Fibonacci series up to that number.
Solution:
#include <iostream>
using namespace std;
int main()
{
int n;
cout << "Enter a number: ";
cin >> n;
int a = 0, b = 1, next;
cout << "Fibonacci series up to " << n << ": ";
while (a <= n)
{
cout << a << " ";
next = a + b;
a = b;
b = next;
}
cout << endl;
return 0;
}
Sample run:
Enter a number: 20
Fibonacci series up to 20: 0 1 1 2 3 5 8 13
Trace, n=20:
| a (printed) | b | next = a+b | a ≤ 20? |
|---|---|---|---|
| 0 | 1 | 1 | yes |
| 1 | 1 | 2 | yes |
| 1 | 2 | 3 | yes |
| 2 | 3 | 5 | yes |
| 3 | 5 | 8 | yes |
| 5 | 8 | 13 | yes |
| 8 | 13 | 21 | yes |
| 13 | 21 | 34 | yes |
| 21 | — | — | no → loop stops |
Key reasoning points:
- The question says "up to the number," meaning the loop should stop once a Fibonacci term exceeds
n— hence the conditiona <= n, not a fixed count of terms - Only two variables (
a,b) are needed to track state — no array or recursion required, since each term only depends on the previous two - Common exam variant to watch for: if the question instead says "print the first
nFibonacci numbers" (a count, not a ceiling), the loop condition changes to aforloop running exactlyntimes:for (int i = 0; i < n; i++). Read the question carefully — "up to N" and "first N terms" are different problems that examiners deliberately word similarly to test attention to detail. - Recursive alternative (useful if the question specifically asks for recursion):
int fib(int n) { return (n <= 1) ? n : fib(n-1) + fib(n-2); }— correct but exponential time; the iterative version above is preferred for large inputs.
Optimized Fibonacci Algorithms (beyond the basic O(n) solution above)
| Method | Time | Space |
|---|---|---|
| Naive recursion | O(2ⁿ) | O(n) stack |
| Memoized recursion | O(n) | O(n) |
| Iterative (2-variable) | O(n) | O(1) |
| Matrix exponentiation | O(log n) | O(1) |
| Fast doubling | O(log n) | O(log n) stack |
Matrix exponentiation — O(log n):
Based on the identity: [F(n+1) F(n); F(n) F(n-1)] = [1 1; 1 0]^n
void multiply(long long F[2][2], long long M[2][2]) {
long long a = F[0][0]*M[0][0] + F[0][1]*M[1][0];
long long b = F[0][0]*M[0][1] + F[0][1]*M[1][1];
long long c = F[1][0]*M[0][0] + F[1][1]*M[1][0];
long long d = F[1][0]*M[0][1] + F[1][1]*M[1][1];
F[0][0]=a; F[0][1]=b; F[1][0]=c; F[1][1]=d;
}
void matrixPower(long long F[2][2], int n) {
if (n <= 1) return;
long long M[2][2] = {{1,1},{1,0}};
matrixPower(F, n/2);
multiply(F, F);
if (n % 2 != 0) multiply(F, M);
}
int fib(int n) {
if (n == 0) return 0;
long long F[2][2] = {{1,1},{1,0}};
matrixPower(F, n-1);
return F[0][0];
}
Squaring the matrix halves n each recursive call — same principle as fast exponentiation for x^n.
Fast doubling — O(log n), no matrices needed (cleanest exam answer):
// F(2k) = F(k)*[2F(k+1)-F(k)], F(2k+1) = F(k)^2 + F(k+1)^2
pair<long long,long long> fib(int n) {
if (n == 0) return {0, 1};
auto [a, b] = fib(n / 2); // a=F(k), b=F(k+1)
long long c = a * (2*b - a);
long long d = a*a + b*b;
if (n % 2 == 0) return {c, d};
else return {d, c + d};
}
// fib(n).first == F(n)
Extension — Binet's formula: a floating-point approximation is useful for discussion but is not a reliable exact large-n algorithm. Its true cost and precision depend on the arithmetic model; avoid claiming universally exact O(1) computation.
9. 📜 Past-Paper Questions & MCQs (CSS 2016–2026)
2016 · Paper-I · Q7(a) · 5 marks
Recall cue: Show source → preprocessing → compilation → assembly → linking; loading happens after the executable exists.
Q. Draw a diagram of translating a C++ source file into an executable. Give an example of each phase.
Answer — key points
Source (.cpp) → Preprocessor → Compiler → Assembler → Linker → Loader → Running program
- Preprocessor: expands
#includeand#define(the iostream header text is inserted). - Compiler: checks syntax and meaning, then produces assembly code (
x = a + b;becomes load, add and store instructions). - Assembler: turns assembly into machine-code object file (
.objor.o). - Linker: combines object files and libraries (such as the standard library for
cout) into an executable (.exe). - Loader: places the executable in memory and starts it.
Recall check and coverage
- Explain the three learning outcomes at the top without looking.
- Reproduce one example or trace and test a boundary case.
- Answer each self-test, then compare with the key.
- Attempt each recorded past-paper task on this page.
Recorded written references: 2016 · Paper-I · Q7(a) · 5 marks
Evidence limit: this page preserves the question records found in your Notion notes. Disputed labels and questions not checked against original PDFs remain flagged in the Question Bank; this is not a claim of exhaustive original-paper verification.