Для мене це просто здається прикольним 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 байт. Тепер розглянемо той самий вираз, який використовується з оператором "адреса" &:
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 Kennedy вище ")
Ще однією важливою особливістю 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
інструкція для множення регістрів на 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
само, як, MOV
але ти фактично не перенаправляєш пам'ять. Іншими словами:
MOV EAX, [ESP+4]
Це перемістить зміст того, на що ESP+4
вказує EAX
.
LEA EAX, [EBX*8]
Це перемістить ефективну адресу EBX * 8
в EAX, а не те, що знаходиться в цьому місці. Як бачите, також можна помножити на два коефіцієнти (масштабування), а а 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 для зберігання даних за адресою, що стосується ВР, маючи змогу сказати
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
Зауважте, що забуття світового "зміщення" призведе до того, що вміст розташування [BP + 8], а не значення 8, буде додано до DI. На жаль
Як зазначаються в існуючих відповідях, 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
більшість інших основних арифметичних операцій на чотирьох різних АЛУ, але може виконуватись лише lea
на одному (складному lea
) або двох (простих lea
). Що ще важливіше, ці lea
АРУ з двома можливостями - це просто два з чотирьох, які можуть виконувати інші вказівки, тому немає переваги паралелізму, як заявлено.
Інструкція 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]
тому, що це спрощення, яке пропонує ОП (наскільки я розумію): 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 (оптимізувати) як варіант компілятора, gcc знайде інструкцію lea для вказаного рядка коду.
Здається, що безліч відповідей уже завершено, я хотів би додати ще один приклад коду, який показує, як інструкції lea та move працюють по-різному, коли вони мають однаковий формат вираження.
Щоб зробити короткий короткий опис, інструкції lea та 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, зберігаючи mov
. Ви також пропустили ранню заяву про вихід. Це наочно демонструє те, що ви намагаєтесь продемонструвати, але також є оманливим поганим прикладом вбудованої ASM, яка порушиться, якщо використовувати її в інших контекстах. Це погана річ для відповіді на переповнення стека.
%%
всі ці імена реєстру у Розширеному asm, використовуйте обмеження введення. як asm("lea 4(%%ebx, %%eax, 8), %%edx" : "=d"(result_of_lea) : "a"(1), "b"(2));
. Якщо дозволити компілятору init регістри, це означає, що вам також не потрібно оголошувати клобери. Ви надмірно ускладнюєте речі за допомогою xor-zeroing перед тим, як mov-негайд також перепише весь реєстр.
mov 4(%ebx, %eax, 8), %edx
це недійсно? Так чи інакше, так як mov
було б сенс написати, "a"(1ULL)
щоб сказати компілятору, що ви маєте 64-бітове значення, і, таким чином, йому потрібно переконатися, що він розширений, щоб заповнити весь реєстр. На практиці це все ще використовуватиметься mov $1, %eax
, оскільки запис EAX zero-extends в RAX, якщо тільки у вас не дивна ситуація з оточуючим кодом, де компілятор знав, що RAX = 0xff00000001
чи щось таке. Тому що lea
ви все ще використовуєте 32-розрядний розмір операнду, тому будь-які збиті високі біти у вхідних регістрах не впливають на 32-бітний результат.
LEA: просто "арифметична" інструкція ..
MOV передає дані між операндами, але lea просто обчислює
mov eax, offset GLOBALVAR
замість цього. Ви можете використовувати LEA, але це трохи більший розмір коду, ніж mov r32, imm32
і працює на меншій кількості портів, оскільки він все ще проходить процес обчислення адреси . lea reg, symbol
корисний лише в 64-розрядному для RIP-відносного 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 не має простого способу обчислити повну лінійну адресу (ефективна + база сегмента) в регістр або пара-регістр.