← Back to Course
# RISC-V Assembly: Strings and Bits ## CS 631 Systems Foundations — Mar 10, 2026 --- ## Today's Agenda 1. Endianness 2. Byte load/store instructions 3. String operations (`strlen`, `strcpy`) 4. Bitwise operations and shifts 5. Bit sequence extraction (`get_bitseq`) 6. Byte packing/unpacking --- ## Endianness How are multi-byte values stored in memory? Consider `0x22AA33CC`: | Address | Big Endian | Little Endian | |---|---|---| | 0 | 0x22 (MSB) | 0xCC (LSB) | | 1 | 0xAA | 0x33 | | 2 | 0x33 | 0xAA | | 3 | 0xCC (LSB) | 0x22 (MSB) |
RISC-V is little-endian
: least significant byte at lowest address.
--- ## Detecting Endianness ```rust fn main() { let x: i32 = 1; let ptr = &x as *const i32 as *const u8; let first_byte = unsafe { *ptr }; if first_byte == 1 { println!("Little-endian"); } else { println!("Big-endian"); } } ``` `1` = `0x00000001`. On little-endian, the byte at the lowest address is `0x01`. --- ## Byte Access in Little-Endian ```asm # 0x22AA33CC stored at address in a0 lw t0, (a0) # t0 = 0x22AA33CC (full word) lb t1, 0(a0) # t1 = 0xCC (byte 0 — LSB) lb t2, 1(a0) # t2 = 0x33 (byte 1) lb t3, 2(a0) # t3 = 0xAA (byte 2) lb t4, 3(a0) # t4 = 0x22 (byte 3 — MSB) ```
lb
at offset 0 gives the
least significant
byte, not the most significant.
--- ## Byte Load/Store Instructions | Instruction | Description | Sign Extension | |---|---|---| | `lb rd, off(rs)` | Load byte, sign-extend | Bit 7 → bits 8–63 | | `lbu rd, off(rs)` | Load byte unsigned | Zeros in bits 8–63 | | `sb rs, off(rd)` | Store low byte | N/A | - `lb` sign-extends: byte `0xFF` → `0xFFFFFFFFFFFFFFFF` (-1) - `lbu` zero-extends: byte `0xFF` → `0x00000000000000FF` (255) - Use `lbu` for ASCII to avoid negative values --- ## Strings in Memory C strings are null-terminated byte arrays: ```text "hello" → | 'h' | 'e' | 'l' | 'l' | 'o' | '\0' | 0x68 0x65 0x6C 0x6C 0x6F 0x00 ``` - Process character by character with `lb` / `sb` - Loop until null terminator (`0x00`) **Rust FFI pattern**: `CString::new(s)` creates a null-terminated string, `unsafe { fn_s(...) }` calls assembly. --- ## `strlen_s` — String Length
**Rust** ```rust use std::ffi::CString; extern "C" { fn strlen_s(s: *const u8) -> usize; } fn main() { let cs = CString::new("hello").unwrap(); let r = unsafe { strlen_s(cs.as_ptr() as *const u8) }; println!("Asm: {}", r); // 5 } ```
**Assembly** ```asm strlen_s: li t0, 0 # length = 0 strlen_s_loop: lb t1, 0(a0) # load byte beqz t1, strlen_s_done addi t0, t0, 1 # length++ addi a0, a0, 1 # advance ptr j strlen_s_loop strlen_s_done: mv a0, t0 # return length ret ```
--- ## `strcpy_s` — String Copy
**Rust** ```rust use std::ffi::{CStr, CString}; extern "C" { fn strcpy_s(dest: *mut u8, src: *const u8) -> *mut u8; } fn main() { let mut buf = vec![0u8; 256]; let cs = CString::new("hello").unwrap(); unsafe { strcpy_s(buf.as_mut_ptr(), cs.as_ptr() as *const u8) }; let r = CStr::from_bytes_until_nul(&buf) .unwrap(); println!("Asm: {}", r.to_str().unwrap()); } ```
**Assembly** ```asm strcpy_s: mv t0, a0 # save dest strcpy_s_loop: lb t1, 0(a1) # load from src sb t1, 0(a0) # store to dest beqz t1, strcpy_s_done addi a0, a0, 1 # advance dest addi a1, a1, 1 # advance src j strcpy_s_loop strcpy_s_done: mv a0, t0 # return dest ret ```
Copy each byte including the null terminator. Copy first, then check. --- ## Two's Complement Review ```rust fn main() { let vals: Vec
= vec![0, 1, -1, 127, -128, 42, -42]; for v in vals { println!("{:4} = 0x{:02x}", v, v as u8); } } ``` ```text 0 = 0x00 127 = 0x7f 1 = 0x01 -128 = 0x80 -1 = 0xff 42 = 0x2a -42 = 0xd6 ``` MSB is the sign bit. `-1` = all ones (`0xFF`). --- ## Bitwise Operations: Assembly ```asm and_s: # a0 = a & b and a0, a0, a1 ret or_s: # a0 = a | b or a0, a0, a1 ret xor_s: # a0 = a ^ b xor a0, a0, a1 ret not_s: # a0 = ~a not a0, a0 ret ``` Each maps to a **single instruction**. --- ## Bitwise Operations: Truth Tables With `a = 0b11001100`, `b = 0b10101010`: | Operation | Result | Binary | |---|---|---| | `a & b` (AND) | `0x88` | `10001000` | | `a \| b` (OR) | `0xEE` | `11101110` | | `a ^ b` (XOR) | `0x66` | `01100110` | | `~a` (NOT) | `0x33` | `00110011` | - **AND**: 1 only if both bits are 1 — useful for masking - **OR**: 1 if either bit is 1 — useful for setting bits - **XOR**: 1 if bits differ — useful for toggling - **NOT**: flip all bits --- ## Shift Operations ```asm sll_w: # a0 = a << n (logical) sllw a0, a0, a1 ret srl_w: # a0 = a >> n (logical, unsigned) srlw a0, a0, a1 ret sra_w: # a0 = a >> n (arithmetic, signed) sraw a0, a0, a1 ret ``` | Instruction | Fill Bits | Use Case | |---|---|---| | `sllw` | Zeros on right | Multiply by 2^n | | `srlw` | Zeros on left | Unsigned divide by 2^n | | `sraw` | **Sign bit** on left | Signed divide by 2^n | --- ## Logical vs Arithmetic Shift ```text 0xF0000000 >> 4: Logical (srlw): 0x0F000000 ← zeros fill in Arithmetic(sraw): 0xFF000000 ← sign bit fills in ```
srlw
fills with
zeros
— treats value as unsigned.
sraw
fills with the
sign bit
— preserves the sign of negative numbers.
--- ## Bit Sequence Extraction: Algorithm Given a value, extract bits from position `start` to `end`: 1. **Shift right** by `start` — move target bits to position 0 2. **Create mask**: `(1 << len) - 1` where `len = end - start + 1` 3. **AND** with mask — zero out everything else ```text Extract bits 3-5 from 552 (0b1000101000): 552 >> 3 = 69 = 0b1000101 mask = (1 << 3) - 1 = 0b111 69 & 7 = 0b101 = 5 ``` --- ## `get_bitseq_s` — Implementation
**Rust** ```rust fn get_bitseq(num: u32, start: u32, end: u32) -> u32 { let shifted = num >> start; let mask = (1u32 << (end - start + 1)) - 1; shifted & mask } ```
**Assembly** ```asm get_bitseq_s: srl a0, a0, a1 # >> start sub t0, a2, a1 # end - start addi t0, t0, 1 # + 1 = len li t1, 1 sll t1, t1, t0 # 1 << len addi t1, t1, -1 # mask and a0, a0, t1 # & mask ret ```
Leaf function — no `call`, no stack frame needed. --- ## Signed Bit Sequence Extraction Treat extracted bits as a **signed** value: 1. Get unsigned value with `get_bitseq` 2. Shift left to put sign bit at MSB 3. Arithmetic shift right to propagate sign ```text Extract bits 4-7 from 94117 (unsigned = 10 = 0b1010): shift_amt = 32 - 4 = 28 0b1010 << 28 = 0xA0000000 0xA0000000 >> 28 (arithmetic) = 0xFFFFFFFA = -6 ```
The shift-left-then-arithmetic-shift-right trick sign-extends any bit width to a full register.
--- ## Byte Packing Combine four bytes into one 32-bit value: ```asm # a0=b3 (MSB), a1=b2, a2=b1, a3=b0 (LSB) pack_bytes_s: mv t0, a0 # val = b3 slli t0, t0, 8 # val <<= 8 or t0, t0, a1 # val |= b2 slli t0, t0, 8 or t0, t0, a2 # val |= b1 slli t0, t0, 8 or t0, t0, a3 # val |= b0 mv a0, t0 ret ``` Pattern: shift left to make room, OR in the next byte. --- ## Byte Unpacking Extract individual bytes from a 32-bit value: ```asm # a0 = value, a1 = array of uint32_t[4] unpack_bytes_s: li t0, 0 # index li t1, 4 # limit unpack_loop: beq t0, t1, unpack_done andi t2, a0, 0xFF # mask low byte sw t2, (a1) # store to array srli a0, a0, 8 # next byte addi t0, t0, 1 addi a1, a1, 4 # advance array ptr j unpack_loop unpack_done: ret ``` Pattern: mask low byte with `0xFF`, store, shift right by 8. --- ## Key Takeaways - **RISC-V is little-endian**: LSB at lowest address - **`lb`** sign-extends, **`lbu`** zero-extends — use `lbu` for ASCII - **Strings**: pointer-advancing loop, check for null terminator - **Bitwise ops** (`and`, `or`, `xor`, `not`): one instruction each - **Logical shift** fills with zeros; **arithmetic shift** fills with sign bit - **`get_bitseq`**: shift right → create mask → AND - **Sign extension**: shift left then arithmetic shift right --- ## Further Reading - [RISC-V ISA Specification](https://riscv.org/technical/specifications/) - [RISC-V Assembly Programmer's Manual](https://github.com/riscv-non-isa/riscv-asm-manual/blob/main/riscv-asm.md) - [The RISC-V Reader](http://www.riscvbook.com/) — Patterson & Waterman - [Hacker's Delight](https://en.wikipedia.org/wiki/Hacker%27s_Delight) (Warren) — bit manipulation techniques