The Instruction That Computes, Not Loads

lea is the instruction that confuses people the longest, and the instruction that, once it clicks, makes you feel like you can really read assembly. It looks like a memory access but it never touches memory. It is written like a mov from a bracketed operand but it does something subtly different and far more useful.

The name stands for Load Effective Address. Here is the whole idea in one sentence: lea takes a memory operand, computes the address that operand describes, and puts that address into a register, without ever reading from memory.

Contrast it with a normal load. Given [rdi + rsi*4]:

  • mov eax, [rdi+rsi*4] computes the address rdi + rsi*4, then goes to memory at that address and loads the value there into eax.
  • lea rax, [rdi+rsi*4] computes the same address rdi + rsi*4 and stops there, putting the address itself into rax. No memory is accessed.

So lea keeps the address-computing half of a memory operand and throws away the memory-access half. The previous lesson showed how rich those address recipes are (displacement[base + index*scale]). lea hands you that whole calculation as a general-purpose arithmetic engine.

The Honest Use: Taking an Address

The literal purpose of lea is computing the address of something, which is exactly what C++'s & operator and array-element addresses need. Consider:

int* addressOf(int* base, int i)
{
    return &base[i];
}

At -O2:

addressOf(int*, int):
	movsx	rsi, esi
	lea	rax, [rdi+rsi*4]
	ret

Compare this directly with elementAt from the previous lesson, which returned base[i]:

	mov	eax, DWORD PTR [rdi+rsi*4]   ; elementAt: returns the value
	lea	rax, [rdi+rsi*4]            ; addressOf: returns the address

Identical address computation; the only difference is mov versus lea. elementAt dereferences and returns the int; addressOf returns the address itself. That is &base[i] in C++. This is the most natural way to understand lea: it is what & compiles to whenever an address needs to be calculated. You also see it constantly in -O0 code as lea rax, -4[rbp], which is just "compute the address of a local variable."

The Clever Use: Fast Arithmetic

Here is where lea earns its reputation. Because the address recipe base + index*scale + displacement is just arithmetic, and because lea performs that arithmetic and writes the result to a register, the compiler can hijack lea to do ordinary integer math, no memory involved.

Look again at the add function from Chapter 1:

int add(int a, int b)
{
    return a + b;
}
add(int, int):
	lea	eax, [rdi+rsi]
	ret

There is no array, no pointer, nothing to do with memory. a is in rdi, b in rsi, and lea eax, [rdi+rsi] computes rdi + rsi and writes the sum to eax. The compiler used the address calculator as an adder. Why not add? Because lea can read two source registers and write a third destination in one instruction, whereas add eax, esi would overwrite one of its inputs. lea lets the compiler add a + b into eax while leaving both rdi and rsi untouched, all in a single instruction.

It gets more impressive with a richer expression:

int combine(int a, int b)
{
    return a + b * 4 + 7;
}
combine(int, int):
	lea	eax, 7[rdi+rsi*4]
	ret

One instruction. The operand 7[rdi+rsi*4] computes rdi + rsi*4 + 7, which is a + b*4 + 7 exactly. A multiply by 4, an add, and another add, three arithmetic operations, folded into a single lea. This is why you see lea so often in optimized code where you might have expected a sequence of imul and add: when a computation fits the base + index*scale + displacement shape, one lea does the lot.

How to Tell Which Use You Are Looking At

Since lea serves both purposes, how do you know whether a particular lea is computing an address or just doing math? Context, mostly:

  • If the destination register is later dereferenced (used inside brackets in a subsequent mov), the lea was computing an address.
  • If the destination is used as a plain value (returned, added to, compared), the lea was doing arithmetic.

In practice you often do not even need to decide. lea rax, [rdi+rsi*4] computes rdi + rsi*4 either way; whether you call the result "an address" or "a number" is just interpretation. The instruction is the same. That dual nature is precisely what makes lea so handy to the compiler and, at first, so puzzling to the reader.

Two Things lea Does Not Do

A couple of clarifications that save confusion:

  • lea never reads or writes memory. Despite the brackets, the only thing it writes is the destination register, and the only things it reads are the registers inside the brackets. It cannot fault on a bad address because it never visits one.
  • lea does not set flags. Ordinary arithmetic like add and sub update the CPU's status flags (covered in Chapter 3). lea leaves them alone. That is occasionally another reason a compiler prefers lea: it can compute a value without disturbing flags that a nearby comparison still needs.

The Reading Habit

When you hit a lea, do two quick things. First, read the bracket as the arithmetic expression base + index*scale + displacement, the same way you learned to read any memory operand. Second, decide from context whether the result is being used as an address or as a number. With those two moves, lea goes from the most mysterious instruction in the listing to one of the most informative: it often packs an entire small computation into a single line.

Key Takeaways

  • lea (Load Effective Address) computes the address described by a memory operand and stores that address in a register, without accessing memory.
  • The difference from mov: mov eax, [x] loads the value at address x; lea eax, [x] loads the address x itself.
  • Its honest use is taking addresses, which is how C++ & and array-element addresses compile (lea rax, [rdi+rsi*4] for &base[i]).
  • Its clever use is fast arithmetic: lea eax, [rdi+rsi] adds two registers, and lea eax, 7[rdi+rsi*4] computes a + b*4 + 7 in one instruction.
  • Compilers favour lea because it reads multiple registers, writes a separate destination, and does not modify the flags.
  • To read a lea: expand the bracket into base + index*scale + displacement, then use context to tell whether the result is an address or a value.