Какова цель инструкции LEA?
Для меня это просто похоже на фанк MOV. Каково его назначение и когда я должен его использовать?
18 ответов
Как уже отмечали другие, LEA (эффективный адрес загрузки) часто используется как "трюк" для выполнения определенных вычислений, но это не является его основной целью. Набор команд x86 был разработан для поддержки языков высокого уровня, таких как Pascal и C, где обычно используются массивы, особенно массивы целых или небольшие структуры. Рассмотрим, например, структуру, представляющую (x, y) координаты:
struct Point
{
int xcoord;
int ycoord;
};
Теперь представьте себе заявление вроде:
int y = points[i].ycoord;
где points[]
это массив Point
, Предполагая, что база массива уже находится в EBX
и переменная i
в EAX
, а также xcoord
а также ycoord
каждый 32 бит (так ycoord
находится в смещении 4 байта в структуре), этот оператор может быть скомпилирован в:
MOV EDX, [EBX + 8*EAX + 4] ; right side is "effective address"
который приземлится y
в EDX
, Коэффициент масштабирования 8, потому что каждый Point
имеет размер 8 байт. Теперь рассмотрим то же выражение, которое используется с оператором "address of" &:
int *p = &points[i].ycoord;
В этом случае вы не хотите, чтобы значение ycoord
, но его адрес. Это где LEA
(загрузить эффективный адрес) приходит. Вместо MOV
, компилятор может генерировать
LEA ESI, [EBX + 8*EAX + 4]
который загрузит адрес в ESI
,
Из "Дзен Собрания" Абраша:
LEA
единственная инструкция, которая выполняет вычисления адресации памяти, но фактически не обращается к памяти.LEA
принимает стандартный операнд адресации памяти, но ничего не делает, кроме как сохраняет вычисленное смещение памяти в указанном регистре, который может быть любым регистром общего назначения.Что это нам дает? Две вещи, которые
ADD
не обеспечивает:
- способность выполнять сложение с двумя или тремя операндами, и
- возможность сохранения результата в любом регистре; не только один из исходных операндов.
А также LEA
не изменяет флаги.
Примеры
LEA EAX, [ EAX + EBX + 1234567 ]
исчисляетEAX + EBX + 1234567
(это три операнда)LEA EAX, [ EBX + ECX ]
исчисляетEBX + ECX
без переопределения ни с результатом.- умножение на константу (на два, три, пять или девять), если вы используете его как
LEA EAX, [ EBX + N * EBX ]
(N может быть 1,2,4,8).
Другой случай использования удобен в циклах: разница между LEA EAX, [ EAX + 1 ]
а также INC EAX
является то, что последний меняется EFLAGS
но первое не делает; это сохраняет CMP
государство.
Еще одна важная особенность LEA
инструкция заключается в том, что он не изменяет коды условий, такие как CF
а также ZF
при вычислении адреса с помощью арифметических инструкций, таких как ADD
или же MUL
делает. Эта функция снижает уровень зависимости между инструкциями и тем самым освобождает место для дальнейшей оптимизации компилятором или аппаратным планировщиком.
Несмотря на все объяснения, LEA - это арифметическая операция:
LEA Rt, [Rs1+a*Rs2+b] => Rt = Rs1 + a*Rs2 + b
Просто его название крайне глупо для операции shift+add. Причина этого уже была объяснена в самых рейтинговых ответах (т. Е. Она была разработана для непосредственного отображения ссылок на память высокого уровня).
Может быть, просто еще одна вещь о инструкции LEA. Вы также можете использовать LEA для быстрого умножения регистров на 3, 5 или 9.
LEA EAX, [EAX * 2 + EAX] ;EAX = EAX * 3
LEA EAX, [EAX * 4 + EAX] ;EAX = EAX * 5
LEA EAX, [EAX * 8 + EAX] ;EAX = EAX * 9
lea
это сокращение от "эффективный адрес загрузки". Он загружает адрес ссылки на местоположение исходного операнда в целевой операнд. Например, вы можете использовать его для:
lea ebx, [ebx+eax*8]
двигаться ebx
указатель eax
элементы далее (в массиве 64-бит / элемент) с одной инструкцией. По сути, вы получаете преимущества от сложных режимов адресации, поддерживаемых архитектурой x86, для эффективного управления указателями.
Самая большая причина, которую вы используете LEA
через MOV
если вам нужно выполнить арифметику для регистров, которые вы используете для вычисления адреса. По сути, вы можете выполнить то, что равнозначно арифметике указателей в нескольких регистрах в комбинации, для "бесплатно".
Что действительно смущает, так это то, что вы обычно пишете LEA
совсем как MOV
но вы на самом деле не разыменовываете память. Другими словами:
MOV EAX, [ESP+4]
Это переместит содержание того, что ESP+4
указывает на EAX
,
LEA EAX, [EBX*8]
Это переместит эффективный адрес EBX * 8
в EAX, а не то, что находится в этом месте. Как вы можете видеть, также можно умножить в два раза (масштабирование) в то время как MOV
ограничивается сложением / вычитанием.
8086 имеет большое семейство команд, которые принимают операнд регистра и эффективный адрес, выполняют некоторые вычисления, чтобы вычислить смещенную часть этого эффективного адреса, и выполняют некоторую операцию, включающую регистр и память, указанную в вычисленном адресе. Было довольно просто заставить одну из инструкций в этом семействе вести себя так же, как указано выше, за исключением того, что пропускалась эта фактическая операция с памятью. Это, инструкции:
mov ax,[bx+si+5]
lea ax,[bx+si+5]
были реализованы почти одинаково внутри. Разница - пропущенный шаг. Обе инструкции работают примерно так:
temp = fetched immediate operand (5)
temp += bx
temp += si
address_out = temp (skipped for LEA)
trigger 16-bit read (skipped for LEA)
temp = data_in (skipped for LEA)
ax = temp
Что касается того, почему Intel считала, что эта инструкция стоит того, чтобы ее включить, я не совсем уверен, но тот факт, что ее реализация была дешевой, был бы важным фактором. Другим фактором мог быть тот факт, что ассемблер Intel позволял определять символы относительно регистра BP. Если fnord
был определен как относительный символ BP (например, BP+8), можно сказать:
mov ax,fnord ; Equivalent to "mov ax,[BP+8]"
Если кто-то хотел использовать что-то вроде stosw для хранения данных по адресу, относящемуся к BP, он мог сказать
mov ax,0 ; Data to store
mov cx,16 ; Number of words
lea di,fnord
rep movs fnord ; Address is ignored EXCEPT to note that it's an SS-relative word ptr
было удобнее чем:
mov ax,0 ; Data to store
mov cx,16 ; Number of words
mov di,bp
add di,offset fnord (i.e. 8)
rep movs fnord ; Address is ignored EXCEPT to note that it's an SS-relative word ptr
Обратите внимание, что если забыть о "смещении" мира, в DI будет добавлено содержимое местоположения [BP+8], а не значение 8. К сожалению.
Инструкция LEA (Load Effective Address) - это способ получения адреса, который возникает в любом из режимов адресации памяти процессора Intel.
То есть, если у нас есть движение данных, как это:
MOV EAX, <MEM-OPERAND>
он перемещает содержимое назначенной ячейки памяти в целевой регистр.
Если мы заменим MOV
от LEA
то адрес ячейки памяти вычисляется точно так же <MEM-OPERAND>
обращаясь к выражению. Но вместо содержимого ячейки памяти мы получаем само местоположение в месте назначения.
LEA
не является конкретной арифметической инструкцией; это способ перехвата эффективного адреса, возникающего в любом из режимов адресации памяти процессора.
Например, мы можем использовать LEA
только по простому прямому адресу. Никакая арифметика не используется вообще:
MOV EAX, GLOBALVAR ; fetch the value of GLOBALVAR into EAX
LEA EAX, GLOBALVAR ; fetch the address of GLOBALVAR into EAX.
Это действительно; мы можем проверить это в командной строке Linux:
$ as
LEA 0, %eax
$ objdump -d a.out
a.out: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <.text>:
0: 8d 04 25 00 00 00 00 lea 0x0,%eax
Здесь нет добавления масштабированного значения и смещения. Ноль перемещается в EAX. Мы могли бы сделать это, используя MOV с непосредственным операндом.
Это причина, почему люди, которые думают, что скобки в LEA
лишние - сильно ошибаются; скобки не LEA
синтаксис, но являются частью режима адресации.
LEA реально на аппаратном уровне. Сгенерированная инструкция кодирует фактический режим адресации, и процессор выполняет его до момента вычисления адреса. Затем он перемещает этот адрес к месту назначения вместо генерации ссылки на память. (Поскольку вычисление адреса в режиме адресации в любой другой инструкции не влияет на флаги ЦП, LEA
не влияет на флаги процессора.)
Контраст с загрузкой значения с нулевого адреса:
$ as
movl 0, %eax
$ objdump -d a.out | grep mov
0: 8b 04 25 00 00 00 00 mov 0x0,%eax
Это очень похожая кодировка, понимаете? Просто 8d
из LEA
изменился на 8b
,
Конечно, это LEA
кодирование длиннее, чем перемещение непосредственного нуля в EAX
:
$ as
movl $0, %eax
$ objdump -d a.out | grep mov
0: b8 00 00 00 00 mov $0x0,%eax
Там нет причин для LEA
исключить эту возможность, хотя бы потому, что существует более короткая альтернатива; это просто объединение в ортогональной форме с доступными режимами адресации.
Как уже упоминалось в существующих ответах, LEA
имеет преимущества выполнения арифметики адресации памяти без доступа к памяти, сохраняя арифметический результат в другом регистре вместо простой формы инструкции добавления. Реальное преимущество в производительности заключается в том, что современный процессор имеет отдельный блок LEA ALU и порт для эффективной генерации адреса (включая LEA
и другой адрес памяти), это означает, что арифметическая операция в LEA
и другие нормальные арифметические операции в ALU могут выполняться параллельно в одном ядре.
Посмотрите эту статью об архитектуре Haswell, чтобы узнать подробности об устройстве LEA: http://www.realworldtech.com/haswell-cpu/4/
Еще один важный момент, который не упоминается в других ответах: LEA REG, [MemoryAddress]
инструкция является PIC (позиционно-независимым кодом), который кодирует относительный адрес ПК в этой инструкции для справки MemoryAddress
, Это отличается от MOV REG, MemoryAddress
который кодирует относительный виртуальный адрес и требует перемещения / исправления в современных операционных системах (например, ASLR является обычной функцией). Так LEA
может быть использован для преобразования таких не PIC в PIC.
Кажется, что многие ответы уже завершены, я хотел бы добавить еще один пример кода для демонстрации того, как команды lea и move работают по-разному, когда они имеют одинаковый формат выражения.
Короче говоря, можно использовать как инструкции Le, так и инструкции MOV с круглыми скобками, включающими операнд src инструкций. Когда они заключены в (), выражение в () вычисляется таким же образом; однако две инструкции по-разному интерпретируют вычисленное значение в операнде src.
Независимо от того, используется ли выражение с lea или mov, значение src рассчитывается, как показано ниже.
D (Rb, Ri, S) => (Reg [Rb] + S * Reg [Ri] + D)
Однако, когда он используется с инструкцией mov, он пытается получить доступ к значению, указанному адресом, сгенерированным вышеприведенным выражением, и сохранить его в месте назначения.
В отличие от этого, когда инструкция lea выполняется с вышеприведенным выражением, она загружает сгенерированное значение в том виде, в котором оно находится, к месту назначения.
Приведенный ниже код выполняет инструкцию lea и инструкцию mov с одним и тем же параметром. Однако, чтобы уловить разницу, я добавил обработчик сигнала уровня пользователя, чтобы отследить ошибку сегментации, вызванную обращением к неправильному адресу в результате команды mov.
Пример кода
#define _GNU_SOURCE 1 /* To pick up REG_RIP */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <signal.h>
uint32_t
register_handler (uint32_t event, void (*handler)(int, siginfo_t*, void*))
{
uint32_t ret = 0;
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_sigaction = handler;
act.sa_flags = SA_SIGINFO;
ret = sigaction(event, &act, NULL);
return ret;
}
void
segfault_handler (int signum, siginfo_t *info, void *priv)
{
ucontext_t *context = (ucontext_t *)(priv);
uint64_t rip = (uint64_t)(context->uc_mcontext.gregs[REG_RIP]);
uint64_t faulty_addr = (uint64_t)(info->si_addr);
printf("inst at 0x%lx tries to access memory at %ld, but failed\n",
rip,faulty_addr);
exit(1);
}
int
main(void)
{
int result_of_lea = 0;
register_handler(SIGSEGV, segfault_handler);
//initialize registers %eax = 1, %ebx = 2
// the compiler will emit something like
// mov $1, %eax
// mov $2, %ebx
// because of the input operands
asm("lea 4(%%rbx, %%rax, 8), %%edx \t\n"
:"=d" (result_of_lea) // output in EDX
: "a"(1), "b"(2) // inputs in EAX and EBX
: // no clobbers
);
//lea 4(rbx, rax, 8),%edx == lea (rbx + 8*rax + 4),%edx == lea(14),%edx
printf("Result of lea instruction: %d\n", result_of_lea);
asm volatile ("mov 4(%%rbx, %%rax, 8), %%edx"
:
: "a"(1), "b"(2)
: "edx" // if it didn't segfault, it would write EDX
);
}
Результат выполнения
Result of lea instruction: 14
inst at 0x4007b5 tries to access memory at 14, but failed
Инструкция LEA может использоваться, чтобы избежать трудоемких вычислений эффективных адресов процессором. Если адрес используется неоднократно, более эффективно сохранять его в регистре, а не вычислять эффективный адрес каждый раз, когда он используется.
Вот пример.
// compute parity of permutation from lexicographic index
int parity (int p)
{
assert (p >= 0);
int r = p, k = 1, d = 2;
while (p >= k) {
p /= d;
d += (k << 2) + 6; // only one lea instruction
k += 2;
r ^= p;
}
return r & 1;
}
С опцией -O (optimize) в качестве опции компилятора, gcc найдет инструкцию lea для указанной строки кода.
не в стиле фанк. Когда вы используете, он вычисляет адрес и обращается к памяти. просто вычисляет адрес, на самом деле он не обращается к памяти. В этом разница. просто устанавливает сумму до двух исходных регистров (один может быть умножен) и непосредственное значение регистра назначения. Например,
lea bp, [bx*2+si+3]
устанавливает в регистр bp сумму
bx
умноженный на два плюс
si
плюс 3. Вы не можете выполнить этот расчет, чтобы сохранить результат в регистр с
MOV
.
В отличие от
LEA
, есть инструкции и, наоборот, загружают значения из памяти в пару регистров: один сегментный регистр (
DS
или
ES
) и один общий регистр. Также есть версии для других регистров:
LFS
,
LGS
и
LSS
для
FS
,
GS
и
SS
сегментные регистры соответственно (введены в 80386).
Таким образом, эти инструкции загружают "дальний" указатель - указатель, состоящий из 16-битного селектора сегмента и 16-битного (или 32-битного, в зависимости от режима) смещения, поэтому общий размер дальнего указателя был 32-битным в 16-битный режим и 48-битный в 32-битном режиме.
Это удобные инструкции для 16-битного режима, будь то 16-битный реальный режим или 16-битный защищенный режим.
В 32-битном режиме эти инструкции не нужны, поскольку все регистры сегментов по существу указывают на одни и те же места, поэтому нет необходимости загружать регистры сегментов.
В 64-битных режимах эти инструкции не реализованы. Их коды операций выдают прерывание (исключение) при нарушении доступа. Начиная с реализации Intel VEX - "векторные расширения - (SSE2) - Intel взяла свои коды операций
LDS
и
LES
и начал использовать их для префиксов VEX. В результате 256-битные регистры AVX (YMM) доступны только в 64-битном режиме.
LEA: просто "арифметическая" инструкция..
MOV передает данные между операндами, но Леа только вычисляет
Все обычные "расчетные" инструкции, такие как сложение умножения, исключение или установка флагов состояния, таких как ноль, знак. Если вы используете сложный адрес,AX xor:= mem[0x333 +BX + 8*CX]
флаги устанавливаются в соответствии с операцией xor.
Теперь вы можете использовать адрес несколько раз. Загрузка таких адресов в регистр никогда не предназначена для установки флагов состояния, и, к счастью, это не так. Фраза "загрузить эффективный адрес" дает понять программисту об этом. Вот откуда взялось странное выражение.
Понятно, что как только процессор сможет использовать сложный адрес для обработки своего содержимого, он сможет вычислить его для других целей. Действительно, его можно использовать для преобразованияx <- 3*x+1
в одной инструкции. Это общее правило программирования на ассемблере: используйте инструкции, как бы они ни были. Единственное, что имеет значение, - это то, полезно ли вам конкретное преобразование, воплощенное в инструкции.
Нижняя граница
MOV, X| T| AX'| R| BX|
а также
LEA, AX'| [BX]
имеют такое же влияние на AX, но не на флаги состояния. (Это обозначение ciasdis.)
Простите меня, если кто-то уже упоминал, но во времена x86, когда сегментация памяти все еще была актуальной, вы могли не получить те же результаты из этих двух инструкций:
LEA AX, DS:[0x1234]
а также
LEA AX, CS:[0x1234]
Это потому, что вместо того, чтобы написать код
mov dx,offset something
ты можешь просто написать
lea dx,something