04. MIPS Data and Memory
MIPS Data and Memory#
Leaving the Registers#
Registers are excellent for small, temporary values, but they cannot hold an array, a string, or an entire struct. Those objects live in memory. MIPS therefore separates two jobs:
- arithmetic and comparisons operate primarily on registers;
- load and store instructions move values between memory and registers.
flowchart TD
M["Memory at an address"] -->|lb / lh / lw| R["Register"]
R --> C["Compute"]
C --> R
R -->|sb / sh / sw| M
Loading does not remove a value from memory; it copies its bits into a register. Storing copies the low part of a register into memory.
Memory Is Byte-Addressed#
MIPS memory can be imagined as a one-dimensional array of bytes. Every byte has a unique address. A multi-byte value occupies several consecutive addresses.
| C-like value | Typical size in the course MIPS environment |
|---|---|
char / byte |
1 byte |
short / half-word |
2 bytes |
int / word |
4 bytes |
| pointer | 4 bytes |
An address is itself a 32-bit value, so a pointer fits in a general-purpose register. The object pointed to normally remains in memory.
Note
- Mipsy's simplified memory layout
Mipsy places user code in a text region, global data in a data region, and function-call storage in a stack which grows towards lower addresses. Its model is intentionally friendlier than a full operating system. The official MIPS guide documents the current segment addresses and permissions.
Global and Local Values#
A global object is placed in the .data section:
.data
counter:
.word 0 # int counter = 0;Small local values are often kept in registers while possible. Values whose address is needed, large objects, and values which cannot remain in registers may instead live in memory. MIPS Functions explains stack storage and register preservation.
Defining Data#
Assembler directives reserve and optionally initialise bytes:
| Directive | Effect |
|---|---|
.byte 7 |
emit one byte |
.half 7 |
emit a two-byte half-word |
.word 42 |
emit a four-byte word |
.space 16 |
reserve 16 uninitialised bytes |
.ascii "abc" |
emit three character bytes |
.asciiz "abc" |
emit the characters and a terminating zero byte |
.align 2 |
align the next object to a multiple of 4 bytes in mipsy |
Directives do not execute at runtime. They tell the assembler what bytes should exist in a section.
.data
letter:
.byte 'Q'
answer:
.word 42
numbers:
.word 1, 3, 5, 7, 9
message:
.asciiz "hello"
buffer:
.space 64Load and Store Instructions#
| Size | Load | Store |
|---|---|---|
| 1 byte | lb |
sb |
| 2 bytes | lh |
sh |
| 4 bytes | lw |
sw |
The width should match the object. Using lw for a character would read the character and three neighbouring bytes as one word, assuming the address is aligned at all.
Signed and Unsigned Loads#
lb and lh sign-extend their result to fill the 32-bit register. If the highest bit of the loaded byte or half-word is 1, the upper register bits become 1, preserving a negative two's-complement value. Their unsigned counterparts lbu and lhu instead fill the upper bits with zeroes.
lb $t0, 0($t2) # load a signed int8_t-like value
lbu $t1, 0($t2) # load a uint8_t-like valueStores do not need signed and unsigned forms: sb simply writes the low eight bits, while sh writes the low sixteen. Signedness matters when the bits are interpreted after loading.
Loading and Updating a Word#
int counter = 0;
counter++; la $t0, counter # $t0 = &counter
lw $t1, 0($t0) # $t1 = counter
addi $t1, $t1, 1 # $t1++
sw $t1, 0($t0) # counter = $t1
.data
counter:
.word 0The CPU cannot increment the memory object directly with addi. It loads, computes, then stores.
Mipsy also accepts convenient label forms:
lw $t1, counter
addi $t1, $t1, 1
sw $t1, counterThe explicit address-in-a-register form is usually better preparation for arrays, pointers, and structs.
Effective Addresses#
The common memory operand has the form:
offset(base_register)
The effective address is:
value in base_register + constant byte offset
lw $t0, 8($t1) # load a word from address $t1 + 8
sb $t2, 3($t3) # store one byte at address $t3 + 3The offset is measured in bytes, even for lw. lw $t0, 4($t1) accesses the next word after the word at 0($t1).
Addresses, Pointers, and Dereferencing#
Consider:
int answer = 42;
int *p = &answer;
int i = *p;
*p = 27;In MIPS:
la $t0, answer # int *p = &answer;
lw $t1, 0($t0) # int i = *p;
li $t2, 27
sw $t2, 0($t0) # *p = 27;
.data
answer:
.word 42la performs address-taking; lw and sw perform dereferencing. Keep the distinction explicit:
| C | MIPS idea |
|---|---|
&answer |
la the label's address |
*p as a read |
load from 0(p_register) |
*p = value |
store to 0(p_register) |
Alignment#
An object's required alignment is determined by its type and the platform's ABI; it is not generally equal to the object's total size. In the course MIPS environment, a two-byte half-word is aligned to a multiple of two and a four-byte word used by lw or sw is aligned to a multiple of four. A struct normally inherits the strictest alignment required by any of its fields, which is why a 60-byte struct containing words needs four-byte alignment rather than 60-byte alignment.
This can break when a string or byte array appears before a word:
.data
name:
.asciiz "hello" # 6 bytes including terminator
count:
.space 4 # may begin unalignedSafer options include defining the word with .word, reordering objects, or explicitly aligning it:
.data
name:
.asciiz "hello"
.align 2 # next address is divisible by 2^2 = 4
count:
.space 4Hexadecimal addresses ending in 0, 4, 8, or C are divisible by four.
One-Dimensional Arrays#
An array is a sequence of equal-sized elements. If base is the address of its first element, then:
address of array[i] = base + i * sizeof(element)
Byte Array#
For char letters[], each element is one byte:
la $t0, letters # base
li $t1, 3 # i
add $t2, $t0, $t1 # &letters[i]
lb $t3, 0($t2) # letters[i]
.data
letters:
.byte 'a', 'z', 'b', 'f', 'G'Word Array#
For int numbers[], each element occupies four bytes:
la $t0, numbers # base
li $t1, 3 # i
mul $t2, $t1, 4 # byte_offset = i * sizeof(int)
add $t3, $t0, $t2 # &numbers[i]
lw $t4, 0($t3) # numbers[i]
.data
numbers:
.word 16, 4, 1, 9, 2A common mistake is using i directly as the offset for an int array. That produces an unaligned or incorrect address because MIPS offsets are bytes, not elements.
Looping Through an Array#
int sum = 0;
for (int i = 0; i < LENGTH; i++) {
sum += numbers[i];
}LENGTH = 5
li $t0, 0 # int sum = 0;
li $t1, 0 # int i = 0;
loop_sum__cond:
bge $t1, LENGTH, loop_sum__end
mul $t2, $t1, 4 # byte_offset = i * 4;
lw $t3, numbers($t2) # int value = numbers[i];
add $t0, $t0, $t3 # sum += value;
addi $t1, $t1, 1
b loop_sum__cond
loop_sum__end:Pointer Arithmetic#
C automatically scales pointer arithmetic by the pointed-to type:
char *c = address;
c++; // address increases by 1
int *p = address;
p++; // address increases by 4MIPS registers have no pointer type, so you perform that scaling yourself. Increment a character pointer by 1, and an integer pointer by 4.
Index-based loops are often easier to debug because the logical index and computed byte offset remain separate. Pointer loops can still be useful, particularly for strings:
la $t0, string # char *p = string;
loop_string__cond:
lb $t1, 0($t0) # char c = *p;
beq $t1, 0, loop_string__end
# use c
addi $t0, $t0, 1 # p++;
b loop_string__cond
loop_string__end:Note
Checkpoint
- Memory is byte-addressed, so every load or store begins with an address.
- The instruction width (
lb,lh, orlw) must match the object being accessed. - Array indexing becomes address arithmetic: base address plus index multiplied by element size.
Two-Dimensional Arrays#
C stores a normal two-dimensional array in row-major order: all of row 0, then all of row 1, and so on. Memory remains one-dimensional, so the two indices must be flattened.
element_index = row * N_COLS + col
byte_offset = element_index * sizeof(element)
address = base + byte_offset
Conceptual grid: int a[3][4]
row 0: [0][0] [0][1] [0][2] [0][3]
row 1: [1][0] [1][1] [1][2] [1][3]
row 2: [2][0] [2][1] [2][2] [2][3]
^ a[2][1]
Flattened row-major indices:
row 0 -> 0, 1, 2, 3
row 1 -> 4, 5, 6, 7
row 2 -> 8, 9, 10, 11
a[2][1]:
element index = 2 * 4 + 1 = 9
byte offset = 9 * 4 = 36
For int matrix[N_ROWS][N_COLS], with row in $t0 and column in $t1:
mul $t2, $t0, N_COLS # row * N_COLS
add $t2, $t2, $t1 # row * N_COLS + col
mul $t2, $t2, 4 # byte offset
lw $t3, matrix($t2) # matrix[row][col]For a byte matrix, omit the final multiplication by four and use lb or sb.
Tip
Separate the formula
When debugging, inspect row * N_COLS, then the flattened element index, then the byte offset, then the final address. A single giant expression is shorter to write but harder to verify.
Structs#
A struct is a sequence of fields at known offsets from one base address. Suppose:
struct student {
int zid;
char first[20];
char last[20];
int program;
char alias[10];
};The fields begin at:
| Field | Offset | Size |
|---|---|---|
zid |
0 | 4 |
first |
4 | 20 |
last |
24 | 20 |
program |
44 | 4 |
alias |
48 | 10 |
The raw fields occupy 58 bytes. Because the struct contains four-byte integers, its total size is padded to a multiple of four: 60 bytes. Tail padding ensures that every element of an array of these structs also begins correctly aligned.
Define constants rather than scattering unexplained numbers:
STUDENT_ZID_OFFSET = 0
STUDENT_FIRST_OFFSET = 4
STUDENT_LAST_OFFSET = 24
STUDENT_PROGRAM_OFFSET = 44
STUDENT_ALIAS_OFFSET = 48
SIZEOF_STUDENT = 60If $t0 contains struct student *s:
lw $t1, STUDENT_ZID_OFFSET($t0) # s->zid
lw $t2, STUDENT_PROGRAM_OFFSET($t0) # s->program
addi $t3, $t0, STUDENT_FIRST_OFFSET # &s->first[0]For an array students[i], first calculate i * SIZEOF_STUDENT, add the array base, then use the field offset.
address of students[i].program
= base + i * SIZEOF_STUDENT + STUDENT_PROGRAM_OFFSET
Common Mistakes#
- Confusing an address with the value stored at that address.
- Using
lawhenlwis required, or loading a value when its address is required. - Forgetting that load/store offsets are measured in bytes.
- Using
lw/swon an unaligned address. - Scaling an
intarray index by1instead of4. - Using
lwfor a byte orlbfor an entire word. - Modifying a loaded value but never storing it back.
- Calculating
row * N_ROWS + colinstead ofrow * N_COLS + col. - Ignoring padding when computing struct size or field offsets.
A Reliable Address Checklist#
Whenever memory code looks wrong, state these five things explicitly:
- What object am I accessing?
- What is its base address?
- What is the logical index or field?
- What byte offset does that imply?
- Which width—byte, half-word, or word—must I load or store?
If those answers are correct, the instruction is usually straightforward.