gcc 64-bit x86-64 inline-assembly calling-convention

gcc - Llamar a printf en ASM en línea extendido



64-bit x86-64 (1)

Problema específico de su código: RDI no se mantiene a través de una llamada de función (ver más abajo). Es correcto antes de la primera llamada a printf pero es impreso por printf . Primero deberá almacenarlo temporalmente en otro lugar. Un registro que no esté protegido será conveniente. Luego puede guardar una copia antes de printf y copiarla nuevamente en RDI después.

No recomiendo hacer lo que está sugiriendo (hacer llamadas a funciones en ensamblador en línea). Será muy difícil para el compilador optimizar las cosas. Es muy fácil equivocarse. David Wohlferd escribió un muy buen artículo sobre las razones para no usar el ensamblaje en línea a menos que sea ​​absolutamente necesario.

Entre otras cosas, el System V ABI de 64 bits exige una zona roja de 128 bytes. Eso significa que no puede empujar nada a la pila sin corrupción potencial. Recuerde: hacer una LLAMADA empuja una dirección de retorno en la pila. Una forma rápida y sucia de resolver este problema es restar 128 de RSP cuando se inicia su ensamblador en línea y luego agregar 128 nuevamente cuando haya terminado.

El área de 128 bytes más allá de la ubicación señalada por% rsp se considera reservada y no se debe modificar mediante controladores de señal o interrupción.8 Por lo tanto, las funciones pueden usar esta área para datos temporales que no son necesarios en las llamadas de función. En particular, las funciones de hoja pueden usar esta área para todo el marco de la pila, en lugar de ajustar el puntero de la pila en el prólogo y el epílogo. Esta área se conoce como la zona roja.

Otro tema que debe preocuparse es el requisito de que la pila esté alineada con 16 bytes (o posiblemente con 32 bytes según los parámetros) antes de cualquier llamada a la función. Esto también es requerido por la ABI de 64 bits:

El final del área de argumento de entrada se alineará en un límite de 16 bytes (32, si se pasa __m256 en la pila). En otras palabras, el valor (% rsp + 8) siempre es un múltiplo de 16 (32) cuando el control se transfiere al punto de entrada de la función.

Nota : Este requisito para la alineación de 16 bytes en una LLAMADA a una función también se requiere en Linux de 32 bits para GCC > = 4.5:

En el contexto del lenguaje de programación C, los argumentos de función se insertan en la pila en el orden inverso. En Linux, GCC establece el estándar de facto para llamar a convenciones. Desde GCC versión 4.5, la pila debe estar alineada con un límite de 16 bytes cuando se llama a una función (las versiones anteriores solo requerían una alineación de 4 bytes).

Como llamamos a printf en el ensamblador en línea, debemos asegurarnos de alinear la pila a un límite de 16 bytes antes de realizar la llamada.

También debe tener en cuenta que cuando se llama a una función, algunos registros se conservan en una llamada de función y otros no. Específicamente, aquellos que pueden ser golpeados por una llamada de función se enumeran en la Figura 3.4 de la ABI de 64 bits (ver enlace anterior). Esos registros son RAX , RCX , RDX , RD8 - RD11 , XMM0 - XMM15 , MMX0 - MMX7 , ST0 - ST7 . Todos estos están potencialmente destruidos, por lo que deben colocarse en la lista de clobber si no aparecen en las restricciones de entrada y salida.

El siguiente código debe satisfacer la mayoría de las condiciones para garantizar que el ensamblador en línea que llama a otra función no registra inadvertidamente registros, conserva la zona roja y mantiene la alineación de 16 bytes antes de una llamada:

int main() { const char* test = "test/n"; long dummyreg; /* dummyreg used to allow GCC to pick available register */ __asm__ __volatile__ ( "add $-128, %%rsp/n/t" /* Skip the current redzone */ "mov %%rsp, %[temp]/n/t" /* Copy RSP to available register */ "and $-16, %%rsp/n/t" /* Align stack to 16-byte boundary */ "mov %[test], %%rdi/n/t" /* RDI is address of string */ "xor %%eax, %%eax/n/t" /* Variadic function set AL. This case 0 */ "call printf/n/t" "mov %[test], %%rdi/n/t" /* RDI is address of string again */ "xor %%eax, %%eax/n/t" /* Variadic function set AL. This case 0 */ "call printf/n/t" "mov %[temp], %%rsp/n/t" /* Restore RSP */ "sub $-128, %%rsp/n/t" /* Add 128 to RSP to restore to orig */ : [temp]"=&r"(dummyreg) /* Allow GCC to pick available output register. Modified before all inputs consumed so use & for early clobber*/ : [test]"r"(test), /* Choose available register as input operand */ "m"(test) /* Dummy constraint to make sure test array is fully realized in memory before inline assembly is executed */ : "rax", "rcx", "rdx", "rsi", "rdi", "r8", "r9", "r10", "r11", "xmm0","xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7", "xmm8","xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15", "mm0","mm1", "mm2", "mm3", "mm4", "mm5", "mm6", "mm6", "st", "st(1)", "st(2)", "st(3)", "st(4)", "st(5)", "st(6)", "st(7)" ); return 0; }

Usé una restricción de entrada para permitir que la plantilla elija un registro disponible para usar para pasar la dirección str . Esto garantiza que tengamos un registro para almacenar la dirección str entre las llamadas a printf . También obtengo la plantilla de ensamblador para elegir una ubicación disponible para almacenar RSP temporalmente mediante el uso de un registro ficticio. Los registros elegidos no incluirán ninguno ya elegido / listado como un operando de entrada / salida / clobber.

Esto se ve muy desordenado, pero no hacerlo correctamente podría ocasionar problemas más adelante a medida que su programa se vuelva más complejo. Esta es la razón por la cual las funciones de llamada que se ajustan a la ABI de 64 bits de System V dentro del ensamblador en línea generalmente no son la mejor manera de hacer las cosas.

Estoy tratando de generar la misma cadena dos veces en ASM en línea extendido en GCC , en Linux de 64 bits.

int main() { const char* test = "test/n"; asm( "movq %[test], %%rdi/n" // Debugger shows rdi = *address of string* "movq $0, %%rax/n" "push %%rbp/n" "push %%rbx/n" "call printf/n" "pop %%rbx/n" "pop %%rbp/n" "movq %[test], %%rdi/n" // Debugger shows rdi = 0 "movq $0, %%rax/n" "push %%rbp/n" "push %%rbx/n" "call printf/n" "pop %%rbx/n" "pop %%rbp/n" : : [test] "g" (test) : "rax", "rbx","rcx", "rdx", "rdi", "rsi", "rsp" ); return 0; }

Ahora, la cadena se emite solo una vez. He intentado muchas cosas, pero creo que me faltan algunas advertencias sobre la convención de convocatoria. Ni siquiera estoy seguro de si la lista de clobber es correcta o si necesito guardar y restaurar RBP y RBX .

¿Por qué la cadena no se emite dos veces?

Mirar con un depurador me muestra que de alguna manera cuando la cadena se carga en rdi por segunda vez tiene el valor 0 lugar de la dirección real de la cadena.

No puedo explicar por qué, ¿parece que después de la primera llamada la pila está dañada? ¿Tengo que restaurarlo de alguna manera?