Hacer referencia al contenido de una ubicación de memoria. (modos de direccionamiento x86)

Resuelto DrakeJacks asked hace 8 años • 2 respuestas

Tengo una ubicación de memoria que contiene un carácter que quiero comparar con otro carácter (y no está en la parte superior de la pila, por lo que no puedo simplemente pophacerlo). ¿Cómo hago referencia al contenido de una ubicación de memoria para poder compararlo?

Básicamente, ¿cómo lo hago sintácticamente?

DrakeJacks avatar Dec 03 '15 11:12 DrakeJacks
Aceptado

Para obtener una discusión más amplia sobre los modos de direccionamiento (16/32/64 bits), consulte la guía "Optimización del ensamblaje" de Agner Fog , sección 3.3. Esa guía tiene muchos más detalles 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 opcional y disp8/disp32 bytes), lo que deja claro qué es codificable y por qué existen límites.

Consulte también: tabla de sintaxis de AT&T(GNU) frente a sintaxis de NASM para diferentes modos de direccionamiento , incluidos saltos/llamadas indirectas. Consulte también 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 este que omiten uno o más componentes . Esto significa que no necesita cero index_regpara acceder, [rsi]por ejemplo.

En el código fuente de ASM , no importa en qué orden escribas las cosas: [5 + rax + rsp + 15*4 + MY_ASSEMBLER_MACRO*2]funciona bien. (Todos los cálculos sobre constantes ocurren en el momento del ensamblaje, lo que da como resultado un único desplazamiento constante).

Todos los registros deben tener el mismo tamaño entre sí. Y el 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 un byte de prefijo adicional. 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 usarlos movsxdpara extender con signo un desplazamiento posiblemente negativo de 32 bits en un registro para Ancho de puntero de 64 bits.

Si desea utilizarlo alcomo índice de matriz, por ejemplo , debe extenderlo con cero o signo hasta el ancho del puntero. (A veces es posible tener los bits superiores raxya puestos a cero antes de jugar con los registros de bytes, y es una buena manera de lograrlo).


Las limitaciones reflejan lo que se puede codificar en código máquina, como es habitual en el lenguaje ensamblador. El factor de escala es un recuento de desplazamiento de 2 bits. Los bytes 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 que rbp no está permitido como base SIB? para los detalles de codificación, como por qué [rsp]siempre se necesita un byte SIB.

Todos los subconjuntos posibles del caso general son codificables, excepto los que usan e/rsp*scale(obviamente inútil en el código "normal" que siempre mantiene un puntero para apilar la memoria 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 (base de índice de escala))
  • el desplazamiento puede ser 0, 1 o 4 bytes (signo extendido a 32 o 64, según el tamaño de la dirección). Por lo tanto, los desplazamientos desde [-128 to +127]pueden utilizar la disp8codificación más compacta, ahorrando 3 bytes frente a disp32.

ModRM siempre está presente y sus bits indican si también hay un SIB presente. Similar a disp8/disp32. Excepciones de tamaño de código:

  • [reg*scale]por sí solo sólo puede codificarse con un desplazamiento de 32 bits (que por supuesto puede ser cero). Los ensambladores inteligentes solucionan esto codificando lea eax, [rdx*2]como lea eax, [rdx + rdx], pero ese truco solo funciona para escalar a 2. De cualquier manera, se requiere un byte SIB, además de ModRM.

  • Es imposible codificar e/rbpo r13como registro base sin un byte de desplazamiento, por lo que [ebp]se codifica como [ebp + byte 0]. Las codificaciones sin desplazamiento con ebpun registro base significan que no hay un registro base (por ejemplo, para [disp + reg*scale]).

  • [e/rsp]requiere un byte SIB incluso si no hay un registro de índice. (si hay o no 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 modo de 32 y 64 bits. Agregar codificación relativa a RIP no entró en conflicto con ninguna otra codificación, incluso sin un prefijo REX).

En términos de 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. Minimizar los uops de dominios fusionados suele ser más importante.


como se usan

(Esta pregunta fue etiquetada como MASM, pero parte de esta respuesta habla sobre la versión NASM de la sintaxis Intel, especialmente donde difieren para el direccionamiento relativo a RIP x86-64. 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 distingo entre usar una etiqueta (por ejemplo, datos globales o estáticos) versus usar un pequeño desplazamiento constante. Así que estoy cubriendo modos de direccionamiento de hardware + soporte de enlazador para símbolos.

(Nota: generalmente querrá movzx eax, byte [esi]o movsxcuando la fuente es un byte, pero mov al, byte_srcse ensambla y es común en el código antiguo, fusionándose en el byte bajo de EAX/RAX. Consulte ¿Por qué GCC no usa registros parciales? y Cómo aislar elementos de matriz de bytes y palabras en un registro de 64 bits )

Si tiene un int*, a menudo usaría el factor de escala para escalar un índice según el tamaño del elemento de la matriz 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 del 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 tiene un puntero char array*enesi :

  • mov al, esi: no válido, no se ensambla. 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 decir, array[0]o *array.

  • mov al, [esi + ecx]cargas array[ecx].

  • mov al, [esi + 10]cargas array[10].

  • mov al, [esi + ecx*8 + 200]cargasarray[ecx*8 + 200]

  • mov al, [global_array + 10]cargas de global_array[10]. En modo de 64 bits, esta puede y debe ser una dirección relativa a RIP. Se recomienda utilizar NASM DEFAULT RELpara generar direcciones relativas a RIP de forma predeterminada en lugar de tener que utilizar siempre [rel global_array + 10]. Creo que MASM hace esto de forma predeterminada. No hay forma de utilizar un registro de índice con una dirección relativa a RIP directamente. El método normal es lea rax, [global_array] mov al, [rax + rcx*8 + 10]o similar.

    Consulte ¿Cómo funcionan las referencias de variables relativas a RIP como "[RIP + _a]" en la sintaxis Intel x86-64 GAS? para obtener más detalles y sintaxis de GAS .intel_syntax, NASM y GAS AT&T.

  • mov al, [global_array + ecx + edx*2 + 10]cargas desde global_array[ecx + edx*2 + 10] Obviamente, puede indexar una matriz estática/global con un solo registro. Incluso es posible una matriz 2D que utilice dos registros separados. (preescalado uno con una instrucción adicional, para factores de escala distintos de 2, 4 u 8). Tenga en cuenta que los global_array + 10cálculos se realizan en el momento del enlace. El archivo objeto (salida del ensamblador, entrada del vinculador) informa al vinculador del +10 que debe agregar a la dirección absoluta final, para colocar el desplazamiento correcto de 4 bytes en el ejecutable (salida del vinculador). Esta es la razón por la que no se pueden utilizar expresiones arbitrarias en constantes de tiempo de enlace que no sean constantes de tiempo de ensamblaje (por ejemplo, direcciones de símbolos).

    En el modo de 64 bits, esto todavía necesita una dirección absolutaglobal_array de 32 bits para la pieza, que solo funciona en un ejecutable de Linux dependiente de la posición , o largeaddressaware=no Windows.disp32

  • mov al, 0ABhNo es una carga en absoluto, sino una constante inmediata que se almacenó dentro de la instrucción. (Tenga en cuenta que debe anteponer a 0para que el ensamblador sepa que es una constante, no un símbolo. Algunos ensambladores también lo aceptarán 0xAB, y algunos no lo aceptarán 0ABh: ver más ).

    Puede utilizar un símbolo como constante inmediata para obtener una dirección en un registro:

    • NASM: mov esi, global_arrayse ensambla en un mov esi, imm32que pone la dirección en esi.
    • MASM: mov esi, OFFSET global_arrayes necesario para hacer lo mismo.
    • MASM: mov esi, global_arrayensambla en una carga: mov esi, dword [global_array].

    En el modo de 64 bits, la forma estándar de colocar 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 de forma predeterminada. NASM necesita una default reldirectiva, o [rel global_array]. GAS lo necesita explícitamente en cada modo de direccionamiento. Cómo cargar la dirección de una función o etiqueta en el registro en GNU Assembler . mov r64, imm64Por lo general, también se admite para 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 printfnormalmente funciona en NASM, pero no es eficiente.

    Como optimización, cuando las direcciones se pueden representar como un absoluto de 32 bits (en lugar de un desplazamiento rel32 de la posición actual), mov reg, imm32sigue siendo óptima al igual que en el código de 32 bits. (Linux ejecutable no PIE o Windows con LargeAddressAware=no). Pero tenga en cuenta que en modo de 32 bits, nolea eax, [array] es eficiente: desperdicia un byte de tamaño de código (ModRM + disp32 absoluto) y no puede ejecutarse en tantos puertos de ejecución como . El modo de 32 bits no tiene direccionamiento relativo a RIP.mov eax, imm32

    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. No se requiere código independiente de la posición para los ejecutables, pero también podría ser necesario 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 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 codificada global_arraycomo desplazamiento de 32 bits ( que se extenderá con signo a 64b ).


Todos y cada uno de estos modos de direccionamiento se pueden utilizar para LEArealizar operaciones matemáticas con números enteros con la ventaja de no afectar a los indicadores , independientemente de si se trata de una dirección válida. ¿Utiliza LEA en valores que no son direcciones/punteros?

[esi*4 + 10]Por lo general, 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]debe ensamblarse en [esi*4 + 0], con 4 bytes de ceros para un desplazamiento de 32 bits. A menudo todavía vale la pena copiar+cambiar 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 un caché de uop decodificado.


Puede especificar anulaciones de segmentos comomov al, fs:[esi] (sintaxis NASM). Una anulación de segmento simplemente agrega un byte de prefijo delante de la codificación habitual. Todo lo demás sigue igual, con la misma sintaxis.

Incluso puede utilizar anulaciones de segmentos con direccionamiento relativo a RIP. El direccionamiento absoluto de 32 bits requiere un byte más para codificarse que el RIP relativo, por lo que mov eax, fs:[0]se puede codificar de manera más eficiente utilizando un desplazamiento relativo que produzca 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 relespecificador. 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/ qwordpara 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 modos de direccionamiento de 32 bits .


Modos de direccionamiento de 16 bits

El tamaño de dirección de 16 bits no puede utilizar un byte SIB, por lo que todos los modos de direccionamiento de uno y dos registros se codifican en un único byte mod/rm. reg1puede ser BX o BP, y reg2puede ser SI o DI (o puede usar cualquiera de esos 4 registros por sí solo). La escala no está disponible. El código de 16 bits está obsoleto por muchas razones, incluida ésta, y no vale la pena aprenderlo si no es necesario.

Tenga en cuenta que las restricciones de 16 bits se aplican en código de 32 bits cuando se utiliza el prefijo de tamaño de dirección, por lo que las matemáticas LEA de 16 bits son muy restrictivas. Sin embargo, puedes solucionar esto: 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 sobre los modos de direccionamiento, para 16 bits . Los 16 bits tienen un conjunto limitado de modos de direccionamiento (sólo unos pocos registros son válidos y ningún factor de escala), pero es posible que desees leerlo para comprender algunos conceptos básicos sobre cómo las CPU x86 usan las direcciones porque parte de eso no ha cambiado desde hace tiempo. Modo de 32 bits.


Temas relacionados:

Muchos de ellos también están vinculados anteriormente, pero no todos.

  • Consulte la página wiki de la etiqueta SO x86 para obtener enlaces a documentos y manuales de referencia, incluidos los manuales de Intel.
  • Los wikis de etiquetas de sintaxis de Intel y de AT&T cubren las diferencias entre ellos y (para Intel) los diferentes tipos de sintaxis de Intel.
  • Consecuencias del rendimiento de los modos de direccionamiento y microfusión 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 Accediendo a Array Direccionamiento de MacOS de 64 bits
  • ¿Ya no se permiten direcciones absolutas de 32 bits en Linux x86-64? (Linux PIE versus ejecutables dependientes de la posición)
  • ¿Cómo funcionan las referencias de variables relativas a RIP como "[RIP + _a]" en la sintaxis Intel x86-64 GAS? (también cubre NASM y GAS AT&T)
  • Cómo cargar la dirección de una función o etiqueta en un registro en GNU Assembler. Cómo colocar de manera eficiente direcciones de símbolos en registros, en lugar de simplemente usarlos en modo de direccionamiento directamente.
  • ¿Por qué la dirección de las variables estáticas es relativa al puntero de instrucción? y ¿ Por qué esta instrucción MOVSS utiliza direccionamiento relativo a RIP? - RIP-relative es la forma eficiente estándar de cargar/almacenar datos estáticos, y funciona incluso 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/ La biblioteca en su conjunto es independiente de la posición).
Peter Cordes avatar Dec 03 '2015 05:12 Peter Cordes

Aquí hay una hoja de referencia rápida, recuperada de este sitio . Muestra los diversos métodos disponibles para direccionar la memoria principal en un ensamblado x86:

+------------------------+----------------------------+-----------------------------+
| Mode                   | Intel                      | AT&T                        |
+------------------------+----------------------------+-----------------------------+
| Absolute               | MOV EAX, [0100]            | movl           0x0100, %eax |
| Register               | MOV EAX, [ESI]             | movl           (%esi), %eax |
| Reg + Off              | MOV EAX, [EBP-8]           | movl         -8(%ebp), %eax |
| Reg*Scale + Off        | MOV EAX, [EBX*4 + 0100]    | movl   0x100(,%ebx,4), %eax |
| Base + Reg*Scale + Off | MOV EAX, [EDX + EBX*4 + 8] | movl 0x8(%edx,%ebx,4), %eax |
+------------------------+----------------------------+-----------------------------+

En su caso específico, si el elemento está ubicado a un desplazamiento de 4la base de la pila EBP, usaría la Reg + Offnotación:

MOV EAX, [ EBP - 4 ]

Esto copiaría el artículo en el registro EAX.

Jet Blue avatar Aug 12 '2019 02:08 Jet Blue