Arrays and strings

An array is a first address and a size per element, and every loop over one is built out of those two numbers. Scaling an index by hand, walking bytes to a terminator, and the row times columns of a grid.

An array in memory has no length, no bounds and no element names. What it has is a first address and a size per element, and every loop over it is built out of those two numbers. On RISC-V the size is also what you multiply the index by, because offset(base) adds a register to nothing and scales nothing.

The size decides the shift

a[i] in C is the base plus i times the size of an element. Here that multiplication is a shift you write: two places for a word, one for a half, none at all for a byte.

t3 comes out at 30, t6 at 3 and s2 at 7, the third element of each of the three arrays. The three la results say where the assembler put them: 10010000 for the words, 10010010 for the halves, since four words take sixteen bytes, and 10010018 for the bytes.

An array of bytes needs no shift, which is why a string is walked with addi t0, t0, 1 and nothing else.

Strings are bytes with a zero at the end

.asciz "Assembly" writes nine bytes: the eight character codes and the terminator the directive adds. To the CPU 'A' is the number 65, or 0x41, which is what ASCII assigns to that letter, and nothing anywhere marks that byte as a letter instead of a number.

Nothing records how long a string is either, so a loop finds out by reading until it reads a zero.

t1 comes out at 8, the eight characters without the terminator, and t0 at 10010008, the address of the zero byte. The memory panel at 10010000 reads 41 73 73 65 6D 62 6C 79 00, and its text button draws those same bytes as Assembly.

lb sign extends, so a byte above 127 comes back negative. Every ASCII character is 127 or under, so lb is safe for text. Bytes that hold numbers instead of letters want lbu, which keeps them in 0 to 255.

Copying one

strcpy in C copies characters until it has copied the terminator. Copying it is the point: a copy without a terminator is not a string.

source is nine bytes at 0x10010000, so dest begins at 0x10010009, and after the run the memory panel shows the same nine bytes twice: 48 69 20 74 68 65 72 65 00 and then the same again.

The bnez t2, loop at the bottom is the test on the byte that was just copied, which is why the terminator gets written before the loop ends. Comparing two strings is the same loop with a bne between the two bytes in it, and the sb swapped for a second lb.

dest is a .space, so it is not word aligned and a sw into it would end the run. Bytes are fine anywhere, which is why this loop does not care.

Two dimensions

A 2D array is a 1D array read in rows. grid[row][col] is the base plus (row * COLS + col) times the size of an element, and RISC-V makes you write both multiplications.

t5 comes out at 23, the last element of the last row, and t3 at 22, which is the byte offset into the block. The three .half lines are one array: the rows are a convenience for whoever reads the source, and the twelve halves sit end to end from 0x10010000, which is what COLS in the index arithmetic assumes.

mul t3, t0, t2 is a real instruction of the M extension and it is what a row of any width needs. When the width is a power of two, slli does it in one cheaper instruction, and the two shifts can be added together: a grid of 4 halves is slli t3, t0, 3 for the row and then the column shifted by 1.

Try changing li t0, 2 to li t0, 0 and li t1, 3 to li t1, 1. t5 comes out at 1, the second element of the first row.

Your turn

text at 0x10010000 is a string with a zero at the end. Leave its length, not counting the terminator, in t0. For "Assembly" that is 8.

Show solution

The second one turns text into upper case in place, so the memory at 0x10010000 ends up holding HELLO and its terminator. A lower case letter is 'a' to 'z' and subtracting 32 from its code gives the capital. Remember that a RISC-V branch compares two registers, so the two bounds go into registers before the loop.

Show solution