¿Cuál es el propósito de la instrucción LEA?

Para mí, parece un MOV funky. ¿Cuál es su propósito y cuándo debería usarlo?

Como han señalado otros, LEA (dirección efectiva de carga) a menudo se usa como un “truco” para hacer ciertos cálculos, pero ese no es su objective principal. El conjunto de instrucciones x86 fue diseñado para admitir lenguajes de alto nivel como Pascal y C, donde las matrices, especialmente las matrices de ints o pequeñas estructuras, son comunes. Considere, por ejemplo, una estructura que representa coordenadas (x, y):

struct Point { int xcoord; int ycoord; }; 

Ahora imagine una afirmación como:

 int y = points[i].ycoord; 

donde points[] es una matriz de Point . Suponiendo que la base de la matriz ya está en EBX , y la variable i está en EAX , y xcoord y ycoord son cada uno de 32 bits (por lo que ycoord está en offset 4 bytes en la estructura), esta statement se puede comstackr a:

 MOV EDX, [EBX + 8*EAX + 4] ; right side is "effective address" 

que aterrizará en EDX . El factor de escala de 8 es porque cada Point tiene un tamaño de 8 bytes. Ahora considere la misma expresión utilizada con el operador “dirección de” y:

 int *p = &points[i].ycoord; 

En este caso, no desea el valor de ycoord , pero su dirección. Ahí es donde entra LEA (dirección efectiva de carga). En lugar de un MOV , el comstackdor puede generar

 LEA ESI, [EBX + 8*EAX + 4] 

que cargará la dirección en ESI .

Del “Zen of Assembly” de Abrash:

LEA , la única instrucción que realiza cálculos de direccionamiento de memoria pero que en realidad no aborda la memoria. LEA acepta un operando de direccionamiento de memoria estándar, pero no hace nada más que almacenar la compensación de memoria calculada en el registro especificado, que puede ser cualquier registro de propósito general.

¿Qué nos da eso? Dos cosas que ADD no proporciona:

  1. la capacidad de realizar adiciones con dos o tres operandos, y
  2. la capacidad de almacenar el resultado en cualquier registro; no solo uno de los operandos fuente.

Y LEA no altera las banderas.

Ejemplos

  • LEA EAX, [ EAX + EBX + 1234567 ] calcula EAX + EBX + 1234567 (son tres operandos)
  • LEA EAX, [ EBX + ECX ] calcula EBX + ECX sin anulación ni con el resultado.
  • multiplicación por constante (por dos, tres, cinco o nueve), si la usa como LEA EAX, [ EBX + N * EBX ] (N puede ser 1,2,4,8).

Otro uso es útil en los bucles: la diferencia entre LEA EAX, [ EAX + 1 ] y INC EAX es que este último cambia EFLAGS pero el primero no; esto preserva el estado de CMP .

Otra característica importante de la instrucción LEA es que no altera los códigos de condición como CF y ZF , mientras que calcula la dirección mediante instrucciones aritméticas como ADD o MUL does. Esta característica disminuye el nivel de dependencia entre las instrucciones y, por lo tanto, deja espacio para una mayor optimización por parte del comstackdor o planificador de hardware.

A pesar de todas las explicaciones, LEA es una operación aritmética:

 LEA Rt, [Rs1+a*Rs2+b] => Rt = Rs1 + a*Rs2 + b 

Es solo que su nombre es extremadamente estúpido para una operación shift + add. La razón de esto ya se explicó en las respuestas mejor valoradas (es decir, se diseñó para mapear directamente referencias de memoria de alto nivel).

Tal vez solo otra cosa sobre la instrucción LEA. También puede usar LEA para registros rápidos de multiplicación por 3, 5 o 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 es una abreviatura de “dirección efectiva de carga”. Carga la dirección de la referencia de ubicación por el operando de origen al operando de destino. Por ejemplo, podrías usarlo para:

 lea ebx, [ebx+eax*8] 

para mover los elementos eax puntero ebx más lejos (en una matriz de elementos de 64 bits) con una sola instrucción. Básicamente, se beneficia de los modos de direccionamiento complejos soportados por la architecture x86 para manipular punteros de manera eficiente.

La razón principal por la que utiliza LEA en lugar de MOV es si necesita realizar operaciones aritméticas en los registros que está utilizando para calcular la dirección. De hecho, puede realizar lo que equivale a la aritmética del puntero en varios de los registros en combinación efectiva para “libre”.

Lo que es realmente confuso es que normalmente se escribe un LEA como un MOV pero en realidad no se está eliminando la referencia de la memoria. En otras palabras:

MOV EAX, [ESP+4]

Esto moverá el contenido de lo que ESP+4 apunta a EAX .

LEA EAX, [EBX*8]

Esto moverá la dirección efectiva EBX * 8 a EAX, no lo que se encuentra en esa ubicación. Como puede ver, también, es posible multiplicar por factores de dos (escalado) mientras que un MOV se limita a sumr / restar.

El 8086 tiene una gran familia de instrucciones que aceptan un operando de registro y una dirección efectiva, realizan algunos cálculos para calcular la parte compensada de esa dirección efectiva y realizan alguna operación que involucra el registro y la memoria a la que hace referencia la dirección calculada. Era bastante simple tener una de las instrucciones en esa familia que se comportan como arriba, excepto por omitir esa operación de memoria real. Esto, las instrucciones:

 mov ax,[bx+si+5] lea ax,[bx+si+5] 

fueron implementados de manera casi idéntica internamente. La diferencia es un paso omitido. Ambas instrucciones funcionan de la siguiente manera:

 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 

En cuanto a por qué Intel pensó que valía la pena incluir esta instrucción, no estoy del todo seguro, pero el hecho de que era barato de implementar habría sido un factor importante. Otro factor habría sido el hecho de que el ensamblador de Intel permitía definir símbolos en relación con el registro BP. Si fnord se definió como un símbolo relativo de BP (por ejemplo, BP + 8), se podría decir:

 mov ax,fnord ; Equivalent to "mov ax,[BP+8]" 

Si uno quisiera usar algo como stosw para almacenar datos en una dirección relativa a BP, ser capaz de decir

 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 

fue más conveniente que:

 mov ax,0 ; Data to store mov cx,16 ; Number of words mov di,bp add di,offset fnord (ie 8) rep movs fnord ; Address is ignored EXCEPT to note that it's an SS-relative word ptr 

Tenga en cuenta que si se olvida el “desplazamiento” mundial, el contenido de la ubicación [BP + 8], en lugar del valor 8, se agregará a DI. Oops.

Como se menciona en las respuestas existentes, LEA tiene las ventajas de realizar aritmética de direcciones de memoria sin acceder a la memoria, guardando el resultado aritmético en un registro diferente en lugar de la forma simple de agregar instrucciones. El beneficio de rendimiento real subyacente es que el procesador moderno tiene una unidad LEA ALU separada y puerto para la generación efectiva de direcciones (incluyendo LEA y otra dirección de referencia de memoria), esto significa que la operación aritmética en LEA y otras operaciones aritméticas normales en ALU podrían realizarse en paralelo en un núcleo

Consulte este artículo de la architecture de Haswell para conocer algunos detalles sobre la unidad LEA: http://www.realworldtech.com/haswell-cpu/4/

Otro punto importante que no se menciona en otras respuestas es LEA REG, [MemoryAddress] instrucción LEA REG, [MemoryAddress] es PIC (código de posición independiente) que codifica la dirección relativa de la PC en esta instrucción para hacer referencia a MemoryAddress . Esto es diferente de MOV REG, MemoryAddress que codifica la dirección virtual relativa y requiere reubicación / parcheo en los sistemas operativos modernos (como ASLR es una característica común). Entonces LEA se puede usar para convertir dicho PIC no a PIC.

La instrucción LEA se puede utilizar para evitar cálculos de direcciones efectivas por parte de la CPU. Si una dirección se usa repetidamente, es más efectivo almacenarla en un registro en lugar de calcular la dirección efectiva cada vez que se usa.

Aquí hay un ejemplo.

 // 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; } 

Con -O (optimizar) como opción de comstackción, gcc encontrará la instrucción lea para la línea de código indicada.

La instrucción LEA (Load Effective Address) es una forma de obtener la dirección que surge de cualquiera de los modos de direccionamiento de memoria del procesador Intel.

Es decir, si tenemos un movimiento de datos como este:

 MOV EAX,  

mueve los contenidos de la ubicación de memoria designada al registro de destino.

Si reemplazamos el MOV por LEA , entonces la dirección de la ubicación de la memoria se calcula exactamente de la misma manera mediante la expresión de direccionamiento . Pero en lugar de los contenidos de la ubicación de la memoria, obtenemos la ubicación en el destino.

LEA no es una instrucción aritmética específica; es una forma de interceptar la dirección efectiva que surge de cualquiera de los modos de direccionamiento de memoria del procesador.

Por ejemplo, podemos usar LEA solo con una dirección directa simple. Ninguna aritmética está involucrada en absoluto:

 MOV EAX, GLOBALVAR ; fetch the value of GLOBALVAR into EAX LEA EAX, GLOBALVAR ; fetch the address of GLOBALVAR into EAX. 

Esto es válido; podemos probarlo en el prompt 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 

Aquí no se agrega un valor escalado ni compensación. Zero se mueve a EAX. Podríamos hacer eso usando MOV con un operando inmediato también.

Esta es la razón por la cual las personas que piensan que los corchetes en LEA son superfluos están gravemente equivocados; los corchetes no son syntax LEA pero son parte del modo de direccionamiento.

LEA es real a nivel de hardware. La instrucción generada codifica el modo de direccionamiento real y el procesador lo lleva a cabo hasta el punto de calcular la dirección. Luego mueve esa dirección al destino en lugar de generar una referencia de memoria. (Dado que el cálculo de la dirección de un modo de direccionamiento en cualquier otra instrucción no tiene ningún efecto en las banderas de la CPU, LEA no tiene efecto en las banderas de la CPU).

Contraste con la carga del valor de la dirección cero:

 $ as movl 0, %eax $ objdump -d a.out | grep mov 0: 8b 04 25 00 00 00 00 mov 0x0,%eax 

Es una encoding muy similar, ¿ves? Solo el 8d de LEA ha cambiado a 8b .

Por supuesto, esta encoding LEA es más larga que mover un cero inmediato a EAX :

 $ as movl $0, %eax $ objdump -d a.out | grep mov 0: b8 00 00 00 00 mov $0x0,%eax 

No hay ninguna razón para que LEA excluya esta posibilidad aunque solo haya una alternativa más corta; simplemente se está combinando de forma ortogonal con los modos de direccionamiento disponibles.

LEA: solo una instrucción “aritmética” ..

MOV transfiere datos entre operandos pero lea solo calcula

porque en su lugar escribes el código

 mov dx,offset something 

simplemente puedes escribir

 lea dx,something