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.

Next section: Conditional branching