c assembly x86 memory-address

¿Usa LEA en valores que no son direcciones/punteros?



assembly x86 (3)

Estaba tratando de entender cómo funciona la instrucción de cálculo de direcciones, especialmente con el comando leaq . Luego me confundo cuando veo ejemplos que usan leaq para hacer cálculos aritméticos. Por ejemplo, el siguiente código C,

long m12(long x) { return x*12; }

En asamblea,

leaq (%rdi, %rdi, 2), %rax salq $2, $rax

Si entiendo bien, leaq debe mover cualquier dirección (%rdi, %rdi, 2) , que debe ser 2*%rdi+%rdi , evaluar a %rax . Lo que me confunde es que, dado que el valor x se almacena en %rdi , que es solo la dirección de memoria, ¿por qué multiplicado por% rdi por 3 y luego a la izquierda, esta dirección de memoria por 2 es igual a x multiplicado por 12? ¿No es eso cuando multiplicamos %rdi por 3, saltamos a otra dirección de memoria que no tiene el valor x?


LEA es para calcular la dirección . No hace referencia a la dirección de memoria

Debería ser mucho más legible en la sintaxis de Intel

m12(long): lea rax, [rdi+rdi*2] sal rax, 2 ret

Entonces, la primera línea es equivalente a rax = rdi*3 Luego, el desplazamiento a la izquierda es multiplicar rax por 4, lo que resulta en rdi*3*4 = rdi*12


leaq no tiene que operar en direcciones de memoria, y calcula una dirección, en realidad no lee del resultado, así que hasta que un mov o similar intente usarlo, es solo una forma esotérica de agregar un número, más 1, 2, 4 u 8 veces otro número (o el mismo número en este caso). Con frecuencia se abusa con fines matemáticos, como puede ver. 2*%rdi+%rdi es solo 3 * %rdi , por lo que está calculando x * 3 sin involucrar la unidad multiplicadora en la CPU.

Del mismo modo, el desplazamiento hacia la izquierda, para enteros, duplica el valor de cada bit desplazado (cada cero agregado a la derecha), gracias a la forma en que funcionan los números binarios (de la misma manera en números decimales, agregando ceros en la derecha se multiplica por 10).

Entonces, se está abusando de la instrucción leaq para lograr la multiplicación por 3, luego se cambia el resultado para lograr una multiplicación adicional por 4, para un resultado final de multiplicar por 12 sin usar realmente una instrucción de multiplicación (que presumiblemente cree que correría más lentamente, y por lo que sé, podría ser correcto; adivinar el compilador suele ser un juego perdedor).


lea (consulte la entrada manual del conjunto de instrucciones de Intel) es una instrucción shift-and-add que utiliza la sintaxis de los operandos de memoria y la codificación de la máquina. Esto explica el nombre, pero no es lo único para lo que es bueno. En realidad, nunca accede a la memoria, por lo que es como usar & en C.

Consulte, por ejemplo, ¿Cómo multiplicar un registro por 37 utilizando solo 2 instrucciones de lealtad consecutivas en x86?

En C, es como uintptr_t foo = &arr[idx] . Tenga en cuenta & para obtener el resultado de arr + idx , incluida la escala del tamaño del objeto de arr . En C, esto sería un abuso de la sintaxis y los tipos de lenguaje, pero en x86 los punteros de ensamblaje y los enteros son lo mismo. Todo es solo bytes, y depende del programa poner las instrucciones en el orden correcto para obtener resultados útiles.

El diseñador / arquitecto original del conjunto de instrucciones de 8086 ( Stephen Morse ) podría o no haber tenido las matemáticas de puntero en mente como el principal caso de uso, pero los compiladores modernos lo consideran simplemente como otra opción para hacer aritmética en punteros / enteros, y eso es cómo deberías pensar en eso también.

(Tenga en cuenta que los modos de direccionamiento de 16 bits no incluyen cambios, solo [BP|BX] + [SI|DI] + disp8/disp16 , por lo que LEA no era tan útil para las matemáticas sin puntero antes de 386. Vea esta respuesta para más sobre los modos de direccionamiento de 32/64 bits, aunque esa respuesta usa la sintaxis de Intel como [rax + rdi*4] lugar de la sintaxis de AT&T utilizada en esta pregunta. El código de máquina x86 es el mismo independientemente de la sintaxis que use para crearlo. )

Tal vez los arquitectos del 8086 simplemente quisieron exponer el hardware de cálculo de direcciones para usos arbitrarios porque podían hacerlo sin usar muchos transistores adicionales. El decodificador ya debe poder decodificar los modos de direccionamiento, y otras partes de la CPU deben poder realizar cálculos de dirección. Poner el resultado en un registro en lugar de usarlo con un valor de registro de segmento para el acceso a la memoria no requiere muchos transistores adicionales. Ross Ridge confirma que LEA en el 8086 original reutiliza el hardware de cálculo y decodificación de direcciones efectivas de las CPU.

Tenga en cuenta que la mayoría de las CPU modernas ejecutan LEA en las mismas ALU que las instrucciones normales de agregar y cambiar . Tienen AGU dedicadas (unidades de generación de direcciones), pero solo las usan para operandos de memoria reales. Atom en orden es una excepción; LEA se ejecuta antes en la tubería que las ALU: las entradas deben estar listas antes, pero las salidas también están listas antes. Las CPU de ejecución fuera de orden (la gran mayoría para el x86 moderno) no quieren que LEA interfiera con las cargas / tiendas reales, por lo que lo ejecutan en una ALU.

lea tiene buena latencia y rendimiento, pero no tan buen rendimiento como add o mov r32, imm32 en la mayoría de las CPU, así que solo use lea cuando pueda guardar instrucciones con él en lugar de add . (Consulte la guía de microarquitectura x86 de Agner Fog y el manual de optimización de asm .)

La implementación interna es irrelevante, pero es una apuesta segura que decodificar los operandos a LEA comparte transistores con modos de direccionamiento de decodificación para cualquier otra instrucción . (Por lo tanto, hay reutilización / uso compartido de hardware incluso en las CPU modernas que no ejecutan lea en una AGU). Cualquier otra forma de exponer una instrucción shift-and-add de entradas múltiples habría requerido una codificación especial para los operandos.

Por lo tanto, 386 recibió una instrucción ALU shift-and-add para "libre" cuando extendió los modos de direccionamiento para incluir el índice escalado, y al poder usar cualquier registro en un modo de direccionamiento hizo que LEA fuera mucho más fácil de usar también para los no punteros .

x86-64 obtuvo acceso barato al contador del programa (en lugar de tener que leer qué call presionó ) "gratis" a través de LEA porque agregó el modo de direccionamiento relativo a RIP, haciendo que el acceso a datos estáticos sea significativamente más barato en x86-64 independiente de la posición código que en PIC de 32 bits. (El pariente RIP necesita un soporte especial en las ALU que manejan LEA, así como en las AGU separadas que manejan las direcciones reales de carga / almacenamiento. Pero no se necesitaban nuevas instrucciones).

Es tan bueno para la aritmética arbitraria como para los punteros, por lo que es un error pensar que está destinado a los punteros en estos días . No es un "abuso" o "truco" usarlo para personas que no usan punteros, porque todo es un número entero en lenguaje ensamblador. Tiene un rendimiento más bajo que el add , pero es lo suficientemente barato como para usarlo casi todo el tiempo cuando guarda incluso una instrucción. Pero puede guardar hasta tres instrucciones:

;; Intel syntax. lea eax, [rdi + rsi*4 - 8] ; 3 cycle latency on Intel SnB-family ; 2-component LEA is only 1c latency ;;; without LEA: mov eax, esi ; maybe 0 cycle latency, otherwise 1 shl eax, 2 ; 1 cycle latency add eax, edi ; 1 cycle latency sub eax, 8 ; 1 cycle latency

En algunas CPU AMD, incluso una LEA compleja es solo de 2 ciclos de latencia, pero la secuencia de 4 instrucciones sería de 4 ciclos de latencia desde que esi esté listo hasta que el eax final esté listo. De cualquier manera, esto ahorra 3 uops para que el front-end se decodifique y emita, y eso ocupa espacio en el búfer de reordenamiento hasta el retiro.

lea tiene varios beneficios importantes , especialmente en el código de 32/64 bits, donde los modos de direccionamiento pueden usar cualquier registro y pueden cambiar:

  • no destructivo: salida en un registro que no es una de las entradas . A veces es útil solo como copiar y agregar como lea 1(%rdi), %eax o lea (%rdx, %rbp), %ecx .
  • puede hacer 3 o 4 operaciones en una sola instrucción (ver arriba).
  • Las matemáticas sin modificar EFLAGS pueden ser útiles después de una prueba antes de un cmovcc . O tal vez en un bucle de agregar con acarreo en CPU con paradas de bandera parcial.
  • x86-64: el código de posición independiente puede usar un LEA relativo a RIP para obtener un puntero a datos estáticos.

    7 bytes byte lea foo(%rip), %rdi es ligeramente más grande y más lento que mov $foo, %edi (5 bytes), por lo que prefiere mov r32, imm32 en código dependiente de la posición en sistemas operativos donde los símbolos están en los 32 bits bajos de espacio de direcciones virtuales, como Linux. Es posible que deba deshabilitar la configuración PIE predeterminada en gcc para usar esto.

    En el código de 32 bits, mov edi, OFFSET symbol es similarmente más corto y rápido que lea edi, [symbol] . ( OFFSET el OFFSET en la sintaxis NASM). El relativo a RIP no está disponible y las direcciones se ajustan en un inmediato de 32 bits, por lo que no hay razón para considerar lea lugar de mov r32, imm32 si necesita obtener direcciones de símbolos estáticos en los registros .

Aparte de LEA relativo a RIP en modo x86-64, todos estos se aplican por igual al cálculo de punteros en comparación con el cálculo de suma / desplazamiento de enteros sin puntero.

Consulte también el wiki de etiquetas x86 para obtener guías / manuales de ensamblaje e información de rendimiento.

Tamaño de operando versus tamaño de dirección para lea x86-64

Vea también ¿Qué operaciones de enteros complementarios de 2 se pueden usar sin poner a cero los bits altos en las entradas, si solo se desea la parte baja del resultado? . El tamaño de dirección de 64 bits y el tamaño de operando de 32 bits es la codificación más compacta (sin prefijos adicionales), por lo tanto, prefiera lea (%rdx, %rbp), %ecx cuando sea posible en lugar de lea (%rdx, %rbp), %rcx de 64 bits lea (%rdx, %rbp), %rcx o lea (%edx, %ebp), %ecx 32 bits lea (%edx, %ebp), %ecx .

x86-64 lea (%edx, %ebp), %ecx siempre es un desperdicio de un prefijo de tamaño de dirección vs. lea (%rdx, %rbp), %ecx , pero obviamente se requiere un tamaño de dirección / operando de 64 bits para haciendo matemática de 64 bits. (El desensamblador objconv de Agner Fog incluso advierte sobre prefijos de tamaño de dirección inútiles en LEA con un tamaño de operando de 32 bits).

Excepto tal vez en Ryzen, donde Agner Fog informa que el tamaño de operando de 32 bits en modo de 64 bits tiene un ciclo adicional de latencia. No sé si anular el tamaño de la dirección a 32 bits puede acelerar LEA en el modo de 64 bits si necesita truncar a 32 bits.

Esta pregunta es casi un duplicado de la muy votada. ¿Cuál es el propósito de la instrucción LEA? , pero la mayoría de las respuestas lo explican en términos de cálculo de dirección en datos de puntero reales. Eso es solo un uso.