Para mí, solo parece un MOV funky. ¿Cuál es su propósito y cuándo debo usarlo?
Para mí, solo parece un MOV funky. ¿Cuál es su propósito y cuándo debo usarlo?
Respuestas:
Como otros han señalado, la LEA (dirección efectiva de carga) a menudo se usa como un "truco" para hacer ciertos cálculos, pero ese no es su propósito 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 estructuras pequeñas, son comunes. Considere, por ejemplo, una estructura que representa las coordenadas (x, y):
struct Point
{
int xcoord;
int ycoord;
};
Ahora imagine una declaració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
es en EAX
, y xcoord
y ycoord
son cada uno de 32 bits (de modo ycoord
está en el desplazamiento de 4 bytes en la struct), esta declaración puede ser compilado para:
MOV EDX, [EBX + 8*EAX + 4] ; right side is "effective address"
que aterrizará y
en EDX
. El factor de escala de 8 se debe a que cada uno 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
, sino su dirección. Ahí es donde LEA
entra (dirección efectiva de carga). En lugar de a MOV
, el compilador puede generar
LEA ESI, [EBX + 8*EAX + 4]
que cargará la dirección en ESI
.
mov
instrucciones y dejar los corchetes? MOV EDX, EBX + 8*EAX + 4
MOV
fuente indirecta, excepto que solo hace la indirecta y no la MOV
. En realidad, no lee de la dirección calculada, solo la calcula.
Del "Zen de la Asamblea" 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 más que almacenar el desplazamiento de memoria calculado en el registro especificado, que puede ser cualquier registro de propósito general.¿Qué nos da eso? Dos cosas que
ADD
no proporcionan:
- la capacidad de realizar sumas con dos o tres operandos, y
- 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
(eso son tres operandos)LEA EAX, [ EBX + ECX ]
calcula EBX + ECX
sin anular ni con el resultado.LEA EAX, [ EBX + N * EBX ]
(N puede ser 1,2,4,8).Otro caso de uso es útil en bucles: la diferencia entre LEA EAX, [ EAX + 1 ]
y INC EAX
es que este último cambia EFLAGS
pero el primero no; Esto conserva el CMP
estado.
LEA EAX, [ EAX + EBX + 1234567 ]
calcula la suma de EAX
, EBX
y 1234567
(eso es tres operandos). LEA EAX, [ EBX + ECX ]
calcula EBX + ECX
sin anular ni con el resultado. La tercera cosa que LEA
se usa para (no enumerada por Frank) es la 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 caso de uso es útil en bucles: la diferencia entre LEA EAX, [ EAX + 1 ]
y INC EAX
es que este último cambia EFLAGS
pero el primero no; esto preserva el CMP
estado
LEA
se pueden usar para ... (ver "LEA (dirección efectiva de carga) a menudo se usa como un" truco "para hacer ciertos cálculos" en la respuesta popular de IJ Kennedy arriba)
Otra característica importante de la LEA
instrucción es que no altera los códigos de condición como CF
y ZF
, mientras 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 compilador o el planificador de hardware.
lea
veces es útil para el compilador (o el codificador humano) hacer cálculos matemáticos sin tropezar con un resultado distintivo. Pero lea
no es más rápido que add
. La mayoría de las instrucciones x86 escriben banderas. Las implementaciones x86 de alto rendimiento tienen que cambiar el nombre de EFLAGS o evitar el riesgo de escritura después de la escritura para que el código normal se ejecute rápidamente, por lo que las instrucciones que evitan las escrituras del indicador no son mejores debido a eso. ( las cosas de marca parcial pueden crear problemas, vea la instrucción INC vs ADD 1: ¿Importa? )
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 calificadas (es decir, se diseñó para asignar directamente referencias de memoria de alto nivel).
LEA
en las AGU sino en las ALU enteras normales. Uno tiene que leer las especificaciones de la CPU muy de cerca en estos días para descubrir "dónde se ejecutan las cosas" ...
LEA
le proporciona la dirección que surge de cualquier modo de direccionamiento relacionado con la memoria. No es un turno y agregar operación.
Tal vez solo otra cosa sobre la instrucción LEA. También puede usar LEA para registros de multiplicación rápida 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 EAX, [EAX*3]
?
shl
instrucción para multiplicar registros por 2,4,8,16 ... es más rápido y más corto. Pero para multiplicar con números diferentes de potencias de 2 usamos normalmente una mul
instrucción que es más pretenciosa y más lenta.
lea eax,[eax*3]
se traduciría al equivalente de lea eax,[eax+eax*2]
.
lea
es una abreviatura de "dirección efectiva de carga". Carga la dirección de la referencia de ubicación del operando de origen al operando de destino. Por ejemplo, podría usarlo para:
lea ebx, [ebx+eax*8]
para mover elementos de ebx
puntero eax
más lejos (en una matriz de 64 bits / elemento) con una sola instrucción. Básicamente, se beneficia de modos de direccionamiento complejos compatibles con la arquitectura x86 para manipular punteros de manera eficiente.
La razón más importante que utiliza LEA
sobre un MOV
es si necesita realizar operaciones aritméticas en los registros que está utilizando para calcular la dirección. Efectivamente, puede realizar lo que equivale a la aritmética del puntero en varios de los registros en combinación de manera efectiva para "gratis".
Lo que es realmente confuso al respecto es que normalmente escribes una LEA
letra como una MOV
pero no estás desreferenciando 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 (escala) mientras que a MOV
se limita a sumar / restar.
LEA
hace.
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 desplazada de esa dirección efectiva y realizan algunas operaciones que involucran el registro y la memoria a la que se refiere la dirección calculada. Era bastante simple hacer que una de las instrucciones de esa familia se comportara como anteriormente, excepto para saltarse esa operación de memoria real. Esto, las instrucciones:
mov ax,[bx+si+5]
lea ax,[bx+si+5]
se implementaron casi de forma idéntica internamente. La diferencia es un paso omitido. Ambas instrucciones funcionan algo así como:
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 exactamente seguro, pero el hecho de que su implementación fuera barata habría sido un factor importante. Otro factor habría sido el hecho de que el ensamblador de Intel permitió que se definieran símbolos en relación con el registro de BP. Si fnord
se definió como un símbolo relativo a 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, poder 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 (i.e. 8)
rep movs fnord ; Address is ignored EXCEPT to note that it's an SS-relative word ptr
Tenga en cuenta que si olvida el "desplazamiento" mundial, el contenido de la ubicación [BP + 8], en lugar del valor 8, se agregará a DI. Ups
Como se mencionó en las respuestas existentes, LEA
tiene las ventajas de realizar aritmética de direccionamiento de memoria sin acceder a la memoria, guardando el resultado aritmético en un registro diferente en lugar de la forma simple de agregar instrucción. El beneficio real de rendimiento subyacente es que el procesador moderno tiene una unidad LEA ALU y un puerto separados para la generación efectiva de direcciones (incluidas LEA
y otras direcciones de referencia de memoria), esto significa que la operación aritmética en LEA
y otra operación aritmética normal en ALU podría realizarse en paralelo en uno núcleo.
Consulte este artículo de la arquitectura Haswell para obtener algunos detalles sobre la unidad LEA: http://www.realworldtech.com/haswell-cpu/4/
Otro punto importante que no se menciona en otras respuestas es que la LEA REG, [MemoryAddress]
instrucción es PIC (código independiente de la posición) que codifica la dirección relativa de la PC en esta instrucción para referencia MemoryAddress
. Esto es diferente de lo 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). Por LEA
lo tanto, se puede utilizar para convertir ese tipo de PIC a PIC.
lea
en una o más de las mismas ALU que ejecutan otras instrucciones aritméticas (pero generalmente menos que otras aritméticas). Por ejemplo, la CPU Haswell mencionada puede ejecutar add
o la sub
mayoría de las operaciones aritméticas básicas en cuatro ALU diferentes , pero solo puede ejecutarse lea
en una (compleja lea
) o dos (simple lea
). Más importante aún, esas lea
ALU de dos capacidades son simplemente dos de las cuatro que pueden ejecutar otras instrucciones, por lo que no existe un beneficio de paralelismo como se afirma.
La instrucción LEA se puede utilizar para evitar cálculos de direcciones efectivas que requieren mucho tiempo 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.
[esi]
lo tanto, rara vez es más barato que decir [esi + 4200]
y rara vez es más barato que [esi + ecx*8 + 4200]
.
[esi]
no es más barato que [esi + ecx*8 + 4200]
. ¿Pero por qué molestarse en comparar? No son equivalentes. Si desea que el primero designe la misma ubicación de memoria que el segundo, necesita instrucciones adicionales: debe agregar al esi
valor de ecx
multiplicado por 8. Uh, ¡la multiplicación va a golpear los indicadores de su CPU! Luego debe agregar el 4200. Estas instrucciones adicionales se suman al tamaño del código (ocupando espacio en la memoria caché de instrucciones, ciclos para buscar).
[esi + 4200]
repetidamente en una secuencia de instrucciones, entonces es mejor cargar primero la dirección efectiva en un registro y usarla. Por ejemplo, en lugar de escribir add eax, [esi + 4200]; add ebx, [esi + 4200]; add ecx, [esi + 4200]
, debería preferir lea edi, [esi + 4200]; add eax, [edi]; add ebx, [edi]; add ecx, [edi]
, que rara vez es más rápido. Al menos esa es la interpretación clara de esta respuesta.
[esi]
y [esi + 4200]
(o [esi + ecx*8 + 4200]
es que esta es la simplificación que propone el OP (según tengo entendido): que N instrucciones con la misma dirección compleja se transforman en N instrucciones con direccionamiento simple (un reg), más uno lea
, ya abordar complejo es "lento" de hecho, es más lento incluso en x86 moderna, pero sólo en cuanto a la latencia que parece poco probable que la materia para obtener instrucciones consecutivos con la misma dirección..
lea
lo que aumenta la presión en ese caso. En general, almacenar productos intermedios es una causa de presión de registro, no una solución, pero creo que en la mayoría de las situaciones es un lavado. @Kaz
La instrucción LEA (Dirección efectiva de carga) 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, <MEM-OPERAND>
mueve el contenido de la ubicación de memoria designada al registro de destino.
Si reemplazamos el MOV
by LEA
, la dirección de la ubicación de la memoria se calcula exactamente de la misma manera mediante la <MEM-OPERAND>
expresión de direccionamiento. Pero en lugar del contenido de la ubicación de la memoria, obtenemos la ubicación en sí misma 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 una dirección directa simple. No hay aritmética 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 valido; podemos probarlo en el indicador 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 hay adición de un valor escalado, ni desplazamiento. Zero se traslada 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 LEA
son superfluos están gravemente equivocados; los corchetes no son LEA
sintaxis 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 efecto en los indicadores de la CPU, LEA
no tiene ningún efecto en los indicadores de la CPU).
Contraste con cargar el 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 codificación muy similar, ¿ves? Solo el 8d
de LEA
ha cambiado a 8b
.
Por supuesto, esta LEA
codificación 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 LEA
excluir esta posibilidad, solo porque hay una alternativa más corta; solo se combina de forma ortogonal con los modos de direccionamiento disponibles.
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 del compilador, gcc encontrará la instrucción lea para la línea de código indicada.
Parece que muchas respuestas ya están completas, me gustaría agregar un código de ejemplo más para mostrar cómo las instrucciones lea y move funcionan de manera diferente cuando tienen el mismo formato de expresión.
Para acortar la historia, las instrucciones lea y mov pueden usarse con los paréntesis que encierran el operando src de las instrucciones. Cuando están encerrados con () , la expresión en () se calcula de la misma manera; sin embargo, dos instrucciones interpretarán el valor calculado en el operando src de una manera diferente.
Si la expresión se usa con lea o mov, el valor de src se calcula de la siguiente manera.
D (Rb, Ri, S) => (Reg [Rb] + S * Reg [Ri] + D)
Sin embargo, cuando se usa con la instrucción mov, intenta acceder al valor apuntado por la dirección generada por la expresión anterior y almacenarlo en el destino.
En contraste, cuando la instrucción lea se ejecuta con la expresión anterior, carga el valor generado tal como está en el destino.
El siguiente código ejecuta la instrucción lea y la instrucción mov con el mismo parámetro. Sin embargo, para detectar la diferencia, agregué un controlador de señal a nivel de usuario para detectar la falla de segmentación causada por acceder a una dirección incorrecta como resultado de la instrucción mov.
Código de ejemplo
#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
);
}
Resultado de ejecución
Result of lea instruction: 14
inst at 0x4007b5 tries to access memory at 14, but failed
=d
para decirle al compilador que el resultado está en EDX, guardando a mov
. También omitió una declaración de clobber temprano en la salida. Esto demuestra lo que está tratando de demostrar, pero también es un mal ejemplo engañoso de asm en línea que se romperá si se usa en otros contextos. Eso es algo malo para una respuesta de desbordamiento de pila.
%%
en todos esos nombres de registro en Asm extendido, use restricciones de entrada. como asm("lea 4(%%ebx, %%eax, 8), %%edx" : "=d"(result_of_lea) : "a"(1), "b"(2));
. Dejar que el compilador init se registre significa que tampoco tiene que declarar clobbers. Estás complicando demasiado las cosas haciendo xor-zeroing antes de que mov-inmediatamente sobrescriba también todo el registro.
mov 4(%ebx, %eax, 8), %edx
no es válido? De todos modos, sí, porque mov
tendría sentido escribir "a"(1ULL)
para decirle al compilador que tiene un valor de 64 bits y, por lo tanto, debe asegurarse de que se extienda para llenar todo el registro. En la práctica, seguirá utilizándose mov $1, %eax
, porque escribir EAX cero se extiende a RAX, a menos que tenga una situación extraña de código circundante donde el compilador sabía que RAX = 0xff00000001
o algo así. Para lea
, todavía está utilizando el tamaño de operando de 32 bits, por lo que cualquier bit alto perdido en los registros de entrada no tiene ningún efecto en el resultado de 32 bits.
LEA: solo una instrucción "aritmética".
MOV transfiere datos entre operandos pero lea solo está calculando
mov eax, offset GLOBALVAR
en su lugar. Usted puede utilizar LEA, pero es un poco más grande que el tamaño del código mov r32, imm32
y se ejecuta en un menor número de puertos, ya que todavía pasa por el proceso de dirección de cálculo . lea reg, symbol
solo es útil en 64 bits para un LEA relativo a RIP, cuando necesita PIC y / o direcciones fuera de los 32 bits bajos. En el código de 32 o 16 bits, hay cero ventajas. LEA es una instrucción aritmética que expone la capacidad de la CPU para decodificar / calcular modos de direccionamiento.
imul eax, edx, 1
no se calcula: simplemente copia edx a eax. Pero en realidad ejecuta sus datos a través del multiplicador con latencia de 3 ciclos. O eso rorx eax, edx, 0
solo copia (gira por cero).
Todas las instrucciones normales de "cálculo", como agregar multiplicación, excluir o establecer los indicadores de estado como cero, firmar. Si utiliza una dirección complicada, AX xor:= mem[0x333 +BX + 8*CX]
los indicadores se configuran de acuerdo con la operación xor.
Ahora es posible que desee utilizar la dirección varias veces. Cargar tales direcciones en un registro nunca tiene la intención de establecer indicadores de estado y afortunadamente no lo hace. La frase "cargar dirección efectiva" hace que el programador se dé cuenta de eso. De ahí proviene la expresión extraña.
Está claro que una vez que el procesador es capaz de usar la dirección complicada para procesar su contenido, es capaz de calcularlo para otros fines. De hecho, se puede utilizar para realizar una transformación x <- 3*x+1
en una sola instrucción. Esta es una regla general en la programación de ensamblaje: use las instrucciones, sin embargo, sacudirá su bote.
Lo único que cuenta es si la transformación particular encarnada por la instrucción es útil para usted.
Línea de fondo
MOV, X| T| AX'| R| BX|
y
LEA, AX'| [BX]
tienen el mismo efecto en AX pero no en los indicadores de estado. (Esta es la notación ciasdis ).
call lbl
lbl: pop rax
técnicamente "trabajar" como una forma de obtener el valor de rip
, pero harás que la predicción de rama sea muy infeliz. Use las instrucciones como desee, pero no se sorprenda si hace algo complicado y tiene consecuencias que no previó
Perdóname si alguien ya lo mencionó, pero en los días de x86 cuando la segmentación de la memoria aún era relevante, es posible que no obtengas los mismos resultados de estas dos instrucciones:
LEA AX, DS:[0x1234]
y
LEA AX, CS:[0x1234]
seg:off
par. LEA no se ve afectada por la base del segmento; ambas instrucciones (ineficientemente) se colocarán 0x1234
en AX. Desafortunadamente, x86 no tiene una manera fácil de calcular una dirección lineal completa (efectiva + base de segmento) en un registro o par de registros.