Partie générale
EDIT :les parties non pertinentes de Linux ont été supprimées
Bien que ce ne soit pas totalement faux, nous nous limitons à int 0x80
et syscall
simplifie à l'extrême la question comme avec sysenter
il y a au moins une 3ème option.
L'utilisation de 0x80 et eax pour le numéro d'appel système, ebx, ecx, edx, esi, edi et ebp pour passer les paramètres n'est qu'un des nombreux autres choix possibles pour implémenter un appel système, mais ces registres sont ceux que l'ABI Linux 32 bits a choisis .
Avant d'examiner de plus près les techniques impliquées, il convient de préciser qu'elles tournent toutes autour du problème de l'évasion de la prison du privilège dans laquelle chaque processus s'exécute.
Un autre choix à ceux présentés ici offert par l'architecture x86 aurait été l'utilisation d'un call gate (voir :http://en.wikipedia.org/wiki/Call_gate)
La seule autre possibilité présente sur toutes les machines i386 est d'utiliser une interruption logicielle, qui permet à l'ISR (Interrupt Service Routine ou simplement un gestionnaire d'interruptions ) pour s'exécuter à un niveau de privilège différent de celui d'avant.
(Fait amusant :certains systèmes d'exploitation i386 ont utilisé une exception d'instruction non valide pour entrer dans le noyau pour les appels système, car c'était en fait plus rapide qu'un int
instruction sur 386 processeurs. Voir l'activation des instructions OsDev syscall/sysret et sysenter/sysexit pour un résumé des mécanismes d'appel système possibles.)
Interruption logicielle
Ce qui se passe exactement une fois qu'une interruption est déclenchée dépend du fait que le passage à l'ISR nécessite ou non un changement de privilège :
(Manuel du développeur de logiciels pour les architectures Intel® 64 et IA-32)
6.4.1 Opération d'appel et de retour pour les procédures de gestion des interruptions ou des exceptions
...
Si le segment de code de la procédure de gestionnaire a le même niveau de privilège que le programme ou la tâche en cours d'exécution, la procédure de gestionnaire utilise la pile actuelle ; si le gestionnaire s'exécute à un niveau plus privilégié, le processeur bascule vers la pile correspondant au niveau de privilège du gestionnaire.
....
Si un commutateur de pile se produit, le processeur effectue les actions suivantes :
Enregistre temporairement (en interne) le contenu actuel des registres SS, ESP, EFLAGS, CS et> EIP.
Charge le sélecteur de segment et le pointeur de pile pour la nouvelle pile (c'est-à-dire la pile pour le niveau de privilège appelé) du TSS dans les registres SS et ESP et bascule vers la nouvelle pile.
Pousse les valeurs SS, ESP, EFLAGS, CS et EIP temporairement enregistrées pour la pile de la procédure interrompue vers la nouvelle pile.
Pousse un code d'erreur sur la nouvelle pile (le cas échéant).
Charge le sélecteur de segment pour le nouveau segment de code et le nouveau pointeur d'instruction (depuis la porte d'interruption ou la porte de déroutement) dans les registres CS et EIP, respectivement.
Si l'appel passe par une porte d'interruption, efface le drapeau IF dans le registre EFLAGS.
Commence l'exécution de la procédure de gestionnaire au nouveau niveau de privilège.
... soupir, cela semble être beaucoup à faire et même une fois que nous avons terminé, cela ne va pas beaucoup mieux :
(extrait tiré de la même source que celle mentionnée ci-dessus :Intel® 64 and IA-32 Architectures Software Developer’s Manual)
Lors de l'exécution d'un retour à partir d'une interruption ou d'un gestionnaire d'exceptions à partir d'un niveau de privilège différent de celui de la procédure interrompue, le processeur effectue ces actions :
Effectue une vérification des privilèges.
Restaure les registres CS et EIP à leurs valeurs avant l'interruption ou l'exception.
Restaure le registre EFLAGS.
Restaure les registres SS et ESP à leurs valeurs avant l'interruption ou l'exception, ce qui entraîne un basculement de pile vers la pile de la procédure interrompue.
Reprend l'exécution de la procédure interrompue.
Sysenter
Une autre option sur la plate-forme 32 bits qui n'est pas du tout mentionnée dans votre question, mais néanmoins utilisée par le noyau Linux est le sysenter
instruction.
(Manuel du développeur de logiciels pour les architectures Intel® 64 et IA-32 Volume 2 (2A, 2B et 2C) :Référence du jeu d'instructions, A-Z)
Description Exécute un appel rapide à une procédure ou routine système de niveau 0. SYSENTER est une instruction complémentaire à SYSEXIT. L'instruction est optimisée pour fournir les performances maximales pour les appels système du code utilisateur exécuté au niveau de privilège 3 au système d'exploitation ou aux procédures exécutives exécutées au niveau de privilège 0.
Un inconvénient de l'utilisation de cette solution est qu'elle n'est pas présente sur toutes les machines 32 bits, donc le int 0x80
la méthode doit toujours être fournie au cas où le CPU ne le sache pas.
Les instructions SYSENTER et SYSEXIT ont été introduites dans l'architecture IA-32 du processeur Pentium II. La disponibilité de ces instructions sur un processeur est indiquée par l'indicateur de fonction SYSENTER/SYSEXITprésent (SEP) renvoyé au registre EDX par l'instruction CPUID. Un système d'exploitation qui qualifie l'indicateur SEP doit également qualifier la famille et le modèle de processeur pour s'assurer que les instructions SYSENTER/SYSEXIT sont bien présentes
Appel système
La dernière possibilité, le syscall
instruction, permet à peu près la même fonctionnalité que le sysenter
instruction. L'existence des deux est due au fait que l'un (systenter
) a été introduit par Intel tandis que l'autre (syscall
) a été introduit par AMD.
Spécifique à Linux
Dans le noyau Linux, l'une des trois possibilités mentionnées ci-dessus peut être choisie pour réaliser un appel système.
Voir aussi Le guide définitif des appels système Linux .
Comme déjà indiqué ci-dessus, le int 0x80
est la seule des 3 implémentations choisies, qui peut s'exécuter sur n'importe quel processeur i386, c'est donc la seule qui soit toujours disponible pour l'espace utilisateur 32 bits.
(syscall
est le seul qui soit toujours disponible pour l'espace utilisateur 64 bits, et le seul que vous devriez jamais utiliser dans le code 64 bits; Les noyaux x86-64 peuvent être construits sans CONFIG_IA32_EMULATION
, et int 0x80
appelle toujours l'ABI 32 bits qui tronque les pointeurs vers 32 bits.)
Pour permettre de basculer entre les 3 choix, chaque exécution de processus a accès à un objet partagé spécial qui donne accès à l'implémentation d'appel système choisie pour le système en cours d'exécution. C'est l'étrange linux-gate.so.1
vous avez peut-être déjà rencontré une bibliothèque non résolue lors de l'utilisation de ldd
ou similaire.
(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;
}
Pour l'utiliser il vous suffit de charger tous vos registres numéro d'appel système en eax, paramètres en ebx, ecx, edx, esi, edi comme avec int 0x80
implémentation des appels système et call
la routine principale.
Malheureusement, ce n'est pas si facile; afin de minimiser le risque de sécurité d'une adresse prédéfinie fixe, l'emplacement auquel le vdso
(objet partagé dynamique virtuel ) sera visible dans un processus aléatoire, vous devrez donc d'abord déterminer l'emplacement correct.
Cette adresse est propre à chaque processus et est transmise au processus une fois qu'il est démarré.
Au cas où vous ne le sauriez pas, lorsqu'il est démarré sous Linux, chaque processus obtient des pointeurs vers les paramètres passés une fois qu'il a été démarré et des pointeurs vers une description des variables d'environnement sous lesquelles il s'exécute passé sur sa pile - chacun d'eux terminé par NULL.
En plus de ceux-ci, un troisième bloc de vecteurs dits elf-auxiliaires est passé après ceux mentionnés précédemment. L'emplacement correct est codé dans l'un de ceux-ci portant l'identifiant de type AT_SYSINFO
.
Ainsi, la disposition de la pile ressemble à ceci (les adresses grandissent vers le bas) :
- paramètre-0
- ...
- paramètre-m
- NULL
- environnement-0
- ....
- environnement-n
- NULL
- ...
- vecteur elfe auxiliaire :
AT_SYSINFO
- ...
- vecteur elfe auxiliaire :
AT_NULL
Exemple d'utilisation
Pour trouver l'adresse correcte, vous devrez d'abord ignorer tous les arguments et tous les pointeurs d'environnement, puis lancer la recherche de AT_SYSINFO
comme indiqué dans l'exemple ci-dessous :
#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;
}
Comme vous le verrez en jetant un œil à l'extrait suivant de /usr/include/asm/unistd_32.h
sur mon système :
#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
L'appel système que j'ai utilisé est celui numéroté 4 (écriture) tel qu'il est passé dans le registre eax. En prenant filedescriptor (ebx =1), data-pointer (ecx =&c) et size (edx =1) comme arguments, chacun passé dans le registre correspondant.
Pour résumer une longue histoire
Comparaison d'un int 0x80
censé fonctionner lentement appel système sur tout Processeur Intel avec une implémentation (espérons-le) beaucoup plus rapide en utilisant le syscall
(véritablement inventé par AMD) l'instruction est de comparer des pommes à des oranges.
IMHO :Probablement le sysenter
instruction au lieu de int 0x80
devrait être à l'épreuve ici.
Il y a trois choses qui doivent se produire lorsque vous appelez le noyau (en faisant un appel système) :
- Le système passe du "mode utilisateur" au "mode noyau" (ring 0).
- La pile passe du "mode utilisateur" au "mode noyau".
- Un saut est effectué vers une partie appropriée du noyau.
De toute évidence, une fois à l'intérieur du noyau, le code du noyau devra savoir ce que vous voulez réellement que le noyau fasse, donc mettre quelque chose dans EAX, et souvent plus de choses dans d'autres registres puisqu'il y a des choses comme "nom du fichier que vous voulez ouvrir " ou "tampon pour lire les données d'un fichier dans" etc, etc.
Différents processeurs ont différentes manières de réaliser les trois étapes ci-dessus. Dans x86, il y a plusieurs choix, mais les deux plus populaires pour l'asm écrit à la main sont int 0xnn
(mode 32 bits) ou syscall
(mode 64 bits). (Il y a aussi le mode 32 bits sysenter
, introduit par Intel pour la même raison qu'AMD a introduit la version en mode 32 bits de syscall
:comme alternative plus rapide au lent int 0x80
. La glibc 32 bits utilise le mécanisme d'appel système efficace disponible, en utilisant uniquement le lent int 0x80
si rien de mieux n'est disponible.)
La version 64 bits du syscall
L'instruction a été introduite avec l'architecture x86-64 comme un moyen plus rapide d'entrer un appel système. Il possède un ensemble de registres (utilisant les mécanismes x86 MSR) qui contiennent l'adresse RIP à laquelle nous souhaitons accéder, les valeurs de sélecteur à charger dans CS et SS, et pour effectuer la transition Ring3 à Ring0. Il stocke également l'adresse de retour dans ECX/RCX. [Veuillez lire le manuel du jeu d'instructions pour tous les détails de cette instruction - ce n'est pas entièrement trivial !]. Puisque le processeur sait que cela passera à Ring0, il peut directement faire ce qu'il faut.
L'un des points clés est que syscall
manipule uniquement les registres ; il ne fait aucun chargement ni stockage. (C'est pourquoi il écrase RCX avec le RIP enregistré et R11 avec le RFLAGS enregistré). L'accès à la mémoire dépend des tables de pages, et les entrées de la table de pages ont un bit qui peut les rendre valides uniquement pour le noyau, pas pour l'espace utilisateur, donc l'accès à la mémoire pendant changer le niveau de privilège peut nécessiter d'attendre plutôt que d'écrire simplement des registres. Une fois en mode noyau, le noyau utilisera normalement swapgs
ou une autre façon de trouver la pile du noyau. (syscall
n'est pas modifier le RSP ; il pointe toujours vers la pile utilisateur à l'entrée du noyau.)
Lors du retour à l'aide de l'instruction SYSRET, les valeurs sont restaurées à partir de valeurs prédéterminées dans des registres, donc encore une fois, c'est rapide, car le processeur n'a qu'à configurer quelques registres. Le processeur sait qu'il passera de Ring0 à Ring3, il peut donc faire les bonnes choses rapidement.
(Les processeurs AMD prennent en charge le syscall
instruction à partir de l'espace utilisateur 32 bits ; Les processeurs Intel ne le font pas. x86-64 était à l'origine AMD64 ; c'est pourquoi nous avons syscall
en mode 64 bits. AMD a repensé le côté noyau de syscall
pour le mode 64 bits, donc le syscall
64 bits le point d'entrée du noyau est significativement différent du syscall
32 bits point d'entrée dans les noyaux 64 bits.)
Le int 0x80
La variante utilisée en mode 32 bits décidera quoi faire en fonction de la valeur dans la table des descripteurs d'interruption, ce qui signifie lire à partir de la mémoire. Il y trouve les nouvelles valeurs CS et EIP/RIP. Le nouveau registre CS détermine le nouveau niveau de "sonnerie" - Ring0 dans ce cas. Il utilisera ensuite la nouvelle valeur CS pour examiner le segment d'état de la tâche (basé sur le registre TR) pour savoir quel pointeur de pile (ESP/RSP et SS), puis sautera finalement à la nouvelle adresse. Comme il s'agit d'une solution moins directe et plus générique, elle est également plus lente. Les anciens EIP/RIP et CS sont stockés sur la nouvelle pile, avec les anciennes valeurs de SS et ESP/RSP.
Lors du retour, en utilisant l'instruction IRET, le processeur lit l'adresse de retour et les valeurs de pointeur de pile à partir de la pile, chargeant également les nouvelles valeurs de segment de pile et de segment de code à partir de la pile. Encore une fois, le processus est générique et nécessite un certain nombre de lectures de mémoire. Puisqu'il est générique, le processeur devra également vérifier "est-ce que nous changeons de mode de Ring0 à Ring3, si c'est le cas, changez ces choses".
Donc, en résumé, c'est plus rapide parce que c'était censé fonctionner de cette façon.
Pour le code 32 bits, oui, vous pouvez certainement utiliser le int 0x80
lent et compatible si vous voulez.
Pour le code 64 bits, int 0x80
est plus lent que syscall
et tronquera vos pointeurs en 32 bits, alors ne l'utilisez pas. Voir Que se passe-t-il si vous utilisez l'ABI Linux int 0x80 32 bits dans du code 64 bits ? De plus, int 0x80
n'est pas disponible en mode 64 bits sur tous les noyaux, il n'est donc pas sûr même pour un sys_exit
qui ne prend aucun argument de pointeur :CONFIG_IA32_EMULATION
peut être désactivé, et notamment est désactivé sur le sous-système Windows pour Linux.