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 ies en EAX, y xcoordy ycoordson cada uno de 32 bits (de modo ycoordestá 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á yen EDX. El factor de escala de 8 se debe a que cada uno Pointtiene 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 LEAentra (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.
movinstrucciones y dejar los corchetes? MOV EDX, EBX + 8*EAX + 4
MOVfuente 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.LEAacepta 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
ADDno 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 LEAno altera las banderas.
Ejemplos
LEA EAX, [ EAX + EBX + 1234567 ]calcula EAX + EBX + 1234567(eso son tres operandos)LEA EAX, [ EBX + ECX ]calcula EBX + ECXsin 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 EAXes que este último cambia EFLAGSpero el primero no; Esto conserva el CMPestado.
LEA EAX, [ EAX + EBX + 1234567 ]calcula la suma de EAX, EBXy 1234567(eso es tres operandos). LEA EAX, [ EBX + ECX ]calcula EBX + ECX sin anular ni con el resultado. La tercera cosa que LEAse 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 ]( Npuede ser 1,2,4,8). Otro caso de uso es útil en bucles: la diferencia entre LEA EAX, [ EAX + 1 ]y INC EAXes que este último cambia EFLAGSpero el primero no; esto preserva el CMPestado
LEAse 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 LEAinstrucción es que no altera los códigos de condición como CFy ZF, mientras calcula la dirección mediante instrucciones aritméticas como ADDo MULdoes. 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.
leaveces es útil para el compilador (o el codificador humano) hacer cálculos matemáticos sin tropezar con un resultado distintivo. Pero leano 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).
LEAen 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" ...
LEAle 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]?
shlinstrucció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 mulinstrucció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].
leaes 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 ebxpuntero eaxmá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 LEAsobre un MOVes 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 LEAletra como una MOVpero no estás desreferenciando la memoria. En otras palabras:
MOV EAX, [ESP+4]
Esto moverá el contenido de lo que ESP+4apunta a EAX.
LEA EAX, [EBX*8]
Esto moverá la dirección efectiva EBX * 8a 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 MOVse limita a sumar / restar.
LEAhace.
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 fnordse 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, LEAtiene 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 LEAy otras direcciones de referencia de memoria), esto significa que la operación aritmética en LEAy 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, MemoryAddressque 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 LEAlo tanto, se puede utilizar para convertir ese tipo de PIC a PIC.
leaen 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 addo la submayoría de las operaciones aritméticas básicas en cuatro ALU diferentes , pero solo puede ejecutarse leaen una (compleja lea) o dos (simple lea). Más importante aún, esas leaALU 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 esivalor de ecxmultiplicado 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..
lealo 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.
LEAno 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 LEAsolo 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 LEAson superfluos están gravemente equivocados; los corchetes no son LEAsintaxis 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, LEAno 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 8dde LEAha cambiado a 8b.
Por supuesto, esta LEAcodificació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 LEAexcluir 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
=dpara 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), %edxno es válido? De todos modos, sí, porque movtendrí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 = 0xff00000001o 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 GLOBALVARen su lugar. Usted puede utilizar LEA, pero es un poco más grande que el tamaño del código mov r32, imm32y 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, symbolsolo 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, 1no 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, 0solo 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+1en 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 raxté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:offpar. LEA no se ve afectada por la base del segmento; ambas instrucciones (ineficientemente) se colocarán 0x1234en 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.