Un appel système est un mécanisme par lequel une application utilise les fonctions fournies par le système d'exploitation, mais il est important de connaître les appels système afin de comprendre le fonctionnement de l'application.
En effet, presque toutes les choses importantes dans le fonctionnement de l'application sont réalisées à l'aide d'appels système. Par exemple, la communication réseau, l'entrée / sortie de fichier, la création de nouveaux processus, la communication inter-processus, la création de conteneurs, etc. sont réalisées à l'aide d'appels système. Au contraire, la seule chose qu'une application peut faire sans utiliser d'appels système est le calcul sur le CPU et l'entrée / sortie vers / depuis la mémoire.
Le contenu abordé dans cet article concerne la nature générale et la mécanique des appels système. En premier lieu, j'expliquerai ce qu'est un appel système et comment il est réalisé. De plus, nous allons vous montrer comment utiliser directement les appels système sans passer par une bibliothèque, ou comment approfondir le noyau pour examiner l'implémentation des appels système.
Le contenu est le suivant.
Selon Linux Kernel Development etc., un mécanisme appelé appel système est introduit. Il y a deux avantages:
Les appels système fournissent aux applications une interface simple et abstraite pour manipuler le matériel. Cela élimine la nécessité pour le code d'application de connaître les détails matériels sous-jacents.
Par exemple, l'appel système write
est une interface courante pour écrire une chaîne d'octets sur quelque chose.
Il existe une grande variété de choses qui peuvent être spécifiées comme cibles d'écriture, c'est-à-dire qui peuvent être traitées comme des fichiers. Par exemple, divers périphériques de sortie tels que les canaux utilisés pour la communication inter-processus, les sockets utilisés pour la communication réseau, les moniteurs, etc., ainsi que divers types de systèmes de fichiers. Etc.
La philosophie de base de Linux et Unix est «tout est un fichier», mais divers types sont utilisés pour l'entrée et la sortie, y compris «write». On peut dire que l'appel système en est l'incarnation.
Les appels système servent d'intermédiaire entre l'application et les ressources gérées par le système d'exploitation et ont pour rôle d'empêcher l'application d'utiliser la ressource de manière incorrecte ou de l'utiliser d'une manière qui pose un problème de sécurité. Cela permet aux applications d'utiliser les ressources en toute sécurité.
Comme exemple d'utilisation sûre des ressources, par exemple, sécuriser une zone mémoire par un processus (en utilisant mmap
) Il y aura. Une ressource matérielle appelée mémoire est partagée avec d'autres processus et systèmes d'exploitation, et si elle est utilisée de manière incorrecte, il existe un risque de destruction d'autres processus et systèmes d'exploitation. Cet appel système permet au système d'exploitation d'allouer une zone mémoire à chaque processus de manière sécurisée lorsque le processus souhaite allouer une zone mémoire.
En outre, un exemple d'utilisation sécurisée des ressources serait le contrôle d'accès aux fichiers basé sur les informations d'autorisation.
Que fait l'application lors de l'appel d'un appel système? Quelle est la différence avec un appel de fonction normal? Ici, nous allons expliquer comment l'application appelle l'appel système.
Une application doit suivre trois étapes lors de l'appel d'un appel système:
Dans ce qui suit, nous expliquerons la procédure ci-dessus en détail en utilisant le code qui génère des caractères à l'écran (sortie standard) à titre d'exemple.
L'appel système write
est utilisé pour la sortie standard et l'écriture dans un fichier, mais un exemple qui appelle cet appel système Jetons un coup d'œil aux étapes dans lesquelles une application appelle un appel système à l'aide de code.
Tout d'abord, lancez image gcc, qui est utilisée pour exécuter l'exemple de code, en tant que conteneur.
$ docker container run -it --rm gcc /bin/bash
Créez un fichier hi.s
avec le code d'assemblage suivant dans le conteneur lancé. J'expliquerai le code plus tard.
# cat <<EOF > hi.s
.intel_syntax noprefix
.global main
main:
push rbp
mov rbp, rsp
push 0xA216948
mov rax, 1
mov rdi, 1
mov rsi, rsp
mov rdx, 4
syscall
mov rsp, rbp
pop rbp
ret
EOF
Lorsque vous le compilez et l'exécutez, les caractères sont affichés à l'écran comme indiqué ci-dessous.
# gcc -o hi.o hi.s; ./hi.o
Hi!
La méthode d'appel des appels système est légèrement différente selon les spécifications / l'architecture du processeur, mais le code ci-dessus est le plus populaire x86-64. C'est un exemple d'appel de l'appel système write
dans l'architecture.
Avant d'expliquer l'exemple de code présenté ci-dessus, expliquons d'abord comment appeler un appel système sur x86-64. Ce que vous faites avec d'autres architectures ne change pas grand-chose.
L'appel d'un appel système sur x86-64 se fait dans les trois étapes suivantes.
syscall
Tout d'abord, à l'étape 1, spécifiez ici l'appel système appelé numéro d'appel système dans le registre appelé rax
(= la mémoire de 16 à 64 bits intégrée au CPU, accessible à une vitesse extrêmement élevée depuis le CPU). Stocke le numéro à utiliser. Ce numéro vous permet d'identifier l'appel système que le noyau doit effectuer.
L'appel système correspondant au numéro d'appel système est spécifié comme suit, par exemple.
system call number | Nom de l'appel système | Contenu |
---|---|---|
0 | read | Lis |
1 | write | Exportation |
2 | open | Fichier ouvert |
57 | fork | Lancer un nouveau processus |
Un tableau complet qui comprend plus que les exemples ci-dessus peut être trouvé à ici.
Dans l'exemple de code, les parties correspondant à cette étape sont:
mov rax, 1
Ensuite, à l'étape 2, nous stockerons ici les arguments à passer à l'appel système dans le registre.
Vous pouvez passer jusqu'à 6 arguments, et les registres rdi
, rsi
, rdx
, r10
, r9
et r8
doivent être utilisés dans l'ordre du premier argument.
Pour l'appel système write
, les arguments et registres à passer sont spécifiés comme suit:
Nom du registre | rdi | rsi | rdx |
---|---|---|---|
argument | Descripteur de fichier | Adresse de début de la chaîne d'octets à écrire | Taille de la chaîne d'octets à exporter |
Vous pouvez vérifier les arguments à utiliser pour les autres appels système ici [https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/).
À propos, ce qu'est un descripteur de fichier (défini dans le registre rdi
) est comme un numéro de port associé à chaque processus qui contrôle l'entrée / la sortie du processus. Par défaut, le descripteur de fichier peut utiliser trois valeurs de 0 à 2 et correspond respectivement à 0: entrée standard, 1: sortie standard et 2: erreur standard.
Lorsque vous ouvrez un fichier ou un socket, de nouveaux descripteurs de fichier sont assignés successivement, 3, 4, 5, etc., et des appels système tels que write write
et read read
sont exécutés pour eux (appel système). Peut être passé comme argument à).
Dans l'exemple de code, les parties correspondant à cette étape sont:
mov rdi, 1
mov rsi, rsp
mov rdx, 4
L'étape 3 émet une instruction / instruction appelée syscall
.
Cela amène le CPU à interrompre l'exécution du code de l'application, à basculer vers un mode qui exécute le code du noyau, puis à sauter au code qui exécute l'appel système dans le noyau (gestionnaire d'appels système).
Lorsque vous quittez l'instruction syscall
, la valeur de retour est définie dans le registre rax
et peut être référencée si nécessaire.
Cette partie sera expliquée en détail dans [Comment implémenter les appels système et l'implémentation interne] plus loin dans l'article.
Dans l'exemple de code, les parties correspondant à cette étape sont:
syscall
J'ai ajouté un commentaire pour comprendre la signification de l'exemple de code. Le point est la ligne intitulée syscall
et juste avant.
.intel_syntax noprefix #Format de code
.global main #Libellé du point de départ de l'exécution
main:
#Prétraitement
push rbp
mov rbp, rsp
#Chaîne'Hi!'Sur la pile
push 0xA216948
#Étape 1 avec le numéro d'appel système'1'(=Prend en charge l'écriture)Spécifier
mov rax, 1
#Étape 2 Définissez les arguments à transmettre à l'appel système d'écriture dans le registre
mov rdi, 1 #1 dans le descripteur de fichier à exporter(Sortie standard)Ensemble
mov rsi, rsp #Adresse de départ de la pile(lettre`H`Est inclus)Ensemble
mov rdx, 4 #Définir la taille de la chaîne de sortie
#Étape 3 Émettez un appel système
syscall
#Post-traitement
mov rsp, rbp
pop rbp
ret
En passant, si vous ajoutez un peu sur la partie du code ci-dessus qui transmet l'adresse de début de la chaîne de caractères à l'étape 2.
mov rsi, rsp #Adresse de départ de la pile(lettre`H`Est inclus)Ensemble
La chaîne est stockée sur la pile, mais le registre rsp
est un registre spécial qui pointe vers l'adresse de début de la pile. Par conséquent, en définissant la valeur de «rsp» sur «rsi», cela signifie que l'adresse de début de la chaîne de caractères est transmise à l'appel système.
Les appels système sont généralement fournis sous forme de bibliothèque en langage C. Lorsque vous utilisez des appels système, vous pouvez essentiellement utiliser cette bibliothèque d'encapsuleurs et vous n'avez pas besoin d'écrire directement le code d'assemblage.
Par exemple, «write (2)» est défini comme une fonction C avec la signature suivante: ](Http://man7.org/linux/man-pages/man2/write.2.html)
ssize_t write(int fd, const void *buf, size_t count);
D'un autre côté, si vous ne souhaitez pas utiliser la bibliothèque C, vous devrez implémenter vos propres appels système en tant que code d'assemblage, comme vous l'avez fait dans l'exemple ci-dessus. Le langage Go, par exemple, adopte une telle approche.
golang/sys/unix/asm_linux_amd64.s
TEXT ·SyscallNoError(SB),NOSPLIT,$0-48
CALL runtime·entersyscall(SB)
MOVQ a1+8(FP), DI
MOVQ a2+16(FP), SI
MOVQ a3+24(FP), DX
MOVQ $0, R10
MOVQ $0, R8
MOVQ $0, R9
MOVQ trap+0(FP), AX // syscall entry
SYSCALL
MOVQ AX, r1+32(FP)
MOVQ DX, r2+40(FP)
CALL runtime·exitsyscall(SB)
RET
Pour compléter le code ci-dessus, un registre inconnu tel que «AX» est un alias qui pointe vers la [partie 16 bits] comme le registre «rax». ](Https://en.wikibooks.org/wiki/X86_Assembly/X86_Architecture)
Lorsqu'un appel système est émis, le CPU ** saute ** du code en cours d'exécution au code dans le noyau. Comment une telle fonction pour sauter le code à exécuter au milieu est-elle réalisée sur le CPU? Pour comprendre cela, nous devons comprendre comment le CPU exécute le code (langage machine) en premier lieu.
L'exécution du langage machine sur la CPU est effectuée en répétant les étapes suivantes.
Les instructions sont des octets que l'UC peut interpréter comme une seule instruction et avoir une correspondance un à un avec une seule ligne de code d'assemblage. Par exemple, dans le code d'assemblage donné ci-dessus, la correspondance entre le langage machine et les instructions est la suivante.
# objdump -d -M intel ./hi.o | grep syscall -B 4
66c: 48 c7 c0 01 00 00 00 mov rax,0x1
673: 48 c7 c7 01 00 00 00 mov rdi,0x1
67a: 48 89 e6 mov rsi,rsp
67d: 48 c7 c2 20 00 00 00 mov rdx,0x20
684: 0f 05 syscall
De plus, le registre qui stocke l'adresse de l'instruction en cours d'exécution est appelé registre de compteur de programme ou registre de pointeur d'instruction, et lorsqu'une instruction est exécutée, elle est ajoutée et mise à jour à la valeur de l'adresse de l'instruction immédiatement après. Cela provoque l'exécution séquentielle du code.
C'est une fonction pour sauter le code exécuté par le CPU au milieu, mais cela émet une instruction (par exemple, jmp
) qui réécrit directement le registre du compteur de programme. Vous pouvez le faire en faisant. Ce que vous pouvez faire avec cela comprend non seulement le saut de code dans le noyau avec des appels système, mais plus généralement le branchement conditionnel, la boucle, les appels de fonction, etc.
Le flux général de traitement des appels système est le suivant.
Les étapes 1 et 3 entrent et reviennent du noyau, ce qui est réalisé avec des instructions dédiées pour x86-64, «syscall» et «sysretq». En d'autres termes, cette partie est implémentée en matériel, c'est-à-dire par un circuit logique dans le CPU conçu pour répondre aux spécifications de x86-64.
À l'étape 2, cela se fait dans le code appelé le gestionnaire d'appels système dans le noyau. Dans le gestionnaire d'appels système, une répartition est effectuée vers la mise en œuvre de chaque appel système en fonction du numéro d'appel système.
De manière un peu plus détaillée, le traitement des appels système peut être décomposé comme suit.
syscall
et lisez la valeur du registre contenant l'adresse du gestionnaire d'appels système dans le registre du compteur de programme.sysretq
restaure le code d'origine sur le processus utilisateur.Cela ressemble à ceci sur la figure.
Nous expliquerons chaque élément de (1) à (5) ci-dessous.
Lorsque le processeur exécute l'instruction syscall
, ce qui suit est à peu près fait.
FLAGS
avec la valeur du registre ʻIA32_FMASK MSR` et passez à un mode dans lequel le CPU peut exécuter le code du noyau.dans le registre du compteur de programme
RIP` et passez au gestionnaire d'appels système.** 1. ** Le registre FLAGS est un registre qui représente l'état actuel du CPU / le mode d'exécution du CPU, par exemple, le CPU est Protection Ring //ja.wikipedia.org/wiki/%E3%83%AA%E3%83%B3%E3%82%B0%E3%83%97%E3%83%AD%E3%83%86%E3% 82% AF% E3% 82% B7% E3% 83% A7% E3% 83% B3) Il montre où vous êtes. Puisque nous voulons restaurer l'état d'origine lors du retour de l'appel système au processus utilisateur, enregistrez la valeur actuelle dans le registre R11.
** 2. ** En masquant la valeur du registre FLAGS
avec la valeur du registre ʻIA32_FMASK MSR`, le mode CPU est commuté et passe au niveau de privilège 0, c'est-à-dire le mode dans lequel le code du noyau peut être exécuté.
Le niveau de privilège est représenté par 12 ~ 13 bits du registre FLAGS, mais pour faire ces «00» (niveau de privilège 0), c'est comme suit. Les opérations sont effectuées lors de l'appel de syscall
.
RFLAGS ← RFLAGS AND NOT(IA32_FMASK);
L'ensemble des valeurs dans le registre ʻIA32_FMASK`, qui agit comme un masque (notez qu'il prend "NON"), est fait dans le noyau avec le code suivant.
linux/arch/x86/kernel/cpu/common.c
/* Flags to clear on syscall */
wrmsrl(MSR_SYSCALL_MASK,
X86_EFLAGS_TF|X86_EFLAGS_DF|X86_EFLAGS_IF|
X86_EFLAGS_IOPL|X86_EFLAGS_AC|X86_EFLAGS_NT);
Le X86_EFLAGS_IOPL
ci-dessus est un masque pour le rôle de changer 12 \ ~ 13bit en 00
, mais vous pouvez voir qu'il est en fait défini comme une valeur de sorte que seulement 12 \ ~ 13bit devienne 1.
linux/arch/x86/include/uapi/asm/processor-flags.h
#define X86_EFLAGS_IOPL_BIT 12 /* I/O Privilege Level (2 bits) */
#define X86_EFLAGS_IOPL (_AC(3,UL) << X86_EFLAGS_IOPL_BIT)
** 3. ** Sauvegardez la valeur du registre du compteur de programme RIP
dans le registre RCX
. Si vous ne le faites pas, vous ne connaîtrez pas l'emplacement de l'instruction d'origine (= la valeur du compteur de programme) lors du retour du gestionnaire d'appels système au processus utilisateur.
** 4. ** Lit l'adresse du gestionnaire d'appels système défini dans ʻIA32_LSTAR MSR dans le registre du compteur de programme
RIP et saute au gestionnaire d'appels système. La façon dont l'adresse du gestionnaire est lue dans ʻIA32_LSTAR MSR
est présentée dans (5).
Pour un autre comportement détaillé du processeur lorsque syscall
est appelé, veuillez voir ici.
Un gestionnaire d'appels système est un code dans le noyau qui est exécuté après l'appel de l'instruction syscall
, dans lequel l'appel système est traité.
Ici, nous allons introduire la partie pré-traitement du gestionnaire d'appels système, la partie qui emballe la valeur passée au registre en tant qu'argument dans la structure et la transmet au traitement suivant.
Tout d'abord, le point d'entrée / d'entrée du gestionnaire d'appels système ressemble à ceci:
linux/arch/x86/entry/entry_64.S
ENTRY(entry_SYSCALL_64)
UNWIND_HINT_EMPTY
/*
* Interrupts are off on entry.
Divers processus sont exécutés dans cette ʻentry_SYSCALL_64`, et l'un d'eux construit une structure sur la pile dont le champ de valeur est passé en argument au registre comme indiqué ci-dessous. Je vais.
linux/arch/x86/entry/entry_64.S
/* Construct struct pt_regs on stack */
pushq $__USER_DS /* pt_regs->ss */
pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2) /* pt_regs->sp */
pushq %r11 /* pt_regs->flags */
pushq $__USER_CS /* pt_regs->cs */
pushq %rcx /* pt_regs->ip */
GLOBAL(entry_SYSCALL_64_after_hwframe)
pushq %rax /* pt_regs->orig_ax */
PUSH_AND_CLEAR_REGS rax=$-ENOSYS
Dans le code ci-dessus, les valeurs des registres «rcx» et «r11» (qui ont été utilisés pour la sauvegarde lors de l'appel de l'instruction «syscall») et du registre «rax» sont assignées à la structure «pt_regs» sur la pile. De plus, l'attribution de valeurs telles que rdi
et rsi
utilisées comme arguments de l'appel système à la structure est la dernière [macro PUSH_AND_CLEAR_REGS
](https://github.com/torvalds/linux/blob/ Il est défini dans bfeffd155283772bbe78c6a05dec7c0128ee500c / arch / x86 / entry / calling.h # L100-L145).
La structure créée ci-dessus et le numéro d'appel système sont transmis à la fonction do_syscall_64
qui traite l'appel système ci-dessous.
linux/arch/x86/entry/entry_64.S#L173-L175
movq %rax, %rdi
movq %rsp, %rsi
call do_syscall_64 /* returns with IRQs disabled */
Il faut noter que la méthode de passage de l'argument à do_syscall_64
se fait à l'aide d'un registre. En tant que règle lors du passage d'arguments à une fonction sur x86-64, dans l'ordre à partir du premier argument, rdi
, rsi
, rdx
,rcx Vous êtes censé utiliser les registres
, r8
et r9
.
Donc si vous voulez passer un argument à la fonction do_syscall_64
avec la signature suivante,
__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
Vous pouvez le faire en définissant les valeurs que vous voulez passer aux deux registres rdi
et rsi
.
--Dans movq% rax,% rdi
, transmettez le numéro d'appel système défini dans rax
au registre rdi
correspondant au premier argument.
rsp
qui pointe vers l'adresse de début de la pile (c'est-à-dire l'adresse de début de la structure chargée sur la pile) par rapport au registre rsi
correspondant au deuxième argument dans movq% rsp,% rsi
. donnerL'appel d'implémentation pour chaque appel système est effectué dans do_syscall_64
. Plus précisément, en spécifiant un élément avec le numéro d'appel système pour le tableau sys_call_table
qui contient la fonction qui implémente chaque appel système, la distribution est effectuée vers l'implémentation de l'appel système correspondant.
Aussi, l'implémentation de chaque appel système est définie dans la macro SYSCALL_DEFINE *
, que vous pouvez trouver comme repère.
Jetons un coup d'œil au code du noyau pertinent ci-dessous.
En tant qu'argument de la fonction do_syscall_64
, la structure constituée du numéro d'appel système dans le premier argument nr
et de la valeur du registre au moment de l'appel système est passée dans le deuxième argument regs
.
__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
struct thread_info *ti;
enter_from_user_mode();
local_irq_enable();
sys_call_table
implémente le traitement de chaque appel système tel que défini dans here C'est un tableau de fonctions, mais en spécifiant les éléments de ce tableau avec le numéro d'appel système "nr" et en passant la structure "regs", il est envoyé au traitement de chaque appel système.
if (likely(nr < NR_syscalls)) {
nr = array_index_nospec(nr, NR_syscalls);
regs->ax = sys_call_table[nr](regs);
}
regs-> ax = sys_call_table [nr](regs);
est la partie qui distribue. De plus, la valeur de retour de la fonction est définie dans le champ de la structure correspondant au registre «AX» (= registre rax), mais après que cette valeur quitte l'instruction «syscall», il s'agit en fait de «rax» comme valeur de retour. Il sera inscrit dans le registre.
C'est une fonction qui traite en fait les appels système, qui est un élément du tableau sys_call_table
. Par exemple, le traitement de write
est implémenté ci-dessous.
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
size_t, count)
{
return ksys_write(fd, buf, count);
}
En général, l'implémentation de chaque appel système est définie dans la macro SYSCALL_DEFINE *
. Vous pouvez donc l'utiliser comme guide pour trouver chaque implémentation.
Pour plus d'informations sur les macros sys_call_table
et SYSCALL_DEFINE *
, consultez cet article (https://lwn.net/Articles/604287/).
Le retour du gestionnaire d'appels système au code d'origine dans le processus utilisateur est effectué par l'instruction sysretq
. Le traitement effectué par l'instruction "sysetq" est presque l'inverse de "syscall".
--Pour le registre RFLAGS
, lisez la valeur du registre R11
(qui a sauvegardé la valeur d'origine de RFLAGS
) pour la ramener à la valeur d'origine, et le mode (niveau de privilège) de la CPU qui exécute le processus utilisateur. Retour à 3)
--Pour le registre du compteur de programme «RIP», lisez la valeur du registre «RCX» (qui a sauvegardé la valeur d'origine de «RIP») pour le renvoyer à la valeur d'origine, et l'instruction d'origine qui a appelé «syscall». Revenir au point de
Plusieurs autres processus sont en cours d'exécution, mais veuillez vous reporter à ici pour plus de détails.
Le code qui appelle sysretq
dans le noyau, mais le code qui arrive en dernier dans le gestionnaire d'appels système est ci-dessous.
linux/arch/x86/entry/entry_64.S
popq %rdi
popq %rsp
USERGS_SYSRET64
END(entry_SYSCALL_64)
Sysretq
est appelé par ʻUSERGS_SYSRET64` ici.
linux/arch/x86/include/asm/irqflags.h
#define USERGS_SYSRET64 \
swapgs; \
sysretq;
Lorsque l'instruction syscall
a été appelée, l'adresse du gestionnaire d'appels système a été lue à partir du registre ʻIA32_LSTAR MSR pour le registre de compteur de programme
RIP, et un saut vers le gestionnaire d'appels système a été effectué. Alors, comment le registre ʻIA32_LSTAR MSR
connaît-il l'adresse du gestionnaire d'appels système?
L'adresse du gestionnaire d'appels système est lue dans le registre ʻIA32_LSTAR MSR` lors de l'initialisation du CPU.
Ci-dessous, nous présenterons le code du noyau qui prend en charge ce processus d'initialisation.
L'initialisation du CPU se fait ci-dessous,
linux/arch/x86/kernel/cpu/common.c
/*
* cpu_init() initializes state that is per-CPU. Some data is already
* initialized (naturally) in the bootstrap process, such as the GDT
* and IDT. We reload them nevertheless, this function acts as a
* 'CPU state barrier', nothing should get across.
*/
#ifdef CONFIG_X86_64
void cpu_init(void)
{
Définissez l'adresse avec syscall_init ();
appelé dans ce Sera fait.
linux/arch/x86/kernel/cpu/common.c
void syscall_init(void)
{
wrmsr(MSR_STAR, 0, (__USER32_CS << 16) | __KERNEL_CS);
wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
Dans le code ci-dessus
wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
A écrit l'adresse du gestionnaire d'appels système dans le registre ʻIA32_LSTAR MSR`.
Les constantes MSR_LSTAR
et ʻentry_SYSCALL_64, mais la constante
MSR_LSTAR spécifie le registre ʻIA32_LSTAR MSR
comme registre cible d'écriture.
linux/arch/x86/include/asm/msr-index.h
#define MSR_LSTAR 0xc0000082 /* long mode SYSCALL target */
ʻEntry_SYSCALL_64` est la valeur prototypée dans ici Point d'entrée du gestionnaire d'appels](https://github.com/torvalds/linux/blob/cd6c84d8f0cdc911df435bb075ba22ce3c605b07/arch/x86/entry/entry_64.S#L145-L147).
Si vous regardez les appels système, vous rencontrerez souvent le concept des interruptions. Par exemple, dans le noyau, le commentaire dans le code ci-dessous dit «Les interruptions sont désactivées», mais qu'est-ce que «les interruptions» exactement? Qu'est-ce que cela a à voir avec les appels système?
linux/arch/x86/entry/entry_64.S
ENTRY(entry_SYSCALL_64)
UNWIND_HINT_EMPTY
/*
* Interrupts are off on entry.
* We do not frame this tiny irq-off block with TRACE_IRQS_OFF/ON,
* it is too small to ever cause noticeable irq latency.
*/
Premièrement, en ce qui concerne la relation entre les interruptions et les appels système, les appels système peuvent être considérés comme un type d'interruption. Lorsqu'une interruption se produit, le CPU interrompt le code en cours d'exécution (que ce soit dans le processus utilisateur ou dans le noyau), enregistre l'état d'exécution afin qu'il puisse être redémarré plus tard, puis du code spécifique dans le noyau (= inter). Aller à Rapto Handler). C'est à peu près la même chose que les appels système que nous avons vus jusqu'à présent. Il existe deux manières de générer une interruption: celle provoquée par le logiciel est appelée interruption logicielle et celle provoquée par le matériel est appelée interruption matérielle.
Exemple d'interruption de logiciel
--Appeler un appel système
Exemple d'interruption de processus
--Réponse à la saisie au clavier
Grâce au mécanisme d'interruption, il est possible d'implémenter une fonction qui "quand quelque chose se passe, le CPU saute à un code spécifique sans questions et réponses", ce qui permet, par exemple, d'entrer à partir du matériel. Vous serez en mesure de répondre rapidement.
Notez que x86-64 a des instructions dédiées (syscall
et sysretq
) pour les appels système, mais dans x86-32 et d'autres architectures d'il y a une génération, les appels système sont dans le mécanisme d'interruption. Il est réalisé en.
Par exemple, pour implémenter un appel système sur x86-32, spécifiez le vecteur 0x80
dans Instruction ʻint](https://www.felixcloutier.com/x86/intn:into:int3:int1) [ʻint 0x80 Cela a été fait en appelant l'instruction
.
Certains des gestionnaires d'appels système sont exécutés avec les interruptions désactivées, c'est-à-dire «irq» désactivées.
ʻIrq est une demande d'interruption, et lorsque la CPU la reçoit, une interruption est générée. En général, tout ou partie de ʻirq
peut être désactivé lorsque le gestionnaire d'interruption gère l'interruption. Cela évitera la situation où une interruption se produit pendant le traitement d'une interruption.
Si ʻirq est désactivé, il ne peut pas accepter les interruptions, donc il ne peut pas répondre à une saisie au clavier, par exemple. Donc, le code qui tourne avec ʻirq
off devrait être suffisamment court pour ne pas prendre longtemps à traiter.
Si vous voulez en savoir plus sur les interruptions, vous pouvez vous référer à cette page.
Présentation de matériaux et de mots clés pour approfondir les recherches sur les appels système et les noyaux. Il sert également d'introduction à la littérature qui a servi de référence lors de la rédaction de cet article.
J'ai fait référence à ici lorsque j'ai consulté les instructions. Pour une introduction au code d'assemblage x86-64, nous vous recommandons également Introduction à la création d'un compilateur C pour ceux qui veulent connaître les couches inférieures.
Développement du noyau Linux est très bien noté et est à l'intérieur du noyau. Probablement le meilleur livre de commentaires sur la mise en œuvre. Ce n'est pas facile en termes de contenu (du moins pour moi), mais je le recommande car le récit est parfois intéressant et l'explication est très polie. Même si vous n'êtes pas intéressé par le noyau, le chapitre 10 qui explique la technique de programmation parallèle dans le noyau peut être utile. Nous recommandons également cette note, qui résume le contenu de LKD.
En fait, j'ai lu correctement le code du noyau pour la première fois lors de la rédaction de cet article.
L'impression est, bien sûr, que je ne peux pas comprendre tout ce qui est écrit dans le code, mais ce n'est pas si difficile de suivre le flux général du traitement. Les commentaires sont soigneusement rédigés dans plusieurs parties.
Pour le lire, recherchez des mots-clés (appuyez sur la touche /
) sur github, ou ici. Je lisais en utilisant la fonction de saut de source de définition de (dernière / source).
Je suis aussi un amateur en ce qui concerne les noyaux, il y a donc peut-être une meilleure façon de le lire, mais ...
Je pense qu'il est normal de supprimer les processus, la mémoire virtuelle et le multitâche en tant que concepts de base importants liés au système d'exploitation. Les mots clés recommandés sont les suivants.
--Processus --Compteur de programme
--Mémoire virtuelle --Adresse virtuelle --Adresse physique --Unité de gestion de la mémoire --Pagination
--Multitâche
Recommended Posts