Why does compiled x86-64 code for x * 24 contain shift and lea instructions but no multiply?
Shifts and adds are faster than a general multiply, so the compiler strength-reduces a constant multiply into them — e.g. x * 24 = (x + 2x) << 3.
* The compiler rewrites x * 24 as (x + 2x) << 3: one lea builds 3x, one sal $3 multiplies by 8 — no imul at all. *
Because v << k equals v · 2^k for both signed and unsigned values, any multiplication by a compile-time constant can be rebuilt from shifts, adds, and subtracts. For x * 24, note 24 = 3 · 8, so the compiler computes 3x then multiplies by 8 with a shift:
mul24: # x in %rdi, result in %rax
leal (%rdi,%rdi,2), %eax # t = x + x*2 = 3x (lea does the add+scale for free)
sall $3, %eax # return t << 3 = 3x * 8 = 24x
Other constants decompose the same way, sometimes with subtraction:
x * 7 = (x << 3) - x // 8x - x
x * 14 = (x << 4) - (x << 1) // 16x - 2x
Why it matters for reverse engineering: a tight cluster of lea / sal / add / sub with no imul is very often a multiply-by-constant in disguise. Read the shift amounts and adds back out and you can reconstruct the original constant — recognising this pattern is a staple of reading optimised disassembly.
Go deeper:
Compiler Explorer (godbolt.org) — try
return x * 24;and watch the compiler emitlea+salinstead of a multiply.Strength reduction (Wikipedia) — the optimization that swaps costly operations for shifts and adds.