lapseudo
la
rd, labelpseudo
rd = &label

la puts the address of something into a register — its location in memory, not the value stored there. The name is short for load address, and the distinction is the whole point.

First, a label. When you set aside data in memory, you give it a name, like arr: or msg:, so you do not have to track raw address numbers yourself. That name is a label — a human-readable stand-in for a memory address. la t0, arr loads the address that arr marks into t0. It does not read the array's contents; it gives you the location where the array begins.

Why is that the first thing you need? Because the load and store instructions (lw, sw, and the rest) all work from an address held in a register. Before you can read or write named data, you must get its address into a register, and la is how. Loading the value, then, is a two-step affair: la to find where the data is, then lw to read what is there.

This location-versus-contents idea is exactly the difference between a pointer and a value. Once the address sits in a register, you reach individual elements by adding offsets to it — element by element, field by field. The example loads the address of a piece of text into a0, which is just what the print-string service needs: it wants to be told *where* the text is, then reads it from there itself. la is a pseudo-instruction the assembler builds from a couple of real ones.

Expands to
auipc rd, hi(label)
addi  rd, rd, lo(label)

A pseudo-instruction: the assembler turns it into the real instruction(s) above.

.data
msg: .string "hi"
.text
la a0, msg     # a0 = address of msg