CSS Prepare

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

StageTypical resultKey job
PreprocessExpanded sourceIncludes, macros, conditional compilation
CompileAssembly or other target representationParse, check, optimise and generate code
AssembleObject fileEncode instructions plus symbols/relocations
LinkExecutable or libraryCombine objects and resolve/bind dependencies
Load/runProcessMap 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

  1. Source code (.c / .cpp) — human-written code
  2. Preprocessing — preprocessor directives resolved (#include, #define, macro expansion) → produces expanded source
  3. Compilation — compiler translates preprocessed source into assembly code
  4. Assembly — assembler converts assembly code into object code (machine code, .obj/.o), but not yet linked
  5. Linking — linker combines object code with required libraries (standard library, other object files) and resolves external references → produces the final executable (.exe)
  6. 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;
}
  1. Preprocessing: #include <stdio.h> is replaced with the contents of the stdio header; PI is textually replaced with 3.14 everywhere it appears
  2. Compilation: the expanded source is translated into assembly instructions for printf("%f", 3.14); return 0;
  3. Assembly: the assembler turns those assembly instructions into object code (hello.o) — machine code, but printf is only referenced, not yet resolved
  4. Linking: the linker pulls in the actual printf implementation from the C standard library and resolves the reference, producing hello.exe
  5. 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/-c flags 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/.cpp file into a running program passes through six stages, each with its own tool and its own intermediate file:
  1. Source code (.c / .cpp) — the human-written program
  2. Preprocessing — the preprocessor resolves #include, expands #define macros and applies conditional compilation → expanded source (.i)
  3. Compilation — the compiler checks syntax and semantics and translates the expanded source → assembly code (.s)
  4. Assembly — the assembler converts mnemonics into binary → object code (.o / .obj), still holding unresolved references
  5. Linking: combine objects, resolve static symbols and arrange dynamic dependencies → executable/library.
  6. 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.
AspectObject Code (.o / .obj)Executable (.exe / a.out)
Produced byAssemblerLinker
Scope coveredOne source file onlyAll files plus libraries
External referencesUnresolvedAll resolved
AddressesRelative / relocatableFinal, ready for the loader
Can it be run?NoYes
Entry pointAbsentPresent (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.
AspectStatic LinkingDynamic Linking
When binding occursAt link (build) timeAt load time or run time
Library code locationCopied into the executableStays in a separate .dll / .so file
Executable sizeLargeSmall
Memory when many programs runEach has its own copyOne shared copy for all
Library updateRequires relinking the programReplace the .so/.dll only
Dependency at run timeNone; fully self-containedLibrary must be present
Startup speedFasterSlightly 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 PI expand to 3.14 after #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 macroconst variable
Handled byPreprocessor (before compiling)Compiler
Type checkingNoneFull
Memory usedNone; text is substitutedOccupies storage
Respects scopeNo, file-wide from that pointYes

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.o must be linked with main.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)bnext = a+ba ≤ 20?
011yes
112yes
123yes
235yes
358yes
5813yes
81321yes
132134yes
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 condition a <= 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 n Fibonacci numbers" (a count, not a ceiling), the loop condition changes to a for loop running exactly n times: 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)

MethodTimeSpace
Naive recursionO(2ⁿ)O(n) stack
Memoized recursionO(n)O(n)
Iterative (2-variable)O(n)O(1)
Matrix exponentiationO(log n)O(1)
Fast doublingO(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 #include and #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 (.obj or .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.