To a processor, memory is a long array of bytes, each with its own number, its address. Programs read and write single bytes, 16-bit halfwords and 32-bit words in that array, and the hardware has to agree with the software on exactly which bytes a word occupies and in which order. This article explains byte addressing and the address space, the three access sizes and their instructions, little-endian byte order, alignment, and how the processor picks a byte out of a memory that is organized in 32-bit words, all shown on the real memory image of a RISC-V program.

13 min read | Beginner

What You'll Learn

  • Explain byte addressing and what a 32-bit address space contains
  • Use the load and store instructions for bytes, halfwords and words
  • Read multi-byte values in a little-endian memory
  • Explain alignment and how hardware splits an address into a word and a byte

One Address per Byte

Memory is a row of storage cells, each holding one byte, which is 8 bits. Each cell has a number, its address, starting at 0. RISC-V, like almost every modern processor, is byte-addressed: every address names exactly one byte, and the next address names the next byte. To read the byte at address 0x6C, the processor sends 0x6C to memory and gets 8 bits back.

RISC-V addresses are 32 bits wide, so they can name 232 different bytes, 4 GB (gigabytes). The full range of possible addresses, from 0x0000_0000 to 0xFFFF_FFFF, is the address space. A small system uses only a few regions of it: for example, 64 KB of memory at addresses 0x0000_0000 to 0x0000_FFFF and a couple of device registers far above it. Every other address has nothing behind it. Which address range belongs to which memory or device is the system's memory map.

Three Sizes: Byte, Halfword, Word

Programs work with values of different sizes. A character is one byte; a 16-bit sample from a microphone is two bytes; an integer or an address is four. RISC-V names these sizes and has a load and a store for each:

SizeBytesLoad (signed / unsigned)StoreTypical use
byte1lb / lbusbcharacters, raw data
halfword2lh / lhush16-bit values
word4lwswintegers, addresses, instructions

A load copies a value from memory into a register; a store copies the low byte, halfword or word of a register into memory. Registers are always 32 bits, so a loaded byte or halfword must be widened: lbu and lhu fill the upper bits with zeros, while lb and lh copy the value's top bit into them, so that negative numbers stay negative. Numbers in Hardware explains this sign and zero extension.

Every load and store forms its address the same way: the value of a register plus a 12-bit constant from the instruction, the offset. lw a5, 8(a4) reads the word at the address in register a4 plus 8. One register can thus point to the start of an array or a structure, and the offset selects a field within it.

Memory Accesses in a Real Program

Take a small program that prints a greeting one character at a time. In C, it walks a pointer p along the string and writes each character to a console device. The compiler turned this into the instructions below; the left column is the address of each instruction, the middle its 32-bit encoding, the right its meaning in assembly language. Only the lines that touch memory are shown:

  20:  00000737   lui   a4,0x0
  24:  06c70713   addi  a4,a4,108     # a4 = 0x6C, the address of the text
  30:  00458593   addi  a1,a1,4       # a1 = 0xF000_0004, the console register
  ...
  3c:  00f5a023   sw    a5,0(a1)      # write the character to the console
  40:  00074783   lbu   a5,0(a4)      # read the byte at p again (vowel test)
  44:  00170713   addi  a4,a4,1       # p = p + 1
  ...
  60:  00074783   lbu   a5,0(a4)      # read the next character
  64:  fc079ce3   bnez  a5,3c         # not the terminating zero: repeat

Register a4 holds the pointer. The two instructions at 0x20 and 0x24 build the address 0x6C, where the linker placed the text. Each pass of the loop reads one character with lbu, an unsigned byte load, and then adds 1 to the pointer, because the next character is at the next byte address. A loop over an array of 32-bit integers would use lw and add 4 instead. C marks the end of a string with a zero byte, so the loop stops when lbu returns 0; here that byte is at 0x7E, right after the newline at 0x7D.

The store at 0x3C writes to address 0xF000_0004. No memory is there: the memory map assigns that address to the console device, and whatever a program writes to it appears as text on the console. Reaching a device with ordinary loads and stores is called memory-mapped I/O, and it is how processors talk to almost all hardware. The instruction cannot tell whether an address is memory or a device; only the memory map decides. Memory-Mapped I/O and Registers in the SoC Design tutorial covers this in depth.

Multi-Byte Values and Byte Order

A word occupies four consecutive addresses. Which of its four bytes goes to which address is the byte order, or endianness, and there are two conventions. Little-endian stores the least significant byte (the one holding bits 7 to 0) at the lowest address. Big-endian stores the most significant byte there. RISC-V is little-endian, as are x86 processors and Arm processors in their usual mode.

The message in that program shows the difference. Its first eight characters, "Hello fr", sit at addresses 0x6C to 0x73, one character per byte. Read with byte loads, the characters come out in order. Read as 32-bit words, they look reversed:

The bytes of Hello fr at addresses 0x6C to 0x73, and the words 0x6C6C6548 at 0x6C and 0x7266206F at 0x70 that they form, each word showing its four bytes from most significant to least significant with their addresses

The four bytes at 0x6C to 0x6F are 0x48 ('H'), 0x65 ('e'), 0x6C ('l') and 0x6C ('l'). As a little-endian word, the byte at the lowest address becomes the lowest 8 bits, so the word at 0x6C is 0x6C6C6548. Written as a number, with the most significant digits first as usual, the 'H' ends up at the right-hand end. On a big-endian machine the same four bytes would form 0x48656C6C.

Neither order is better; they are conventions, and what matters is that everyone involved uses the same one. Byte order becomes a problem only when data moves between systems that disagree. Network protocols send multi-byte numbers in big-endian order, many file formats state their order in a header, and device registers follow their hardware spec. Software converts at these boundaries, and a missed conversion shows up as a value with its bytes swapped.

Quick Check

Q: On a little-endian machine, the word 0x12345678 is stored at address 0x100. Which byte does lbu read from address 0x101?

Show Answer

0x56. The least significant byte 0x78 is at 0x100, then 0x56 at 0x101, 0x34 at 0x102 and 0x12 at 0x103.

How Memory Hardware Is Organized: Words

Although every byte has an address, memory hardware is not built as an array of single bytes. Reading one byte at a time would make word accesses four times slower. Instead, a typical memory is an array of 32-bit words: each row holds four bytes, and one access reads or writes a whole row. For a 64 KB memory, the address is split into two parts:

The 16 low bits of address 0x6E: bits 15 to 2 equal 0x1B and pick the word at byte address 0x6C, and bits 1 and 0 equal 2 and pick the third byte of that word

The upper bits select the word, and the lowest two bits select the byte within it. Dividing by 4 and taking the remainder is exactly what dropping or keeping the two lowest bits does in binary, which is why word sizes are powers of two.

For a byte load, the processor reads the whole word, shifts it right by 8 times the byte number so that the wanted byte lands in bits 7 to 0, and then sign- or zero-extends it. For a byte store, it must change only one byte of the word and leave the other three alone. Memories support this with byte enables: four signals, one per byte of the word, that say which bytes the write should change.

Here is a byte store worked through. Register a5 holds 0x0000_0021, the character '!', and the program executes sb a5, 2(a4) with a4 = 0x6C, so the address is 0x6E:

address 0x6E          -> word row 0x1B (byte address 0x6C), byte number 2
store data            -> 0x21 copied into all four byte lanes: 0x2121_2121
byte enables [3:0]    -> 0100  (only byte 2 is written)

word at 0x6C before   -> 0x6C6C_6548   ("Hell")
word at 0x6C after    -> 0x6C21_6548   ("He!l")

Copying the byte into every lane means the data is in the right position whichever byte is selected, so the memory only needs the enables to know where to write. A halfword store works the same way with two enables, 0011 for the lower half or 1100 for the upper half, and a word store sets all four. The Memory Hierarchy and Locality shows how every level of memory carries these enables.

Alignment

A value is aligned when its address is a multiple of its size: a word at an address divisible by 4, a halfword at an address divisible by 2. Bytes are always aligned. An aligned word fits exactly into one row of a word-organized memory; its address has 00 in bits 1 to 0.

A misaligned word, for example one starting at 0x6E, would span two rows: two bytes in the row at 0x6C and two in the row at 0x70. The hardware would need two accesses and extra logic to glue the halves together. RISC-V lets each processor decide whether to support misaligned accesses slowly or to signal an error when one occurs, and many small processors simply expect aligned accesses. Larger processors usually handle them in hardware, because the extra logic is a small price at that size. C compilers place every variable at an aligned address, which is why structs sometimes contain unused padding bytes between fields.

Where Does This Matter?

Device drivers read hardware registers whose fields follow a byte layout fixed in a datasheet. Network code converts between the network's big-endian order and the processor's little-endian order. Embedded programmers reading a memory dump in a debugger must know whether the tool shows bytes or words, or the data looks scrambled.

Quick Check

Q: Is a word at address 0x0000_0106 aligned? Is a halfword at the same address aligned?

Show Answer

0x106 = 262 is not divisible by 4 (its bits 1 to 0 are 10), so the word is misaligned and would span the rows at 0x104 and 0x108. It is divisible by 2, so a halfword there is aligned and fits in the upper half of the row at 0x104.

Where Memory Ends

An address can name a location that does not exist. What happens then is a design choice, not a law. The simplest hardware ignores the problem: a load from an address outside the memory just returns 0, and a store disappears. A program that follows a wrong pointer then gets zeros instead of an error, and the mistake can stay hidden for a long time.

Real systems handle such accesses more strictly. A full SoC answers every address that no memory or device owns with an error response, and the processor turns it into an exception, so a stray pointer is caught where it happens instead of silently reading zeros. The SoC Design tutorial's Memory Maps and Address Decoding shows how.

Common Mistakes

Mistake #1: Reading a word dump as text

A memory dump displayed as little-endian words shows each word's bytes in reverse address order. 0x6C6C6548 is "Hell", not "lleH". Check whether a tool shows bytes or words before reading it.

Mistake #2: Loading characters with lb

Characters and raw data bytes are unsigned. Loading them with lb turns every byte above 0x7F into a negative number, which breaks comparisons and table lookups. Use lbu.

Mistake #3: Assuming any address can hold any value

Casting a byte pointer to a word pointer at an odd address produces a misaligned access, which is slow or causes an error depending on the processor. Keep multi-byte values at aligned addresses.

Practice Exercise

Challenge: Decode a Memory Word

In the memory of the greeting program, the word at address 0x70 is 0x7266206F. (a) Which four characters does it hold, in address order? (b) At which address is the character "f"? (c) For that address, which word row is read, and which byte number inside the word is selected?

Hint

Split the word into bytes starting from the least significant end: the lowest byte belongs to address 0x70. The character codes are 0x6F 'o', 0x20 space, 0x66 'f', 0x72 'r'.

Solution

(a) From address 0x70 upward the bytes are 0x6F, 0x20, 0x66 and 0x72: "o", space, "f", "r". (b) The "f" is at 0x72. (c) 0x72 is binary ...0111 0010: bits [15:2] select the word at 0x70 (row 0x1C), and bits [1:0] = 10 select byte 2 of that word. Together with "Hell" in the word at 0x6C, this spells "Hello fr", the start of the greeting.

Summary

  • Every address names one byte; 32-bit addresses span a 4 GB address space, and a system's memory map says which regions are used.
  • RISC-V loads and stores bytes, halfwords and words; addresses are a register plus an offset.
  • RISC-V is little-endian: the bytes "Hell" at 0x6C read as the word 0x6C6C6548.
  • Memory hardware works in words: the upper address bits pick the word and the lowest two bits pick the byte inside it.
  • Aligned values fit in one word row; misaligned ones need two accesses or cause an error.
Did you find what you were looking for?

Quiz

No quiz questions available for this article.