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

Resuelto Peter Cordes asked hace 7 años • 1 respuestas

int 0x80en Linux siempre invoca la ABI de 32 bits, independientemente del modo desde el que se llame: argumentos en ebx, ecx, ... y números de llamada al sistema desde /usr/include/asm/unistd_32.h. (O falla en kernels de 64 bits compilados sin CONFIG_IA32_EMULATION).

El código de 64 bits debe usarsyscall , con números de identificación de /usr/include/asm/unistd_64.h, y argumentos en rdi, rsi, etc. Consulte ¿Cuáles son las convenciones de llamada para llamadas de sistemas UNIX y Linux en i386 y x86-64 ? Si su pregunta fue marcada como un duplicado de esta, consulte ese enlace para obtener detalles sobre cómo debe realizar llamadas al sistema en código de 32 o 64 bits. Si quieres entender qué pasó exactamente, sigue leyendo.

(Para ver un ejemplo de 32 bits frente a 64 bits sys_write, consulte Uso de la interrupción 0x80 en Linux de 64 bits )


syscalllas llamadas al sistema son más rápidas que int 0x80las llamadas al sistema, así que use 64 bits nativos syscalla menos que esté escribiendo código de máquina políglota que se ejecute igual cuando se ejecuta en 32 o 64 bits. ( sysentersiempre regresa en modo de 32 bits, por lo que no es útil en un 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) para saber cómo realizar llamadas int 0x80al sysentersistema de 32 bits, o syscallllamadas al sistema de 64 bits, o llamar al vDSO para llamadas al sistema "virtuales" como gettimeofday. Además, información sobre de qué se tratan las llamadas al sistema.


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

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

Ver elx86tag wiki para guías para principiantes, manuales x86, documentación oficial y guías/recursos de optimización del rendimiento.


Pero dado que la gente sigue publicando preguntas sobre código que se utiliza int 0x80en código de 64 bits , o creando accidentalmente archivos binarios de 64 bits a partir de fuentes escritas para 32 bits, me pregunto qué sucede exactamente en el Linux actual.

¿ int 0x80Guarda/restaura todos los registros de 64 bits? ¿Se trunca algún registro a 32 bits? ¿Qué sucede si pasa argumentos de puntero que tienen mitades superiores distintas de cero?

¿Funciona si le pasas punteros de 32 bits?

Peter Cordes avatar Sep 07 '17 11:09 Peter Cordes
Aceptado

TL:DR : int 0x80funciona cuando se usa correctamente, siempre que los punteros quepan en 32 bits ( los punteros de pila no encajan ). Pero tenga cuidado porque stracelo decodifica mal a menos que tenga un kernel strace + muy reciente.

int 0x80pone a cero r8-r11 por motivos y conserva todo lo demás. Úselo exactamente como lo haría con el código de 32 bits, con los números de identificación de 32 bits. (O mejor, ¡no lo uses!)

No todos los sistemas lo admiten int 0x80: el subsistema de Windows para Linux versión 1 (WSL1) es estrictamente de 64 bits: int 0x80no funciona en absoluto . También es posible construir kernels de Linux sin emulación IA-32 . (No hay soporte para ejecutables de 32 bits, no hay soporte para llamadas al sistema de 32 bits). Vea esto con respecto a: asegurarse de que su WSL sea en realidad WSL2 (que utiliza un kernel de Linux real en una máquina virtual).


Los detalles: qué se guarda/restaura, qué partes de qué registros usa el kernel

int 0x80utiliza eax(no el completo rax) como número de llamada del sistema, despachando a la misma tabla de punteros de función que utiliza el espacio de usuario de 32 bits int 0x80. (Estos indicadores son para sys_whateverimplementaciones o envoltorios para la implementación nativa de 64 bits dentro del kernel. Las llamadas al sistema son en realidad llamadas a funciones a través del límite usuario/kernel).

Sólo se pasan los 32 bits inferiores de los registros arg. Las mitades superiores de rbx- rbpse conservan, pero int 0x80las llamadas al sistema las ignoran. Tenga en cuenta que pasar un puntero incorrecto a una llamada al sistema no da como resultado SIGSEGV; en su lugar, la llamada al sistema regresa -EFAULT. Si no verifica los valores de retorno de error (con un depurador o una herramienta de seguimiento), parecerá que falla silenciosamente.

Todos los registros (excepto eax, por supuesto) se guardan/restauran (incluidos RFLAGS y los 32 registros superiores de números enteros), excepto que r8-r11 se ponen a cero . r12-r15se conservan las llamadas en la convención de llamada de funciones de x86-64 SysV ABI, por lo que los registros que se ponen a cero int 0x80en 64 bits son el subconjunto de los registros "nuevos" que agregó AMD64.

Este comportamiento se ha conservado a través de algunos cambios internos en la forma en que se implementó el guardado de registros dentro del kernel, y los comentarios en el kernel mencionan que se puede utilizar desde 64 bits, por lo que esta ABI probablemente sea estable. (Es decir, puede contar con que r8-r11 se pondrá a cero y todo lo demás se conservará).

El valor de retorno tiene extensión de signo para completar 64 bits rax. (Linux declara que las funciones sys_ de 32 bits regresan con signolong ). Esto significa que los valores de retorno del puntero (como from void *mmap()) deben extenderse a cero antes de usarse en 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 da como sysenterresultado la configuración del kernel csen $__USER32_CS, que selecciona un descriptor para un segmento de código de 32 bits).


Los anteriores stracedecodifican int 0x80incorrectamente procesos de 64 bits. Se decodifica como si el proceso se hubiera utilizado syscallen lugar de int 0x80. Esto puede ser muy confuso . por ejemplo, straceimprime write(0, NULL, 12 <unfinished ... exit status 1>para eax=1/ int $0x80, que en realidad _exit(ebx)no lo es write(rdi, rsi, rdx).

No sé la versión exacta donde PTRACE_GET_SYSCALL_INFOse agregó la función, pero el kernel de Linux 5.5/strace 5.5 la maneja. Dice engañosamente que el proceso "se ejecuta en modo de 32 bits", pero se decodifica correctamente. ( Ejemplo ).


int 0x80funciona siempre que 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 ("pequeño") en la ABI SysV x86-64 . (Sección 3.5.1: se sabe que todos los símbolos están ubicados en las direcciones virtuales en el rango 0x00000000de0x7effffff , por lo que puede hacer cosas como mov edi, hello(AT&T mov $hello, %edi) para obtener un puntero a 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 gccde forma predeterminada (y habilitan ASLR para los ejecutables). Por ejemplo, compilé hello.cen Arch Linux y establecí un punto de interrupción al inicio de main. La constante de cadena pasada a putsestaba en 0x555555554724, por lo que una llamada al sistema ABI de 32 bits writeno funcionaría. (GDB deshabilita ASLR de forma predeterminada, por lo que siempre verá la misma dirección de una ejecución a otra, si ejecuta desde GDB).

Linux coloca la pila cerca del "espacio" 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). Entonces rsp, la entrada a _startun ejecutable típico vinculado estáticamente es algo así como 0x7fffffffe550, dependiendo del tamaño de env vars y args. Truncar este puntero espno apunta a ninguna memoria válida, por lo que las llamadas al sistema con entradas de puntero normalmente regresarán -EFAULTsi intenta pasar un puntero de pila truncado. (Y su programa fallará si trunca rspla esppila y luego hace algo con ella, 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.Sse define ENTRY(entry_INT80_compat). Tanto los procesos de 32 como los de 64 bits utilizan el mismo punto de entrada cuando se ejecutan int 0x80.

entry_64.SEsto define puntos de entrada nativos para un kernel de 64 bits, que incluye controladores de interrupciones/fallos y syscallllamadas nativas al sistema desde procesos de modo largo (también conocido como modo de 64 bits) .

entry_64_compat.Sdefine puntos de entrada de llamadas al sistema desde el modo de compatibilidad en un kernel de 64 bits, más el caso especial de int 0x80en un proceso de 64 bits. ( sysenteren un proceso de 64 bits también puede ir a ese punto de entrada, pero empuja $__USER32_CS, por lo que siempre regresará en modo de 32 bits). Hay una versión de 32 bits de la syscallinstrucción, compatible con CPU AMD y compatible con Linux. también para llamadas rápidas al sistema de 32 bits desde procesos de 32 bits.

Supongo que un posible caso de uso en int 0x80modo de 64 bits es si desea utilizar un descriptor de segmento de código personalizado que instaló con modify_ldt. int 0x80empuja el segmento que se registra para su uso irety Linux siempre regresa de int 0x80las llamadas al sistema a través de iret. syscallEl punto de entrada de 64 bits establece pt_regs->csy ->ssen constantes __USER_CSy __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.Sdefine puntos de entrada en un kernel de 32 bits y no está involucrado en absoluto.

El int 0x80punto 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 extiende cero eax a rax, luego inserta todos los registros en la pila del kernel para formar un archivo struct pt_regs. Aquí es desde donde se restaurará cuando regrese la llamada al sistema. Tiene un diseño estándar para registros de espacio de usuario guardados (para cualquier punto de entrada), por lo que ptracedesde otro proceso (como gdb o strace) leerán y/o escribirán esa memoria si la usan ptracemientras este proceso está dentro de una llamada al sistema. ( ptraceLa modificación de los registros es algo que complica las rutas de retorno para los otros puntos de entrada. Ver comentarios).

Pero empuja $0en lugar de r8/r9/r10/r11. ( sysentery syscall32los puntos de entrada de 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 la confirmación de configuración de pt_regs completos para todas las llamadas al sistema compatibles , el punto de entrada solo guardaba los registros de llamadas de C. Se envió directamente desde asm con call *ia32_sys_call_table(, %rax, 8), y esas funciones siguen la convención de llamada, por lo que conservan rbx, rbp, rspy r12-r15. Ponerlos a cero r8-r11en lugar de dejarlos sin definir fue para evitar fugas de información desde un kernel de 64 bits a un espacio de usuario de 32 bits (que podría pasar a un segmento de código de 64 bits para leer cualquier cosa que el kernel dejara allí).

La implementación actual ( Linux 4.12) envía llamadas al sistema ABI de 32 bits desde C, recargando los archivos ebx, ecxetc. pt_regs(Las llamadas al sistema nativo de 64 bits se envían directamente desde asm, y solo esmov %r10, %rcx necesario tener en cuenta la pequeña diferencia en la convención de llamadas entre funciones y syscall. Desafortunadamente, no siempre se 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 syscalltodavía requiere decenas de ciclos).

De todos modos, en el Linux actual, las llamadas al sistema de 32 bits (incluidas int 0x80las de 64 bits) eventualmente terminan en formato do_syscall_32_irqs_on(struct pt_regs *regs). Se envía a un puntero de función ia32_sys_call_table, con 6 argumentos extendidos a cero. Esto tal vez evite la necesidad de un contenedor alrededor de la función de llamada al sistema nativa de 64 bits en más casos para preservar ese comportamiento, por lo que más ia32entradas de la tabla pueden ser la implementación de la llamada al sistema nativo directamente.

Linux 4.12arch/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 todavía lo hacían los de 64 bits hasta 4.15 1 ), el punto de entrada int80 coloca argumentos en los registros correctos con instrucciones movy xchg, utilizando registros de 32 bits. Incluso se utiliza mov %edx,%edxpara extender EDX a cero en RDX (porque arg3 usa el mismo registro en ambas convenciones). código aquí . Este código está duplicado en los puntos de entrada sysentery syscall32.

Nota al pie 1: Linux 4.15 (creo) introdujo mitigaciones de Spectre/Meltdown y una renovación importante de los puntos de entrada que los convirtió en un trampolín para el caso de fusión. También desinfectó los registros entrantes para evitar que valores de espacio de usuario distintos de los argumentos reales estuvieran en los registros durante la llamada (cuando se podría ejecutar algún dispositivo Spectre), almacenándolos, poniendo a cero todo y luego llamando a un contenedor C que recarga solo los anchos correctos. de argumentos de la estructura guardados al ingresar.

Estoy planeando dejar esta respuesta que describe el mecanismo mucho más simple porque la parte conceptualmente útil aquí es que el lado del núcleo de una llamada al sistema implica el uso de EAX o RAX como índice en una tabla de punteros de función, y otros valores de registro entrantes copiados van a los lugares donde la convención de llamada quiere que vayan los argumentos. es decir, syscalles sólo una forma de realizar una llamada al kernel, a su código de envío.


Ejemplo simple/programa de prueba:

Escribí un Hola mundo simple (en sintaxis NASM) que configura todos los registros para que tengan mitades superiores distintas de cero, luego realiza dos write()llamadas al sistema con int 0x80, una con un puntero a una cadena en .rodata(éxito), la segunda con un puntero a la pila (falla con -EFAULT).

Luego usa la syscallABI nativa de 64 bits para write()los caracteres de la pila (puntero de 64 bits) y nuevamente para salir.

Entonces, todos estos ejemplos utilizan las ABI correctamente, excepto el segundo int 0x80que intenta pasar un puntero de 64 bits y lo trunca.

Si lo construyera como un ejecutable independiente de la posición, el primero también fallaría. (Tendría que usar un pariente de RIP leaen lugar de movobtener la dirección hello:en un registro).

Usé gdb, pero usa el depurador que prefieras. Utilice uno que resalte los registros modificados desde el último paso. gdbguiFunciona bien para depurar el código fuente de ASM, pero no es excelente para desensamblarlo. Aún así, tiene un panel de registro que funciona bien al menos para registros enteros, y funcionó muy bien en este ejemplo.

Vea los ;;;comentarios en línea que describen cómo las llamadas al sistema cambian los registros

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

Constrúyalo 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

Correr gdb ./abi32-from-64. En gdb, ejecuta set disassembly-flavor intely layout regsi aún no lo tienes en el tuyo ~/.gdbinit. (GAS .intel_syntaxes como MASM, no NASM, pero son lo suficientemente parecidos como para que sea fácil de leer si te gusta la sintaxis 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 estropee. Esto sucede fácilmente, incluso cuando los programas no se imprimen por sí solos.

Peter Cordes avatar Sep 07 '2017 04:09 Peter Cordes