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''sentry_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.