COMP1521 678 words·4 min read

00. Preamble

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, prepares its address space, loads or maps the required code and data into memory, and starts execution at the executable's entry point. Startup code eventually calls main; main is important to C programmers, but it is not usually the first machine instruction executed.

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.

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:

CODE
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. If main.o calls a function defined in math.o, the linker connects the call to that definition. 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.

What You Should Carry Into MIPS#

The important chain is:

CODE
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#