¿Qué ocurre si usa el int 32 bits ABI de Linux 0x80 en código de 64 bits?

int 0x80 en Linux siempre invoca el ABI de 32 bits, independientemente del modo desde el que se llama: args en ebx , ecx , … y números de syscall de /usr/include/asm/unistd_32.h . (O se bloquea en kernels de 64 bits comstackdos sin CONFIG_IA32_EMULATION ).

El código de 64 bits debe usar syscall , con números de llamada de /usr/include/asm/unistd_64.h y args en rsi , rsi , etc. Consulte las convenciones de llamada para llamadas al sistema UNIX y Linux en i386 y x86-64 . Si su pregunta fue marcada como duplicada, consulte ese enlace para obtener detalles sobre cómo debe hacer las llamadas al sistema en un código de 32 o 64 bits. Si quieres entender qué pasó exactamente, sigue leyendo.


syscall llamadas al sistema syscall son más rápidas que las llamadas al sistema int 0x80 , por lo tanto, use syscall nativo de 64 bits a menos que esté escribiendo un código de máquina políglota que se ejecute igual cuando se ejecute como 32 o 64 bits. ( sysenter siempre regresa en modo de 32 bits, por lo que no es útil desde el espacio de usuario de 64 bits, aunque es una instrucción x86-64 válida).

Relacionado: La guía definitiva para llamadas al sistema Linux (en x86) sobre cómo realizar llamadas al sistema int 0x80 o sysenter 32 bits, llamadas al sistema syscall 64 bits o llamar al vDSO para llamadas al sistema “virtual” como gettimeofday . Además de información sobre de qué se trata el sistema de llamadas.


El uso de int 0x80 permite escribir algo que se ensamblará en el modo de 32 o 64 bits, por lo que es útil para un exit_group() al final de un microbenchmark o algo así.

Los archivos PDF actuales de los documentos oficiales i386 y x86-64 System V psABI que estandarizan las convenciones de llamadas de función y llamada de sistema están vinculados desde https://github.com/hjl-tools/x86-psABI/wiki/X86-psABI .

Consulte la wiki de la etiqueta x86 para ver las guías para principiantes, los manuales x86, la documentación oficial y las guías / recursos de optimización del rendimiento.


Pero dado que la gente sigue publicando preguntas con código que usa int 0x80 en código de 64 bits o crea accidentalmente binarios de 64 bits a partir de la fuente escrita para 32 bits, me pregunto qué ocurre exactamente en el Linux actual.

¿ int 0x80 guarda / restaura todos los registros de 64 bits? ¿Truncará cualquier registro a 32 bits? ¿Qué sucede si pasa args de puntero que tienen mitades superiores a cero?

¿Funciona si le pasa punteros de 32 bits?

TL: DR : int 0x80 funciona cuando se usa correctamente, siempre y cuando los punteros encajen en 32 bits ( los punteros de stack no encajan ). Además, strace decodifica incorrectamente , descodificando los contenidos del registro como si fuera el syscall ABI de 64 bits.

int 0x80 ceros r8-r11, y conserva todo lo demás. Úselo exactamente como lo haría en el código de 32 bits, con los números de llamada de 32 bits. (¡O mejor, no lo use!)

No todos los sistemas admiten incluso int 0x80 : el subsistema Windows Ubuntu es estrictamente de 64 bits solamente: int 0x80 no funciona en absoluto . También es posible construir kernels Linux sin emulación IA-32 . (No es compatible con ejecutables de 32 bits, no admite llamadas al sistema de 32 bits).


Los detalles: lo que se guarda / restaura, qué partes de las reglas utiliza el kernel

int 0x80 usa eax (no el rax completo) como el número de llamada del sistema, que se envía a la misma tabla de indicadores de función que utiliza el espacio de usuario de 32 bits int 0x80 . (Estos indicadores sirven para sys_whatever implementaciones o envolturas para la implementación nativa de 64 bits dentro del kernel. Las llamadas al sistema son realmente llamadas a funciones a través del límite usuario / kernel).

Solo se pasan los 32 bits bajos de los registros arg. Las mitades superiores de rbxrbp se conservan, pero se ignoran mediante las llamadas al sistema int 0x80 . Tenga en cuenta que pasar un puntero malo a una llamada del sistema no da como resultado SIGSEGV; en su lugar, la llamada al sistema devuelve -EFAULT . Si no verifica los valores de retorno de error (con una herramienta de depuración o seguimiento), parecerá que falla silenciosamente.

Todos los registros (excepto eax, por supuesto) se guardan / restauran (incluyendo RFLAGS, y los 32 superiores de regs enteros), excepto que r8-r11 se ponen a cero . r12-r15 se conservan en llamada en la convención de llamada a funciones x86-64 SysV ABI, por lo que los registros que se ponen a cero por int 0x80 en 64 bits son el subconjunto llamado de los “nuevos” registros que AMD64 agregó.

Este comportamiento se ha conservado con respecto a algunos cambios internos de cómo se implementó el almacenamiento de registros dentro del kernel, y los comentarios en el kernel mencionan que es utilizable desde 64 bits, por lo que este ABI probablemente sea estable. (Es decir, puede contar con que r8-r11 se ponga a cero y se guarde todo lo demás).

El valor de retorno tiene extensión extendida para rellenar rax 64 bits. (Linux declara que las funciones sys_ de 32 bits retornan con la firma long ). Esto significa que los valores de retorno del puntero (como from void *mmap() ) deben extenderse a cero antes de usarse en los modos de direccionamiento de 64 bits.

A diferencia de sysenter , conserva el valor original de cs , por lo que regresa al espacio de usuario en el mismo modo en que fue llamado. (El uso de sysenter da sysenter resultado que kernel establezca cs en $__USER32_CS , que selecciona un descriptor para 32 bits segmento de código)


strace decodifica int 0x80 incorrectamente para procesos de 64 bits. Se decodifica como si el proceso hubiera usado syscall lugar de int 0x80 . Esto puede ser muy confuso . por ejemplo, desde strace prints write(0, NULL, 12 para eax=1 / int $0x80 , que en realidad es _exit(ebx) , no write(rdi, rsi, rdx) .


int 0x80 funciona siempre que todos los argumentos (incluidos los punteros) quepan en el bajo 32 de un registro . Este es el caso del código estático y los datos en el modelo de código predeterminado (“pequeño”) en el x86-64 SysV ABI . (Sección 3.5.1: se sabe que todos los símbolos están ubicados en las direcciones virtuales en el rango 0x00000000 a 0x7effffff , por lo que puede hacer cosas como mov edi, hello (AT & T mov $hello, %edi ) para obtener un puntero en un registro con una instrucción de 5 bytes).

Pero este no es el caso de los ejecutables independientes de la posición , que muchas distribuciones de Linux ahora configuran gcc por defecto (y habilitan ASLR para ejecutables). Por ejemplo, compilé un hello.c en Arch Linux, y establecí un punto de interrupción al comienzo de main. La constante de cadena transferida a puts estaba en 0x555555554724 , por lo que una llamada al sistema de write ABI de 32 bits no funcionaría. (GDB inhabilita ASLR de manera predeterminada, por lo que siempre verá la misma dirección desde la ejecución hasta la ejecución, si ejecuta desde dentro de GDB).

Linux coloca la stack cerca del “espacio” entre los rangos superior e inferior de direcciones canónicas , es decir, con la parte superior de la stack en 2 ^ 48-1. (O en algún lugar al azar, con ASLR habilitado). Por lo tanto, la entrada de _start en _start en un ejecutable típico enlazado estáticamente es algo así como 0x7fffffffe550 , dependiendo del tamaño de env vars y args. Truncar este puntero a esp no apunta a ninguna memoria válida, por lo que las llamadas al sistema con entradas de puntero generalmente devolverán -EFAULT si intentas pasar un puntero de stack truncado. (Y su progtwig se bloqueará si trunca rsp a esp y luego hace algo con la stack, por ejemplo, si creó una fuente asm de 32 bits como un ejecutable de 64 bits).


Cómo funciona en el kernel:

En el código fuente de Linux, arch/x86/entry/entry_64_compat.S define ENTRY(entry_INT80_compat) . Ambos procesos de 32 y 64 bits usan el mismo punto de entrada cuando ejecutan int 0x80 .

entry_64.S define puntos de entrada nativos para un núcleo de 64 bits, que incluye controladores de interrupción / falla y llamadas al sistema nativo syscall desde procesos de modo largo (también conocido como modo de 64 bits) .

entry_64_compat.S define los puntos de entrada de llamada al sistema desde el modo compat en un kernel de 64 bits, más el caso especial de int 0x80 en un proceso de 64 bits. ( sysenter en un proceso de 64 bits también puede ir a ese punto de entrada, pero presiona $__USER32_CS , por lo que siempre regresará en modo de 32 bits). Hay una versión de 32 bits de la instrucción syscall , compatible con las CPU AMD , y Linux también lo admite para llamadas de sistema rápidas de 32 bits desde procesos de 32 bits.

Supongo que un posible caso de uso para int 0x80 en el modo de 64 bits es si desea utilizar un descriptor de segmento de código personalizado que haya instalado con modify_ldt . int 0x80 empuja registros de segmento para usar con iret , y Linux siempre regresa desde llamadas de sistema int 0x80 a través de iret . El punto de entrada de syscall 64 bits establece pt_regs->cs y ->ss en constantes, __USER_CS y __USER_DS . (Es normal que SS y DS utilicen los mismos descriptores de segmento. Las diferencias de permisos se realizan con paginación, no con segmentación).

entry_32.S define puntos de entrada en un kernel de 32 bits, y no está involucrado en absoluto.

El punto de entrada int 0x80 en Linux 4.12’s entry_64_compat.S :

 /* * 32-bit legacy system call entry. * * 32-bit x86 Linux system calls traditionally used the INT $0x80 * instruction. INT $0x80 lands here. * * This entry point can be used by 32-bit and 64-bit programs to perform * 32-bit system calls. Instances of INT $0x80 can be found inline in * various programs and libraries. It is also used by the vDSO's * __kernel_vsyscall fallback for hardware that doesn't support a faster * entry method. Restarted 32-bit system calls also fall back to INT * $0x80 regardless of what instruction was originally used to do the * system call. * * This is considered a slow path. It is not used by most libc * implementations on modern hardware except during process startup. ... */ ENTRY(entry_INT80_compat) ... (see the github URL for the full source) 

El código zero-extends eax en rax, luego empuja todos los registros en la stack kernel para formar una struct pt_regs . Aquí es donde se restaurará cuando regrese la llamada del sistema. Se encuentra en un diseño estándar para los registros de espacio de usuario guardados (para cualquier punto de entrada), de modo que la ptrace de otro proceso (como gdb o strace ) leerá y / o escribirá esa memoria si usan ptrace mientras este proceso está dentro de una llamada al sistema. (La modificación de los registros de las ptrace es una de las cosas que complica las rutas de retorno de los demás puntos de entrada. Consulte los comentarios).

Pero empuja $0 lugar de r8 / r9 / r10 / r11. (los puntos de entrada syscall32 sysenter y AMD almacenan ceros para r8-r15).

Creo que esta reducción a cero de r8-r11 debe coincidir con el comportamiento histórico. Antes de configurar la configuración de pt_regs completa para todas las llamadas de sys de compatibilidad , el punto de entrada solo guardaba los registros call-clobbered de C. Se envió directamente desde asm con la call *ia32_sys_call_table(, %rax, 8) , y esas funciones siguen la convención de llamadas, por lo que conservan rbx , rbp , rsp y r12-r15 . r8-r11 cero r8-r11 lugar de dejarlos indefinidos fue probablemente una forma de evitar las filtraciones de información del kernel. IDK cómo manejó ptrace si la única copia de los registros preservados de llamada del espacio de usuario estaba en la stack del kernel donde una función C los salvó. Dudo que usara metadatos para desenrollar la stack para encontrarlos allí.

La implementación actual (Linux 4.12) distribuye llamadas al sistema ABI de 32 bits desde C, recargando el pt_regs ebx , ecx , etc. pt_regs de pt_regs . (El sistema nativo de 64 bits llama a dispatch directamente desde asm, con solo un mov %r10, %rcx necesario para tener en cuenta la pequeña diferencia en la convención de llamadas entre funciones y syscall . Desafortunadamente no siempre puede usar sysret , porque los errores de la CPU lo hacen inseguro con direcciones no canónicas. Lo intenta, por lo que el camino rápido es bastante rápido, aunque syscall aún tarda decenas de ciclos).

De todos modos, en Linux actual, las llamadas de sistema de 32 bits (incluyendo int 0x80 de 64 bits) finalmente terminan en do_syscall_32_irqs_on(struct pt_regs *regs) . Se distribuye a un puntero de función ia32_sys_call_table , con 6 args de extensión cero. Esto quizás evite la necesidad de un envoltorio alrededor de la función syscall nativa de 64 bits en más casos para preservar ese comportamiento, por lo que la mayoría de las entradas de la tabla ia32 pueden ser la implementación de llamada del sistema nativo directamente.

Linux 4.12 arch/x86/entry/common.c

 if (likely(nr < IA32_NR_syscalls)) { /* * It's possible that a 32-bit syscall implementation * takes a 64-bit parameter but nonetheless assumes that * the high bits are zero. Make sure we zero-extend all * of the args. */ regs->ax = ia32_sys_call_table[nr]( (unsigned int)regs->bx, (unsigned int)regs->cx, (unsigned int)regs->dx, (unsigned int)regs->si, (unsigned int)regs->di, (unsigned int)regs->bp); } syscall_return_slowpath(regs); 

En versiones anteriores de Linux que envían llamadas al sistema de 32 bits desde asm (como en el caso de 64 bits), el punto de entrada int80 coloca args en los registros correctos con las instrucciones mov y xchg , usando registros de 32 bits. Incluso usa mov %edx,%edx para extender cero EDX en RDX (porque arg3 pasa a usar el mismo registro en ambas convenciones). código aquí Este código está duplicado en los puntos de entrada sysenter y syscall32 .


Ejemplo simple / progtwig de prueba:

Escribí un simple Hello World (en syntax NASM) que establece que todos los registros tengan mitades superiores distintas de cero, luego realiza dos llamadas al sistema write() con int 0x80 , uno con un puntero a una cadena en .rodata (sucede), el segundo con un puntero a la stack (falla con -EFAULT ).

Luego utiliza la syscall nativa de 64 bits syscall ABI para write() los caracteres de la stack (puntero de 64 bits) y nuevamente para salir.

Entonces, todos estos ejemplos están usando los ABIs correctamente, excepto el 2nd int 0x80 que intenta pasar un puntero de 64 bits y lo trunca.

Si lo construiste como un ejecutable independiente de la posición, el primero también fallaría. (Tendría que usar un lea relativo de RIP en vez de mov para obtener la dirección de hello: en un registro).

Usé gdb, pero use el depurador que prefiera. Use uno que resalte los registros modificados desde el último paso único. gdbgui funciona bien para depurar origen de gdbgui , pero no es ideal para el desassembly. Aún así, tiene un panel de registro que funciona bien para regs enteros al menos, y funcionó muy bien en este ejemplo.

Ver el en línea ;;; comentarios que describen cómo las llamadas al sistema cambian el registro

 global _start _start: mov rax, 0x123456789abcdef mov rbx, rax mov rcx, rax mov rdx, rax mov rsi, rax mov rdi, rax mov rbp, rax mov r8, rax mov r9, rax mov r10, rax mov r11, rax mov r12, rax mov r13, rax mov r14, rax mov r15, rax ;; 32-bit ABI mov rax, 0xffffffff00000004 ; high garbage + __NR_write (unistd_32.h) mov rbx, 0xffffffff00000001 ; high garbage + fd=1 mov rcx, 0xffffffff00000000 + .hello mov rdx, 0xffffffff00000000 + .hellolen ;std after_setup: ; set a breakpoint here int 0x80 ; write(1, hello, hellolen); 32-bit ABI ;; succeeds, writing to stdout ;;; changes to registers: r8-r11 = 0. rax=14 = return value ; ebx still = 1 = STDOUT_FILENO push 'bye' + (0xa< <(3*8)) mov rcx, rsp ; rcx = 64-bit pointer that won't work if truncated mov edx, 4 mov eax, 4 ; __NR_write (unistd_32.h) int 0x80 ; write(ebx=1, ecx=truncated pointer, edx=4); 32-bit ;; fails, nothing printed ;;; changes to registers: rax=-14 = -EFAULT (from /usr/include/asm-generic/errno-base.h) mov r10, rax ; save return value as exit status mov r8, r15 mov r9, r15 mov r11, r15 ; make these regs non-zero again ;; 64-bit ABI mov eax, 1 ; __NR_write (unistd_64.h) mov edi, 1 mov rsi, rsp mov edx, 4 syscall ; write(edi=1, rsi='bye\n' on the stack, rdx=4); 64-bit ;; succeeds: writes to stdout and returns 4 in rax ;;; changes to registers: rax=4 = length return value ;;; rcx = 0x400112 = RIP. r11 = 0x302 = eflags with an extra bit set. ;;; (This is not a coincidence, it's how sysret works. But don't depend on it, since iret could leave something else) mov edi, r10d ;xor edi,edi mov eax, 60 ; __NR_exit (unistd_64.h) syscall ; _exit(edi = first int 0x80 result); 64-bit ;; succeeds, exit status = low byte of first int 0x80 result = 14 section .rodata _start.hello: db "Hello World!", 0xa, 0 _start.hellolen equ $ - _start.hello 

Comstackrlo en un binario estático de 64 bits con

 yasm -felf64 -Worphan-labels -gdwarf2 abi32-from-64.asm ld -o abi32-from-64 abi32-from-64.o 

Ejecute gdb ./abi32-from-64 . En gdb , ejecuta set disassembly-flavor intel y layout reg si ya no tienes eso en tu ~/.gdbinit . (GAS .intel_syntax es como MASM, no NASM, pero está lo suficientemente cerca como para que sea fácil de leer si le gusta la syntax de NASM).

 (gdb) set disassembly-flavor intel (gdb) layout reg (gdb) b after_setup (gdb) r (gdb) si # step instruction press return to repeat the last command, keep stepping 

Presione control-L cuando el modo TUI de gdb se arruina. Esto sucede con facilidad, incluso cuando los progtwigs no se imprimen para su propia configuración.