tutorial smart remix programador online español curso linux assembly x86-64 system-calls abi

linux - remix - smart contracts ethereum



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

int 0x80 en Linux siempre invoca la ABI de 32 bits, independientemente del modo desde el que se llame: args en ebx , ecx , ... y números de syscall de /usr/include/asm/unistd_32.h . (O se bloquea en núcleos de 64 bits compilados 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 ¿Cuáles son las convenciones de llamadas para las llamadas al sistema UNIX y Linux en i386 y x86-64? . Si su pregunta fue marcada como un duplicado de esto, consulte ese enlace para obtener detalles sobre cómo debe hacer llamadas al sistema en código de 32 o 64 bits. Si quieres entender qué sucedió exactamente, sigue leyendo.

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

syscall sistema syscall son más rápidas que las llamadas al sistema int 0x80 , por lo tanto, use la syscall nativa de 64 bits a menos que esté escribiendo un código de 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 una instrucción x86-64 válida).

Relacionado: La guía definitiva para las llamadas al sistema Linux (en x86) sobre cómo realizar llamadas int 0x80 o sysenter al sistema de 32 bits, o llamadas al sistema syscall 64 bits, o llamar al vDSO para llamadas al sistema "virtual" como gettimeofday . Además de los antecedentes de las llamadas al sistema.

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

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

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

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

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

¿Funciona si lo pasas punteros de 32 bits?


TL: DR : int 0x80 funciona cuando se usa correctamente, siempre que los punteros quepan en 32 bits ( los punteros de pila no encajan ). Además, strace decodifica incorrectamente , decodificando el contenido del registro como si fuera la ABI de syscall 64 bits. (Todavía no hay una manera simple / confiable para que strace cuente ).

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 uses!)

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

Los detalles: qué se ha guardado / restaurado, qué partes de qué reglas usa el núcleo

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

Solo se pasan los 32 bits bajos de registros arg. Las mitades superiores de rbx - rbp se conservan, pero las llamadas al sistema int 0x80 ignoran. Tenga en cuenta que pasar un puntero incorrecto a una llamada del sistema no produce SIGSEGV; en cambio, la llamada al sistema devuelve -EFAULT . Si no verifica los valores de retorno de error (con un depurador o una herramienta de rastreo), parecerá que falla silenciosamente.

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

Este comportamiento se ha conservado durante algunos cambios internos en cómo se implementó el guardado 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 todo lo demás se conserve).

El valor de retorno se amplía mediante signos para rellenar rax 64 bits. (Linux declara que las funciones sys_ de 32 bits devuelven con signo long ). Esto significa que los valores de retorno del puntero (como de void *mmap() ) deben extenderse a cero antes de usarse en modos de direccionamiento de 64 bits

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

strace decodifica int 0x80 incorrectamente para procesos de 64 bits. Se decodifica como si el proceso hubiera utilizado syscall lugar de int 0x80 . Esto puede ser muy confuso . por ejemplo, ya que strace imprime write(0, NULL, 12 <unfinished ... exit status 1> 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) se ajusten a los 32 bajos de un registro . Este es el caso del código estático y los datos en el modelo de código predeterminado ("pequeño") https://github.com/hjl-tools/x86-psABI/wiki/X86-psABI . (Sección 3.5.1: se sabe que todos los símbolos se encuentran en las direcciones virtuales en el rango de 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 de manera predeterminada (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 0x555555554724 estaba en 0x555555554724 , por lo que una llamada al sistema de write ABI de 32 bits 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 dentro de GDB).

Linux coloca la pila cerca del "espacio" entre los rangos superior e inferior de las direcciones canónicas , es decir, con la parte superior de la pila en 2 ^ 48-1. (O en algún lugar al azar, con ASLR habilitado). Entonces, rsp al ingresar 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 generalmente devolverán -EFAULT si intenta pasar un puntero de pila truncado. (Y su programa se bloqueará si trunca rsp a esp y luego hace algo 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 usan el mismo punto de entrada cuando ejecutan int 0x80 .

entry_64.S define puntos de entrada nativos para un kernel de 64 bits, que incluye manejadores de interrupciones / fallas 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 la llamada al sistema desde el modo compat en 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 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 instrucción syscall , compatible con CPU AMD , y Linux también lo admite para llamadas rápidas del sistema de 32 bits a partir de procesos de 32 bits.

Supongo que un posible caso de uso para int 0x80 en modo de 64 bits es si desea utilizar un descriptor de segmento de código personalizado que instaló con modify_ldt . int 0x80 empuja los registros de segmento para su uso con iret , y Linux siempre regresa de las llamadas del 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 segmentación).

entry_32.S define los 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 cero-extiende eax en rax, luego empuja todos los registros a la pila del núcleo para formar una struct pt_regs . Aquí es donde se restaurará cuando vuelva 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. (La modificación de los registros es una cosa que complica las rutas de retorno para los otros puntos de entrada. Ver comentarios).

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

Creo que esta reducción a cero de r8-r11 es coincidir con el comportamiento histórico. Antes de la configuración de pt_regs completo para todas las confirmaciones de syscalls compatibles , el punto de entrada solo guardaba los registros de llamadas en 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 , rsp y r12-r15 . r8-r11 cero r8-r11 lugar de dejarlos indefinidos era probablemente una forma de evitar filtraciones de información del núcleo. IDK cómo manejó ptrace si la única copia de los registros conservados de llamadas del espacio de usuario estaba en la pila del kernel donde una función C los salvó. Dudo que haya usado metadatos de desenrollado de pila para encontrarlos allí.

La implementación actual (Linux 4.12) despacha llamadas del sistema ABI de 32 bits desde C, recargando ebx , ecx , etc. pt_regs desde pt_regs . (Las llamadas al sistema nativo de 64 bits se envían directamente desde asm, con solo un mov %r10, %rcx necesario para explicar 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. Intenta hacerlo, por lo que la ruta rápida es bastante rápida, aunque la syscall sí misma todavía toma decenas de ciclos).

De todos modos, en Linux actual, las llamadas al sistema de 32 bits (incluido int 0x80 de 64 bits) finalmente terminan en do_syscall_32_irqs_on(struct pt_regs *regs) . Se envía a un puntero de función ia32_sys_call_table , con 6 ia32_sys_call_table cero. Esto quizás evite la necesidad de un contenedor 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 la 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 despachan llamadas de sistema de 32 bits desde asm (como todavía lo hace 64 bits), el punto de entrada int80 en sí coloca args en los registros correctos con instrucciones mov y xchg , utilizando registros de 32 bits. Incluso usa mov %edx,%edx para 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 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 realiza dos llamadas al sistema write() con int 0x80 , una con un puntero a una cadena en .rodata (éxito), el segundo con un puntero a la pila (falla con -EFAULT ).

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

Por lo tanto, todos estos ejemplos están utilizando las ABI correctamente, excepto el segundo int 0x80 que intenta pasar un puntero de 64 bits y lo trunca.

Si lo compiló como un ejecutable independiente de la posición, el primero también fallaría. (Tendría que usar un lea relativo a RIP en lugar 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 cambiados desde el último paso único. gdbgui funciona bien para depurar la fuente asm, pero no es excelente para el desmontaje. Aún así, tiene un panel de registro que funciona bien para registros 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

Conviértalo 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 , ejecute set disassembly-flavor intel ~/.gdbinit set disassembly-flavor intel and layout reg si aún no lo tiene en su ~/.gdbinit . (GAS .intel_syntax es como MASM, no NASM, pero están lo suficientemente cerca como para que sea fácil de leer si le 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 de gdb se estropea. Esto sucede fácilmente, incluso cuando los programas no se imprimen en stdout.