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:
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 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
rbx together and stores the result in
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
mov rbx, 2
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
rbx, which is currently 5. After this instruction executes, the
result (9) will be stored in
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
Type the program above into the "math.asm" file and run it:
You should see the value "9" written out to the console.
Try changing the values around a bit and seeing how it responds.
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
%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.
starts at 8, so
8 - 2 - 2 = 4. After these instructions complete,
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 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
; 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 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):
|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
rdx:rax. This is a new notation.
rdx:rax means that the value is spread
across two registers:
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
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)
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
; 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.