¿Qué sucede si utiliza el 32-bit int 0x80 Linux ABI en código de 64 bits?


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

64-código de bits debe utilizar syscall, con números de llamada desde /usr/include/asm/unistd_64.h, y args en rdi, rsi, etc. Ver Cuáles son las convenciones de llamadas para llamadas al sistema UNIX y Linux en i386 y x86-64. Si su pregunta fue marcada como un duplicado de esto, vea que enlace para detalles sobre cómo debe hacer llamadas al sistema en código de 32 o 64 bits. Si quieres entender lo que sucedió exactamente, sigue leyendo.


syscall las llamadas al sistema son más rápidas que las llamadas al sistema int 0x80, así que use syscall nativo de 64 bits a menos que esté escribiendo código máquina políglota que se ejecute igual cuando se ejecuta 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 un x86-64 válido instrucción.)

Relacionados: La Guía Definitiva para Llamadas al Sistema Linux (en x86) para saber cómo hacer llamadas al sistema int 0x80 o sysenter de 32 bits, o llamadas al sistema syscall de 64 bits, o llamar al vDSO para llamadas al sistema "virtuales" como gettimeofday. Además de antecedentes sobre lo que se trata de las llamadas al sistema.


Con int 0x80 hace posible escribir algo que se reunirá en 32 o 64 bits, por lo que es útil para un exit_group() al final de un microbenchmark o algo.

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

Vea el wiki de etiquetas x86 para guías para principiantes, manuales x86, documentación oficial y guías / recursos de optimización de rendimiento.


Pero dado que la gente sigue publicando preguntas con código que usa int 0x80 en 64 bits code , or accidentally building 64-bit binarios from source written for 32-bit, I wonder what exactly does happen on current Linux?

¿int 0x80 guarda/restaura todos los registros de 64 bits? ¿Trunca algún registro a 32 bits? ¿Qué sucede si pasa args de puntero que tienen mitades superiores distintas de cero?

¿Funciona si le pasas punteros de 32 bits?

Author: Peter Cordes, 2017-09-07

1 answers

TL: DR: int 0x80 funciona cuando se usa correctamente, siempre y cuando los punteros quepan en 32 bits (los punteros de pila no quepan). También, strace decodifica mal , decodificando el contenido del registro como si fuera el ABI de 64 bits syscall.

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

No todos los sistemas soportan int 0x80: El Ubuntu de Windows el subsistema es estrictamente de 64 bits: int 0x80 no funciona en absoluto . También es posible construir núcleos Linux sin emulación IA-32. (Sin soporte para ejecutables de 32 bits, sin soporte para llamadas al sistema de 32 bits).


Los detalles: qué se guarda/restaura, qué partes de qué reglas usa el núcleo

int 0x80 usa eax (no el rax completo) como el número de llamada al sistema, enviando a la misma tabla de punteros de función que el espacio de usuario de 32 bits int 0x80 usos. (Estos punteros son para sys_whatever implementaciones o envoltorios para la implementación nativa de 64 bits dentro del núcleo. 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 registros arg. Las mitades superiores de rbx-rbp se conservan, pero son ignorados por las llamadas al sistema int 0x80. Tenga en cuenta que pasar un puntero malo a una llamada al sistema no resulta en SIGSEGV; en su lugar, la llamada al sistema devuelve -EFAULT. Si no compruebe los valores de retorno de error (con un depurador o herramienta de rastreo), 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 la convención de llamada a la función ABI de SysV x86-64, por lo que los registros que se ponen a cero por int 0x80 en 64 bits son el subconjunto de llamada clobbered de los registros "nuevos" que AMD64 agregó.

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

El valor devuelto se extiende por signo para rellenar 64 bits rax. (Linux declara funciones sys_ de 32 bits como devolver firmado long.) Esto significa que el puntero devuelve valores (como from void *mmap()) necesidad de ser cero extendido antes de usar en modos de direccionamiento de 64 bits

A diferencia de sysenter, conserva el valor original de cs, por lo que vuelve al espacio de usuario en el mismo modo en el que fue llamado. (Usando sysenter resulta en la configuración del kernel cs a $__USER32_CS, que selecciona un descriptor para un segmento de código de 32 bits.)


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


int 0x80 funciona siempre y cuando todos los argumentos (incluidos los punteros) quepan en el 32 bajo de un registro. Este es el caso del código estático y los datos en el modelo de código predeterminado ("small") en el x86-64 SysV ABI. (Sección 3.5.1 : se sabe que todos los símbolos se encuentran en las direcciones virtuales en el rango 0x00000000 para 0x7effffff, así que usted 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 es no el caso de ejecutables independientes de la posición, que muchas distribuciones de Linux ahora configuran gcc para hacer 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 pasada a puts estaba en 0x555555554724, por lo que un ABI de 32 bits write la llamada al sistema no funcionaría. (GDB deshabilita ASLR de forma predeterminada, por lo que siempre verá la misma dirección de ejecución en ejecución, si ejecuta desde GDB.)

Linux coloca la pila cerca de la "brecha" entre los rangos superior e inferior de direcciones canónicas, es decir, con la parte superior de la pila en 2^48-1. (O en algún lugar aleatorio, con ASLR habilitado). Así que rsp en la entrada a _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 normalmente devolverán -EFAULT si intenta pasar un puntero de pila truncado. (Y su programa se bloqueará si trunca rsp a esp y luego hace cualquier cosa con la pila, por ejemplo, si construyó una fuente asm de 32 bits como un ejecutable de 64 bits.)


Cómo funciona en el núcleo:

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

entry_64.S is define puntos de entrada nativos para un núcleo de 64 bits, que incluye manejadores de interrupciones / fallos y syscall llamadas al sistema nativas desde procesos long mode (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 a un núcleo de 64 bits, más el caso especial de int 0x80 en un proceso de 64 bits. (sysenter en un proceso de 64 bits puede ir a ese punto de entrada también, pero empuja $__USER32_CS, por lo que siempre retorno en modo de 32 bits.) Hay una versión de 32 bits de la instrucción syscall, compatible con las CPU de AMD, y Linux también lo admite para llamadas rápidas al sistema de 32 bits desde procesos de 32 bits.

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

entry_32.S define los puntos de entrada en un núcleo de 32 bits, y no está involucrado en absoluto.

El int 0x80 punto de entrada en Linux 4.12entry_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 cero-extiende eax en rax, luego empuja todos los se registra en la pila del kernel para formar una struct pt_regs. Aquí es donde se restaurará cuando regrese la llamada al sistema. Está en un diseño estándar para registros de espacio de usuario guardados (para cualquier punto de entrada), por lo que 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. (ptrace la modificación de registros es una cosa que complica las rutas de retorno para los otros puntos de entrada. Ver comentario.)

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

Creo que esta reducción a cero de r8-r11 es para que coincida con el comportamiento histórico. Antes de que Configure pt_regs completos para todas las llamadas de sistema compat commit, el punto de entrada solo guardaba los registros clobbered de llamada C. Se envía directamente desde asm con call *ia32_sys_call_table(, %rax, 8), y esas funciones siguen la convención de llamada, por lo que preservan rbx, rbp, rsp, y r12-r15. Poner a cero r8-r11 en lugar de dejarlos indefinidos fue probablemente una forma de evitar filtraciones de información del núcleo. IDK cómo manejaba ptrace si la única copia de los registros conservados de llamadas del espacio de usuario estaba en la pila del núcleo donde una función C los guardaba. Dudo que usara metadatos de desbobinado de pila para encontrarlos allí.

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

De todos modos, en Linux actual, las llamadas de sistema de 32 bits (incluyendo int 0x80 de 64 bits) eventualmente terminan endo_syscall_32_irqs_on(struct pt_regs *regs). Se envía a un puntero de función ia32_sys_call_table, con 6 args cero extendidos. Esto tal vez evita la necesidad de una envoltura alrededor de la función syscall nativa de 64 bits en más casos para preservar ese comportamiento, por lo que más de las entradas de la tabla ia32 pueden ser la implementación de llamadas al sistema nativa 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 sigue haciendo 64 bits), el punto de entrada int80 en sí coloca args en los registros correctos con las instrucciones mov y xchg, usando registros de 32 bits. Incluso usa mov %edx,%edx para extender EDX a RDX (porque arg3 usa el mismo registro en ambas convenciones). code here. Este código se duplica en los puntos de entrada sysenter y syscall32.


Ejemplo simple / programa de prueba:

Escribí un simple Hello World (en sintaxis NASM) que establece que todos los registros tengan mitades superiores distintas de cero, luego hace dos write() llama al sistema con int 0x80, una con un puntero a una cadena en .rodata (tiene éxito), la segunda con un puntero a la pila (falla con -EFAULT).

Luego usa el ABI nativo de 64 bits syscall para write() los caracteres de la pila (puntero de 64 bits), y de nuevo para salir.

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

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

Usé gdb, pero use el depurador que prefiera. Utilice uno que resalte los registros cambiados desde el último paso. gdbgui funciona bien para depurar la fuente asm, pero no es ideal para el desmontaje. Aún así, tiene un panel de registro que funciona bien para regs enteros al menos, y funcionó muy bien en esto ejemplo.

Consulte los comentarios inline ;;; que describen cómo cambian los registros las llamadas al sistema

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

Build it 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

Corre gdb ./abi32-from-64. En gdb, ejecute set disassembly-flavor intel y layout reg si no tiene eso en su ~/.gdbinit ya. (GAS .intel_syntax es como MASM, no NASM, pero están lo suficientemente cerca como para que sea fácil de leer si te gusta la sintaxis 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 del gdb se un desastre. Esto sucede fácilmente, incluso cuando los programas no imprimen en stdout por sí mismos.

 30
Author: Peter Cordes,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-03-19 07:43:43