The Full Journey from Source Code to Execution
A program does not go directly from source code to running on hardware. It passes through several distinct tools, each transforming the program into a different intermediate form before it can finally execute.
- The
Compilertranslates high-level source code into assembly language specific to the target processor. - The
Assemblertranslates that assembly language into anObject File, a binary file containing machine instructions along with extra bookkeeping information not yet fully finalized. - The
Linkercombines multiple separately compiled object files, along with any needed library code, into a single completeExecutable File, resolving references between different files so that a function call made in one file correctly points to that function's actual location in another. - The
Loadertakes the finished executable file, places its instructions and data into memory, and prepares the processor to begin executing it.
Each of these tools solves a distinct problem: the compiler handles language translation, the assembler handles the encoding of individual instructions, the linker handles combining independently developed pieces of a program, and the loader handles the transition from a static file on disk to an actively running process in memory.
Why Separate Compilation Matters
Large software projects are rarely written as a single source file. Splitting a program across many files, compiling each independently, and only combining them at the linking stage allows different parts of a large system to be developed, tested, and recompiled separately — a program does not need to be entirely rebuilt from scratch just because one small file changed.
Putting It All Together: Translating a C Sort Routine
To see these ideas applied concretely, consider a simplified sorting function written in C that sorts an array of integers using a basic exchange-based approach.
void sort(long v[], long n) {
for (long i = 0; i < n; i += 1) {
for (long j = i; j > 0 && v[j-1] > v[j]; j -= 1) {
swap(v, j);
}
}
}Translating this into RISC-V assembly requires combining nearly every concept covered so far in this chapter:
- The outer and inner
forloops becomeConditional Branchinstructions checking loop bounds, combined withUnconditional Jumpsback to the top of each loop. - Array indexing such as
v[j]requires computing a memory address by combining a base address with an offset, then issuing aData Transfer Instructionto load or store the value. - The call to
swapis implemented as aProcedure Call, following the register-saving conventions covered earlier, sincesortmust preserve its own loop-counter registers across the call. - Comparisons such as
v[j-1] > v[j]are built from subtraction and branch instructions, since RISC-V has no single dedicated "greater than" branch for every case.
A simplified fragment showing the innermost comparison and loop-back logic:
Loop:
bge x0, j, Exit
ld t0, -8(v_addr)
ld t1, 0(v_addr)
ble t0, t1, Exit
jal ra, swap
addi j, j, -1
jal x0, Loop
Exit:This example demonstrates that even a short, ordinary piece of application code compiles down to a substantial sequence of simple instructions, each doing exactly one small job.
Why Walking Through a Full Example Matters
Studying individual instruction types in isolation is useful, but a realistic program combines loops, array access, function calls, and comparisons all at once. Seeing how a familiar algorithm like sorting maps onto RISC-V instructions makes clear how the small set of primitives covered throughout this chapter compose into everything real software needs.