ASM Arithmetics
Table of contents
Performance is one of the main reasons people use assembly language programming. Assembly code can offer significant performance benefits over high-level languages due to its closeness to the hardware. This is especially true for arithmetic operations.
When performing arithmetic in assembly code, you generally use an instruction keyword along with one or two registers. For example:
ADD reg1, reg2 - Adds the values in reg1 and reg2 and stores the result in reg1
SUB reg1, reg2 - Subtracts reg2 from reg1 and stores the result in reg1
MUL reg1, reg2 - Multiplies reg1 and reg2, storing the lower 32 bits in reg1 and the upper 32 bits in EDX
DIV reg1 - Divides reg1 by the value in reg2, storing the quotient in reg1 and the remainder in EDX
The register used depends on the operation. For addition and subtraction, you typically use EAX, EBX, or ECX. For multiplication, the lower 32 bits go in one register (EAX) and the upper 32 bits go in another (EDX). Division stores the quotient in one register and remainder in another.
The performance benefits come from a few factors:
Pipelining - Instructions can be executed simultaneously since the CPU handles each part separately.
Register access - Reading from/writing to registers is much faster than memory access.
Instruction optimization - The CPU can optimize simple assembly instructions more efficiently.
Instruction parallelism - The CPU can sometimes execute multiple instructions at once.
So in summary, arithmetic operations in assembly involve choosing an instruction keyword along with one or two registers to store the result. This closeness to the hardware allows for performance benefits through pipelining, register access, instruction optimization, and parallelism - which is a key reason assembly is used.
Here is the table summarizing the arithmetic operations in assembly language:
Operation | Syntax | Description |
Addition | ADD reg1, reg2 | Adds the values in reg1 and reg2 and stores the result in reg1 |
Subtraction | SUB reg1, reg2 | Subtracts reg2 from reg1 and stores the result in reg1 |
Multiplication | MUL reg1, reg2 | Multiplies reg1 and reg2, storing the lower 32 bits in reg1 and the upper 32 bits in EDX |
Division | DIV reg1, reg2 | Divides reg1 by reg2, storing the quotient in reg1 and the remainder in EDX |
Increment | INC reg1 | Increments the value in reg1 by 1 |
Decrement | DEC reg1 | Decrements the value in reg1 by 1 |
Example
Here is the full assembly code example within a single code block:
section .text ; Section declaration
global _start ; Linker needs entry point defined
_start: ; Entry label
; Initialize registers
mov eax, 10 ; EAX = 10
mov ebx, 5 ; EBX = 5
; Addition
add eax, ebx ; EAX = EAX + EBX = 10 + 5 = 15
; Subtraction
sub eax, ebx ; EAX = EAX - EBX = 15 - 5 = 10
; Multiplication
mul ebx ; EAX = EAX * EBX = 10 * 5 = 50
; Division
mov eax, 50 ; EAX = 50
div ebx ; EAX = EAX / EBX, 50 / 5 = 10, 0
; Modulus
mov eax, 50 ; EAX = 50
mov ebx, 7 ; EBX = 7
idiv ebx ; EAX = quotient, EDX = remainder = 7, 2
; Increment
inc eax ; Increment EAX by 1, EAX = 11
; Decrement
dec eax ; Decrement EAX by 1, EAX = 10
mov eax, 1 ; Exit syscall code
int 0x80 ; Linux syscall "exit"
Note 1: Assembly language does not enforce any indentation standards. Indentation is used by programmers to improve readability, but the assembler only cares about the actual assembly instructions and not the indentation.
So while indentation can be helpful when writing assembly code for readability, as in the examples I have provided, the assembler will ignore the indentation and only recognize the actual assembly instructions. The indentation is for the human reader, not the assembler.
The key thing is that the assembler only cares about valid assembly instructions, labels, directives, etc. Things like indentation, spacing, and comments are ignored by the assembler and only used by programmers to make the code more readable and understandable.
Clarification
Here are some things to keep in mind when performing arithmetic operations in assembly language:
The result of most arithmetic operations is stored in one of the registers, typically EAX, EBX, or ECX. So you need to make sure the destination register is chosen appropriately.
For operations that produce more than 32 bits of data (like the multiplication of two 16-bit numbers), the higher-order bits are stored in EDX. So you need to consider both EAX and EDX for the full result.
If the result does not fit in 32 bits, you will get an overflow. You need to handle overflows appropriately by checking the overflow flag (OF) after an operation.
DIV has only one operand listed because the other register - which contains the divisor value - is implied. The DIV instruction knows to use that register to perform the division.
EAX is used for division by default in assembly language for a few reasons:
EAX is the "accumulator" register. It is designed to hold operands for arithmetic and logical instructions. So it makes sense to use EAX as the dividend in division operations.
EAX has the largest register size (32 bits) compared to the other general-purpose registers. This means it can hold the largest numeric values, making it suitable for holding dividends in division operations.
Using EAX as the default dividend register follows the principle of consistency. It makes the assembly code more readable and predictable when the same register is used by convention for the same purpose.
EAX has the fastest access time compared to the other registers. So using it for the dividend optimizes the performance of the division operation.
So in summary, EAX is used for holding the dividend by default in assembly language division instructions for reasons of:
It being the accumulator register
Its large 32-bit size
Consistency and readability of the code
Performance optimization due to its fast access time
While any register can theoretically be used as an operand for DIV, EAX is almost always used by convention to hold the dividend to follow these best practices in assembly language.
Expressions
Complex expressions are difficult in assembly language due to the following reasons:
Assembly only has basic instructions like add, sub, mul, div, etc. It does not have complex operators like ^ (exponentiation) or math functions like sin, cos, log, etc.
To perform a complex expression in assembly, you have to break it down into a series of basic instructions. For example, to calculate A^B where A = 5 and B = 3:
You would first load A into a register, say eax = 5
Then you would load B into another register, say ebx = 3
Then you would multiply eax by itself ebx times:
mov eax, 5 ; A = 5
mov ebx, 3 ; B = 3
mov ecx, ebx ; Copy B into ECX as a counter
loop:
mul eax ; Multiply EAX (A) by itself
loop loop ; Decrement ECX and repeat loop until ECX is 0
So as you can see, a simple expression like A^B requires multiple assembly instructions.
- Since assembly has only a handful of basic operations, complex math problems require breaking them down into a series of basic operations, resulting in a lot more code compared to high-level languages.
So in summary, the lack of complex operators and functions, and the need to break expressions down into basic operations are why complex math in assembly language requires a lot more code compared to high-level languages.
Best practices
Store the result in an appropriate register based on the operation - use EAX for additions/subtractions, use EDX:EAX for multiplications that produce more than 32 bits.
Check the overflow flag (OF) after the operation to detect overflows. Reset the flag using CLD instruction after checking.
If an overflow occurs, handle it accordingly - either saturate the result, wrap it around, or flag it as an error.
If the result needs to be stored in memory, first move it from the register(s) to a memory variable using MOV instruction.
If the result is an intermediate value and needs to be used in the next operation, keep it in the appropriate register(s).
So in summary, choosing the right destination register, handling overflow, checking and resetting flags, and properly storing or reusing the result are some best practices for assembly arithmetic.
64 Bit Registry
Here is a summary of the main differences between 64-bit and 32-bit operations in Assembly:
Register Size: In 64-bit mode, registers are 64 bits wide while in 32-bit mode, registers are 32 bits wide. This means 64-bit registers can store larger numeric values.
Registers Used: In 32-bit mode, registers like EAX, EBX, ECX, EDX are used. In 64-bit mode, registers like RAX, RBX, RCX, RDX are used. The R prefixes indicate that they are 64-bit extensions of the 32-bit registers.
Address Bus Width: The address bus is 32 bits wide in 32-bit mode, limiting the addressable memory to 4GB. The address bus is 64 bits wide in 64-bit mode, allowing access to a much larger memory address space of 16 exabytes.
Data Types: In 32-bit mode, data types like DWORD (32 bits) and WORD (16 bits) are used. In 64-bit mode, new data types like QWORD (64 bits) and DQWORD (128 bits) are introduced.
Instruction Set: While most instructions are the same in 32-bit and 64-bit modes, some new instructions were added for 64-bit mode to support the larger registers and data types.
Performance: In general, 64-bit code runs slightly slower than 32-bit code due to the larger registers and memory accesses. However, it can access more than 4GB of RAM which may improve performance for memory-intensive tasks.
Disclaim: This article is generated with AI. I have done my best to ask questions. I'm a prompt Engineer, not an Assembly specialist. If something is wrong or unclear please comment below, we may ask new questions about this topic and clarify.
Subscribe to my newsletter
Read articles from Elucian Moise directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Elucian Moise
Elucian Moise
Software engineer instructor, software developer and community leader. Computer enthusiast and experienced programmer. Born in Romania, living in US.