Parte general
EDITAR:Se eliminaron partes irrelevantes de Linux
Si bien no es del todo incorrecto, se reduce a int 0x80
y syscall
simplifica demasiado la pregunta como con sysenter
hay al menos una tercera opción.
Usar 0x80 y eax para el número de llamada del sistema, ebx, ecx, edx, esi, edi y ebp para pasar parámetros es solo una de las muchas otras opciones posibles para implementar una llamada al sistema, pero esos registros son los que eligió la ABI de Linux de 32 bits .
Antes de analizar más de cerca las técnicas involucradas, debe señalarse que todas giran en torno al problema de escapar de la prisión de privilegios en la que se ejecuta cada proceso.
Otra opción a las presentadas aquí que ofrece la arquitectura x86 habría sido el uso de una puerta de llamada (ver:http://en.wikipedia.org/wiki/Call_gate)
La única otra posibilidad presente en todas las máquinas i386 es usar una interrupción de software, que permite que la ISR (Rutina de servicio de interrupción o simplemente un controlador de interrupciones ) para ejecutarse con un nivel de privilegio diferente al anterior.
(Dato curioso:algunos sistemas operativos i386 han usado una excepción de instrucción no válida para ingresar al kernel para llamadas al sistema, porque en realidad era más rápido que un int
instrucción en 386 CPU. Consulte las instrucciones de OsDev syscall/sysret y sysenter/sysexit para obtener un resumen de los posibles mecanismos de llamada al sistema).
Interrupción de software
Lo que sucede exactamente una vez que se activa una interrupción depende de si cambiar a ISR requiere un cambio de privilegio o no:
(Manual del desarrollador de software de las arquitecturas Intel® 64 e IA-32)
6.4.1 Operación de llamada y devolución para procedimientos de manejo de interrupciones o excepciones
...
Si el segmento de código para el procedimiento del controlador tiene el mismo nivel de privilegio que el programa o la tarea que se está ejecutando actualmente, el procedimiento del controlador usa la pila actual; si el controlador se ejecuta en un nivel más privilegiado, el procesador cambia a la pila para el nivel de privilegio del controlador.
....
Si se produce un cambio de pila, el procesador hace lo siguiente:
-
Guarda temporalmente (internamente) el contenido actual de los registros SS, ESP, EFLAGS, CS y> EIP.
-
Carga el selector de segmento y el puntero de pila para la nueva pila (es decir, la pila para el nivel de privilegio que se está llamando) del TSS en los registros SS y ESP y cambia a la nueva pila.
-
Empuja los valores de SS, ESP, EFLAGS, CS y EIP guardados temporalmente para la pila del procedimiento interrumpido en la nueva pila.
-
Inserta un código de error en la nueva pila (si corresponde).
-
Carga el selector de segmento para el nuevo segmento de código y el nuevo puntero de instrucción (desde la puerta de interrupción o la puerta trampa) en los registros CS y EIP, respectivamente.
-
Si la llamada se realiza a través de una puerta de interrupción, borra el indicador IF en el registro EFLAGS.
-
Comienza la ejecución del procedimiento del controlador en el nuevo nivel de privilegio.
... suspiro, esto parece ser mucho por hacer e incluso una vez que terminamos, no mejora mucho:
(extracto tomado de la misma fuente mencionada anteriormente:Intel® 64 and IA-32 Architectures Software Developer's Manual)
Al ejecutar un retorno desde un controlador de interrupción o excepción desde un nivel de privilegio diferente al del procedimiento interrumpido, el procesador realiza estas acciones:
-
Realiza una verificación de privilegios.
-
Restaura los registros CS y EIP a sus valores anteriores a la interrupción o excepción.
-
Restaura el registro EFLAGS.
-
Restaura los registros SS y ESP a sus valores anteriores a la interrupción o excepción, lo que da como resultado un cambio de pila a la pila del procedimiento interrumpido.
-
Reanuda la ejecución del procedimiento interrumpido.
Sistema
Otra opción en la plataforma de 32 bits que no se menciona en su pregunta, pero que sin embargo es utilizada por el kernel de Linux es el sysenter
instrucción.
(Manual del desarrollador de software de las arquitecturas Intel® 64 e IA-32 Volumen 2 (2A, 2B y 2C):Referencia del conjunto de instrucciones, A-Z)
Descripción Ejecuta una llamada rápida a un procedimiento o rutina del sistema de nivel 0. SYSENTER es una instrucción complementaria de SYSEXIT. La instrucción está optimizada para proporcionar el máximo rendimiento para las llamadas al sistema desde el código de usuario que se ejecuta en el nivel de privilegio 3 hasta el sistema operativo o los procedimientos ejecutivos que se ejecutan en el nivel de privilegio 0.
Una desventaja de usar esta solución es que no está presente en todas las máquinas de 32 bits, por lo que int 0x80
todavía se debe proporcionar el método en caso de que la CPU no lo sepa.
Las instrucciones SYSENTER y SYSEXIT se introdujeron en la arquitectura IA-32 en el procesador Pentium II. La disponibilidad de estas instrucciones en un procesador se indica con el indicador de función SYSENTER/SYSEXITpresent (SEP) devuelto al registro EDX por la instrucción CPUID. Un sistema operativo que califica el indicador SEP también debe calificar la familia y el modelo del procesador para garantizar que las instrucciones SYSENTER/SYSEXIT estén realmente presentes
Llamada del sistema
La última posibilidad, el syscall
instrucción, prácticamente permite la misma funcionalidad que el sysenter
instrucción. La existencia de ambos se debe a que uno (systenter
) fue introducido por Intel mientras que el otro (syscall
) fue presentado por AMD.
Específico de Linux
En el kernel de Linux se puede elegir cualquiera de las tres posibilidades mencionadas anteriormente para realizar una llamada al sistema.
Consulte también La guía definitiva de las llamadas al sistema Linux .
Como ya se indicó anteriormente, el int 0x80
El método es la única de las 3 implementaciones elegidas, que puede ejecutarse en cualquier CPU i386, por lo que es la única que siempre está disponible para el espacio de usuario de 32 bits.
(syscall
es el único que siempre está disponible para el espacio de usuario de 64 bits y el único que debe usar en el código de 64 bits; Los núcleos x86-64 se pueden compilar sin CONFIG_IA32_EMULATION
y int 0x80
aún invoca la ABI de 32 bits que trunca los punteros a 32 bits).
Para permitir cambiar entre las 3 opciones, cada ejecución de proceso tiene acceso a un objeto compartido especial que da acceso a la implementación de llamada al sistema elegida para el sistema en ejecución. Este es el extraño linux-gate.so.1
es posible que ya haya encontrado una biblioteca sin resolver al usar ldd
o similares.
(arch/x86/vdso/vdso32-setup.c)
if (vdso32_syscall()) {
vsyscall = &vdso32_syscall_start;
vsyscall_len = &vdso32_syscall_end - &vdso32_syscall_start;
} else if (vdso32_sysenter()){
vsyscall = &vdso32_sysenter_start;
vsyscall_len = &vdso32_sysenter_end - &vdso32_sysenter_start;
} else {
vsyscall = &vdso32_int80_start;
vsyscall_len = &vdso32_int80_end - &vdso32_int80_start;
}
Para utilizarlo, todo lo que tiene que hacer es cargar todos los números de llamada del sistema de registros en eax, parámetros en ebx, ecx, edx, esi, edi como con int 0x80
implementación de llamadas al sistema y call
la rutina principal.
Desafortunadamente, no es tan fácil; para minimizar el riesgo de seguridad de una dirección fija predefinida, la ubicación en la que vdso
(objeto virtual dinámico compartido ) será visible en un proceso aleatorio, por lo que primero deberá averiguar la ubicación correcta.
Esta dirección es individual para cada proceso y se pasa al proceso una vez que se inicia.
En caso de que no lo supiera, cuando se inicia en Linux, cada proceso obtiene punteros a los parámetros pasados una vez que se inició y punteros a una descripción de las variables de entorno en las que se ejecuta pasado en su pila, cada uno de ellos terminado por NULL.
Además de estos, se pasa un tercer bloque de los llamados vectores auxiliares elf después de los mencionados anteriormente. La ubicación correcta está codificada en uno de estos con el identificador de tipo AT_SYSINFO
.
Entonces, el diseño de la pila se ve así (las direcciones crecen hacia abajo):
- parámetro-0
- ...
- parámetro-m
- NULO
- entorno-0
- ....
- entorno-n
- NULO
- ...
- vector elfo auxiliar:
AT_SYSINFO
- ...
- vector elfo auxiliar:
AT_NULL
Ejemplo de uso
Para encontrar la dirección correcta, primero deberá omitir todos los argumentos y todos los punteros del entorno y luego comenzar a buscar AT_SYSINFO
como se muestra en el siguiente ejemplo:
#include <stdio.h>
#include <elf.h>
void putc_1 (char c) {
__asm__ ("movl $0x04, %%eax\n"
"movl $0x01, %%ebx\n"
"movl $0x01, %%edx\n"
"int $0x80"
:: "c" (&c)
: "eax", "ebx", "edx");
}
void putc_2 (char c, void *addr) {
__asm__ ("movl $0x04, %%eax\n"
"movl $0x01, %%ebx\n"
"movl $0x01, %%edx\n"
"call *%%esi"
:: "c" (&c), "S" (addr)
: "eax", "ebx", "edx");
}
int main (int argc, char *argv[]) {
/* using int 0x80 */
putc_1 ('1');
/* rather nasty search for jump address */
argv += argc + 1; /* skip args */
while (*argv != NULL) /* skip env */
++argv;
Elf32_auxv_t *aux = (Elf32_auxv_t*) ++argv; /* aux vector start */
while (aux->a_type != AT_SYSINFO) {
if (aux->a_type == AT_NULL)
return 1;
++aux;
}
putc_2 ('2', (void*) aux->a_un.a_val);
return 0;
}
Como verá al echar un vistazo al siguiente fragmento de /usr/include/asm/unistd_32.h
en mi sistema:
#define __NR_restart_syscall 0
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
#define __NR_close 6
La llamada al sistema que utilicé es la numerada 4 (escribir) como se pasó en el registro eax. Tomando el descriptor de archivo (ebx =1), el puntero de datos (ecx =&c) y el tamaño (edx =1) como sus argumentos, cada uno pasado en el registro correspondiente.
Para resumir una larga historia
Comparando un int 0x80
supuestamente lento llamada al sistema en any CPU Intel con una implementación (con suerte) mucho más rápida usando el (genuinamente inventado por AMD) syscall
la instrucción es comparar manzanas con naranjas.
En mi humilde opinión:Lo más probable es que el sysenter
instrucción en lugar de int 0x80
debería estar a prueba aquí.
Hay tres cosas que deben suceder cuando llama al kernel (haciendo una llamada al sistema):
- El sistema pasa de "modo usuario" a "modo kernel" (anillo 0).
- La pila cambia de "modo usuario" a "modo kernel".
- Se realiza un salto a una parte adecuada del kernel.
Obviamente, una vez dentro del kernel, el código del kernel necesitará saber lo que realmente quiere que haga el kernel, por lo tanto, debe poner algo en EAX y, a menudo, más cosas en otros registros, ya que hay cosas como "nombre del archivo que desea abrir". " o "búfer para leer datos de un archivo en", etc, etc.
Los diferentes procesadores tienen diferentes formas de lograr los tres pasos anteriores. En x86, hay varias opciones, pero las dos más populares para asm escrito a mano son int 0xnn
(modo de 32 bits) o syscall
(modo de 64 bits). (También hay un modo de 32 bits sysenter
, presentado por Intel por la misma razón por la que AMD introdujo la versión en modo de 32 bits de syscall
:como una alternativa más rápida al lento int 0x80
. La glibc de 32 bits usa cualquier mecanismo eficiente de llamada al sistema que esté disponible, solo usando el lento int 0x80
si no hay nada mejor disponible.)
La versión de 64 bits del syscall
La instrucción se introdujo con la arquitectura x86-64 como una forma más rápida de ingresar una llamada al sistema. Tiene un conjunto de registros (utilizando los mecanismos MSR x86) que contienen la dirección RIP a la que deseamos saltar, qué valores de selector cargar en CS y SS, y para hacer la transición de Ring3 a Ring0. También almacena la dirección de retorno en ECX/RCX. [Lea el manual del conjunto de instrucciones para conocer todos los detalles de esta instrucción; ¡no es del todo trivial!]. Dado que el procesador sabe que esto cambiará a Ring0, puede hacer lo correcto directamente.
Uno de los puntos clave es que syscall
solo manipula registros; no realiza cargas ni almacena. (Por eso sobrescribe RCX con el RIP guardado y R11 con los RFLAGS guardados). El acceso a la memoria depende de las tablas de páginas, y las entradas de las tablas de páginas tienen un bit que puede hacer que solo sean válidas para el kernel, no para el espacio del usuario, por lo que el acceso a la memoria mientras cambiar el nivel de privilegio puede necesitar esperar en lugar de solo escribir registros. Una vez en modo kernel, el kernel normalmente usará swapgs
o alguna otra forma de encontrar la pila del kernel. (syscall
no modificar RSP; todavía está apuntando a la pila del usuario al ingresar al kernel).
Al regresar usando la instrucción SYSRET, los valores se restauran a partir de valores predeterminados en los registros, por lo que nuevamente, es rápido, porque el procesador solo tiene que configurar algunos registros. El procesador sabe que cambiará de Ring0 a Ring3, por lo que puede hacer lo correcto rápidamente.
(Las CPU de AMD son compatibles con syscall
instrucciones desde el espacio de usuario de 32 bits; Las CPU Intel no. x86-64 era originalmente AMD64; por eso tenemos syscall
en modo de 64 bits. AMD rediseñó el lado del kernel de syscall
para el modo de 64 bits, por lo que el syscall
de 64 bits el punto de entrada del kernel es significativamente diferente del syscall
de 32 bits punto de entrada en kernels de 64 bits).
El int 0x80
La variante utilizada en el modo de 32 bits decidirá qué hacer en función del valor en la tabla de descriptores de interrupción, lo que significa leer de la memoria. Allí encuentra los nuevos valores CS y EIP/RIP. El nuevo registro CS determina el nuevo nivel de "anillo" - Ring0 en este caso. A continuación, utilizará el nuevo valor de CS para buscar en el segmento de estado de la tarea (basado en el registro TR) para averiguar qué puntero de pila (ESP/RSP y SS), y finalmente salta a la nueva dirección. Dado que esta es una solución menos directa y más genérica, también es más lenta. El EIP/RIP y CS antiguos se almacenan en la nueva pila, junto con los valores antiguos de SS y ESP/RSP.
Al regresar, utilizando la instrucción IRET, el procesador lee la dirección de retorno y los valores del puntero de la pila, cargando también el nuevo segmento de la pila y los valores del segmento de código de la pila. Una vez más, el proceso es genérico y requiere bastantes lecturas de memoria. Dado que es genérico, el procesador también tendrá que verificar "¿estamos cambiando el modo de Ring0 a Ring3, si es así, cambie estas cosas".
Entonces, en resumen, es más rápido porque estaba destinado a funcionar de esa manera.
Para el código de 32 bits, sí, definitivamente puede usar el lento y compatible int 0x80
si quieres.
Para código de 64 bits, int 0x80
es más lento que syscall
y truncará sus punteros a 32 bits, así que no lo use. Consulte ¿Qué sucede si usa la ABI de Linux int 0x80 de 32 bits en código de 64 bits? Además, int 0x80
no está disponible en modo de 64 bits en todos los núcleos, por lo que no es seguro ni siquiera para un sys_exit
que no toma ningún argumento de puntero:CONFIG_IA32_EMULATION
se puede deshabilitar, y en particular es deshabilitado en el subsistema de Windows para Linux.