GNU/Linux >> Tutoriels Linux >  >> Linux

Comment invoquer un appel système via syscall ou sysenter en assembleur en ligne ?

Variables de registre explicites

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

Je pense que cela devrait maintenant être généralement l'approche recommandée par rapport aux contraintes de registre car :

  • il peut représenter tous les registres, y compris r8 , r9 et r10 qui sont utilisés pour les arguments d'appel système :comment spécifier des contraintes de registre sur le registre Intel x86_64 r8 à r15 dans l'assemblage en ligne GCC ?
  • c'est la seule option optimale pour les autres ISA en plus de x86 comme ARM, qui n'ont pas les noms de contrainte de registre magique :comment spécifier un registre individuel comme contrainte dans l'assemblage en ligne ARM GCC ? (en plus d'utiliser un registre temporaire + clobbers + et une instruction mov supplémentaire)
  • Je soutiendrai que cette syntaxe est plus lisible que l'utilisation de mnémoniques à une seule lettre tels que S -> rsi

Les variables de registre sont utilisées par exemple dans la glibc 2.29, voir :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 en amont.

Compiler et exécuter :

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

Sortie

hello world
0

À titre de comparaison, l'analogue suivant à Comment invoquer un appel système via syscall ou sysenter dans l'assemblage en ligne ? produit un assemblage équivalent :

main_constraint.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 en amont.

Démontage des deux avec :

objdump -d main_reg.out

est presque identique, voici le main_reg.c un :

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   

Nous voyons donc que GCC a intégré ces minuscules fonctions d'appel système comme on le souhaiterait.

my_write et my_exit sont les mêmes pour les deux, mais _start en main_constraint.c est légèrement différent :

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 

Il est intéressant d'observer que dans ce cas GCC a trouvé un encodage équivalent légèrement plus court en choisissant :

    104b:   89 c7                   mov    %eax,%edi

pour définir le fd à 1 , ce qui équivaut à 1 à partir du numéro d'appel système, plutôt qu'un plus direct :

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

Pour une discussion approfondie des conventions d'appel, voir aussi :Quelles sont les conventions d'appel pour les appels système UNIX et Linux (et les fonctions de l'espace utilisateur) sur i386 et x86-64

Testé dans Ubuntu 18.10, GCC 8.2.0.


Tout d'abord, vous ne pouvez pas utiliser en toute sécurité GNU C Basic asm(""); syntaxe pour cela (sans contraintes d'entrée/sortie/clobber). Vous avez besoin d'Extended asm pour informer le compilateur des registres que vous modifiez. Voir l'asm en ligne dans le manuel GNU C et le wiki de balises d'assemblage en ligne pour des liens vers d'autres guides pour des détails sur des choses comme "D"(1) signifie dans le cadre d'un asm() déclaration.

Vous avez également besoin de asm volatile car ce n'est pas implicite pour Extended asm instructions avec 1 ou plusieurs opérandes de sortie.

Je vais vous montrer comment exécuter des appels système en écrivant un programme qui écrit Hello World! à la sortie standard en utilisant le write() appel système. Voici la source du programme sans implémentation de l'appel système réel :

#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;
}

Vous pouvez voir que j'ai nommé ma fonction d'appel système personnalisée en tant que my_write afin d'éviter les conflits de noms avec le write "normal" , fourni par libc. Le reste de cette réponse contient la source de my_write pour i386 et amd64.

i386

Les appels système dans i386 Linux sont implémentés à l'aide du 128e vecteur d'interruption, par ex. en appelant le int 0x80 dans votre code assembleur, après avoir défini les paramètres en conséquence au préalable, bien sûr. Il est possible de faire de même via SYSENTER , mais l'exécution réelle de cette instruction est réalisée par le VDSO mappé virtuellement à chaque processus en cours d'exécution. Depuis SYSENTER n'a jamais été conçu comme un remplacement direct du int 0x80 API, il n'est jamais directement exécuté par les applications utilisateur - à la place, lorsqu'une application a besoin d'accéder à du code du noyau, elle appelle la routine virtuellement mappée dans le VDSO (c'est ce que le call *%gs:0x10 dans votre code est pour), qui contient tout le code supportant le SYSENTER instruction. Il y en a beaucoup à cause du fonctionnement réel de l'instruction.

Si vous voulez en savoir plus à ce sujet, consultez ce lien. Il contient un aperçu assez succinct des techniques appliquées dans le noyau et le VDSO. Voir aussi The Definitive Guide to (x86) Linux System Calls - certains appels système comme getpid et clock_gettime sont si simples que le noyau peut exporter du code + des données qui s'exécutent dans l'espace utilisateur afin que le VDSO n'ait jamais besoin d'entrer dans le noyau, ce qui le rend beaucoup plus rapide même que sysenter pourrait être.

Il est beaucoup plus facile d'utiliser le int $0x80 plus lent pour invoquer l'ABI 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;
}

Comme vous pouvez le voir, en utilisant le int 0x80 L'API est relativement simple. Le numéro du syscall va au eax registre, tandis que tous les paramètres nécessaires au syscall vont respectivement dans ebx , ecx , edx , esi , edi , et ebp . Les numéros d'appel système peuvent être obtenus en lisant le fichier /usr/include/asm/unistd_32.h .

Des prototypes et des descriptions des fonctions sont disponibles dans la 2ème section du manuel, donc dans ce cas write(2) .

Le noyau enregistre/restaure tous les registres (sauf EAX) afin que nous puissions les utiliser comme opérandes d'entrée uniquement pour l'asm en ligne. Voir Quelles sont les conventions d'appel pour les appels système UNIX et Linux (et les fonctions de l'espace utilisateur) sur i386 et x86-64

Gardez à l'esprit que la liste de clobber contient également le memory paramètre, ce qui signifie que l'instruction répertoriée dans la liste d'instructions fait référence à la mémoire (via le buf paramètre). (Une entrée de pointeur vers asm inline n'implique pas que la mémoire pointée est également une entrée. Voir Comment puis-je indiquer que la mémoire *pointée* par un argument ASM inline peut être utilisée ?)

amd64

Les choses semblent différentes sur l'architecture AMD64 qui arbore une nouvelle instruction appelée SYSCALL . Il est très différent de l'original SYSENTER instruction, et certainement beaucoup plus facile à utiliser à partir d'applications utilisateur - cela ressemble vraiment à un CALL normal , en fait, et en adaptant l'ancien int 0x80 au nouveau SYSCALL est assez banal. (Sauf qu'il utilise RCX et R11 au lieu de la pile du noyau pour enregistrer le RIP et le RFLAGS de l'espace utilisateur afin que le noyau sache où retourner).

Dans ce cas, le numéro de l'appel système est encore passé dans le registre rax , mais les registres utilisés pour contenir les arguments correspondent maintenant presque à la convention d'appel de fonction :rdi , rsi , rdx , r10 , r8 et r9 dans cet ordre. (syscall détruit lui-même rcx donc r10 est utilisé à la place de rcx , laissant les fonctions wrapper libc utiliser simplement 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;
}

(Voir la compilation sur Godbolt)

Remarquez à quel point la seule chose à changer était pratiquement les noms de registre et l'instruction réelle utilisée pour effectuer l'appel. Ceci est principalement dû aux listes d'entrée/sortie fournies par la syntaxe d'assemblage en ligne étendue de gcc, qui fournit automatiquement les instructions de déplacement appropriées nécessaires à l'exécution de la liste d'instructions.

Le "0"(callnum) la contrainte de correspondance pourrait être écrite sous la forme "a" car l'opérande 0 (le "=a"(ret) output) n'a qu'un seul registre parmi lequel choisir ; nous savons qu'il choisira EAX. Utilisez ce que vous trouvez le plus clair.

Notez que les systèmes d'exploitation non Linux, comme MacOS, utilisent des numéros d'appel différents. Et même différentes conventions de passage d'arguments pour 32 bits.


Linux
  1. Comment configurer la virtualisation sur Redhat Linux

  2. Comment mmaper la pile pour l'appel système clone() sous Linux ?

  3. x86_64 Assemblage Linux System Call Confusion

  4. Appel système Linux le plus rapide

  5. Comment passer des paramètres à l'appel système Linux ?

Comment installer Cockpit sur Debian 10

Comment mettre à niveau Ubuntu 18.04 vers Ubuntu 20.04

Comment exécuter et répertorier les tâches Cron pour un système Linux via PHP

Comment installer Nginx sur CentOS 8

Comment enregistrer de l'audio dans Ubuntu 20.04

Comment monter NFS sur Debian 11