Este equipo redactor ha pasado mucho tiempo investigando la resolución a tus búsquedas, te ofrecemos la respuestas de modo que nuestro objetivo es resultarte de mucha ayuda.
Para un análisis más extenso de los modos de direccionamiento (16/32 / 64bit), consulte la guía “Optimizing Assembly” de Agner Fog, sección 3.3. Esa guía tiene mucho más detalle que esta respuesta para la reubicación de símbolos o código independiente de la posición de 32 bits, entre otras cosas.
Y, por supuesto, los manuales de Intel y AMD tienen secciones completas sobre los detalles de las codificaciones de ModRM (y SIB y disp8 / disp32 bytes opcionales), lo que deja en claro qué es codificable y por qué existen los límites.
Consulte también: tabla de sintaxis de AT&T (GNU) frente a sintaxis NASM para diferentes modos de direccionamiento, incluidos saltos / llamadas indirectas. También vea la colección de enlaces al final de esta respuesta.
x86 (32 y 64 bits) tiene varios modos de direccionamiento para elegir. Son todos de la forma:
[base_reg + index_reg*scale + displacement] ; or a subset of this
[RIP + displacement] ; or RIP-relative: 64bit only. No index reg is allowed
(donde la escala es 1, 2, 4 u 8, y el desplazamiento es una constante de 32 bits con signo). Todas las demás formas (excepto las relativas a RIP) son subconjuntos de esto que omiten uno o más componentes. Esto significa que no necesita un cero index_reg
acceder [rsi]
por ejemplo.
En código fuente asm, no importa en qué orden escribas las cosas: [5 + rax + rsp + 15*4 + MY_ASSEMBLER_MACRO*2]
funciona bien. (Todas las matemáticas sobre las constantes ocurren en el momento del ensamblaje, lo que resulta en un solo desplazamiento constante).
Todos los registros deben ser del mismo tamaño. como el uno al otro. Y del mismo tamaño que el modo en el que se encuentra, a menos que utilice un tamaño de dirección alternativo, lo que requiere una prefix byte. Los punteros estrechos rara vez son útiles fuera de la ABI x32 (ILP32 en modo largo) donde es posible que desee ignorar los 32 bits superiores de un registro, por ejemplo, en lugar de usar movsxd
para extender el signo de un desplazamiento posiblemente negativo de 32 bits en un registro a un ancho de puntero de 64 bits.
Si quieres usar al
como un array index, por ejemplo, es necesario extenderlo a cero o con signo hasta el ancho del puntero. (Teniendo los bits superiores de rax
ya puesto a cero antes de jugar con los registros de bytes a veces es posible, y es una buena manera de lograrlo).
Las limitaciones reflejan lo que se puede codificar en código máquina, como es habitual para el lenguaje ensamblador. El factor de escala es un recuento de cambios de 2 bits. Los bytes de ModRM (y SIB opcional) pueden codificar hasta 2 registros pero no más, y no tienen ningún modo que reste registros, solo sume. Cualquier registro puede ser una base. Cualquier registro excepto ESP / RSP puede ser un índice. ¿Ves rbp no permitido como base SIB? para los detalles de codificación, como por qué [rsp]
siempre necesita un byte SIB.
Cada posible subconjunto del caso general es codificable, excepto los que usan e/rsp*scale
(obviamente inútil en el código “normal” que siempre mantiene un puntero para apilar memoria en esp
).
Normalmente, el tamaño del código de las codificaciones es:
- 1B para modos de un registro (mod / rm (Modo / Registro o memoria))
- 2B para modos de dos registros (mod / rm + byte SIB (Scale Index Base))
- el desplazamiento puede ser de 0, 1 o 4 bytes (signo extendido a 32 o 64, según el tamaño de la dirección). Entonces los desplazamientos de
[-128 to +127]
puede usar el más compactodisp8
codificación, ahorrando 3 bytes vs.disp32
.
ModRM siempre está presente y sus bits señalan si un SIB también está presente. Similar para disp8 / disp32. Excepciones de tamaño de código:
-
[reg*scale]
por sí mismo solo se puede codificar con un desplazamiento de 32 bits (que, por supuesto, puede ser cero). Los ensambladores inteligentes resuelven eso codificandolea eax, [rdx*2]
comolea eax, [rdx + rdx]
pero ese truco solo funciona para escalar en 2. De cualquier manera, se requiere un byte SIB, además de ModRM. -
Es imposible de codificar
e/rbp
or13
como el registro base sin un byte de desplazamiento, entonces[ebp]
está codificado como[ebp + byte 0]
. Las codificaciones sin desplazamiento conebp
como registro base, significa que hay no registro base (por ejemplo, para[disp + reg*scale]
). -
[e/rsp]
requiere un byte SIB incluso si no hay registro de índice. (haya o no un desplazamiento). La codificación mod / rm que especificaría[rsp]
en su lugar, significa que hay un byte SIB.
Consulte la Tabla 2-5 en el manual de referencia de Intel y la sección circundante para obtener detalles sobre los casos especiales. (Son iguales en los modos de 32 y 64 bits. Agregar codificación relativa a RIP no entra en conflicto con ninguna otra codificación, incluso sin un REX prefix.)
Por rendimiento, normalmente no vale la pena gastar una instrucción adicional solo para obtener un código de máquina x86 más pequeño. En las CPU Intel con caché uop, es más pequeño que L1 I $ y es un recurso más valioso. Por lo general, es más importante minimizar las uops de dominio fusionado.
Como se usan
(Esta pregunta se etiquetó como MASM, pero parte de esta respuesta habla sobre la versión de NASM de la sintaxis de Intel, especialmente cuando difieren para el direccionamiento relativo a x86-64 RIP. La sintaxis de AT&T no está cubierta, pero tenga en cuenta que es solo otra sintaxis para el mismo código de máquina, por lo que las limitaciones son las mismas).
Esta tabla no coincide exactamente con las codificaciones de hardware de los posibles modos de direccionamiento, ya que estoy distinguiendo entre usar una etiqueta (por ejemplo, global o static datos) frente a utilizar un pequeño desplazamiento constante. Así que estoy cubriendo los modos de direccionamiento de hardware + soporte de enlazador para símbolos.
(Nota: normalmente querrías movzx eax, byte [esi]
o movsx
cuando la fuente es un byte, pero mov al, byte_src
se ensambla y es común en el código antiguo, fusionándose con el byte bajo de EAX / RAX. Consulte ¿Por qué GCC no usa registros parciales? y cómo aislar bytes y palabras array elementos en un registro de 64 bits)
Si tienes un int*
, a menudo usarías el factor de escala para escalar un índice por el array tamaño del elemento si tiene un índice de elemento en lugar de un desplazamiento de bytes. (Prefiera compensaciones de bytes o punteros para evitar modos de direccionamiento indexados por razones de tamaño de código y rendimiento en algunos casos, especialmente en CPU Intel donde puede dañar la microfusión). Pero también puedes hacer otras cosas.
Si tienes un puntero char array*
en esi
:
-
mov al, esi
: inválido, no se ensamblará. Sin corchetes, no es una carga en absoluto. Es un error porque los registros no son del mismo tamaño. -
mov al, [esi]
carga el byte al que apunta, es decirarray[0]
o*array
. -
mov al, [esi + ecx]
cargasarray[ecx]
. -
mov al, [esi + 10]
cargasarray[10]
. -
mov al, [esi + ecx*8 + 200]
cargasarray[ecx*8 + 200]
-
mov al, [global_array + 10]
cargas deglobal_array[10]
. En el modo de 64 bits, esta puede y debe ser una dirección relativa a RIP. Usando NASMDEFAULT REL
Se recomienda generar direcciones relativas a RIP de forma predeterminada en lugar de tener que utilizar siempre[rel global_array + 10]
. MASM hace esto por defecto, creo. No hay forma de utilizar un registro de índice con una dirección relativa a RIP directamente. El método normal eslea rax, [global_array]
mov al, [rax + rcx*8 + 10]
o similar.Consulte ¿Cómo se hacen las referencias de variables relativas a RIP como “[RIP + _a]”en x86-64 GAS Intel-syntax work? para más detalles y sintaxis para GAS
.intel_syntax
, NASM y GAS AT&T. -
mov al, [global_array + ecx + edx*2 + 10]
cargas deglobal_array[ecx + edx*2 + 10]
Obviamente, puede indexar un static/global array con un solo registro. Incluso un 2D array es posible utilizar dos registros separados. (preescalado de uno con una instrucción adicional, para factores de escala distintos de 2, 4 u 8). Tenga en cuenta que elglobal_array + 10
las matemáticas se realizan en el momento del enlace. El archivo de objeto (salida del ensamblador, entrada del enlazador) informa al enlazador del +10 para agregar a la dirección absoluta final, para colocar el desplazamiento correcto de 4 bytes en el ejecutable (salida del enlazador). Esta es la razón por la que no puede usar expresiones arbitrarias en constantes de tiempo de enlace que no son constantes de tiempo de ensamblaje (por ejemplo, direcciones de símbolo).En el modo de 64 bits, esto todavía necesita el
global_array
como 32 bits absoluto dirección para eldisp32
part, que solo funciona en un ejecutable de Linux dependiente de la posición, o largeaddressaware = no Windows. -
mov al, 0ABh
No es una carga en absoluto, sino una constante inmediata que se almacena dentro de la instrucción. (Tenga en cuenta que necesita prefix a0
para que el ensamblador sepa que es una constante, no un símbolo. Algunos ensambladores también aceptarán0xAB
, y algunos de ellos no aceptarán0ABh
: ver más).usted pueden use un símbolo como la constante inmediata, para obtener una dirección en un registro:
- NASM:
mov esi, global_array
se ensambla en unmov esi, imm32
que pone la dirección en esi. - MASM:
mov esi, OFFSET global_array
es necesario para hacer lo mismo. - MASM:
mov esi, global_array
se ensambla en una carga:mov esi, dword [global_array]
.
En el modo de 64 bits, la forma estándar de poner una dirección de símbolo en un registro es una LEA relativa a RIP. La sintaxis varía según el ensamblador. MASM lo hace por defecto. NASM necesita un
default rel
directiva, o[rel global_array]
. GAS lo necesita explícitamente en todos los modos de direccionamiento. Cómo cargar la dirección de la función o la etiqueta en el registro en GNU Assembler.mov r64, imm64
generalmente también se admite para el direccionamiento absoluto de 64 bits, pero normalmente es la opción más lenta (el tamaño del código crea cuellos de botella en el front-end).mov rdi, format_string
/call printf
normalmente funciona en NASM, pero no es eficiente.Como una optimización cuando las direcciones se pueden representar como 32 bits absoluto (en lugar de como un desplazamiento rel32 de la posición actual),
mov reg, imm32
sigue siendo óptimo al igual que en el código de 32 bits. (Ejecutable de Linux no PIE o Windows con LargeAddressAware = no). Pero tenga en cuenta que en el modo de 32 bits,lea eax, [array]
es no eficiente: desperdicia un byte de tamaño de código (ModRM + disp32 absoluto) y no puede ejecutarse en tantos puertos de ejecución comomov eax, imm32
. El modo de 32 bits no tiene direccionamiento relativo a RIP.Tenga en cuenta que OS X carga todo el código en una dirección fuera de los 32 bits bajos, por lo que el direccionamiento absoluto de 32 bits no se puede utilizar. El código independiente de la posición no requerido para ejecutables, pero también podría hacerlo porque el direccionamiento absoluto de 64 bits es menos eficiente que el relativo a RIP. El formato de archivo de objeto macho64 no admite reubicaciones para direcciones absolutas de 32 bits como lo hace Linux ELF. Asegúrese de no utilizar un nombre de etiqueta como una constante de 32 bits en tiempo de compilación en ningún lugar. Una dirección efectiva como
[global_array + constant]
está bien porque se puede ensamblar en un modo de direccionamiento relativo a RIP. Pero[global_array + rcx]
no está permitido porque RIP no se puede usar con ningún otro registro, por lo que tendría que ensamblarse con la dirección absoluta deglobal_array
codificado como el desplazamiento de 32 bits (que se extenderá con el signo a 64b). - NASM:
Todos y cada uno de estos modos de direccionamiento se pueden utilizar con LEA
para hacer cálculos matemáticos enteros con la ventaja de no afectar las banderas, independientemente de si es una dirección válida. Usar LEA en valores que no lo son direcciones / punteros?
[esi*4 + 10]
generalmente solo es útil con LEA (a menos que el desplazamiento sea un símbolo, en lugar de una pequeña constante). En el código de máquina, no hay codificación solo para el registro escalado, por lo que [esi*4]
tiene que ensamblar para [esi*4 + 0]
, con 4 bytes de ceros para un desplazamiento de 32 bits. Todavía a menudo vale la pena copiar + shift en una instrucción en lugar de un mov + shl más corto, porque generalmente el rendimiento de uop es más un cuello de botella que el tamaño del código, especialmente en CPU con una caché de uop decodificada.
Puede especificar anulaciones de segmento como mov al, fs:[esi]
(Sintaxis NASM). Una anulación de segmento simplemente agrega un prefix-byte delante de la codificación habitual. Todo lo demás permanece igual, con la misma sintaxis.
Incluso puede utilizar anulaciones de segmento con direccionamiento relativo a RIP. El direccionamiento absoluto de 32 bits requiere un byte más para codificar que el relativo a RIP, por lo que mov eax, fs:[0]
se puede codificar de la manera más eficiente utilizando un desplazamiento relativo que produce una dirección absoluta conocida. es decir, elija rel32 para que RIP + rel32 = 0. YASM hará esto con mov ecx, [fs: rel 0]
, pero NASM siempre usa el direccionamiento absoluto disp32, ignorando el rel
especificador. No he probado MASM ni gas.
Si el tamaño del operando es ambiguo (por ejemplo, en una instrucción con un operando inmediato y uno de memoria), utilice byte
/ word
/ dword
/ qword
para especificar:
mov dword [rsi + 10], 123 ; NASM
mov dword ptr [rsi + 10], 123 ; MASM and GNU .intex_syntax noprefix
movl $123, 10(%rsi) # GNU(AT&T): operand size from mnemonic suffix
Consulte los documentos de yasm para conocer las direcciones efectivas de sintaxis NASM y / o la sección de entrada de wikipedia x86 sobre modos de direccionamiento.
La página wiki dice lo que está permitido en el modo de 16 bits. Aquí hay otra “hoja de trucos” para los modos de direccionamiento de 32 bits.
Modos de direccionamiento de 16 bits
El tamaño de la dirección de 16 bits no puede usar un byte SIB, por lo que todos los modos de direccionamiento de uno y dos registros están codificados en un solo byte mod / rm. reg1
puede ser BX o BP, y reg2
puede ser SI o DI (o puede usar cualquiera de esos 4 registros por sí mismo). La escala no está disponible. El código de 16 bits es obsoleto por muchas razones, incluida esta, y no vale la pena aprenderlo si no es necesario.
Tenga en cuenta que las restricciones de 16 bits se aplican en el código de 32 bits cuando el tamaño de la dirección prefix se utiliza, por lo que LEA-math de 16 bits es muy restrictivo. Sin embargo, puede solucionarlo: lea eax, [edx + ecx*2]
conjuntos ax = dx + cx*2
, porque la basura en los bits superiores de los registros fuente no tiene ningún efecto.
También hay una guía más detallada para los modos de direccionamiento, para 16 bits. 16 bits tiene un conjunto limitado de modos de direccionamiento (solo unos pocos registros son válidos y no hay factores de escala), pero es posible que desee leerlo para comprender algunos fundamentos sobre cómo las CPU x86 usan direcciones porque algo de eso no ha cambiado para Modo de 32 bits.
Temas relacionados:
Muchos de estos también están vinculados anteriormente, pero no todos.
- Ver el Wiki de etiquetas SO x86 página para enlaces a documentos y manuales de referencia, incluidos los manuales de Intel.
- Los wikis de sintaxis de Intel y de etiquetas de sintaxis de AT&T cubren las diferencias entre ellos y (para Intel) los diferentes sabores de la sintaxis de Intel.
- Modos de micro fusión y direccionamiento Consecuencias de rendimiento de los modos de direccionamiento indexados en la familia Sandybridge: deslaminación excepto en casos limitados.
- El formato Mach-O de 64 bits no admite direcciones absolutas de 32 bits. NASM Accessing Array MacOS 64-bit direccionamiento
- ¿Ya no se permiten direcciones absolutas de 32 bits en Linux x86-64? (Linux PIE frente a la posición-dependiente ejecutables)
- ¿Cómo las referencias de variables relativas a RIP como “[RIP + _a]”en x86-64 GAS Intel-syntax work? (también cubre NASM y GAS AT&T)
- Cómo cargar la dirección de la función o etiqueta en el registro en GNU Assembler, cómo colocar de manera eficiente las direcciones de los símbolos en los registros, en lugar de simplemente usarlos en un modo de direccionamiento directamente.
- ¿Por qué la dirección de static variables relativas al puntero de instrucción? y ¿Por qué esta instrucción MOVSS utiliza direccionamiento relativo a RIP? – Relativo a RIP es la forma eficiente estándar de cargar / almacenar desde static data, y funciona aunque los datos estén en una sección diferente del código (debido a cómo funcionan los enlazadores / cargadores de programas, el desplazamiento relativo permanece constante aunque el programa / biblioteca en su conjunto es independiente de la posición).
Ten en cuenta dar difusión a esta división si lograste el éxito.