Usando el registro de puntero base en C ++ en línea asm

Resuelto jaw asked hace 8 años • 2 respuestas

Quiero poder utilizar el registro de puntero base ( %rbp) dentro del conjunto en línea. Un ejemplo de juguete de esto es así:

void Foo(int &x)
{
    asm volatile ("pushq %%rbp;"         // 'prologue'
                  "movq %%rsp, %%rbp;"   // 'prologue'
                  "subq $12, %%rsp;"     // make room

                  "movl $5, -12(%%rbp);" // some asm instruction

                  "movq %%rbp, %%rsp;"  // 'epilogue'
                  "popq %%rbp;"         // 'epilogue'
                  : : : );
    x = 5;
}

int main() 
{
    int x;
    Foo(x);
    return 0;
}

Esperaba que, dado que estoy usando el método habitual de llamada de función de prólogo/epílogo para empujar y hacer estallar el antiguo %rbp, esto estaría bien. Sin embargo, detecta fallas cuando intento acceder xdespués del conjunto en línea.

El código ensamblador generado por GCC (ligeramente simplificado) es:

_Foo:
    pushq   %rbp
    movq    %rsp, %rbp
    movq    %rdi, -8(%rbp)

    # INLINEASM
    pushq %rbp;          // prologue
    movq %rsp, %rbp;     // prologue
    subq $12, %rsp;      // make room
    movl $5, -12(%rbp);  // some asm instruction
    movq %rbp, %rsp;     // epilogue
    popq %rbp;           // epilogue
    # /INLINEASM

    movq    -8(%rbp), %rax
    movl    $5, (%rax)      // x=5;
    popq    %rbp
    ret

main:
    pushq   %rbp
    movq    %rsp, %rbp
    subq    $16, %rsp
    leaq    -4(%rbp), %rax
    movq    %rax, %rdi
    call    _Foo
    movl    $0, %eax
    leave
    ret

¿Alguien puede decirme por qué falla este segmento? Parece que de alguna manera corrompo %rbppero no veo cómo. Gracias de antemano.

Estoy ejecutando GCC 4.8.4 en Ubuntu 14.04 de 64 bits.

jaw avatar Dec 30 '15 05:12 jaw
Aceptado

Consulte la parte inferior de esta respuesta para ver una colección de enlaces a otras preguntas y respuestas sobre asm en línea.

Su código no funciona porque pisa la zona roja debajo de RSP (con push) donde GCC mantenía un valor.


¿Qué espera aprender a lograr con el ensamblaje en línea? Si desea aprender asm en línea, aprenda a usarlo para crear código eficiente, en lugar de cosas horribles como esta. Si desea escribir prólogos de funciones y presionar/hacer estallar para guardar/restaurar registros, debe escribir funciones completas en asm . (Entonces puede usar fácilmente nasm o yasm, en lugar de la sintaxis menos preferida por la mayoría de AT&T con directivas de ensamblador GNU 1 ).

GNU inline asm es difícil de usar, pero le permite mezclar fragmentos de asm personalizados en C y C++ mientras permite que el compilador maneje la asignación de registros y cualquier guardado/restauración si es necesario. A veces, el compilador podrá evitar guardar y restaurar dándole un registro que puede ser golpeado. Sin volatile, incluso puede sacar declaraciones asm de los bucles cuando la entrada sería la misma. (es decir, a menos que utilice volatile, se supone que las salidas son una función "pura" de las entradas).

Si simplemente estás intentando aprender asm en primer lugar, GNU inline asm es una elección terrible. Debe comprender completamente casi todo lo que sucede con el conjunto y comprender lo que el compilador necesita saber para escribir las restricciones de entrada/salida correctas y hacer todo bien. Los errores provocarán golpes en las cosas y roturas difíciles de depurar. La ABI de llamada de función es mucho más simple y fácil de realizar un seguimiento del límite entre su código y el código del compilador.


¿Por qué esto se rompe?

Compiló con-O0 , por lo que el código de gcc transfiere el parámetro de función %rdia una ubicación en la pila. (Esto podría suceder en una función no trivial incluso con -O3).

Dado que la ABI de destino es la ABI x86-64 SysV , utiliza la "Zona Roja" (128 bytes por debajo %rspde la cual ni siquiera los manejadores de señales asíncronos pueden golpear), en lugar de desperdiciar una instrucción disminuyendo el puntero de la pila para reservar espacio.

Almacena la función de puntero 8B arg en -8(rsp_at_function_entry). Luego, su conjunto en línea empuja %rbp, lo que disminuye %rsp en 8 y luego escribe allí, golpeando el 32b bajo de &x(el puntero).

Cuando termine su ensamblaje en línea,

  • gcc recarga -8(%rbp)(que se ha sobrescrito con %rbp) y lo usa como dirección para una tienda 4B.
  • Foovuelve a maincon %rbp = (upper32)|5 (valor original con el 32 bajo establecido en 5).
  • maincarreras leave: %rsp = (upper32)|5
  • mainse ejecuta retcon %rsp = (upper32)|5, leyendo la dirección del remitente de la dirección virtual (void*)(upper32|5), que es de su comentario 0x7fff0000000d.

No lo verifiqué con un depurador; Uno de esos pasos puede estar ligeramente desviado, pero el problema definitivamente es que golpeas la zona roja , lo que hace que el código de gcc destruya la pila.

Incluso agregar un golpe de "memoria" no hace que gcc evite el uso de la zona roja, por lo que parece que asignar su propia memoria de pila desde un conjunto en línea es simplemente una mala idea. (Un golpe de memoria significa que es posible que haya escrito algo de memoria en la que se le permite escribir, por ejemplo, una variable global o algo señalado por un global, no que haya sobrescrito algo que se supone que no debe escribir).

Si desea utilizar el espacio temporal del conjunto en línea, probablemente debería declarar una matriz como una variable local y usarla como un operando de solo salida (del cual nunca lee).

AFAIK, no hay sintaxis para declarar que modificas la zona roja, por lo que tus únicas opciones son:

  • utilice un "=m"operando de salida (posiblemente una matriz) para el espacio temporal; el compilador probablemente completará ese operando con un modo de direccionamiento relativo a RBP o RSP. Puedes indexarlo con constantes como 4 + %[tmp]o lo que sea. Es posible que reciba una advertencia del ensamblador 4 + (%rsp), pero no un error.
  • omita la zona roja con add $-128, %rsp/ sub $-128, %rspalrededor de su código. (Es necesario si desea utilizar una cantidad desconocida de espacio de pila adicional, por ejemplo, insertar un bucle o realizar una llamada a una función. Otra razón más para eliminar la referencia a un puntero de función en C puro, no en un conjunto en línea).
  • compilar con -mno-red-zone(no creo que puedas habilitar eso por función, solo por archivo)
  • En primer lugar, no utilices espacio temporal. Dígale al compilador qué registros está golpeando y deje que los guarde.

Esto es lo que deberías haber hecho :

void Bar(int &x)
{
    int tmp;
    long tmplong;
    asm ("lea  -16 + %[mem1], %%rbp\n\t"
         "imul $10, %%rbp, %q[reg1]\n\t"  // q modifier: 64bit name.
         "add  %k[reg1], %k[reg1]\n\t"    // k modifier: 32bit name
         "movl $5, %[mem1]\n\t" // some asm instruction writing to mem
           : [mem1] "=m" (tmp), [reg1] "=r" (tmplong)  // tmp vars -> tmp regs / mem for use inside asm
           :
           : "%rbp" // tell compiler it needs to save/restore %rbp.
  // gcc refuses to let you clobber %rbp with -fno-omit-frame-pointer (the default at -O0)
  // clang lets you, but memory operands still use an offset from %rbp, which will crash!
  // gcc memory operands still reference %rsp, so don't modify it.  Declaring a clobber on %rsp does nothing
         );
    x = 5;
}

Tenga en cuenta el push/pop %rbpen el código fuera de la sección #APP/ #NO_APP, emitido por gcc. También ten en cuenta que la memoria scratch que te proporciona está en la zona roja. Si compila con -O0, verá que está en una posición diferente de donde se derrama &x.

Para obtener más registros reutilizables, es mejor simplemente declarar más operandos de salida que nunca sean utilizados por el código circundante que no es ASM. Eso deja la asignación de registros al compilador, por lo que puede ser diferente cuando se inserta en diferentes lugares. Elegir con antelación y declarar un golpe sólo tiene sentido si necesita utilizar un registro específico (por ejemplo, recuento de turnos en %cl). Por supuesto, una restricción de entrada como "c" (count)hace que gcc ponga el recuento en rcx/ecx/cx/cl, para no emitir un archivo mov %[count], %%ecx.

Si esto parece demasiado complicado, no utilice inline asm . Dirija el compilador al conjunto que desea con C que sea como el conjunto óptimo, o escriba una función completa en conjunto.

Cuando utilice asm en línea, manténgalo lo más pequeño posible: idealmente solo una o dos instrucciones que gcc no emite por sí solo, con restricciones de entrada/salida para indicarle cómo ingresar o sacar datos de la declaración asm. Para esto está diseñado.

Regla general: si su conjunto en línea GNU C comienza o termina con a mov, generalmente lo está haciendo mal y debería haber usado una restricción en su lugar.


Notas a pie de página :

  1. Puede usar la sintaxis intel de GAS en inline-asm compilando con -masm=intel(en cuyo caso su código solo funcionará con esa opción) o usando alternativas de dialecto para que funcione con el compilador en la sintaxis de salida asm de Intel o AT&T. Pero eso no cambia las directivas y la sintaxis Intel de GAS no está bien documentada. (Sin embargo, es como MASM, no NASM). Realmente no lo recomiendo a menos que realmente odies la sintaxis de AT&T.

Enlaces de ensamblaje en línea:

  • x86wiki. (La etiqueta wiki también enlaza con esta pregunta, para esta colección de enlaces)

  • Elensamblaje en líneaetiqueta wiki

  • El manual . Lee esto. Tenga en cuenta que el asm en línea fue diseñado para envolver instrucciones individuales que el compilador normalmente no emite. Es por eso que está redactado para decir cosas como "la instrucción", no "el bloque de código".

  • Un tutorial

  • Recorrer matrices con ensamblaje en línea Usar rrestricciones para punteros/índices y usar su elección de modo de direccionamiento, en lugar de usar mrestricciones para permitir que gcc elija entre punteros incrementales o matrices de indexación.

  • ¿Cómo puedo indicar que se puede utilizar la memoria *señalada* por un argumento ASM en línea? (Las entradas de puntero en los registros no implican que la memoria apuntada se lea y/o escriba, por lo que es posible que no esté sincronizada si no se lo informa al compilador).

  • En el ensamblaje en línea de GNU C, ¿cuáles son los modificadores para xmm/ymm/zmm para un solo operando? . Usar %q0para obtener %raxversus %w0obtener %ax. Usando %g[scalar]para obtener %zmm0en lugar de %xmm0.

  • Suma eficiente de 128 bits usando el indicador de acarreo La respuesta de Stephen Canon explica un caso en el que se necesita una declaración temprana en un operando de lectura+escritura . También tenga en cuenta que el asm en línea x86/x86-64 no necesita declarar una "cc"paliza (los códigos de condición, también conocidos como banderas); es implícito. (gcc6 introduce una sintaxis para usar condiciones de bandera como operandos de entrada/salida . Antes de eso, debe setccregistrar un registro al que gcc emitirá código test, lo cual obviamente es peor).

  • Preguntas sobre el rendimiento de diferentes implementaciones de strlen : mi respuesta a una pregunta con algún conjunto en línea mal utilizado, con una respuesta similar a esta.

  • informes llvm: ensamblaje en línea no compatible: entrada con tipo 'void *' que coincide con salida con tipo 'int' : uso de operandos de memoria compensables (en x86, todas las direcciones efectivas son compensables: siempre puede agregar un desplazamiento).

  • Cuándo no usar asm en línea , con un ejemplo de 32b/32b => 32bdivisión y resto que el compilador ya puede hacer con un solo archivo div. (El código en la pregunta es un ejemplo de cómo no usar asm en línea: muchas instrucciones para configurar y guardar/restaurar deben dejarse en manos del compilador escribiendo las restricciones de entrada/salida adecuadas).

  • Conjunto en línea de MSVC frente a conjunto en línea de GNU C para encapsular una sola instrucción , con un ejemplo correcto de conjunto en línea para 64b/32b=>32bitdivisión . El diseño y la sintaxis de MSVC requieren un viaje de ida y vuelta a través de la memoria para entradas y salidas, lo que lo hace terrible para funciones cortas. También "nunca es muy confiable" según el comentario de Ross Ridge sobre esa respuesta.

  • Usando punto flotante x87 y operandos conmutativos . No es un gran ejemplo, porque no encontré una manera de hacer que gcc emita el código ideal.

Algunos de ellos reiteran algunas de las mismas cosas que expliqué aquí. No los releí para intentar evitar redundancias, lo siento.

Peter Cordes avatar Dec 30 '2015 04:12 Peter Cordes

En x86-64, el puntero de la pila debe estar alineado con 8 bytes.

Este:

subq $12, %rsp;      // make room

debiera ser:

subq $16, %rsp;      // make room
Mats Petersson avatar Dec 29 '2015 22:12 Mats Petersson