Doing math

Doing math

All calculations take place between registers. An arithmetic instruction names the destination first, followed by the two sources it reads: means dest = src1 + src2.

Arithmetic operation
t2, t0, t1←add
t0 = 7t1 = 5+t2 = 12

add places t0 + t1 in t2.

The two lines entering from the left are t0 and t1, always in that order. They enter the same shape regardless of which operation is running: add, subtract, multiply, divide, or remainder. Two values go in, one operation happens, and one result comes out into t2 on the right. names t2 first because that is where the result lands, which matches the one ordering rule that every arithmetic instruction in this lesson follows.

Adding and subtracting

takes the numbers held in two registers and stores their sum in a third. does the same for subtraction: computes t0 - t1. Both instructions read from registers only.

Register arithmetic
add t2, t0, t1 # t2 = t0 + t1
sub t2, t0, t1 # t2 = t0 - t1

One thing to know: a register holds only 32 bits, so there is a ceiling. If a result is too big to fit, no error is raised. The value quietly wraps around to very low numbers and execution continues. Everyday values will not reach this limit, but the ceiling is worth knowing about.

To use a fixed number in place of a second register, add the letter i, which stands for immediate: adds the constant 1. There is no subi, so to subtract a constant you add a negative one: .

💡
adds a constant. adds two registers. Confusing the two is the most common beginner error.

Multiplying and dividing

multiplies two registers. divides them as whole numbers and discards the fraction, so 7 / 2 yields 3. gives the remainder, so 7 % 2 yields 1. Division never causes a crash. Dividing by zero returns -1.

Multiply, divide, remainder
mul t2, t0, t1 # t2 = t0 * t1
div t2, t0, t1 # t2 = t0 / t1 (integer)
rem t2, t0, t1 # t2 = t0 % t1 (remainder)

Putting it together

Add two numbers and print
li t0, 7
li t1, 5
add t1, t0, t1 # t1 = 7 + 5 = 12
mv a0, t1 # the print service reads a0
li a7, 1
ecall
li a7, 10
ecall

loads 7 into t0. loads 5 into t1. adds them and writes 12 into t1. copies the result into a0, because the print service always reads its value from a0. Then selects the print-integer service and runs it. Finally, and end the program.

Bonus: multiplying with shifts

When the factor is a power of two, a faster method than exists. (shift left logical) moves a register's bits to the left, and every position you shift doubles the value: shift by 1 to multiply by 2, by 2 for 4, by 3 for 8. Shifting costs less than multiplying, so compilers use it often.

Multiply by 8 using shifts
li t0, 5 # start with 5
# shift left by 3 bits (5 * 2^3 = 5 * 8)
sll a0, t0, 3 # a0 = 40
li a7, 1 # print integer
ecall
li a7, 10
ecall

puts 5 in t0. shifts its bits three places left. Each place doubles the value, so 5 doubles three times (5, 10, 20, 40), giving 40 in a0. The rest follows the print pattern from before. Try changing the 3 to a 1 or a 2 and predict the result before you run it.

Multiply by shifting
5sll a0, t0, 0
×2 →
10sll a0, t0, 1
×2 →
20sll a0, t0, 2
×2 →
40sll a0, t0, 3
0
0
0
0
0
1
0
1

5 shifted left 0 bits stays 5.

The eight boxes are the binary form of the number above them, one bit each, read left to right as usual. Shifting the whole row one position to the left is the same operation as multiplying by 2, because moving a digit one place to the left in any base multiplies its value by that base. Binary has only two digits, so the multiplier is 2. Shifting three times compounds this into a multiplication by 8, which matches what does to the number 5.