Для меня это просто похоже на фанк MOV. Каково его назначение и когда я должен его использовать?
Для меня это просто похоже на фанк MOV. Каково его назначение и когда я должен его использовать?
Ответы:
Как уже отмечали другие, 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
.
mov
инструкцию и убрать скобки? MOV EDX, EBX + 8*EAX + 4
MOV
на косвенный источник, за исключением того, что он только косвенный, а не MOV
. На самом деле он не читает с вычисленного адреса, просто вычисляет его.
Из «Дзен Собрания» Абраша:
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 EAX, [ EAX + EBX + 1234567 ]
вычисляет сумму EAX
, EBX
и 1234567
(это три операнда). LEA EAX, [ EBX + ECX ]
рассчитывает EBX + ECX
без переопределения либо с результатом. Третье, для чего LEA
используется (не перечислено Фрэнком) умножение на константу (на два, три, пять или девять), если вы используете это как LEA EAX, [ EBX + N * EBX ]
( N
может быть 1,2,4,8). Другой вариант использования удобен в циклах: разница между LEA EAX, [ EAX + 1 ]
и INC EAX
заключается в том, что последний меняется, EFLAGS
а первый нет; это сохраняет CMP
состояние
LEA
показывает, какие виды «уловок» можно использовать для ... (см. «LEA (эффективный адрес загрузки) часто используется в качестве« уловки »для выполнения определенных вычислений» в популярном ответе IJ Кеннеди выше)
Еще одной важной особенностью LEA
инструкции является то, что она не изменяет коды состояния, такие как CF
и ZF
, при вычислении адреса с помощью арифметических команд, таких как ADD
или MUL
делает. Эта функция снижает уровень зависимости между инструкциями и тем самым освобождает место для дальнейшей оптимизации компилятором или аппаратным планировщиком.
lea
иногда полезно, чтобы компилятор (или человеческий кодер) выполнял математику, не забивая результат флага. Но lea
не быстрее чем add
. Большинство инструкций x86 пишут флаги. Высокопроизводительные реализации x86 должны переименовывать EFLAGS или иным образом избегать опасности записи после записи для нормального выполнения кода, поэтому инструкции, которые избегают флаговых записей, не лучше из-за этого. ( неполный флаг может создать проблемы, см. инструкцию INC против ADD 1: имеет ли это значение? )
Несмотря на все объяснения, LEA является арифметической операцией:
LEA Rt, [Rs1+a*Rs2+b] => Rt = Rs1 + a*Rs2 + b
Просто его название крайне глупо для операции shift + add. Причина этого уже была объяснена в самых рейтинговых ответах (т. Е. Она была разработана для непосредственного сопоставления высокоуровневых ссылок на память).
LEA
на AGU, а на обычных целочисленных ALU. В наши дни нужно очень внимательно прочитать спецификации процессора, чтобы выяснить, «где все работает» ...
LEA
дает вам адрес, который возникает в любом режиме адресации, связанном с памятью. Это не операция сдвига и добавления.
Может быть, просто еще одна вещь о инструкции 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 EAX, [EAX*3]
?
shl
команду shift left как инструкцию для умножения регистров на 2,4,8,16 ... это быстрее и короче. Но для умножения на числа, отличающиеся степенью 2, мы обычно используем mul
инструкцию, которая более претенциозна и медленнее.
lea eax,[eax*3]
будет переводить в эквивалент lea eax,[eax+eax*2]
.
lea
это сокращение от "эффективный адрес загрузки". Он загружает адрес ссылки на местоположение исходного операнда в целевой операнд. Например, вы можете использовать его для:
lea ebx, [ebx+eax*8]
перемещать элементы ebx
указателя eax
дальше (в 64-битном массиве / элементном массиве) с помощью одной инструкции. По сути, вы получаете преимущества от сложных режимов адресации, поддерживаемых архитектурой x86, для эффективного управления указателями.
Самая большая причина, по которой вы используете « LEA
а», MOV
заключается в том, что вам нужно выполнить арифметику с регистрами, которые вы используете для вычисления адреса. По сути, вы можете выполнить то, что равнозначно арифметике указателей в нескольких регистрах в комбинации, для «бесплатно».
Что действительно сбивает с толку, так это то, что вы обычно пишете LEA
как a, MOV
но на самом деле вы не разыменовываете память. Другими словами:
MOV EAX, [ESP+4]
Это переместит содержание того, на что ESP+4
указывает EAX
.
LEA EAX, [EBX*8]
Это переместит эффективный адрес EBX * 8
в EAX, а не тот, который находится в этом месте. Как вы можете видеть, также можно умножить на два (масштабирование), в то время как a MOV
ограничено сложением / вычитанием.
LEA
делает.
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
имеет преимущества выполнения арифметики адресации памяти без обращения к памяти, сохранения арифметического результата в другом регистре вместо простой формы инструкции добавления. Реальное основное преимущество в производительности заключается в том, что современный процессор имеет отдельный блок 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
на одном или нескольких из тех же ALU, которые выполняют другие арифметические инструкции (но, как правило, их меньше, чем других арифметических операций). Например, упомянутый процессор Haswell может выполнять add
или выполнять sub
большинство других основных арифметических операций на четырех различных ALU, но может выполняться только lea
на одном (сложном lea
) или двух (простых lea
). Что еще более важно, эти два- lea
способных ALU - это просто два из четырех, которые могут выполнять другие инструкции, поэтому, как утверждается, преимущества параллелизма нет.
Инструкция LEA может использоваться, чтобы избежать трудоемких вычислений эффективных адресов процессором. Если адрес используется неоднократно, более эффективно сохранять его в регистре, а не вычислять эффективный адрес каждый раз, когда он используется.
[esi]
редко бывает дешевле, чем сказать, [esi + 4200]
и только редко дешевле, чем [esi + ecx*8 + 4200]
.
[esi]
не дешевле чем [esi + ecx*8 + 4200]
. Но зачем сравнивать? Они не эквивалентны. Если вы хотите, чтобы первое указывало ту же ячейку памяти, что и второе, вам нужны дополнительные инструкции: вам нужно прибавить к esi
значению, ecx
умноженному на 8. Ой, умножение приведет к засорению ваших флагов ЦП! Затем вы должны добавить 4200. Эти дополнительные инструкции увеличивают размер кода (занимают место в кеше команд, циклы выборки).
[esi + 4200]
в последовательности инструкций, то лучше сначала загрузить эффективный адрес в регистр и использовать его. Например, вместо того, чтобы писать add eax, [esi + 4200]; add ebx, [esi + 4200]; add ecx, [esi + 4200]
, вы должны предпочесть lea edi, [esi + 4200]; add eax, [edi]; add ebx, [edi]; add ecx, [edi]
, что редко бывает быстрее. По крайней мере, это простое толкование этого ответа.
[esi]
и [esi + 4200]
(или [esi + ecx*8 + 4200]
заключается в том, что это упрощение, которое предлагает OP (насколько я понимаю): что N инструкций с одинаковым комплексным адресом преобразуются в N инструкций с простой (одна рег) адресацией плюс одна lea
, так как сложная адресация «отнимает много времени». На самом деле она медленнее даже на современном x86, но только с задержкой, что вряд ли будет иметь значение для последовательных инструкций с одним и тем же адресом
lea
чтобы в этом случае он увеличился. В общем, хранение промежуточных звеньев - причина регистрационного давления, а не его решение - но я думаю, что в большинстве случаев это промывка. @Kaz
Инструкция 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
исключать эту возможность, хотя бы потому, что есть более короткая альтернатива; это просто объединение в ортогональной форме с доступными режимами адресации.
Вот пример.
// 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 и 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
=d
чтобы сообщить компилятору результат в EDX, сохранив a mov
. Вы также пропустили раннюю клобберную декларацию на выходе. Это действительно демонстрирует то, что вы пытаетесь продемонстрировать, но также является вводящим в заблуждение плохим примером встроенного asm, который сломается, если используется в других контекстах. Это плохая вещь для ответа переполнения стека.
%%
все эти имена регистров в Extended asm, используйте ограничения ввода. как asm("lea 4(%%ebx, %%eax, 8), %%edx" : "=d"(result_of_lea) : "a"(1), "b"(2));
. Разрешение регистров инициализации компилятора означает, что вам также не нужно объявлять клобберы. Вы слишком усложняете вещи, обнуляя xor до того, как mov-немедленное перезаписывает также весь регистр.
mov 4(%ebx, %eax, 8), %edx
это недействительно? В любом случае, да, поскольку mov
было бы разумно написать "a"(1ULL)
компилятору, что у вас есть 64-битное значение, и поэтому ему нужно убедиться, что оно расширено для заполнения всего регистра. На практике он все еще будет использоваться mov $1, %eax
, потому что написание EAX с нулевым расширением распространяется на RAX, если только у вас нет странной ситуации с окружающим кодом, когда компилятор знал, что RAX = 0xff00000001
или что-то еще. Ведь lea
вы все еще используете 32-битный размер операнда, поэтому любые старшие биты во входных регистрах не влияют на 32-битный результат.
LEA: просто "арифметическая" инструкция ..
MOV передает данные между операндами, но Леа только вычисляет
mov eax, offset GLOBALVAR
вместо. Вы можете использовать LEA, но он немного больше по размеру кода, чем mov r32, imm32
и работает на меньшем количестве портов, потому что он все еще проходит процесс вычисления адреса . lea reg, symbol
полезно только в 64-битном режиме для REA-относительного LEA, когда вам нужны PIC и / или адреса за пределами младших 32-битных. В 32 или 16-битном коде преимущество нулевое. LEA - это арифметическая инструкция, раскрывающая способность ЦП декодировать / вычислять режимы адресации.
imul eax, edx, 1
он не рассчитывается: он просто копирует edx в eax. Но на самом деле он пропускает ваши данные через множитель с задержкой в 3 цикла. Или это rorx eax, edx, 0
просто копии (повернуть на ноль).
Все обычные «расчетные» инструкции, такие как добавление умножения, исключение или установка флагов состояния, таких как ноль, знак. Если вы используете сложный адрес, AX xor:= mem[0x333 +BX + 8*CX]
флаги устанавливаются в соответствии с операцией xor.
Теперь вы можете использовать адрес несколько раз. Загрузка таких адресов в реестр никогда не предназначена для установки флагов состояния, и, к счастью, это не так. Фраза «загрузить эффективный адрес» заставляет программиста осознать это. Отсюда и странное выражение.
Понятно, что когда процессор способен использовать сложный адрес для обработки своего контента, он может рассчитывать его для других целей. Действительно, его можно использовать для выполнения преобразования x <- 3*x+1
в одной инструкции. Это общее правило в программировании на ассемблере: используйте инструкции, однако это раскачивает вашу лодку.
Единственное, что имеет значение, это то, полезно ли вам конкретное преобразование, воплощенное в инструкции.
Нижняя граница
MOV, X| T| AX'| R| BX|
а также
LEA, AX'| [BX]
имеют такой же эффект на AX, но не на флаги состояния. (Это запись ciasdis .)
call lbl
lbl: pop rax
техническая «работа» как способ получить ценность rip
, но вы сделаете предсказание ветвлений очень несчастным. Используйте инструкции по
Простите, если кто-то уже упоминал, но во времена x86, когда сегментация памяти все еще была актуальна, вы можете не получить те же результаты из этих двух инструкций:
LEA AX, DS:[0x1234]
а также
LEA AX, CS:[0x1234]
seg:off
пары. LEA не зависит от базы сегмента; обе эти инструкции будут (неэффективно) помещены 0x1234
в AX. К сожалению, в x86 нет простого способа вычислить полный линейный адрес (эффективная + сегментная база) в регистр или пару регистров.