Uso del registro de puntero base en asm en línea C++
assembly x86 (2)
Consulte la parte inferior de esta respuesta para obtener una colección de enlaces a otras preguntas y respuestas en línea.
Su código está roto porque pisa la zona roja debajo de RSP (con
push
) donde GCC mantenía un valor.
¿Qué esperas aprender a lograr con asm en línea? Si desea aprender asm en línea, aprenda a usarlo para crear un código eficiente, en lugar de cosas horribles como esta. Si desea escribir prólogos de funciones y pulsar / pop para guardar / restaurar registros, debe escribir funciones completas en asm . (Entonces puede usar fácilmente nasm o yasm, en lugar de la sintaxis de AT&T menos preferida por la mayoría con las directivas de ensamblador GNU 1 ).
GNU inline asm es difícil de usar, pero le permite mezclar fragmentos asm personalizados en C y C ++ al tiempo que 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 al proporcionarle un registro que se puede bloquear.
Sin
volatile
, incluso puede levantar declaraciones asm de 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 solo está tratando de aprender asm en primer lugar, GNU inline asm es una elección terrible. Debe comprender completamente casi todo lo que sucede con el asm y comprender lo que el compilador necesita saber para escribir las restricciones correctas de entrada / salida y hacer todo bien. Los errores conducirán a golpear cosas y a la ruptura difícil de depurar. La función llamada ABI es mucho más simple y fácil de realizar un seguimiento de los límites 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 derrama el parámetro de función de
%rdi
a una ubicación en la pila.
(Esto podría suceder en una función no trivial incluso con
-O3
).
Dado que el ABI de destino es el ABI
x86-64 SysV
, utiliza la
"Zona Roja"
(128 bytes por debajo de
%rsp
que incluso los manejadores de señales asíncronas no pueden tropezar), en lugar de desperdiciar una instrucción que disminuye el puntero de la pila para reservar espacio .
Almacena la función de puntero 8B arg en
-8(rsp_at_function_entry)
.
Luego, su asm en línea empuja
%rbp
, que disminuye% rsp en 8 y luego escribe allí, golpeando los bajos 32b de
&x
(el puntero).
Cuando termine su asm en línea,
-
gcc vuelve a cargar
-8(%rbp)
(que se ha sobrescrito con%rbp
) y lo usa como la dirección de una tienda 4B. -
Foo
vuelve amain
con%rbp = (upper32)|5
(valor original con el valor 32 bajo establecido en5
). -
main
carrerasmain
leave
:%rsp = (upper32)|5
-
main
ejecutaret
con%rsp = (upper32)|5
, leyendo la dirección de retorno de la dirección virtual(void*)(upper32|5)
, que según su comentario es0x7fff0000000d
.
No verifiqué con un depurador; uno de esos pasos puede estar ligeramente desviado, pero el problema definitivamente es que golpeas la zona roja , lo que lleva al código de gcc a la basura.
Incluso agregar un clobber de "memoria" no obtiene gcc para evitar el uso de la zona roja, por lo que parece que asignar su propia memoria de pila desde el asm en línea es una mala idea. (Un clobber de memoria significa que puede haber escrito algo de memoria en la que puede escribir, por ejemplo, una variable global o algo señalado por un global, no es que haya sobrescrito algo que se supone que no debe).
Si desea utilizar el espacio de borrador desde inm asm, probablemente debería declarar una matriz como una variable local y utilizarla como un operando de solo salida (del cual nunca lee).
AFAIK, no hay sintaxis para declarar que modifica la zona roja, por lo que sus únicas opciones son:
-
use un operando de salida
"=m"
(posiblemente una matriz) para el espacio temporal; el compilador probablemente completará ese operando con un modo de direccionamiento relativo a RBP o RSP. Puede indexarlo con constantes como4 + %[tmp]
o lo que sea. Es posible que reciba una advertencia de ensamblador de4 + (%rsp)
pero no un error. -
omita la zona roja con
add $-128, %rsp
/sub $-128, %rsp
alrededor de su código. (Necesario si desea utilizar una cantidad desconocida de espacio de pila adicional, por ejemplo, empujar en un bucle o realizar una llamada a una función. Otra razón más para desrefinir un puntero de función en C puro, no en línea). -
compila con
-mno-red-zone
(no creo que puedas habilitar eso por función, solo por archivo) - No use el espacio para rascar en primer lugar. Dile al compilador qué registra tu clobber y deja 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 de
%rbp
en el código fuera de la sección
#APP
/
#NO_APP
, emitido por gcc.
También tenga en cuenta que la memoria de memoria virtual que le brinda 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 de scratch, es mejor declarar más operandos de salida que el código no asm circundante nunca utiliza.
Eso deja la asignación de registros al compilador, por lo que puede ser diferente cuando se inserta en diferentes lugares.
Elegir con anticipación y declarar un clobber solo tiene sentido si necesita usar un registro específico (por ejemplo, recuento de turnos en
%cl
).
Por supuesto, una restricción de entrada como
"c" (count)
obtiene gcc para poner el conteo en rcx / ecx / cx / cl, por lo que no emite un
mov %[count], %%ecx
potencialmente potencialmente redundante
mov %[count], %%ecx
.
Si esto parece demasiado complicado, no use asm en línea . Dirija el compilador al asm que desea con C, que es como el asm óptimo, o escriba una función completa en asm.
Cuando use asm en línea, manténgalo lo más pequeño posible: idealmente, solo las una o dos instrucciones que gcc no emite por sí solo, con restricciones de entrada / salida para indicarle cómo ingresar / sacar datos de la declaración asm. Para esto está diseñado.
Regla de oro: si su asm en línea GNU C comienza o termina con un
mov
, generalmente lo está haciendo mal y debería haber usado una restricción en su lugar.
Notas al pie :
-
Puede usar la sintaxis intel de GAS en inline-asm
-masm=intel
con-masm=intel
(en cuyo caso su código solo funcionará con esa opción), o usando alternativas dialectales 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 asm en línea:
- Wiki x86 . (La etiqueta wiki también enlaza con esta pregunta, para esta colección de enlaces)
- wiki etiquetas de inline-assembly
- El manual Lee esto. Tenga en cuenta que inline asm 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
-
Bucle sobre matrices con ensamblaje en línea Usar restricciones
r
para punteros / índices y usar el modo de direccionamiento que elija, en lugar de usar restriccionesm
para permitir que gcc elija entre punteros incrementales frente a matrices de indexación. -
¿Cómo puedo indicar que se puede usar la memoria * señalada * por un argumento ASM en línea? (las entradas de puntero en los registros no implican que la memoria apuntada sea leída y / o escrita, por lo que podría no estar sincronizada si no le dice al compilador).
-
En GNU C inline asm, ¿cuáles son los modificadores para xmm / ymm / zmm para un solo operando? . Usando
%q0
para obtener%rax
vs.%w0
para obtener%ax
. Usando%g[scalar]
para obtener%zmm0
lugar de%xmm0
. -
Adición eficiente de 128 bits usando la bandera de acarreo
La respuesta de Stephen Canon explica
un caso en el que se necesita una declaración de clobber temprano en un operando de lectura + escritura
.
También tenga en cuenta que x86 / x86-64 inm asm no necesita declarar un
"cc"
clobber (los códigos de condición, también conocidos como banderas); Es implícito. (gcc6 introduce la sintaxis para usar condiciones de indicador como operandos de entrada / salida . Antes de eso, debesetcc
un registro que gcc emitirá código paratest
, lo que obviamente es peor). - Preguntas sobre el rendimiento de diferentes implementaciones de strlen : mi respuesta a una pregunta con algunos asm en línea mal utilizados, con una respuesta similar a esta.
- informes llvm: asm en línea no admitido: entrada con el tipo ''void *'' salida coincidente con el tipo ''int'' : Uso de operandos de memoria desplazables (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 división
32b/32b => 32b
y el resto que el compilador ya puede hacer con un solodiv
. (El código en la pregunta es un ejemplo de cómo no usar asm en línea: muchas instrucciones para la configuración y guardar / restaurar que deben dejarse al compilador escribiendo restricciones de entrada / salida adecuadas). -
MSVC inline asm vs. GNU C inline asm para ajustar una sola instrucción
, con un ejemplo correcto de asm en línea para
64b/32b=>32bit
división de64b/32b=>32bit
. 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 es "nunca muy confiable" según el comentario de Ross Ridge sobre esa respuesta. - Uso de coma 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 repiten algunas de las mismas cosas que expliqué aquí. No los releí para tratar de evitar la redundancia, lo siento.
Quiero poder utilizar el registro de puntero base (
%rbp
) dentro de asm en línea.
Un ejemplo 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 a funciones de prólogo / epílogo para empujar y reventar el viejo
%rbp
, esto estaría bien.
Sin embargo, seg falla cuando intento acceder a
x
después del asm en línea.
El código de ensamblaje generado por GCC (ligeramente despojado) 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é esto falla?
Parece que de alguna manera
%rbp
pero no veo cómo.
Gracias por adelantado.
Estoy ejecutando GCC 4.8.4 en Ubuntu 14.04 de 64 bits.
En x86-64, el puntero de la pila debe alinearse a 8 bytes.
Esta:
subq $12, %rsp; // make room
debiera ser:
subq $16, %rsp; // make room