Bitwise and Shifts: Working One Bit at a Time
intermediateRISCVLesson 6 of 6
addi t0, zero, 0xB4 # t0 = 1011 0100andi t1, t0, 0x0F # keep low 4 bits: t1 = 0000 0100ori t2, t0, 0x0F # set low 4 bits: t2 = 1011 1111xori t3, t0, 0xFF # flip all 8 bits: t3 = 0100 1011srli t4, t0, 4 # shift right 4: t4 = 0000 1011slli t5, t0, 1 # shift left 1: t5 = 1 0110 1000
registers
memory / stack
pc & flags
Every value you’ve moved so far, you moved as a whole number. But a register is really just 32 bits sitting in a row, and sometimes the number is beside the point — you want to inspect, set, or clear individual bits. Flags packed into one word, a color’s red channel, a permission bit: this is where bitwise operations and shifts earn their keep.
The bitwise trio: AND, OR, XOR
These work on each bit position independently — bit 0 with bit 0, bit 1 with bit 1, and so on. The whole story fits in three tiny rules:
and t2, t0, t1 # 1 only where BOTH inputs are 1
or t2, t0, t1 # 1 where EITHER input is 1
xor t2, t0, t1 # 1 where the inputs DIFFER
Each also has an immediate form that takes a constant instead of a second
register — andi, ori, xori. Same operation, one operand baked into the
instruction.
Masks: the reason they matter
A mask is a constant you choose so the operation touches exactly the bits you mean. This is the single most useful idea in the whole lesson:
andi t1, t0, 0x0F # KEEP bits that are 1 in the mask, clear the rest
ori t2, t0, 0x0F # SET bits that are 1 in the mask, leave the rest
xori t3, t0, 0x0F # FLIP bits that are 1 in the mask, leave the rest
Read it as a sentence: and keeps, or sets, xor flips — but only
where the mask has a 1. Want to test whether the bottom bit is on (is a number
odd?)? andi t1, t0, 1 and check for zero. Want a bitwise NOT? XOR with all ones
(xori t0, t0, -1) — there’s no separate not instruction because this is it.
Shifts: sliding bits sideways
A shift moves every bit left or right by a fixed number of places:
slli t1, t0, 3 # shift left logical: bits move up 3, zeros fill the bottom
srli t1, t0, 3 # shift right logical: bits move down 3, zeros fill the top
srai t1, t0, 3 # shift right arithmetic: fills the top with the SIGN bit
Shifting is multiplication and division in disguise — by powers of two, the only factors a bit-slide can produce:
slli t0, t0, 1doubles a value (<< nmultiplies by 2ⁿ).srli t0, t0, 1halves an unsigned value (>> ndivides by 2ⁿ).
The srli/srai split is the shift version of the lb/lbu distinction from
talking to memory: with
signed numbers you must preserve the sign. srli shoves zeros into the top, which
turns a negative number positive; srai copies the sign bit down so -8 >> 1
stays -4. Use srai for signed division, srli for raw bit patterns.
Step through it
The trace starts with 1011 0100 in t0 and never changes it — each line reads
t0 and writes a different destination, so you can compare all five results side
by side. Watch the notes in binary: andi 0x0F erases the top nibble, ori
0x0F lights up the bottom one, xori 0xFF inverts the byte, and the two shifts
slide the whole pattern down (÷16) and up (×2). Same input, six windows onto it.
Try this
- You have status bits in
t0and want to clear just bit 3. What mask and operation? (andi t0, t0, ~(1<<3)— i.e. AND with a mask that’s all 1s except a 0 at bit 3. AND-with-0 clears; AND-with-1 keeps.) - Why does
srligive the wrong answer for-8 >> 1butsraigets it right? (srlifills the top with zeros, so the sign is lost;sraicopies the sign bit down, preserving negativity.) - How would you multiply a number by 8 without a
mulinstruction? (slli t0, t0, 3— shifting left by 3 multiplies by 2³ = 8, and it’s faster than a multiply.)
Bits are the bedrock everything else is built on. Once you can mask, set, flip, and
slide them, you can decode any packed format the hardware hands you — and you’ve
seen why compilers love turning x * 8 into a single shift.