Appels Système (syscall)

De Justine's wiki
Aller à la navigation Aller à la recherche

Source open-source un peu naze Source meilleure

Définition et présentation

Un appel système (ou syscall) est une façon pour un programme de faire une requête auprès du kernel. Strace est un outil qui permet de voir le lien entre les processus utilisateur et le kernel. Pour comprendre comment fonctionne un OS, il faut comprendre comment fonctionnent les appels système : l'une des fonctionnalités principales d'un OS est de fournir des abstractions aux programmes. Un OS peut grossièrement être divisé en deux modes :

  • Mode kernel : Un mode privilégié utilisé par le noyau en lui-même.
  • Mode user : le mode utilisé par la plupart des applications.

Un appel système fonctionne sur le même principe qu'une fonction : il peut prendre des arguments et renvoyer quelque chose. On peut citer open, fork, read, write, etc... Les appels systèmes peuvent servir à beaucoup de choses. La liste de tous les appels systèmes est disponible dans le man de syscalls. Les appels système peuvent avoir lieu de bien des façons, mais les programmeurs s'en soucient rarement : en C par exemple, la glibc fournit tous les wrappers nécessaires. À bas niveau, la façon de faire un appel système peut changer en fonction de l'architecture du processeur.

Ici, on ne se concentrera que sur un noyau Linux, avec un CPU en amd64.

Niveaux de privilèges

Les programmes ont besoin de pouvoir interagir avec le noyau afin d'effectuer diverses opérations : read / write un fichier, gérer leur mémoire avec mmap, etc. Les programmes ne peuvent pas effectuer ces actions eux-mêmes car les processeurs x86-64 fonctionnent avec un concept de "niveau de privilège". De façon très simple, le niveau de privilège détermine le niveau d'accès aux instructions CPU et IO. Le kernel a le niveau le plus élevé, appellé "Ring 0" (en français, le concept est connu sous le nom d'"anneau de protection", cf ici); un processus utilisateur a généralement un niveau de privilège de l'ordre de "Ring 3".

Pour qu'un programme utilisateur effectue une action privilégiée, il faut qu'il y ait un changement de niveau de privilège (Ring 3 => Ring 0) pour que le kernel exécute les instructions (à bas niveau, cela se traduit par une commande assembleur "SYSENTER"). Il y'a plusieurs moyens de faire cela.

Interruptions

La plus commune est l'interruption. On peut voir une interruption comme un évènement qui est généré (levé) par le matériel ou un logiciel. Une interruption hardware est soulevée par le matériel pour prévenir le kernel d'un évènement : par exemple, une carte réseau a reçu un paquet. Au niveau logiciel, cela est causé par du code. Par exemple, sur un système x86-64, cela est causé par une instruction "int" (pour *interrupt*). Les interruptions ont des nombres qui leur sont associés, avec des significations.

Pour se le représenter au niveau du processeur, on peut imaginer un array qui est situé dans la mémoire du CPU. Pour chaque élement de cette array, il y'a un numéro d'interruption (en clef) ainsi que l'adresse d'une fonction qui sera déclenchée quand l'interruption est reçue (en valeur), avec en plus des options comme le niveau de privilèges dans lequel la fonction devrait être exécutée. Les interruptions sont assez complexes, mais il faut retenir qu'une interruption peut, en fonction de sa nature, être exécutée avec un certain niveau de privilège.

MSR : Model Specific Registers

Les MSR sont des registres CPU qui ont pour but de contrôler certaines fonctions de celui-ci. La documentation CPU liste les adresses de ceux-ci. On peut utiliser les instructions CPU "rdmsr" et "wrmsr" pour y lire et y écrire. Il existe aussi des outils en ligne de commande (comme msr-tools) qui permettent de le faire, mais c'est plutôt risqué. Certaines méthodes d'appel système peuvent faire appel aux MSR.

SysCalls et assembleur

Ce n'est pas une bonne idée de faire des appels système en assembleur (ho ben, ça alors). En effet, certain appels système font appel à la glibc avant ou après l'exécution de l'appel système en lui-même, comme par exemple exit (qui va appeller des fonctions si on a aussi utilisé atexit). Malgré tout, cela reste un apprentissage intéressant.

En pratique

Appels systèmes legacy

Nous savons deux choses :

  • On peut faire exécuter quelque chose au kernel avec une interruption software
  • On peut générer une interruption software avec la commande assembleur "int".

La combinaison de de ces deux concepts amène à l'interface legacy des appels système sur Linux. En effet, le kernel a un numéro d'interruption particulier qu'il utilise pour permettre aux applications utilisateur d'effectuer des appels système. Le kernel enregistre un handler d'interruption (?) nommé ia32_syscall pour le numéro d'interruption 128 (0x80). Exemple de code qui effectue cela (issu de la fonction "trap_init" dans la version 3.13 du kernel): <source> void __init trap_init(void) {

       /* ..... other code ... */
       set_system_intr_gate(IA32_SYSCALL_VECTOR, ia32_syscall);

</source> (La variable IA32_SYSCALL_VECTOR étant définie à 0x80).

Si le kernel a une seule interruption pour mettre à disposition les appels système aux applications, comment le kernel sait-il quel appel système effectuer ? Le programme est sensé déposer le numéro du syscall dans le registre CPU eax. Les arguments pour le syscalls, eux, sont censés aller dans les autres registres "general purpose". C'est documenté dans le kernel: <source>

Emulated IA32 system calls via int 0x80.
*
* Arguments:
* %eax System call number.
* %ebx Arg1
* %ecx Arg2
* %edx Arg3
* %esi Arg4
* %edi Arg5
* %ebp Arg6    [note: not saved in the stack frame, should not be touched]
*

</source> Ainsi, on comprend comment effectuer un appel système en assembleur.

Exemple : appel exit en assembleur

On peut par exemple effectuer un appel système avec un peu d'assembleur inline (pour l'apprentissage, uniquement !). On va voir cela ici avec l'appel système exit, qui a pour unique argument le statut d'exit (le return code). Pour trouver son numéro, le noyau Linux inclut un fichier qui liste tous les appels systèmes. Ce fichier est lu par divers scripts lors de la compilation pour générer les headers qui seront utilisés par les programmes. Celle-ci est dans le code source (ici). On y voit qu'elle a le numéro 1. On devra donc mettre le numéro 1 dans le registre eax, et le premier argument (le rc) dans le registre ebx. Voici du code en C qui fait cela avec de l'assembleur "inline". <source lang="c"> int main(int argc, char *argv[]) { unsigned int syscall_nr = 1; int exit_status = 42;

/* Ici, on fait de l'assembleur. * D'abord on move l'argument 0 (pas le chiffre 0) dans EAX, * puis l'argument 1 dans EBX. * Ensuite ont utilise l'appel système pour une interruption int * avec le code 0x80, pour faire un appel système utilisateur. * Enfin, les m (argument) remplacent les arguments 0 et 1 par les * valeurs définies au-dessus. * La fin, j'ai pas compris. Déso. */

asm("movl %0, %%eax\n\t" "movl %1, %%ebx\n\t" "int $0x80"  :/*Pas de paramètres de sortie */  :/* Les paramètres d'entrée sont 0 et 1, respectivement */ "m" (syscall_nr), "m" (exit_status)  :/*Les registres en questions*/ "eax","ebx"); }

</source>

Ça marche ! on peut le compiler et le tester, et faire un "echo $?" pour voir que la valeur du dernier code retour est la bonne.

(Continuer à "Kernel-side: int $0x80 entry point" sur https://blog.packagecloud.io/the-definitive-guide-to-linux-system-calls/)