Arithmetic, logic and bits
The overview of this topic is in Assembly basics. The same topic in M68K, MIPS, Z80, x86.
add and sub keep the low 32 bits of their result. If a result is outside the range of one word,
it wraps around. Multiplication deserves extra care because a complete product may need 64 bits,
and division produces both a quotient and a remainder.
The multiplication and division instructions in this lecture belong to RISC-V's M extension. The RV32IM instruction set combines the 32-bit integer base with that extension.
Two registers for a complete product
Multiplying two 32-bit values can produce a 64-bit result. Each multiplication instruction writes one 32-bit register, so a complete product uses two instructions:
mulwrites the low 32 bits. Those bits are the same for signed and unsigned multiplication.mulhwrites the high 32 bits after reading both operands as signed.mulhuwrites the high 32 bits after reading both operands as unsigned.mulhsuwrites the high 32 bits with the first operand signed and the second unsigned.
This gives a direct rule for choosing a pair:
| complete product | low half | high half |
|---|---|---|
| signed × signed | mul | mulh |
| unsigned × unsigned | mul | mulhu |
| signed × unsigned | mul | mulhsu |
Operand order matters for mulhsu: its first source is the signed value. If a calculation is
described as unsigned × signed, supply the sources in swapped order so the signed value comes first:
mulhsu high, signed_source, unsigned_source.
.text
main:
li t0, -1
li t1, 2
mul t2, t0, t1 # low half for either interpretation
mulh t3, t0, t1 # high half of signed -1 * 2
mulhu t4, t0, t1 # high half of unsigned 0xFFFFFFFF * 2
All three instructions read the same bits. The interpretation changes the full product:
| interpretation | complete 64-bit product | high half | low half |
|---|---|---|---|
| signed: -1 × 2 | 0xFFFFFFFFFFFFFFFE | FFFFFFFF | FFFFFFFE |
| unsigned: 4,294,967,295 × 2 | 0x00000001FFFFFFFE | 00000001 | FFFFFFFE |
Thus t2 is FFFFFFFE in both cases, while t3 and t4 differ. Pair t2 with t3 for the
signed product and with t4 for the unsigned product.
For another example, 100,000 squared is 10,000,000,000, or 0x00000002540BE400:
.text
main:
li t0, 100000
mul t1, t0, t0 # low half: 0x540BE400
mulhu t2, t0, t0 # high half: 0x00000002
Each instruction writes only its named destination. There is no hidden product register to collect. A program that needs all 64 bits executes the appropriate pair.
Quotient and remainder
Division also has signed and unsigned forms:
divwrites the signed quotient, andremwrites the signed remainder.divuwrites the unsigned quotient, andremuwrites the unsigned remainder.
For a nonzero divisor, signed division truncates the quotient toward zero. The remainder has the dividend's sign when it is nonzero. In every ordinary case, the two results satisfy this mathematical equation:
dividend = quotient * divisor + remainder
The RV32 signed overflow case, -2147483648 / -1, is the exception. Its mathematical quotient
cannot fit in a signed word, so RISC-V supplies the separately defined register results shown below.
.text
main:
li t0, -17
li t1, 4
div t2, t0, t1 # -4: truncate -4.25 toward zero
rem t3, t0, t1 # -1: -17 = (-4 * 4) + -1
li t4, 17
divu t5, t4, t1 # unsigned quotient: 4
remu t6, t4, t1 # unsigned remainder: 1
Division edge cases
RISC-V defines register results for division by zero. The instruction executes without an ISA exception. Add a guard whenever zero is invalid for your program.
| operation | quotient | remainder |
|---|---|---|
signed x / 0 | -1 (0xFFFFFFFF) | x |
unsigned x / 0 | 0xFFFFFFFF | x |
signed -2147483648 / -1 | -2147483648 (0x80000000) | 0 |
The last row is the one signed division whose mathematical quotient does not fit in a 32-bit signed
word. Its quotient wraps to the original 0x80000000 bit pattern. These specified register results
do not satisfy the mathematical equation above; they define the overflow behavior directly.
.text
main:
li t0, 10
li t1, 0
div t2, t0, t1 # signed quotient: -1
rem t3, t0, t1 # signed remainder: 10
divu t4, t0, t1 # unsigned quotient: 0xFFFFFFFF
remu t5, t0, t1 # unsigned remainder: 10
li t0, -2147483648
li t1, -1
div t6, t0, t1 # 0x80000000
rem s0, t0, t1 # 0
When a divisor may be zero and that has no valid meaning in the program, test it before dividing:
beqz t1, zero_divisor
div t2, t0, t1
rem t3, t0, t1
Logic and masks
and, or and xor work independently at every bit position, with no carry between positions.
Their immediate forms are andi, ori and xori. The assembler accepts not t1, t0 as a
convenient spelling of xori t1, t0, -1, which flips all 32 bits.
A mask is a value whose set bits mark the positions to affect:
andwith a mask keeps the marked bits and clears the rest.orwith a mask sets the marked bits.xorwith a mask flips the marked bits.
.text
main:
li t0, 0x12345678
li t1, 0x0000FF00
and t2, t0, t1 # keep bits 8 through 15
srli t2, t2, 8 # move that byte to the bottom
srli t3, t0, 16 # move bits 16 through 23 down first
andi t3, t3, 0xFF # this small mask fits in the instruction
li t4, 0xAB
li t5, 0xCD
slli t6, t4, 8 # make room for the low byte
or t6, t6, t5 # pack the bytes as 0x0000ABCD
ori s0, t0, 0xFF # set the low byte
xori s1, t0, 0xFF # flip the low byte
andi s2, t0, -256 # clear it: -256 becomes 0xFFFFFF00
t2 becomes 00000056, and t3 becomes 00000034. The first sequence masks and then shifts;
the second shifts and then uses a smaller mask. t6 becomes 0000ABCD, while s0, s1 and s2
become 123456FF, 12345687 and 12345600.
The 12-bit immediate limit
The immediate in andi, ori and xori is a signed 12-bit value. It is sign-extended to 32 bits
before the logical operation. Source constants from -2048 through 2047 fit directly.
This has two useful consequences:
- A mask such as
0xFFfits directly. - A wider mask such as
0xFF00or0x00FF0000belongs in a register, loaded withli.
Negative immediates describe masks with leading ones. For example, -256 is sign-extended to
0xFFFFFF00, which makes the final andi above a compact way to clear the low byte.
For a single fixed bit, 1 << n fits as a positive logical immediate only for bit positions 0
through 10. Bit 11 needs the isolated mask 0x00000800, but the 12-bit immediate pattern with its
top bit set would sign-extend to 0xFFFFF800. Load an isolated mask for bit 11 or above into a
register:
li t1, 1
slli t1, t1, 20 # t1 = mask for bit 20
or t0, t0, t1 # set bit 20
Shifts
RISC-V has three shifts, each with a constant form and a register form:
sll,sllishift left and bring zeroes in at the bottom.srl,srlishift right and bring zeroes in at the top.sra,sraishift right and copy the sign bit into the top.
The immediate shift amount is from 0 through 31. A register-form shift uses only the low five bits of its shift-amount register. For example, an amount of 33 acts as an amount of 1 in RV32.
A left shift by n keeps the low 32 bits of multiplication by 2^n. It gives the ordinary
mathematical product while that product fits in the intended 32-bit range. Bits shifted past bit 31
are discarded, so overflow makes the register wrap:
.text
main:
li t0, 3
slli t1, t0, 4 # 3 * 16 = 48
li t2, 0x40000000
slli t3, t2, 2 # low 32 bits are 0 after overflow
Logical right shift matches unsigned division by a power of two, discarding the fractional part.
Arithmetic right shift preserves the sign, but its rounding differs from div for some negative
values. An arithmetic shift rounds downward; signed div truncates toward zero:
.text
main:
li t0, -5
srai t1, t0, 1 # -3: round -2.5 downward
li t2, 2
div t3, t0, t2 # -2: truncate -2.5 toward zero
srli t4, t0, 1 # 0x7FFFFFFD, using the bits as unsigned
For a negative value divisible by the power of two, the results agree. For example, both
srai by 2 and signed division by 4 turn -20 into -5.
Read or change one bit
For a bit position held in a register, shift the chosen bit down and keep the low bit. Here t1
contains the position to read:
.text
main:
li t0, 0x00100000 # bit 20 is set
li t1, 20
srl t2, t0, t1 # move the selected bit to position 0
andi t2, t2, 1 # t2 = 1 if it was set, 0 if it was clear
The same shift-created mask can set, clear or flip a dynamic bit. The outputs below are separate
alternatives computed from the original value in t0:
li t2, 1
sll t2, t2, t1 # t2 = 1 << bit position
or t3, t0, t2 # t3: set the selected bit
xor t4, t0, t2 # t4: flip the selected bit
not t5, t2
and t6, t0, t5 # t6: clear the selected bit
t0 remains unchanged. Choose the output sequence that matches the operation you want.
Counting the set bits in a word repeats the extraction of its low bit:
.text
main:
li t0, 0xF0F0F0F0
li t1, 0 # count = 0
li t2, 32 # bits left
loop:
andi t3, t0, 1 # extract the low bit
add t1, t1, t3 # add either 0 or 1
srli t0, t0, 1 # bring the next bit down
addi t2, t2, -1
bnez t2, loop
t1 finishes at 16. The loop always runs 32 passes, so it also handles an input of zero.
Select with a mask
A comparison result can become an all-zero or all-one mask. That mask can select between two
values. This example computes the signed maximum of t0 and t1 because its comparison uses
slt:
.text
main:
li t0, -5
li t1, 3
slt t2, t0, t1 # 1 because signed -5 < 3
sub t3, zero, t2 # 0 - 1 = 0xFFFFFFFF
xor t4, t0, t1 # positions where the values differ
and t4, t4, t3 # keep that difference when t1 is larger
xor t4, t4, t0 # apply the difference to t0; t4 becomes t1
If slt produces 1, subtracting it from zero produces the all-one mask. The and keeps every
differing bit, and the final xor changes t0 into t1. If slt produces 0, the all-zero mask
clears the difference and the final result stays equal to t0.
Use sltu in the same pattern for an unsigned maximum. This five-instruction sequence is mainly a
compact demonstration of comparison masks and xor selection. A branch can express a maximum more
directly, and performance depends on the processor and surrounding code.
Check the choices
- Which two instructions produce the complete signed product of two registers? Which pair produces the complete unsigned product?
- What do
divandremproduce for -17 divided by 4? What doesdivuproduce when its divisor is zero? - What values do
sraiby 1 and signeddivby 2 produce from -5, and why do they differ? - Can
andi t1, t0, 0x00FF0000encode that mask directly? Give a two-instruction way to extract bits 16 through 23 without loading the wide mask.
Show answers
- Use
mulfor the low half andmulhfor the signed high half. Usemulandmulhufor the unsigned product. divproduces -4 andremproduces -1.divuby zero produces0xFFFFFFFF.sraiproduces -3 because it rounds the shifted negative value downward.divproduces -2 because signed division truncates toward zero.- No. Shift first with
srli t1, t0, 16, then useandi t1, t1, 0xFF.
Your turn
The first exercise starts t0 at -1 and t1 at 2. Leave the complete signed product in t2
and t3: the low 32 bits in t2 and the high 32 bits in t3.
.text
main:
# your code here
Show solution
.text
main:
mul t2, t0, t1 # low half
mulh t3, t0, t1 # signed high half
The next exercise starts t0 at 0x12345678. Extract bits 16 through 23 and leave the byte
0x34 in t1. Use a shift followed by an immediate mask.
.text
main:
# your code here
Show solution
.text
main:
srli t1, t0, 16
andi t1, t1, 0xFF
The third exercise starts t0 at 0x10 and t1 at 3. Flip the bit at the position held in t1
and leave the result 0x18 in t2. Build a one-bit mask with a register-form shift.
.text
main:
# your code here
Show solution
.text
main:
li t2, 1
sll t2, t2, t1 # mask = 1 << position
xor t2, t0, t2 # flip the selected bit
The final exercise starts t0 at 0xF0F0F0F0. Count its set bits and leave the count 16 in t1.
You may destroy t0 while counting.
.text
main:
# your code here
Show solution
.text
main:
li t1, 0
li t2, 32
loop:
andi t3, t0, 1
add t1, t1, t3
srli t0, t0, 1
addi t2, t2, -1
bnez t2, loop