I begun writing a different blog post, but found that some of the background info was making that post too long, so I’ve split it into this seperate post.
Whenever a program reads or writes a value in memory the CPU needs the memory address. Most CPUs provide multiple ways to do this, including x86. This article describes many of these addressing modes. First, we have to introduce some other assembly concepts.
Instruction formats
Machine code is made up of a series of instructions to be executed by the CPU approximately one after another (they can be re-ordered or done in parallel, provided they’re sequentially consistent, just pretend they’re sequential). Each instruction is typically an opcode (what to do) and one or more operands (what to do it with).
For example:
addl %edx, %eax
Add the contents of the edx register to the eax register, the result is stored in the eax register (AT&T syntax). It’s like saying:
eax += edx
Operands may be registers, immediate values or memory locations. Different architectures behave differently here and I only know the details of the x86. x86 instructions vary in the number of operands they take, two operands is fairly common. When two operands are used the following combinations are typically legal, but again this varies by instruction.
-
register to register
-
register to memory
-
memory to register
-
immediate to register
-
immediate to memory
When registers or memory locations are used, the value stored in that register or memory location is part of the computation, but there are exceptions like the lea instruction! Immediate values are values that follow immediately after the other parts of the instruction. Such immediates are used directly in the computation.
Addressing modes
Most if not all CISC-style (like x86) processors provide multiple addressing modes. These provide different ways for a processor to calculate the effective address the logical memory address the instruction should operate on. Some addressing modes for 16-bit code are:
-
reg + reg
-
reg
-
disp16 (a 16bit displacement)
-
reg + reg + disp8/16 (an 8 or 16bit displacement)
-
reg + disp8/16
The registers used (reg) are often somewhat specific, particularly in the addressing modes using multiple registers (usually use the SI or DI registers).
The displacement values are values that following the instruction, like immediates do. An instruction could have a displacement for use a memory operand, and an immediate for the other operand. When combined with prefix bytes instructions can be up to 15 bytes long! (x86).
For 32bit and 64bit memory locations the following addressing modes may be used, they were introduced with the 386:
-
reg
-
reg + disp8/16/32
-
disp32
Now any 32-bit register except esp may be used.
Using an extra byte the 386 and later can also handle:
-
reg + reg*scale + disp8/32
scale is one of 1, 2, 4 or 8.
The encoding of addressing modes, particularly for 64bit mode is fairly complex. There are about 10 pages in the Intel reference manual for this topic, with three whole-page tables.
I read up on this when I found that I could not use a 64 bit number as a memory location, there is no 64-bit displacement. I had to first mov a 64-bit immediate (those do exist) into a register, then use register-indirect addressing to access that memory location.
Why would you use such addressing modes?
The common use is to access a field of a structure or array, or array of structures. You can do so with a single instruction. This means you’re using fewer registers and using less machine code, which takes up less room in memory and caches. A modern CPU may break these up into separate micro-operations to be fed through the pipeline separately. Some CPUs will execute some larger operations in the same time as it takes to execute simpler instructions. And indeed, a modern CPU may fuse together multiple u-ops (yeah, after breaking them apart earlier).
One neat little trick you can do with this is do multiplication and addition, or just multiple additions together in the one instruction, using the lea instruction. lea is load effective address. It calculates the address that would be used, and instead of doing a memory reference, it writes that address into the destination.
lea 6(%eax,%ecx,4), ebx
will execute
ebx = eax + ecx * 4 + 6;
in a single instruction. iximeow reminded me that lea can also be used to multiply by 3, 5 or 9. Which you might normally do with a pair of instructions (shift and add). Multiply eax by 5 and store the result back in eax.
lea (%eax, %eax, 4), %eax
And Matt Giuca reminds us that AT&T syntax sucks. I don’t disagree, particularly for this last example which in Intel syntax is:
lea eax, [eax + eax * 4]
In my defense AT&T syntax was my first x86 syntax, and the only one I’ve ever used in practice. I also prefer the src, dest operand order. Except for those, it sucks, particularly with addressing modes.