Pour moi, cela ressemble à un MOV génial. Quel est son but et quand devrais-je l'utiliser?
Comme d'autres l'ont fait remarquer, le LEA (adresse efficace de chargement) est souvent utilisé comme une "astuce" pour effectuer certains calculs, mais ce n'est pas son objectif principal. Le jeu d'instructions x86 a été conçu pour prendre en charge les langages de haut niveau tels que Pascal et C, où les tableaux, notamment les tableaux d'ints ou de petites structures, sont courants. Considérons, par exemple, une structure représentant les coordonnées (x, y):
struct Point
{
int xcoord;
int ycoord;
};
Maintenant, imaginez une déclaration comme:
int y = points[i].ycoord;
où points[]
est un tableau de Point
. En supposant que la base du tableau est déjà dans EBX
et que la variable i
est dans EAX
, et que xcoord
et ycoord
sont chacun de 32 bits (donc ycoord
est décalé de 4 octets dans la structure), cette instruction peut être compilée comme suit:
MOV EDX, [EBX + 8*EAX + 4] ; right side is "effective address"
qui atterrira y
dans EDX
. Le facteur d'échelle de 8 est dû au fait que chaque Point
a une taille de 8 octets. Considérons maintenant la même expression utilisée avec l'opérateur "address of" &:
int *p = &points[i].ycoord;
Dans ce cas, vous ne voulez pas la valeur de ycoord
, mais son adresse. C'est là que LEA
(adresse effective du chargement) entre en jeu. Au lieu d'une MOV
, le compilateur peut générer
LEA ESI, [EBX + 8*EAX + 4]
qui chargera l'adresse dans ESI
.
De la "Zen of Assembly" par Abrash:
LEA
, la seule instruction qui effectue des calculs d'adressage mémoire mais ne s'adresse pas réellement à la mémoire.LEA
accepte un opérande d'adressage mémoire standard, mais ne fait rien d'autre que stocker le décalage de mémoire calculé dans le registre spécifié, qui peut être n'importe quel registre à usage général.Qu'est-ce que cela nous donne? Deux choses que
ADD
ne fournit pas:
- la possibilité d'effectuer une addition avec deux ou trois opérandes, et
- la possibilité de stocker le résultat dans any register; pas seulement l'un des opérandes source.
Et LEA
ne modifie pas les drapeaux.
Exemples
LEA EAX, [ EAX + EBX + 1234567 ]
calcule EAX + EBX + 1234567
(c'est-à-dire trois opérandes)LEA EAX, [ EBX + ECX ]
calcule EBX + ECX
sans remplacer le résultat.LEA EAX, [ EBX + N * EBX ]
(N peut être 1,2,4,8).Une autre utilisation est pratique dans les boucles: la différence entre LEA EAX, [ EAX + 1 ]
et INC EAX
est que ce dernier change EFLAGS
mais le premier ne change pas; cela préserve l'état CMP
.
Une autre caractéristique importante de l'instruction LEA
est qu'elle ne modifie pas les codes de condition tels que CF
et ZF
, tout en calculant l'adresse par des instructions arithmétiques telles que ADD
ou MUL
Cette fonctionnalité diminue le niveau de dépendance entre les instructions et laisse donc la place à une optimisation ultérieure par le compilateur ou le planificateur matériel.
Malgré toutes les explications, LEA est une opération arithmétique:
LEA Rt, [Rs1+a*Rs2+b] => Rt = Rs1 + a*Rs2 + b
C'est juste que son nom est extrêmement stupide pour une opération shift + add. La raison en était déjà expliquée dans les réponses les mieux notées (c’est-à-dire qu’il était conçu pour mapper directement les références de mémoire de haut niveau).
Peut-être juste une autre chose à propos de l’instruction LEA ... Vous pouvez aussi utiliser LEA pour multiplier rapidement les registres par 3, 5 ou 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
est une abréviation de "load effective address". Il charge l'adresse de la référence d'emplacement de l'opérande source dans l'opérande de destination. Par exemple, vous pouvez l'utiliser pour:
lea ebx, [ebx+eax*8]
pour déplacer les éléments ebx
pointeur eax
(dans un tableau de 64 bits/élément) avec une seule instruction. Fondamentalement, vous bénéficiez de modes d'adressage complexes pris en charge par l'architecture x86 pour manipuler efficacement les pointeurs.
La principale raison pour laquelle vous utilisez LEA
par rapport à MOV
est si vous devez effectuer une opération arithmétique sur les registres que vous utilisez pour calculer l'adresse. Effectivement, vous pouvez effectuer ce que revient à une arithmétique de pointeur sur plusieurs registres combinés efficacement pour "libre".
Ce qui est vraiment déroutant, c’est que vous écrivez généralement une LEA
tout comme un MOV
mais vous ne déréférenciez pas la mémoire. En d'autres termes:
MOV EAX, [ESP+4]
Cela déplacera le contenu de ce que ESP+4
pointe vers EAX
.
LEA EAX, [EBX*8]
Cela déplacera l'adresse effective EBX * 8
dans EAX et non ce qui se trouve à cet emplacement. Comme vous pouvez le constater, il est également possible de multiplier par deux (échelle), alors que MOV
se limite à l’ajout/soustraction.
Le 8086 a une grande famille d'instructions qui acceptent un opérande de registre et une adresse effective, effectuent certains calculs pour calculer la partie de décalage de cette adresse effective et effectuent une opération impliquant le registre et la mémoire auxquels l'adresse calculée est associée. Il était assez simple d’avoir l’une des instructions de cette famille qui se comporte comme ci-dessus, à l’exception du saut de cette opération de mémoire. Ceci, les instructions:
mov ax,[bx+si+5]
lea ax,[bx+si+5]
ont été mis en œuvre presque identiquement en interne. La différence est une étape sautée. Les deux instructions fonctionnent quelque chose comme:
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
Quant à savoir pourquoi Intel pensait que cette instruction valait la peine d’être incluse, je ne suis pas tout à fait sûr, mais le fait qu’elle soit peu coûteuse à mettre en œuvre aurait été un facteur déterminant. Un autre facteur aurait été le fait que l’assembleur d’Intel a permis la définition de symboles par rapport au registre du tiers. Si fnord
était défini comme un symbole relatif à BP (par exemple, BP + 8), on pourrait dire:
mov ax,fnord ; Equivalent to "mov ax,[BP+8]"
Si on voulait utiliser quelque chose comme Stosw pour stocker des données à une adresse relative au partenaire, pouvoir dire:
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
était plus pratique que:
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
Notez qu'oublier le "décalage" dans le monde ferait que le contenu de l'emplacement [BP + 8], plutôt que la valeur 8, soit ajouté à DI. Oops.
Comme indiqué dans les réponses existantes, l'option LEA
présente les avantages d'effectuer un calcul arithmétique d'adressage mémoire sans accéder à la mémoire, en enregistrant le résultat arithmétique dans un registre différent au lieu de la simple forme d'une instruction add. L’avantage réel en termes de performances réside dans le fait que le processeur moderne dispose d’une unité ALU LEA et d’un port distincts pour la génération d’adresses (y compris LEA
et une autre adresse de référence de mémoire), ce qui signifie que l’opération arithmétique est dans LEA
le fonctionnement en ALU pourrait se faire en parallèle dans un noyau.
Consultez cet article sur l'architecture Haswell pour plus de détails sur l'unité LEA: http://www.realworldtech.com/haswell-cpu/4/
Un autre point important qui n’est pas mentionné dans les autres réponses est l’instruction LEA REG, [MemoryAddress]
, c’est le code PIC (code indépendant de la position) qui code l’adresse relative du PC dans cette instruction pour référencer MemoryAddress
. Ceci diffère de MOV REG, MemoryAddress
qui code l'adresse virtuelle relative et nécessite un déplacement/un correctif dans les systèmes d'exploitation modernes (comme ASLR est une fonctionnalité courante). Donc LEA
peut être utilisé pour convertir un tel PIC en PIC.
L'instruction LEA peut être utilisée pour éviter des calculs fastidieux d'adresses effectives par la CPU. Si une adresse est utilisée à plusieurs reprises, il est plus efficace de la stocker dans un registre plutôt que de calculer l'adresse effective à chaque utilisation.
L'instruction LEA (Load Effective Address) est un moyen d'obtenir l'adresse issue de n'importe quel mode d'adressage mémoire du processeur Intel.
C'est-à-dire, si nous avons un mouvement de données comme ceci:
MOV EAX, <MEM-OPERAND>
il déplace le contenu de l'emplacement de mémoire désigné dans le registre cible.
Si nous remplaçons MOV
par LEA
, l'adresse de l'emplacement mémoire est calculée exactement de la même manière par l'expression d'adressage <MEM-OPERAND>
. Mais au lieu du contenu de l'emplacement de mémoire, nous obtenons l'emplacement lui-même dans la destination.
LEA
n'est pas une instruction arithmétique spécifique; c'est un moyen d'intercepter l'adresse effective issue de l'un des modes d'adressage mémoire du processeur.
Par exemple, nous pouvons utiliser LEA
sur une simple adresse directe. Aucune arithmétique n'est impliquée:
MOV EAX, GLOBALVAR ; fetch the value of GLOBALVAR into EAX
LEA EAX, GLOBALVAR ; fetch the address of GLOBALVAR into EAX.
Ceci est valide nous pouvons le tester à l'invite de 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
Ici, il n'y a pas d'ajout de valeur mise à l'échelle, ni de décalage. Zero est déplacé dans EAX. Nous pourrions le faire en utilisant MOV avec un opérande immédiat également.
C'est la raison pour laquelle les personnes qui pensent que les crochets dans LEA
sont superflus se trompent gravement; les crochets ne sont pas de la syntaxe LEA
mais font partie du mode d'adressage.
LEA est réel au niveau du matériel. L'instruction générée code le mode d'adressage réel et le processeur l'exécute jusqu'au moment du calcul de l'adresse. Ensuite, il déplace cette adresse vers la destination au lieu de générer une référence de mémoire. (Puisque le calcul d'adresse d'un mode d'adressage dans une autre instruction n'a aucun effet sur les indicateurs de la CPU, LEA
n'a pas d'effet sur les indicateurs de la CPU.)
Contraste avec le chargement de la valeur de l'adresse zéro:
$ as
movl 0, %eax
$ objdump -d a.out | grep mov
0: 8b 04 25 00 00 00 00 mov 0x0,%eax
C'est un encodage très similaire, vous voyez? Seul le 8d
de LEA
a été remplacé par 8b
.
Bien sûr, cet encodage LEA
est plus long que le déplacement d'un zéro immédiat dans EAX
:
$ as
movl $0, %eax
$ objdump -d a.out | grep mov
0: b8 00 00 00 00 mov $0x0,%eax
LEA
n'a aucune raison d'exclure cette possibilité, simplement parce qu'il existe une alternative plus courte; il ne fait que combiner orthogonalement avec les modes d'adressage disponibles.
Voici un exemple.
// 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;
}
Avec -O (optimiser) comme option de compilation, gcc trouvera l’instruction lea pour la ligne de code indiquée.
LEA: juste une instruction "arithmétique" ..
MOV transfère les données entre les opérandes mais lea est en train de calculer
Il semble que de nombreuses réponses soient déjà terminées. J'aimerais ajouter un autre exemple de code pour montrer comment les instructions lea et move fonctionnent différemment lorsqu'elles ont le même format d'expression.
Pour résumer, les instructions de saut et les instructions de déplacement peuvent être utilisées avec les parenthèses entourant l’opérande src des instructions. Lorsqu'ils sont joints au () , l'expression dans le () est calculée de la même manière; Cependant, deux instructions interpréteront la valeur calculée dans l'opérande src d'une manière différente.
Que l'expression soit utilisée avec lea ou mov, la valeur src est calculée comme ci-dessous.
D (Rb, Ri, S) => (Reg [Rb] + S * Reg [Ri] + D)
Cependant, lorsqu'il est utilisé avec l'instruction mov, il tente d'accéder à la valeur indiquée par l'adresse générée par l'expression ci-dessus et de l'enregistrer dans la destination.
En revanche, lorsque l'instruction lea est exécutée avec l'expression ci-dessus, elle charge la valeur générée telle quelle dans la destination.
Le code ci-dessous exécute les instructions lea et mov avec le même paramètre. Cependant, pour rattraper la différence, j'ai ajouté un gestionnaire de signal de niveau utilisateur pour attraper l'erreur de segmentation provoquée par l'accès à une adresse incorrecte à la suite d'une instruction mov.
Exemple de code
#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
);
}
Résultat d'exécution
Result of lea instruction: 14
inst at 0x4007b5 tries to access memory at 14, but failed