COMP1521 980 words·5 min read

00. Preamble

What Actually Runs?#

Before writing MIPS, it helps to be precise about what a program actually is. A C source file is a description written for humans and compilers; the CPU never reads hello.c, understands a while loop, or calls printf by name. What eventually runs is a sequence of machine instructions and some associated data, encoded as bytes.

This gives us the rough journey:

An executable sitting on an SSD is therefore still dormant. When we run it, the operating system creates a process: one running instance of that program. It gives the process its own view of memory, called an address space, and makes the executable's code and data available there. Finally, it tells the CPU the address of the first instruction to execute. That starting address is the executable's entry point.

The entry point normally belongs to a small piece of startup code supplied by the runtime. That code prepares the C environment and eventually calls main. This is why main is the first function we normally write, but not necessarily the first machine instruction the CPU executes.

Keep the three layers separate: a source file describes a program, an executable is its stored machine-code form, and a process is one running instance with its own memory and operating-system state. The same executable can therefore produce several processes at once.

For example, opening the same program twice does not create two different executable files. It creates two processes from one executable. Each process can have a different value for the same variable because each has its own running state:

one executable file: editor
        |
        +--> process A: current_file = "notes.md"
        |
        +--> process B: current_file = "report.c"

The instructions may have come from the same file on disk, but the registers and writable memory of the two running instances are separate.

Practice: Quick Checks#

  1. Which of these does the CPU directly execute: C source, assembly text, or encoded machine instructions?

Note

- Answer
Encoded machine instructions. C and assembly are source-level representations which must be translated first.

  1. What is the difference between an executable and a process?

Follow-up: two terminal windows start the same executable. Must a variable in one running copy have the same current value as the corresponding variable in the other copy? Explain.

Note

- Answer
An executable is a dormant file containing code and supporting data. A process is a running instance with an address space, registers, and operating-system state.

Note

- Answer
No. Both processes were created from the same stored program, but each running instance has its own registers and writable memory. Changing a variable in one does not ordinarily change the corresponding variable in the other.

Memory Layouts#

A running program sees memory as an address space: a large range of numbered byte locations. An address is essentially an index into that byte-addressed space. In the simplified model used throughout COMP1521, the important regions are:

Typical process memory layout from low to high addresses: text, initialized data, uninitialized data (BSS), a heap growing upward, a stack growing downward, and command-line arguments and environment variables

  • The text segment contains executable machine instructions.
  • The data segment contains long-lived data such as global variables and string literals.
  • The heap contains dynamically allocated objects obtained through functions such as malloc.
  • The stack stores state associated with active function calls, including saved registers and, when necessary, local values.

The exact layout depends on the executable format, operating system, and architecture. The diagram is a useful mental model rather than a promise that every real process is arranged identically. Mipsy also provides a deliberately simplified memory model; see MIPS Data and Memory.

The heap and stack solve different lifetime problems. Heap objects remain allocated until explicitly released with free or until the process ends. Stack storage follows function calls: a function obtains a stack frame while it is active, then relinquishes that space when it returns. This becomes essential in MIPS Functions.

Note

- Aside: virtual addresses
The addresses used by a normal program are generally virtual addresses, not direct physical RAM locations. The operating system and hardware translate them and enforce access permissions. COMP1521 often abstracts this away so that we can concentrate on instructions, byte addresses, and data layout.

The CPU#

Memory can hold a program, but the CPU performs its instructions. At a deliberately simplified level, the processor repeatedly fetches, decodes, and executes:

C
uint32_t program_counter = START_ADDRESS;

while (1) {
	uint32_t instruction = load_word(memory, program_counter);
	program_counter += 4;
	execute(instruction, &program_counter);
}

Here, load_word is deliberately conceptual: it reads the four instruction bytes beginning at the byte address in program_counter. Writing memory[program_counter] as ordinary C would either read only one byte or scale the address by the array element size, depending on how memory was declared.

The real process is far more sophisticated, but this model exposes two important facts:

  1. Instructions are stored in memory just like other data.
  2. The processor needs state telling it which instruction comes next.

That state is held in the program counter (PC). In MIPS32, instructions are normally four bytes, so sequential execution advances to the next four-byte instruction. Branch and jump instructions can instead replace the next address, which is how assembly implements if statements, loops, and function calls.

Registers#

Most CPU computations operate on registers: a small collection of storage locations inside the processor. Values in memory must generally be loaded into registers before arithmetic is performed, and results must be stored back if memory needs to retain them.

MIPS32 exposes 32 general-purpose registers, each 32 bits wide. A register does not possess a C type. The same 32 bits may be interpreted as a signed integer, unsigned integer, character, bit mask, or address; the instruction using those bits determines what happens to them.

Some register names describe a convention rather than a physical difference. $t0 and $s0 are both 32-bit registers, but the calling convention gives them different responsibilities. Other registers have genuinely special behaviour: $zero always reads as zero, $sp tracks the stack, $ra holds a return address, and the program counter controls instruction fetching.

Instructions#

Machine instructions are deliberately small operations. Common categories include:

  • arithmetic and logic, such as addition, subtraction, multiplication, shifts, and bitwise operations;
  • loads and stores, which transfer values between memory and registers;
  • branches and jumps, which alter control flow;
  • system-call mechanisms, which request a service from the surrounding operating environment.

A single C statement may require several assembly instructions. Conversely, optimisation may remove or rearrange operations, so compilation is not a mechanical line-for-line substitution.

Instruction Set Architectures#

An Instruction Set Architecture (ISA) is the interface between software and a processor implementation. It specifies matters such as:

  • the available instructions and their behaviour;
  • the programmer-visible registers;
  • instruction encodings;
  • data sizes and addressing rules;
  • parts of the exception and privilege model.

x86-64, ARM64, RISC-V, and MIPS are different ISAs. They can express similar computations, but do so with different registers and machine-code encodings. Consequently, an x86-64 CPU cannot directly execute a MIPS binary merely because both programs ultimately contain ones and zeroes. Each processor interprets those bits according to its own ISA.

Instruction set architecture shown as the interface between software above and hardware below

MIPS is therefore not simply another general-purpose language like Python or C. MIPS assembly is a human-readable notation for instructions belonging to the MIPS ISA. COMP1521 uses it because the small, regular instruction set makes otherwise hidden ideas—registers, addresses, branches, loads, stores, and calling conventions—visible.

Practice: Writing / Translation#

  1. In one sentence, explain why the same C file can run on both ARM64 and x86-64 while one x86-64 executable cannot directly run on ARM64.

Note

- Answer
C is portable source which can be compiled separately for each ISA, whereas an executable already contains encodings for one target ISA.

Assembly and Machine Code#

Assembly is readable text:

MIPS
	addi	$t1, $t0, 12

Machine code is the encoded instruction the processor executes. In MIPS32, a normal instruction occupies 32 bits. For addi, fields identify the operation, source register, destination register, and immediate value:

MIPS ADDI instruction encoding showing the opcode, source register, destination register, and immediate fields

An assembler performs this translation. Symbolic names and labels make the program manageable for humans; the resulting bits make it executable by the target processor.

Note

- Aside: pseudo-instructions
Not every convenient line of MIPS is a single hardware instruction. An assembler may accept a pseudo-instruction such as li, la, move, or b and expand it into one or more real instructions. You should still reason about its advertised effect unless a question specifically asks about encoding or expansion. Using MIPS develops this distinction further.

Building a C Program#

Running dcc hello.c -o hello looks like one operation, but the compiler driver coordinates several conceptual stages:

The Preprocessor#

The preprocessor handles directives such as #include, #define, and conditional compilation. Its output is still C, although it may be substantially expanded.

BASH
gcc -E hello.c -o hello.i

The Compiler#

The compiler analyses the preprocessed C program and translates it towards a target ISA. It lowers loops, expressions, function calls, and types into lower-level operations, while also checking and optimising the program.

BASH
gcc -S hello.c -o hello.s

The produced assembly depends on the selected target architecture. The same C source can be compiled for x86-64, ARM64, or MIPS, but the resulting assembly will differ.

The Assembler#

The assembler encodes assembly instructions and records the program's sections and symbols. Its output is normally an object file:

hello.s -> hello.o

An object file may already contain machine code while still referring to functions or data whose final locations are unknown.

The Linker#

The linker combines object files and required libraries, resolves symbol references, performs relocation, and produces an executable. A symbol is a recorded name for code or data, such as the function name calculate or the label of a global object. Relocation means assigning final addresses and adjusting instructions or data which refer to those symbols. If main.o calls a function defined in math.o, the linker can therefore replace the unresolved reference with a route to that function. Library references such as printf are resolved according to the selected linking model.

An object file and executable can both contain machine code, but only the latter has been assembled into the form expected by the program loader.

From the Executable Entry Point to main#

The executable records an entry point: the address where execution begins after the loader has mapped the program. In a normal C program, that address belongs to runtime startup code rather than directly to main.

That startup code bridges the operating-system process model and the C language model. Conceptually, it:

  1. receives the process's initial stack and environment from the loader;
  2. identifies values such as the argument count, argument vector, and environment;
  3. performs any runtime or library initialisation required by the executable;
  4. calls main(argc, argv) using the platform's calling convention;
  5. passes the value returned by main into the normal process-exit path.

The exact implementation is platform-specific, but the distinction is durable: the loader starts the executable's entry point, and the runtime eventually calls the C function main. This is also why returning from main can perform normal library cleanup instead of trying to return directly to the kernel as though the kernel had called main itself.

This startup path should not be confused with compilation. Compilation and linking create the executable before it runs; loading and runtime startup happen each time a new process begins executing that file.

Practice: Tracing#

  1. Put these stages in order: assembler, loader, compiler, CPU execution, linker.

Note

- Answer
Compiler → assembler → linker → loader → CPU execution. The preprocessor would appear before the compiler.

Practice: Challenge#

  1. Why is main usually not the first machine instruction executed in a normal process?

Note

- Answer
The executable begins at an entry point in startup code. That code prepares the language runtime and process state before eventually calling main.

What You Should Carry Into MIPS#

The important chain is:

C constructs
    become simpler operations
        represented by assembly
            encoded as machine instructions
                loaded into memory
                    executed using registers, memory, and the program counter

The rest of these notes pulls that chain apart:

  • Running MIPS explains why an emulator is required and how to use mipsy.
  • Using MIPS introduces registers, instructions, assembly syntax, and syscalls.
  • MIPS Control translates conditionals and loops into branches and labels.
  • MIPS Data and Memory explains addresses, loads, stores, arrays, and structs.
  • MIPS Functions explains jal, $ra, the calling convention, and stack frames.

Sources and Further Reading#