Language Processing System in Compiler Design: Preprocessor to Loader with a Worked Trace

Follow one macro-containing C program through every language processor. The trace shows object-file relocation, a linker patch of +4, and the final value 7.

KnowledgeGate Team

Exam prep & CS education

Updated 18 Sep 20265 min read

A compiler is often shorthand for everything between source and execution. That makes the other processors easy to confuse. Each processor has a distinct input, output, and responsibility in the two-file C example, whose linker patches a +4 displacement before the program returns 7. Compiler Design: Six Phases Traced in One Example develops the compiler's internal analysis and synthesis phases; the surrounding toolchain begins with preprocessing and continues through object-file relocation, linking, loading, and execution.

Language processing system: the complete source-to-execution pipeline

A language processing system comprises cooperating programs that transform source text into an executable image and place it in memory. The canonical teaching flow is:

source program -> preprocessor -> modified source -> compiler -> target assembly -> assembler -> relocatable object code -> linker plus library/object files -> executable -> loader -> running memory image

Real toolchains may combine steps, but the responsibilities remain distinct. For the broader exam-preparation map, use GATE CS Exam Preparation.

Program

Input

Output

Job

Preprocessor

Source text

Expanded source

Expands directives

Compiler

Expanded source

Target assembly or object-level code

Analyses meaning and generates code

Assembler

Assembly

Relocatable object

Encodes instructions and records relocations

Linker

Object files and libraries

Executable

Resolves symbols and patches references

Loader

Executable

Memory image and entry point

Maps and starts the executable

Left-to-right pipeline tracing main.c through preprocessor, compiler, assembler, linker, and loader to a running memory image.

Preprocessor: expand directives before translation

The example has two translation units. main.c contains a macro and an external declaration:

c
#define SCALE 4
extern int add(int, int);
int main(void) {
    return add(SCALE, 3);
}

add.c supplies the definition:

c
int add(int a, int b) {
    return a + b;
}

For the relevant part of main.c, preprocessing produces:

c
extern int add(int, int);
int main(void) {
    return add(4, 3);
}

Substitution changes token text from SCALE to 4. It does not calculate 4 + 3, resolve add, or allocate an address. A preprocessor also expands include directives and selects conditional-compilation branches.

Compiler: analyse meaning and generate target code

The compiler is one pipeline component. Lexical analysis identifies tokens and lexemes, while syntax analysis builds structure through parsing. Semantic analysis checks declarations and types. Intermediate representation, optional optimisation, and target-code generation follow.

In this teaching ISA, every instruction is 4 bytes. Registers are R0 and R1, arguments use R0 then R1, and a returned integer remains in R0. Expanded main becomes:

Code
main:
    MOV R0, 4
    MOV R1, 3
    CALL add
    RET

The other unit becomes add: ADD R0, R0, R1; RET. These toy instructions are not bytes from a named processor.

The compiler type-checks add(4, 3) and generates symbolic CALL add. It cannot assign a final address while the files remain separate translation units.

Assembler: create object code, symbols, and relocation records

The assembler creates a 16-byte .text section for main:

Offset

Instruction

0x0000

MOV R0, 4

0x0004

MOV R1, 3

0x0008

CALL add

0x000C

RET

Its symbols are main: defined at .text+0x0000 and add: undefined external. One relocation says to patch the PC-relative call operand at .text+0x0008 for add, measured from the next instruction.

For add, the 8-byte .text section is 0x0000 ADD R0,R0,R1, then 0x0004 RET. Its symbol table defines add at .text+0x0000; no relocation is needed here.

main.o remains valid with unresolved add. A relocatable object is not an executable. The assembler translated mnemonics and recorded linker work; it loaded nothing into memory.

Linker and loader worked example: patch +4, then run

The linker places the 16-byte main.o section at 0x00400000 through 0x0040000F, then the 8-byte add.o section at 0x00400010 through 0x00400017. Therefore:

  • main = 0x00400000

  • add = 0x00400010

CALL begins at 0x00400008. Its 4-byte size makes the next address 0x00400008 + 0x4 = 0x0040000C. Relocation uses a displacement from that next PC:

target - next PC = 0x00400010 - 0x0040000C = 0x00000004 = +4

The linker patches +4. Answers 0x00400010 and +8 are wrong because this relocation stores neither an absolute address nor a displacement from the start of CALL.

The loader maps the executable at the stated virtual addresses and sets PC = 0x00400000. Execution now proceeds step by step:

  1. MOV R0, 4 makes R0=4.

  2. MOV R1, 3 makes R1=3.

  3. CALL +4 transfers control from the next PC, 0x0040000C, to 0x00400010.

  4. ADD R0, R0, R1 computes 4 + 3, so R0=7.

  5. RET returns through main, whose final returned integer is 7.

Linker address map placing add at 0x00400010 with a +4 call patch, beside an execution trace stepping R0 to 7.

Language processor errors and traps that mix the stages up

An error belongs to the first stage unable to continue.

Example

Responsible stage

Reason

#include "missing.h"

Preprocessor

Requested source text cannot be included

int f(void) { return unknown_name; }

Compiler semantic analysis

Identifier is undeclared

MOV R9, R0 in the defined ISA

Assembler

R9 is not a valid register

CALL add with no object or library defining add

Linker

External symbol remains undefined

Executable segment cannot be mapped

Loader

The executable exists but cannot be placed into the process image

Remember four distinctions. The compiler is one component, not the pipeline. Object code may contain unresolved symbols. The linker creates an executable, not source analysis. The loader maps that executable, not C grammar.

Static linking resolves these references before loading. A dynamic loader may perform runtime symbol work, but the fully linked model keeps the displacement calculation unambiguous.

Language processing system questions: four reasoning patterns

Use four miniature checks:

  1. Reorder loader, assembler, preprocessor, linker, compiler. The answer is preprocessor -> compiler -> assembler -> linker -> loader.

  2. Which program produces a relocation record, and which consumes it? The assembler produces it; the linker consumes it.

  3. What displacement is patched into the worked CALL? It is +4 because 0x00400010 - 0x0040000C = 0x4.

  4. Where does an undefined external add become an error? At the linker, if no supplied object or library defines it.

Contrast question: main.o contains a symbolic call to add; is that an error? No. It carries an undefined symbol and relocation, becoming erroneous only if the linker finds no definition in supplied objects or libraries.

The short version: track the artefact, then choose the program

Track the artefact. Directives mean preprocessor; grammar, types, and code generation mean compiler; mnemonics to relocatable object mean assembler; cross-object symbols and relocations mean linker; executable into memory and entry point mean loader.

Keep SCALE=4, second argument 3, add=0x00400010, relocation +4, and returned value 7 fixed. For the broader Compiler Design sequence, GATE Guidance by Sanchit Sir is the next step. If you needed only this distinction, stop after reproducing the artefact table and +4 calculation unaided.