GNU/Linux >> Tutoriales Linux >  >> Linux

¿Cómo invocar una llamada al sistema a través de syscall o sysenter en el ensamblaje en línea?

Variables de registro explícito

https://gcc.gnu.org/onlinedocs/gcc-8.2.0/gcc/Explicit-Register-Variables.html#Explicit-Reg-Vars)

Creo que, en general, este debería ser el enfoque recomendado sobre las restricciones de registro porque:

  • puede representar todos los registros, incluido r8 , r9 y r10 que se utilizan para los argumentos de llamada al sistema:¿Cómo especificar restricciones de registro en el registro Intel x86_64 r8 a r15 en el ensamblaje en línea GCC?
  • es la única opción óptima para otras ISA además de x86 como ARM, que no tienen los nombres de restricción de registro mágico:¿Cómo especificar un registro individual como restricción en el ensamblado en línea ARM GCC? (además de usar un registro temporal + clobbers + y una instrucción de movimiento adicional)
  • Argumentaré que esta sintaxis es más legible que usar mnemónicos de una sola letra como S -> rsi

Las variables de registro se utilizan, por ejemplo, en glibc 2.29, consulte:sysdeps/unix/sysv/linux/x86_64/sysdep.h .

main_reg.c

#define _XOPEN_SOURCE 700
#include <inttypes.h>
#include <sys/types.h>

ssize_t my_write(int fd, const void *buf, size_t size) {
    register int64_t rax __asm__ ("rax") = 1;
    register int rdi __asm__ ("rdi") = fd;
    register const void *rsi __asm__ ("rsi") = buf;
    register size_t rdx __asm__ ("rdx") = size;
    __asm__ __volatile__ (
        "syscall"
        : "+r" (rax)
        : "r" (rdi), "r" (rsi), "r" (rdx)
        : "rcx", "r11", "memory"
    );
    return rax;
}

void my_exit(int exit_status) {
    register int64_t rax __asm__ ("rax") = 60;
    register int rdi __asm__ ("rdi") = exit_status;
    __asm__ __volatile__ (
        "syscall"
        : "+r" (rax)
        : "r" (rdi)
        : "rcx", "r11", "memory"
    );
}

void _start(void) {
    char msg[] = "hello world\n";
    my_exit(my_write(1, msg, sizeof(msg)) != sizeof(msg));
}

GitHub ascendente.

Compilar y ejecutar:

gcc -O3 -std=c99 -ggdb3 -ffreestanding -nostdlib -Wall -Werror \
  -pedantic -o main_reg.out main_reg.c
./main.out
echo $?

Salida

hello world
0

A modo de comparación, lo siguiente es análogo a ¿Cómo invocar una llamada al sistema a través de syscall o sysenter en el ensamblado en línea? produce un ensamblaje equivalente:

restricción_principal.c

#define _XOPEN_SOURCE 700
#include <inttypes.h>
#include <sys/types.h>

ssize_t my_write(int fd, const void *buf, size_t size) {
    ssize_t ret;
    __asm__ __volatile__ (
        "syscall"
        : "=a" (ret)
        : "0" (1), "D" (fd), "S" (buf), "d" (size)
        : "rcx", "r11", "memory"
    );
    return ret;
}

void my_exit(int exit_status) {
    ssize_t ret;
    __asm__ __volatile__ (
        "syscall"
        : "=a" (ret)
        : "0" (60), "D" (exit_status)
        : "rcx", "r11", "memory"
    );
}

void _start(void) {
    char msg[] = "hello world\n";
    my_exit(my_write(1, msg, sizeof(msg)) != sizeof(msg));
}

GitHub ascendente.

Desmontaje de ambos con:

objdump -d main_reg.out

es casi idéntico, aquí está el main_reg.c uno:

Disassembly of section .text:

0000000000001000 <my_write>:
    1000:   b8 01 00 00 00          mov    $0x1,%eax
    1005:   0f 05                   syscall 
    1007:   c3                      retq   
    1008:   0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
    100f:   00 

0000000000001010 <my_exit>:
    1010:   b8 3c 00 00 00          mov    $0x3c,%eax
    1015:   0f 05                   syscall 
    1017:   c3                      retq   
    1018:   0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
    101f:   00 

0000000000001020 <_start>:
    1020:   c6 44 24 ff 00          movb   $0x0,-0x1(%rsp)
    1025:   bf 01 00 00 00          mov    $0x1,%edi
    102a:   48 8d 74 24 f3          lea    -0xd(%rsp),%rsi
    102f:   48 b8 68 65 6c 6c 6f    movabs $0x6f77206f6c6c6568,%rax
    1036:   20 77 6f 
    1039:   48 89 44 24 f3          mov    %rax,-0xd(%rsp)
    103e:   ba 0d 00 00 00          mov    $0xd,%edx
    1043:   b8 01 00 00 00          mov    $0x1,%eax
    1048:   c7 44 24 fb 72 6c 64    movl   $0xa646c72,-0x5(%rsp)
    104f:   0a 
    1050:   0f 05                   syscall 
    1052:   31 ff                   xor    %edi,%edi
    1054:   48 83 f8 0d             cmp    $0xd,%rax
    1058:   b8 3c 00 00 00          mov    $0x3c,%eax
    105d:   40 0f 95 c7             setne  %dil
    1061:   0f 05                   syscall 
    1063:   c3                      retq   

Entonces vemos que GCC incorporó esas pequeñas funciones de llamada al sistema como se desearía.

my_write y my_exit son iguales para ambos, pero _start en main_constraint.c es ligeramente diferente:

0000000000001020 <_start>:
    1020:   c6 44 24 ff 00          movb   $0x0,-0x1(%rsp)
    1025:   48 8d 74 24 f3          lea    -0xd(%rsp),%rsi
    102a:   ba 0d 00 00 00          mov    $0xd,%edx
    102f:   48 b8 68 65 6c 6c 6f    movabs $0x6f77206f6c6c6568,%rax
    1036:   20 77 6f 
    1039:   48 89 44 24 f3          mov    %rax,-0xd(%rsp)
    103e:   b8 01 00 00 00          mov    $0x1,%eax
    1043:   c7 44 24 fb 72 6c 64    movl   $0xa646c72,-0x5(%rsp)
    104a:   0a 
    104b:   89 c7                   mov    %eax,%edi
    104d:   0f 05                   syscall 
    104f:   31 ff                   xor    %edi,%edi
    1051:   48 83 f8 0d             cmp    $0xd,%rax
    1055:   b8 3c 00 00 00          mov    $0x3c,%eax
    105a:   40 0f 95 c7             setne  %dil
    105e:   0f 05                   syscall 
    1060:   c3                      retq 

Es interesante observar que, en este caso, GCC encontró una codificación equivalente ligeramente más corta seleccionando:

    104b:   89 c7                   mov    %eax,%edi

para configurar el fd al 1 , que es igual a 1 desde el número de llamada al sistema, en lugar de un más directo:

    1025:   bf 01 00 00 00          mov    $0x1,%edi    

Para obtener una discusión detallada de las convenciones de llamadas, consulte también:¿Cuáles son las convenciones de llamadas para las llamadas del sistema UNIX y Linux (y funciones de espacio de usuario) en i386 y x86-64?

Probado en Ubuntu 18.10, GCC 8.2.0.


En primer lugar, no puedes usar GNU C Basic asm(""); de forma segura sintaxis para esto (sin restricciones de entrada/salida/golpe). Necesita Extended asm para informarle al compilador sobre los registros que modifica. Consulte el asm en línea en el manual de GNU C y la wiki de etiquetas de ensamblaje en línea para obtener enlaces a otras guías para obtener detalles sobre cosas como "D"(1) significa como parte de un asm() declaración.

También necesitas asm volatile porque eso no está implícito para Extended asm sentencias con 1 o más operandos de salida.

Voy a mostrarte cómo ejecutar llamadas al sistema escribiendo un programa que escriba Hello World! a la salida estándar usando el write() llamada del sistema. Aquí está la fuente del programa sin una implementación de la llamada al sistema real:

#include <sys/types.h>

ssize_t my_write(int fd, const void *buf, size_t size);

int main(void)
{
    const char hello[] = "Hello world!\n";
    my_write(1, hello, sizeof(hello));
    return 0;
}

Puede ver que nombré mi función personalizada de llamada al sistema como my_write para evitar conflictos de nombres con el write "normal" , proporcionado por libc. El resto de esta respuesta contiene la fuente de my_write para i386 y amd64.

i386

Las llamadas al sistema en i386 Linux se implementan utilizando el vector de interrupción 128, p. llamando al int 0x80 en su código ensamblador, habiendo establecido los parámetros en consecuencia de antemano, por supuesto. Es posible hacer lo mismo a través de SYSENTER , pero la ejecución real de esta instrucción se logra mediante el VDSO virtualmente asignado a cada proceso en ejecución. Desde SYSENTER nunca se pensó como un reemplazo directo del int 0x80 API, nunca es ejecutado directamente por las aplicaciones de la zona del usuario; en cambio, cuando una aplicación necesita acceder a algún código del kernel, llama a la rutina asignada virtualmente en el VDSO (eso es lo que el call *%gs:0x10 en su código es para), que contiene todo el código compatible con SYSENTER instrucción. Hay mucho debido a cómo funciona realmente la instrucción.

Si quieres leer más sobre esto, echa un vistazo a este enlace. Contiene una descripción bastante breve de las técnicas aplicadas en el kernel y el VDSO. Consulte también La guía definitiva de llamadas al sistema Linux (x86):algunas llamadas al sistema como getpid y clock_gettime son tan simples que el kernel puede exportar código + datos que se ejecutan en el espacio del usuario, por lo que el VDSO nunca necesita ingresar al kernel, lo que lo hace mucho más rápido incluso que sysenter podría ser.

Es mucho más fácil usar el int $0x80 más lento para invocar la ABI de 32 bits.

// i386 Linux
#include <asm/unistd.h>      // compile with -m32 for 32 bit call numbers
//#define __NR_write 4
ssize_t my_write(int fd, const void *buf, size_t size)
{
    ssize_t ret;
    asm volatile
    (
        "int $0x80"
        : "=a" (ret)
        : "0"(__NR_write), "b"(fd), "c"(buf), "d"(size)
        : "memory"    // the kernel dereferences pointer args
    );
    return ret;
}

Como puede ver, usando el int 0x80 La API es relativamente simple. El número de la llamada al sistema va al eax registrarse, mientras que todos los parámetros necesarios para la llamada al sistema van respectivamente a ebx , ecx , edx , esi , edi y ebp . Los números de llamada del sistema se pueden obtener leyendo el archivo /usr/include/asm/unistd_32.h .

Los prototipos y las descripciones de las funciones están disponibles en la segunda sección del manual, por lo que en este caso write(2) .

El kernel guarda/restaura todos los registros (excepto EAX) para que podamos usarlos como operandos de solo entrada para el asm en línea. Consulte ¿Cuáles son las convenciones de llamada para las llamadas del sistema UNIX y Linux (y funciones de espacio de usuario) en i386 y x86-64?

Tenga en cuenta que la lista de clobber también contiene el memory parámetro, lo que significa que la instrucción enumerada en la lista de instrucciones hace referencia a la memoria (a través del buf parámetro). (Una entrada de puntero a ASM en línea no implica que la memoria apuntada también sea una entrada. Consulte ¿Cómo puedo indicar que se puede usar la memoria *apuntada* por un argumento ASM en línea?)

amd64

Las cosas se ven diferentes en la arquitectura AMD64 que tiene una nueva instrucción llamada SYSCALL . Es muy diferente del SYSENTER original instrucción, y definitivamente mucho más fácil de usar desde aplicaciones de usuario - realmente se parece a un CALL normal , en realidad, y adaptando el antiguo int 0x80 al nuevo SYSCALL es bastante trivial. (Excepto que usa RCX y R11 en lugar de la pila del kernel para guardar el RIP y RFLAGS del espacio de usuario para que el kernel sepa a dónde regresar).

En este caso, el número de la llamada al sistema aún se pasa en el registro rax , pero los registros utilizados para contener los argumentos ahora casi coinciden con la convención de llamada de funciones:rdi , rsi , rdx , r10 , r8 y r9 en ese orden. (syscall mismo destruye rcx entonces r10 se usa en lugar de rcx , dejando que las funciones de envoltura de libc solo usen mov r10, rcx / syscall .)

// x86-64 Linux
#include <asm/unistd.h>      // compile without -m32 for 64 bit call numbers
// #define __NR_write 1
ssize_t my_write(int fd, const void *buf, size_t size)
{
    ssize_t ret;
    asm volatile
    (
        "syscall"
        : "=a" (ret)
        //                 EDI      RSI       RDX
        : "0"(__NR_write), "D"(fd), "S"(buf), "d"(size)
        : "rcx", "r11", "memory"
    );
    return ret;
}

(Véalo compilado en Godbolt)

Observe cómo prácticamente lo único que necesitaba cambiar eran los nombres de los registros y las instrucciones reales utilizadas para realizar la llamada. Esto se debe principalmente a las listas de entrada/salida proporcionadas por la sintaxis de ensamblado en línea extendida de gcc, que automáticamente proporciona las instrucciones de movimiento adecuadas necesarias para ejecutar la lista de instrucciones.

El "0"(callnum) la restricción de coincidencia podría escribirse como "a" porque el operando 0 (el "=a"(ret) salida) solo tiene un registro para elegir; sabemos que elegirá EAX. Usa el que te resulte más claro.

Tenga en cuenta que los sistemas operativos que no son Linux, como MacOS, usan números de llamada diferentes. E incluso diferentes convenciones de paso de argumentos para 32 bits.


Linux
  1. Cómo configurar la virtualización en Redhat Linux

  2. Cómo cambiar el nombre de host en Linux

  3. Cómo actualizar paquetes en Ubuntu a través de la línea de comandos

  4. ¿Cómo mapear la pila para la llamada al sistema clone () en Linux?

  5. Tabla de llamadas del sistema Linux o hoja de trucos para ensamblaje

Cómo instalar Cockpit en Debian 10

Cómo actualizar Ubuntu 18.04 a Ubuntu 20.04

Cómo ejecutar y enumerar trabajos cron para un sistema Linux a través de PHP

Cómo instalar Nginx en CentOS 8

Cómo grabar audio en Ubuntu 20.04

Cómo montar NFS en Debian 11