¿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?
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.
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