linux assembly x86 x86-64 interrupt

linux - Llamada al sistema Intel x86 vs x64



assembly x86-64 (2)

Parte general

EDITAR: partes irrelevantes de Linux eliminadas

Si bien no es totalmente incorrecto, se reduce a int 0x80 y syscall simplifica demasiado la pregunta, ya que con sysenter existe al menos una tercera opción.

Usar 0x80 y eax para el número de syscall , 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.

Antes de examinar más de cerca las técnicas involucradas, debe decirse que todas rodean el problema de escapar de la prisión de privilegio en la que se desarrolla cada proceso.

Otra opción a las presentadas aquí ofrecidas por la arquitectura x86 hubiera 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 el ISR se ejecute en un nivel de privilegio diferente que antes.

Interrupción del software

Lo que ocurre exactamente una vez que se activa una interrupción depende de si el cambio al ISR requiere un cambio de privilegio o no:

(Manual del desarrollador de software Intel® 64 y IA-32 Architectures)

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 utiliza 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:

  1. Guarda temporalmente (internamente) el contenido actual de los registros SS, ESP, EFLAGS, CS y> EIP.

  2. 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 llama) del TSS en los registros SS y ESP y cambia a la nueva pila.

  3. Impulsa los valores de SS, ESP, EFLAGS, CS y EIP guardados temporalmente para la pila del procedimiento interrumpido en la nueva pila.

  4. Inserta un código de error en la nueva pila (si corresponde).

  5. Carga el selector de segmento para el nuevo segmento de código y el nuevo puntero de instrucción (desde la compuerta de interrupción o compuerta de captura) en los registros CS y EIP, respectivamente.

  6. Si la llamada se realiza a través de una puerta de interrupción, borra la bandera IF en el registro EFLAGS.

  7. Comienza la ejecución del procedimiento del manejador en el nuevo nivel de privilegio.

... suspiro, parece que hay mucho que hacer e incluso una vez que terminamos, no hay mucho mejor:

(Extracto tomado de la misma fuente mencionada anteriormente: Intel® 64 y IA-32 Architectures Software Developer''s Manual)

Al ejecutar un retorno desde un manejador de interrupciones o excepciones desde un nivel de privilegio diferente al procedimiento interrumpido, el procesador realiza estas acciones:

  1. Realiza una verificación de privilegios.

  2. Restaura los registros CS y EIP a sus valores antes de la interrupción o excepción.

  3. Restaura el registro EFLAGS.

  4. Restaura los registros SS y ESP a sus valores antes de la interrupción o excepción, lo que da como resultado un cambio de pila a la pila del procedimiento interrumpido.

  5. Reanuda la ejecución del procedimiento interrumpido.

Sysenter

Otra opción en la plataforma de 32 bits no mencionada en su pregunta en absoluto, pero sin embargo utilizada por el kernel de Linux es la instrucción sysenter .

(Intel® 64 y IA-32 Architectures Manual del desarrollador de software Volumen 2 (2A, 2B y 2C): Referencia del conjunto de instrucciones, AZ)

Descripción Ejecuta una llamada rápida a un procedimiento o rutina de sistema de nivel 0. SYSENTER es una instrucción complementaria a 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 del uso de esta solución es que no está presente en todas las máquinas de 32 bits, por lo que aún debe proporcionarse el método int 0x80 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 / SYSEXIT present (SEP) devuelto al registro EDX por la instrucción CPUID. Un sistema operativo que califica la bandera SEP también debe calificar la familia de procesadores y el modelo para garantizar que las instrucciones SYSENTER / SYSEXIT estén realmente presentes.

Syscall

La última posibilidad, la instrucción syscall , prácticamente permite la misma funcionalidad que la instrucción sysenter . La existencia de ambos se debe al hecho de 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.

Como ya se indicó anteriormente, el método int 0x80 es la única de las 3 implementaciones elegidas, que se puede ejecutar en cualquier CPU i386, por lo que esta es la única que siempre está disponible.

Para permitir alternar entre las 3 opciones, cada ejecución del proceso tiene acceso a un objeto compartido especial que da acceso a la implementación de llamada del sistema elegida para el sistema en ejecución. Esta es la extraña apariencia de linux-gate.so.1 que ya habrás encontrado como biblioteca sin resolver cuando usas ldd o similar.

(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 sus registros número de llamada del sistema en eax, parámetros en ebx, ecx, edx, esi, edi como con la implementación de llamadas al sistema int 0x80 y call la rutina principal.

Lamentablemente, no es tan fácil, ya que para minimizar el riesgo de seguridad de una dirección predefinida fija, la ubicación en la que el vdso será visible en un proceso se aleatoriza, por lo que primero tendrá que averiguar la ubicación correcta.

Esta dirección individual para cada proceso se le pasa, 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 bajo las que se ejecuta en su pila, cada una de ellas terminada por NULL.

Además de estos, se pasa un tercer bloque de los llamados elf-auxiliary-vectors siguiendo los mencionados anteriormente. La ubicación correcta está codificada en uno de estos con el identificador de tipo AT_SYSINFO .

Así que el diseño de la pila se ve así:

  • parámetro-0
  • ...
  • parámetro-m
  • NULO
  • entorno-0
  • ....
  • medio ambiente-n
  • NULO
  • ...
  • vector de duende AT_SYSINFO : AT_SYSINFO
  • ...
  • vector AT_NULL elf: AT_NULL

Ejemplo de uso

Para encontrar la dirección correcta, primero deberá omitir todos los argumentos y todos los punteros de entorno y luego comenzar a escanear 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á, echa 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

El syscall que utilicé es el que está numerado 4 (escribir) como pasó en el registro eax. Tomando filedescriptor (ebx = 1), data-pointer (ecx = & c) y size (edx = 1) como sus argumentos, cada uno pasado en el registro correspondiente.

Para resumir una larga historia

Comparando una llamada al sistema supuestamente lenta int 0x80 en cualquier CPU Intel con una implementación (con suerte) mucho más rápida usando la instrucción syscall (realmente inventada por AMD) es comparar manzanas con naranjas.

En mi humilde opinión: lo más probable es que la instrucción sysenter lugar de int 0x80 debe ser la prueba aquí.

Estoy leyendo sobre la diferencia en el ensamblaje entre x86 y x64.

En x86, el número de llamada del sistema se coloca en eax , luego se ejecuta int 80h para generar una interrupción de software.

Pero en x64, el número de llamada del sistema se coloca en rax , luego se ejecuta syscall .

Me dijeron que syscall es más ligero y más rápido que generar una interrupción de software.

¿Por qué es más rápido en x64 que x86 y puedo hacer una llamada al sistema en x64 usando int 80h ?


Hay tres cosas que deben suceder cuando se llama al núcleo (haciendo una llamada al sistema): 1. El sistema pasa de "modo usuario" a "modo núcleo" (anillo 0). 2. La pila cambia de "modo de usuario" a "modo kernel". 3. Se realiza un salto a una parte adecuada del kernel.

Obviamente, una vez dentro del kernel, el código kernel necesitará saber qué es lo que realmente quiere que haga el kernel, de ahí poner algo en EAX, y muchas 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 son int 0xnn o syscall (también hay sysenter )

La instrucción syscall se introdujo con la arquitectura x86-64 como una forma más rápida de ingresar a una llamada al sistema. Tiene un conjunto de registros (usando los mecanismos x86 MSR) que contienen la dirección para EIP / RIP a la que deseamos saltar, qué valores cargar en CS y SS, para hacer la transición de Ring3 a Ring0, y el valor del puntero de la pila. También almacena la dirección de retorno en ECX / RCX. [Lea el manual del juego de instrucciones para obtener todos los detalles de esta instrucción, ¡no es del todo trivial!]. Como el procesador sabe que esto cambiará a Ring0, puede hacer lo correcto directamente.

Al volver utilizando la instrucción SYSRET, los valores se restauran a partir de valores predeterminados en los registros, así que de nuevo, es rápido, porque el procesador solo tiene que configurar unos pocos registros. El procesador sabe que cambiará de Ring0 a Ring3, por lo que puede hacer las cosas correctas rápidamente.

La variante int 0x80 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 desde la memoria. Allí encuentra los nuevos valores de CS y EIP / RIP. El nuevo registro de CS determina el nuevo nivel de "anillo" - Ring0 en este caso. Luego usará el nuevo valor de CS para buscar en el Segmento de estado de la tarea (basado en el registro TR) para descubrir qué puntero de pila (ESP / RSP y SS), y luego finalmente salta a la nueva dirección. Como esta es una solución menos directa y más genérica, también es más lenta. El viejo EIP / RIP y CS se almacena en la nueva pila, junto con los valores anteriores 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 pila de la pila, cargando también los nuevos segmentos de pila y los valores de 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 deberá verificar "¿estamos cambiando el modo de Ring0 a Ring3? De ser 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 int 0x80 . Para el código de 64 bits, no creo que funcione, pero no estoy seguro. Estoy seguro de que tiene poco sentido hacerlo, ¿por qué querrías hacerlo más lento? Necesitarás cambiar el código de todos modos, así que realmente no veo ningún significado al usar int 0x80 en un fragmento de código de 64 bits.