Basic Math
In this section, we'll go over using assembly language to perform some basic math operations like addition and subtraction.
A basic program template
Here is the basic program we'll be modifying to try out some different math operations:
%define sys_exit 60
section .text
global _start
_start:
; Return a number
mov rdi, 7
; End the program
mov rax, sys_exit
syscall
All this does is exit the program, returning 7 as the status code. Type this code into a new file called "math.asm". Use the "run" script from the previous section to assemble, link, and run the new "math.asm" program:
./run math
You should see the number 7 written out to the console. At the end of this
program, whatever value is left in the rdi
register will be returned as the
status code and displayed on the console. This is how we'll see the results of
the math operations in this section.
Note: the status code can only be an integer from 0 to 255. If you try to return negative numbers or numbers larger than 255 you'll get weird results.
Addition
Addition is performed using the add instruction. It takes 2 operands. It adds the value of the second operand to the first operand. Take a look at this example:
add rbx, 3
This adds the number 3 to whatever value is stored in rbx
. The result of the
operation is stored in rbx
. So, if rbx
is set to 5 before this instruction
executes, it will be set to 8 after this instruction executes (5 + 3 = 8).
In this instruction, the value 3 is called an immediate. This means it's a literal value encoded directly into the instruction. Unlike a register or memory address, which refer to storage locations that could have any value, an immediate is a fixed value that never changes.
You can also add two registers together:
add rax, rbx
This adds the values of rax
and rbx
together and stores the result in
rax
.
If rax
was 2 and rbx
was 4, this instruction would change rax
to 6
(2 + 4 = 6). rbx
would not be changed.
Now let's work a few add instructions into the basic program structure from above:
%define sys_exit 60
section .text
global _start
_start:
; Set rbx to 2
mov rbx, 2
; Add 3 to rbx
add rbx, 3
; Set rax to 4
mov rax, 4
; Add rax to rbx
add rbx, rax
; End the program, returning the value in rbx
mov rdi, rbx
mov rax, sys_exit
syscall
Line-by-line:
mov rbx, 2
The rbx
register is undefined at the start of the program. It's probably
0, but it's usually considered best practice to avoid making assumptions about
the state of registers unless we set them ourselves. So, we start by giving
rbx
a value of 2.
add rbx, 3
Now we add 3 to the value in rbx
, which was previously set to 2. After this
instruction executes, the value of rbx
should be 5.
mov rax, 4
Now we set rax
to the value 4. This is to demonstrate adding two registers
together in the next instruction.
add rbx, rax
This adds the two registers together: rax
, which is currently 4, will be
added to rbx
, which is currently 5. After this instruction executes, the
result (9) will be stored in rbx
.
mov rax, sys_exit
mov rdi, rbx
syscall
To return the value in rbx
, we have to move it into rdi
before performing
the sys_exit system call. sys_exit returns the value in rdi
as the exit
status code. In order to see the value in rbx
, we have to copy it to rdi
before exiting.
Type the program above into the "math.asm" file and run it:
./run math
You should see the value "9" written out to the console.
Try changing the values around a bit and seeing how it responds.
Subtraction
Subtraction works much like addition. To subtract, we use the sub instruction. Like the add instruction, it takes 2 operands. It subtracts the value of the second operand from the value of the first operand and stores the result in the first operand.
Here's an example:
mov rbx, 10
sub rbx, 7
This code snippet starts by setting rbx
to a value of 10. Then we subtract 7
from that value, leaving rbx
with a value of 3. Try working this into the
program above:
%define sys_exit 60
section .text
global _start
_start:
; Set rbx to 5
mov rbx, 5
; Add 3 to rbx
add rbx, 3
; Set rax to 2
mov rax, 2
; Subtract rax from rbx twice
sub rbx, rax
sub rbx, rax
; End the program, returning the value in rbx
mov rdi, rbx
mov rax, sys_exit
syscall
Let's go through the changed lines one at a time:
; Set rbx to 5
mov rbx, 5
We start by setting rbx
to 5.
; Add 3 to rbx
add rbx, 3
Next, we add 3 to rbx
. After this instruction executes, rbx
should be 8
(5 + 3).
; Set rax to 2
mov rax, 2
Now we set rax
to a value of 2.
; Subtract rax from rbx twice
sub rbx, rax
sub rbx, rax
Here we subtract the value in rax
from the value in rbx
two times. rbx
starts at 8, so 8 - 2 - 2 = 4
. After these instructions complete, rbx
should be set to 4.
Make these edits and re-run the program. Again, try changing some of the values around and seeing how the output of the program responds.
Multiplication
Multiplication works similarly. Noticing a pattern here? We can multiply values with the imul instruction. Modify "math.asm" to look like this:
%define sys_exit 60
section .text
global _start
_start:
; Set rbx to 3
mov rbx, 3
; Multiply rbx by itself
imul rbx, rbx
; Double the value in rbx
imul rbx, 2
; End the program, returning the value in rbx
mov rdi, rbx
mov rax, sys_exit
syscall
See if you can work out what value this program will return. Once you have your guess, take a look at the breakdown:
; Set rbx to 3
mov rbx, 3
We start by setting rbx
to an initial value of 3.
; Multiply rbx by itself
imul rbx, rbx
Next we multiply rbx
by itself. 3 * 3 = 9, so this instruction will set rbx
to 9.
; Double the value in rbx
imul rbx, 2
Finally, we multiply the value in rbx
by 2, doubling it. The result is 18.
Again, try experimenting with this instruction. Try combining addition, subtraction, and multiplication in the same program.
Division
Division works a bit differently. add, sub, and imul are pretty flexible, in that they can operate on basically any combination of registers and/or immediates. When it comes to the idiv operation, things are a bit more restricted.
First, let's define some terms. In division, a dividend is divided by a divisor, yielding a result and a quotient (or remainder):
Expression | Dividend | Divisor | Result | Quotient (remainder) |
---|---|---|---|---|
14 / 3 = 4 r 2 | 14 | 3 | 4 | 2 |
100 / 13 = 7 r 9 | 100 | 13 | 7 | 9 |
When using the idiv instruction, the dividend is always assumed to be stored
in rdx:rax
. This is a new notation. rdx:rax
means that the value is spread
across two registers: rdx
and rax
. The purpose of this is to allow the
division of very large numbers that don't fit into a single register.
For now, we have no need to use both registers, so we can keep things simple
by just using rax
. However, it's important to realize what's going on.
Whenever we divide a value in rax
, we should make sure that rdx
is
clear (set to 0) so that it doesn't interfere with the division operation. If
rdx
has data in it, it will be included in the division operation and may
produce unexpected results or errors.
A further limitation of the idiv instruction is that it can't divide by an immediate value. This means that the divisor must first be loaded into a register.
idiv stores the result of the operation in rax
and the quotient (remainder)
in rdx
.
Here's a new example program which demonstrates 100 / 13:
%define sys_exit 60
section .text
global _start
_start:
; Set rax to 100 (this is the dividend)
mov rax, 100
; Clear rdx so it doesn't interfere
mov rdx, 0
; Set divisor to 13
mov rbx, 13
; Perform the division
idiv rbx
; End the program, returning the division result in rax
mov rdi, rax
mov rax, sys_exit
syscall
This divides 100 by 13 and returns the result: 7. In more detail:
; Set rax to 100 (this is the dividend)
mov rax, 100
We're going to divide 100 by 13. 100 goes in rax
, since the idiv
instruction doesn't let us pick what to use as the dividend.
; Clear rdx so it doesn't interfere
mov rdx, 0
We don't need the added space of the second register to help store the dividend, so we set it to 0 so that it doesn't interfere with the operation.
Note: 'xor rdx, rdx' is a faster way to set a register to 0. We'll explain it more in a later section about binary operations.
; Set divisor to 13
mov rbx, 13
The divisor can't be an immediate value, it must be a register. So in order to divide by 13, we first have to load that value into a register.
; Perform the division
idiv rbx
Here the division operation is performed. The value in rdx:rax
is divided
by the value in rbx
. The result is placed in rax
and the remainder is
placed in rdx
.
; End the program, returning the division result
mov rdi, rax
mov rax, sys_exit
syscall
Here we return the result of the division instruction.
Type this program in and run it. You should get a result of 7. Next, verify the remainder. Modify the end of the program to return the remainder instead of the result:
; End the program, returning the division remainder
mov rdi, rdx
mov rax, sys_exit
syscall
Run it again, and you should see 9 printed to the console. 100 divided by 13 is 7, with a remainder of 9.
Try combining all of these instructions in various ways until you're comfortable with them.