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

676

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

usuario200557
fuente
2
Consulte también ¿Cómo usar LEA en valores que no son direcciones / punteros? : LEA es solo una instrucción shift-and-add. Probablemente se agregó a 8086 porque el hardware ya está allí para decodificar y calcular los modos de direccionamiento, no porque esté "destinado" solo para usarse con direcciones. Recuerde que los punteros son enteros en el ensamblaje.
Peter Cordes

Respuestas:

798

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.

IJ Kennedy
fuente
112
¿No habría sido más limpio extender las movinstrucciones y dejar los corchetes? MOV EDX, EBX + 8*EAX + 4
Natan Yellin
14
@imacake Al reemplazar LEA con un MOV especializado, se mantiene limpia la sintaxis: [] los corchetes son siempre el equivalente de desreferenciar un puntero en C. Sin corchetes, siempre se trata con el puntero mismo.
Natan Yellin el
139
Hacer matemáticas en una instrucción MOV (EBX + 8 * EAX + 4) no es válido. LEA ESI, [EBX + 8 * EAX + 4] es válido porque este es un modo de direccionamiento que admite x86. en.wikipedia.org/wiki/X86#Addressing_modes
Erik
30
@JonathanDickinson LEA es como una MOVfuente indirecta, excepto que solo hace la indirecta y no la MOV. En realidad, no lee de la dirección calculada, solo la calcula.
hobbs
24
Erik, el comentario del tour no es exacto. MOV eax, [ebx + 8 * ecx + 4] es válido. Sin embargo, MOV devuelve el contenido de la ubicación de memoria thst mientras que LEA devuelve la dirección
Olorin
562

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:

  1. la capacidad de realizar sumas con dos o tres operandos, y
  2. 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.
  • 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 EAXes que este último cambia EFLAGSpero el primero no; Esto conserva el CMPestado.

Frank Krueger
fuente
42
@AbidRahmanK algunos ejemplos: 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
FrankH.
@FrankH. Todavía no entiendo, ¿entonces carga un puntero en otro lugar?
66
@ ripDaddy69 sí, algo así: si por "carga" quieres decir "realiza el cálculo de la dirección / la aritmética del puntero". No accede a la memoria (es decir, no "desreferencia" el puntero como se llamaría en términos de programación en C).
FrankH.
2
+1: Esto hace explícito qué tipos de 'trucos' 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)
Assad Ebrahim
3
Hay una gran diferencia entre 2 operandos LEA que es rápido y 3 operandos LEA que es lento. El manual de Intel Optimization dice que la ruta rápida LEA es de ciclo único y la ruta lenta LEA toma tres ciclos. Además, en Skylake hay dos unidades funcionales de ruta rápida (puertos 1 y 5) y solo hay una unidad funcional de ruta lenta (puerto 1). La regla 33 de codificación del ensamblador / compilador del manual incluso advierte contra el uso de 3 operandos LEA.
Olsonista 01 de
110

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.

Angus Lee
fuente
1
Sí, a 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? )
Peter Cordes
2
@PeterCordes: Odio traer esto aquí pero, ¿estoy solo pensando que esta nueva etiqueta [x86-lea] es redundante e innecesaria?
Michael Petch
2
@ MichaelPetch: Sí, creo que es demasiado específico. Parece confundir a los principiantes que no entienden el lenguaje de máquina y que todo (incluidos los punteros) son solo bits / bytes / enteros, por lo que hay muchas preguntas al respecto con un gran número de votos. Pero tener una etiqueta para ello implica que hay espacio para un número abierto de preguntas futuras, cuando en realidad hay alrededor de 2 o 3 en total que no son solo duplicados. (¿qué es? ¿Cómo usarlo para multiplicar enteros? y cómo funciona internamente en AGU vs. ALU y con qué latencia / rendimiento. Y tal vez sea su propósito "previsto")
Peter Cordes
@PeterCordes: estoy de acuerdo, y en todo caso, todas estas publicaciones que se están editando son prácticamente un duplicado de algunas de las preguntas relacionadas con LEA existentes. En lugar de una etiqueta, cualquier duplicado debe identificarse y marcarse en mi humilde opinión.
Michael Petch
1
@EvanCarroll: espere a etiquetar todas las preguntas de LEA, si aún no ha terminado. Como se discutió anteriormente, creemos que x86-lea es demasiado específico para una etiqueta, y no hay mucho margen para futuras preguntas no duplicadas. Creo que sería mucho trabajo para realmente elegir un "mejor" Q & A como un objetivo de la duplicación de la mayor parte de ellos, sin embargo, o para realmente decidir cuáles de conseguir modificaciones a la fusión.
Peter Cordes
93

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).

hdante
fuente
8
Y que la aritmética la realiza el hardware de cálculo de direcciones.
Ben Voigt
30
@BenVoigt Solía ​​decir eso, porque soy un viejo tipo :-) Tradicionalmente, las CPU x86 usaban las unidades de direccionamiento para esto, de acuerdo. Pero la "separación" se ha vuelto muy borrosa en estos días. Algunas CPU ya no tienen dedicadas AGU , otras han optado por no ejecutarse 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" ...
FrankH.
2
@FrankH .: las CPU fuera de servicio generalmente ejecutan LEA en ALU, mientras que algunas CPU en orden (como Atom) a veces lo ejecutan en una AGU (porque no pueden estar ocupadas manejando un acceso a la memoria).
Peter Cordes
3
No, el nombre no es estúpido. LEAle proporciona la dirección que surge de cualquier modo de direccionamiento relacionado con la memoria. No es un turno y agregar operación.
Kaz
3
FWIW hay muy pocas (si las hay) CPU x86 actuales que realizan la operación en la AGU. La mayoría o todos simplemente usan una ALU como cualquier otra operación aritmética.
BeeOnRope
77

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
GJ
fuente
13
+1 por el truco. Pero me gustaría hacer una pregunta (puede ser estúpida), ¿por qué no multiplicar directamente por tres como este LEA EAX, [EAX*3]?
Abid Rahman K
13
@Abid Rahman K: No existe un conjunto de instrucciones de CPU unde x86.
GJ.
50
@AbidRahmanK a pesar de que la sintaxis de intel asm hace que parezca una multiplicación, la instrucción lea solo puede codificar operaciones de cambio. El código de operación tiene 2 bits para describir el cambio, por lo tanto, puede multiplicar solo por 1,2,4 u 8.
ithkuil
66
@Koray Tugay: Puede usar Mayúsculas como 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.
GJ.
8
@GJ. aunque no existe tal codificación, algunos ensambladores aceptan esto como un acceso directo, por ejemplo, fasm. Entonces, por ejemplo, lea eax,[eax*3]se traduciría al equivalente de lea eax,[eax+eax*2].
Ruslan
59

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.

Mehrdad Afshari
fuente
23

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.

David Hoelzer
fuente
Perdón a todos. @ big.heart me engañó dándome una respuesta a esto hace tres horas, haciendo que aparezca como "nuevo" en mi búsqueda de preguntas de la Asamblea.
David Hoelzer
1
¿Por qué la sintaxis usa corchetes cuando no hace direccionamiento de memoria?
golopot
3
@ q4w56 Esta es una de esas cosas donde la respuesta es: "Así es como se hace". Creo que es una de las razones por las que las personas tienen dificultades para descubrir qué LEAhace.
David Hoelzer
2
@ q4w56: es una instrucción shift + add que utiliza la sintaxis de operandos de memoria y la codificación de código de máquina. En algunas CPU incluso puede usar el hardware AGU, pero eso es un detalle histórico. El hecho aún relevante es que el hardware del decodificador ya existe para decodificar este tipo de shift + add, y LEA nos permite usarlo para aritmética en lugar de direccionamiento de memoria. (O para cálculos de dirección si una entrada es realmente un puntero).
Peter Cordes
20

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

Super gato
fuente
12

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.

Thomson
fuente
2
La parte de "LEA ALU separada" es mayormente falsa. Las CPU modernas se ejecutan 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.
BeeOnRope
El artículo que vinculó (correctamente) muestra que LEA está en el mismo puerto que una ALU entera (add / sub / boolean) y la unidad MUL entera en Haswell. (Y las ALU vectoriales que incluyen FP ADD / MUL / FMA). La unidad LEA de solo simple está en el puerto 5, que también ejecuta ADD / SUB / lo que sea, y baraja de vectores, y otras cosas. La única razón por la que no estoy votando es porque usted señala el uso de LEA relativo a RIP (solo para x86-64).
Peter Cordes
8

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.

consejo
fuente
No necesariamente en x86 moderno. La mayoría de los modos de direccionamiento tienen el mismo costo, con algunas advertencias. Por [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].
BeeOnRope
@BeeOnRope [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).
Kaz
2
@Kaz: creo que te perdiste mi punto (o de lo contrario me perdí el punto del OP) Tengo entendido que el OP dice que si va a usar algo como [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.
BeeOnRope
Entonces, la razón por la que estaba comparando [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..
BeeOnRope
1
Quizás alivie un poco la presión de registro, sí, pero lo contrario puede ser el caso: si los registros con los que generó la dirección efectiva están activos, necesita otro registro para guardar el resultado, por 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
BeeOnRope
7

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.

Kaz
fuente
6

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.

usuario3634373
fuente
6

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
Jaehyuk Lee
fuente
1
Romper su asm en línea en declaraciones separadas no es seguro, y sus listas de clobbers están incompletas. El bloque basic-asm le dice al compilador que no tiene clobbers, pero en realidad modifica varios registros. Además, puede usar =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.
Peter Cordes
Si no desea escribir %%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.
Peter Cordes
@ PeterCordes Gracias, Peter, ¿quieres que elimine esta respuesta o la modifique después de tus comentarios?
Jaehyuk Lee
1
Si arregla el asm en línea, no está haciendo ningún daño y quizás sea un buen ejemplo concreto para principiantes que no entendieron las otras respuestas. No es necesario eliminar, y es una solución fácil como la que mostré en mi último comentario. Creo que valdría la pena un voto positivo si el mal ejemplo de asm en línea se fijara en un "buen" ejemplo. (No voté en contra)
Peter Cordes
1
¿Dónde dice alguien que 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.
Peter Cordes
4

LEA: solo una instrucción "aritmética".

MOV transfiere datos entre operandos pero lea solo está calculando

el contador
fuente
LEA obviamente mueve datos; Tiene un operando de destino. LEA no siempre calcula; calcula si se calcula la dirección efectiva expresada en el operando de origen. LEA EAX, GLOBALVAR no calcula; solo mueve la dirección de GLOBALVAR a EAX.
Kaz
@Kaz gracias por sus comentarios. mi fuente era "LEA (dirección efectiva de carga) es esencialmente una instrucción aritmética: no realiza ningún acceso real a la memoria, pero se usa comúnmente para calcular direcciones (aunque puede calcular enteros de propósito general con ella)". formulario libro Eldad-Eilam página 149
el contador
@Kaz: Es por eso que LEA es redundante cuando la dirección ya es una constante de tiempo de enlace; utilizar 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.
Peter Cordes
@Kaz: por el mismo argumento, se podría decir que 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).
Peter Cordes
@PeterCordes Mi punto es que tanto LEA EAX, GLOBALVAL como MOV EAX, GLOBALVAR simplemente toman la dirección de un operando inmediato. No hay un multiplicador de 1, o un desplazamiento de 0 aplicado podría ser así a nivel de hardware, pero no se ve en el lenguaje ensamblador o el conjunto de instrucciones.
Kaz
1

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 ).

Albert van der Horst
fuente
"Esta es una regla general en la programación de ensamblajes: use las instrucciones de cualquier forma que oscile su bote". Yo personalmente no daría ese consejo, debido a cosas como 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ó
The6P4C
@ The6P4C Esa es una advertencia útil. Sin embargo, si no hay otra alternativa para hacer que la predicción de la rama sea infeliz, hay que ir por ella. Hay otra regla general en la programación de ensamblaje. Puede haber formas alternativas de hacer algo y debe elegir sabiamente entre las alternativas. Hay cientos de formas de obtener el contenido del registro BL en el registro AL. Si no es necesario preservar el resto de RAX, LEA puede ser una opción. No afectar las banderas puede ser una buena idea en algunos de los miles de tipos de procesadores x86. Groetjes Albert
Albert van der Horst
-1

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]
tzoz
fuente
1
La "dirección efectiva" es solo la parte "desplazada" de un 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.
Peter Cordes
@PeterCordes Muy útil, gracias por corregirme.
tzoz