Arrays and strings
The overview of this topic is in Assembly basics. The same topic in M68K, MIPS, Z80, x86.
An array occupies one contiguous run of memory, with equal-sized elements placed one after another. Its memory representation carries no automatic length or bounds. A program can keep the length in a register or memory, define it as a constant, or mark the address just after the final element.
To reach element i, start with the array's base address and add i * element_size. RISC-V addresses
memory in bytes, so the element size turns an index into a byte offset.
Scale a register index
The address inside a load or store has the form constant(base_register). For example,
lw t3, 8(t1) reads four bytes at address t1 + 8. The 8 is a constant byte offset encoded in the
instruction. When an index is held in a register, calculate its byte offset, add that to the base,
and then load or store through the resulting address.
Here the same index selects the third element from arrays whose elements have three different sizes:
.data
words: .word 10, 20, 30, 40
halves: .half 1, 2, 3, 4
bytes: .byte 5, 6, 7, 8
.text
main:
li t0, 2 # i = 2
la t1, words
slli t2, t0, 2 # byte offset = i * 4
add t2, t1, t2
lw t3, 0(t2) # words[i]
la t1, halves
slli t2, t0, 1 # byte offset = i * 2
add t2, t1, t2
lh t4, 0(t2) # halves[i]
la t1, bytes
add t2, t1, t0 # byte offset = i
lb t5, 0(t2) # bytes[i]
After the run, t3 is 30, t4 is 3 and t5 is 7. Shifting left by two multiplies by four, and
shifting left by one multiplies by two. A byte index is already a byte offset, so it needs no shift.
In this simulator, words, halves and bytes begin at 0x10010000, 0x10010010 and
0x10010018. Those addresses follow from this particular data layout; the labels let the program
work without embedding them as constants.
A moving pointer is the other common way to walk an array. Add 4 after visiting a word, 2 after a halfword, or 1 after a byte. An index loop and a pointer loop perform the same address arithmetic in different places.
Zero-terminated strings
A string is an array of character-code bytes with a convention for finding its end. The .asciz
directive appends a zero byte after the characters:
text: .asciz "Assembly"
This writes nine bytes: eight ASCII character codes followed by zero. 'A' has ASCII code 65, or
0x41, so the bytes begin 41 73 73 65 6D 62 6C 79 00. In this simulator, if text is the first
item in .data, those bytes begin at 0x10010000.
The zero is called the terminator. A program finds the string's end by checking one byte at a time, and its character count excludes that terminator. The string itself still occupies one extra byte to hold it.
Choose lb or lbu according to how the loaded byte will be interpreted:
lbsign-extends bit 7 and gives a signed value from -128 through 127.lbuzero-extends and gives an unsigned value from 0 through 255.
ASCII codes use only 0 through 127, so both instructions produce the same register value for ASCII.
Either also works when a byte is only tested for zero or copied with sb: zero remains zero, and
sb writes the low eight bits. Use lbu when later arithmetic or comparison should treat every
possible byte as an unsigned number, and lb when the byte represents a signed 8-bit value.
Copy a string
A complete string copy includes the terminating zero. This loop stores each byte before deciding whether another pass is needed:
.data
source: .asciz "Hi there"
dest: .space 16
.text
main:
la t0, source # source pointer
la t1, dest # destination pointer
loop:
lbu t2, 0(t0) # read one byte
sb t2, 0(t1) # copy that byte, including a possible zero
addi t0, t0, 1
addi t1, t1, 1
bnez t2, loop # continue if the copied byte was not zero
done:
In this simulator, source begins at 0x10010000 and occupies nine bytes, so dest begins at
0x10010009. After the run, both locations contain 48 69 20 74 68 65 72 65 00. The branch tests
the byte that was just stored; when it is zero, the destination already has its terminator.
Compare two strings
Two zero-terminated strings are equal when every corresponding character matches and both reach their terminators together. A mismatch produces 0 here, while matching strings produce 1:
.data
left: .asciz "same"
right: .asciz "sand"
.text
main:
la t0, left
la t1, right
compare:
lbu t2, 0(t0)
lbu t3, 0(t1)
bne t2, t3, different
beqz t2, equal # equal bytes that are zero end both strings
addi t0, t0, 1
addi t1, t1, 1
j compare
different:
li t4, 0
j done
equal:
li t4, 1
done:
With the data shown, the third characters differ, so t4 becomes 0. Change right to "same"
and t4 becomes 1. The beqz is reached only after the two loaded bytes have compared equal. If
that equal byte is zero, both strings ended at the same position and every earlier pair matched.
Two dimensions
A 2D array is stored as one contiguous array, one row after another. If every row has COLS
elements, the element number for grid[row][col] is:
row * COLS + col
Multiply that element number by the element size to obtain the byte offset. This example uses four halfwords per row:
.eqv COLS, 4
.data
grid: .half 0, 1, 2, 3
.half 10, 11, 12, 13
.half 20, 21, 22, 23
.text
main:
li t0, 2 # row
li t1, 3 # column
li t2, COLS
mul t3, t0, t2 # row * COLS
add t3, t3, t1 # row * COLS + column
slli t3, t3, 1 # byte offset: halfwords take two bytes
la t4, grid
add t4, t4, t3
lh t5, 0(t4) # grid[row][column]
t3 becomes 22, the byte offset of element 11, and t5 becomes 23. The line breaks in the data
declaration make the rows visible to a reader; memory contains twelve consecutive halfwords. The
value of COLS is what gives those bytes their row shape during the calculation.
When the row width and element size are powers of two, shifts can express both multiplications
directly. A row above occupies 4 * 2 = 8 bytes, and a column step occupies 2 bytes. This complete
sequence computes the same address:
# t0 = row, t1 = column
slli t2, t0, 3 # row byte offset = row * 8
slli t3, t1, 1 # column byte offset = column * 2
add t2, t2, t3 # total byte offset
la t4, grid
add t4, t4, t2
lh t5, 0(t4)
The shift form makes the power-of-two layout explicit. The multiplication form follows the general formula and also works when the row width is not a power of two.
Four memory exercises
First, load element 4 of the halfword array into t1. The supplied index in t0 is zero-based, so
element 4 holds 23.
.data
values: .half 4, 8, 15, 16, 23, 42
.text
main:
li t0, 4 # index
# your code here
Show solution
.data
values: .half 4, 8, 15, 16, 23, 42
.text
main:
li t0, 4
slli t2, t0, 1 # halfword index to byte offset
la t3, values
add t3, t3, t2
lh t1, 0(t3)
Next, leave the length of text, excluding its terminator, in t0. For "Assembly" the answer is 8.
.data
text: .asciz "Assembly"
.text
main:
# your code here
Show solution
.data
text: .asciz "Assembly"
.text
main:
la t1, text
li t0, 0
loop:
lbu t2, 0(t1)
beqz t2, done
addi t1, t1, 1
addi t0, t0, 1
j loop
done:
For the third exercise, load grid[1][2] into t2. The grid has four halfwords per row.
.eqv COLS, 4
.data
grid: .half 0, 1, 2, 3
.half 10, 11, 12, 13
.half 20, 21, 22, 23
.text
main:
li t0, 1 # row
li t1, 2 # column
# your code here
Show solution
.eqv COLS, 4
.data
grid: .half 0, 1, 2, 3
.half 10, 11, 12, 13
.half 20, 21, 22, 23
.text
main:
li t0, 1
li t1, 2
li t3, COLS
mul t3, t0, t3 # row * COLS
add t3, t3, t1 # add the column
slli t3, t3, 1 # halfword index to byte offset
la t4, grid
add t4, t4, t3
lh t2, 0(t4)
Finally, turn text into upper case in place. A character literal is its numeric character code:
'a' assembles as ASCII 97 and 'z' as ASCII 122. Subtracting 32 from a lowercase ASCII code
produces the corresponding uppercase code. Leave other characters unchanged and preserve the
terminator. In this simulator, this first .data item begins at 0x10010000, which is the memory
address checked below. The result should read HELLO, ASM!: the existing uppercase letter, comma,
space and exclamation mark stay as they are.
.data
text: .asciz "Hello, asm!"
.text
main:
# your code here
Show solution
.data
text: .asciz "Hello, asm!"
.text
main:
la t0, text
li t3, 'a' # ASCII 97
li t4, 'z' # ASCII 122
loop:
lbu t1, 0(t0)
beqz t1, done
blt t1, t3, skip
bgt t1, t4, skip
addi t1, t1, -32
sb t1, 0(t0)
skip:
addi t0, t0, 1
j loop
done: