A propos de beaucoup de choses - dit le morse, - il est temps de parler.
L. Carroll (Citation du livre de B. Straustrap)
au lieu d'une introduction.
En ce qui concerne la structure interne du noyau Linux en général, ses différents sous-systèmes et appels système en particulier, il a déjà été écrit et réécrit dans l'ordre. Probablement, tout auteur qui se respecte devrait écrire à ce sujet au moins une fois, tout comme tout programmeur qui se respecte doit écrire son propre gestionnaire de fichiers :) Bien que je ne sois pas un rédacteur informatique professionnel, et en général, je prends mes notes exclusivement pour son l'utilité d'abord, pour ne pas oublier trop vite ce qui a été appris. Mais, si mes notes de voyage sont utiles à quelqu'un, bien sûr, je n'en serai que ravi. Eh bien, en général, vous ne pouvez pas gâcher la bouillie avec du beurre, donc peut-être même que je pourrai écrire ou décrire quelque chose que personne n'a pris la peine de mentionner.
Théorie. Qu'est-ce qu'un appel système ?
Lorsqu'ils expliquent aux non-initiés ce qu'est un logiciel (ou système d'exploitation), ils disent généralement ce qui suit : l'ordinateur lui-même est un morceau de fer, mais le logiciel est ce qui rend ce morceau de fer utile. Rugueux, bien sûr, mais en général, un peu vrai. Je dirais probablement la même chose à propos du système d'exploitation et des appels système. En fait, dans différents systèmes d'exploitation, les appels système peuvent être implémentés de différentes manières, le nombre de ces mêmes appels peut varier, mais d'une manière ou d'une autre, sous une forme ou une autre, tout système d'exploitation dispose d'un mécanisme d'appel système. Chaque jour, l'utilisateur travaille explicitement ou implicitement avec des fichiers. Bien sûr, il peut explicitement ouvrir le fichier pour l'éditer dans son MS Word ou Bloc-notes préféré, ou il peut simplement lancer un jouet, dont l'image exécutable, soit dit en passant, est également stockée dans un fichier, qui, à son tour, doit être ouvert et lu par les fichiers exécutables du chargeur. À son tour, le jouet peut également ouvrir et lire des dizaines de fichiers au cours de son travail. Naturellement, les fichiers peuvent non seulement être lus, mais aussi écrits (pas toujours, cependant, mais ici nous ne parlons pas de séparation des droits et d'accès discret :)). Le noyau est responsable de tout cela (dans les systèmes d'exploitation à micro-noyau, la situation peut différer, mais maintenant nous allons discrètement nous occuper de l'objet de notre discussion - Linux, nous allons donc ignorer ce point). En soi, la création d'un nouveau processus est également un service fourni par le noyau du système d'exploitation. Tout cela est merveilleux, tout comme le fait que les processeurs modernes fonctionnent à des fréquences de l'ordre du gigahertz et se composent de plusieurs millions de transistors, mais quelle est la prochaine étape ? Oui, et s'il n'y avait pas de mécanisme par lequel les applications utilisateur pourraient effectuer certaines choses assez banales et, en même temps, nécessaires ( en fait, ces actions triviales ne sont en aucun cas effectuées par l'application utilisateur, mais par le noyau du système d'exploitation - auteur.), alors le système d'exploitation n'était qu'une chose en soi - absolument inutile, ou, au contraire, chaque application utilisateur en elle-même devrait devenir un système d'exploitation afin de répondre indépendamment à tous ses besoins. Nice, n'est-ce pas?
Ainsi, nous en sommes arrivés à la définition d'un appel système en première approximation : un appel système est une sorte de service que le noyau de l'OS fournit à une application utilisateur à la demande de celle-ci. Un tel service peut être l'ouverture déjà mentionnée d'un fichier, sa création, sa lecture, son écriture, la création d'un nouveau processus, l'obtention d'un identifiant de processus (pid), le montage du système de fichiers, l'arrêt du système, enfin. Dans la vraie vie, il y a beaucoup plus d'appels système que ceux répertoriés ici.
À quoi ressemble un appel système et qu'est-ce que c'est ? Eh bien, d'après ce qui a été dit ci-dessus, il devient clair qu'un appel système est un sous-programme du noyau qui a la forme appropriée. Ceux qui ont eu une expérience de programmation Win9x/DOS se souviendront probablement de l'interruption int 0x21 avec toutes (ou au moins certaines) de ses nombreuses fonctions. Cependant, il y a une petite bizarrerie qui s'applique à tous les appels système Unix. Par convention, une fonction qui implémente un appel système peut prendre N arguments ou ne pas en prendre du tout, mais d'une manière ou d'une autre, la fonction doit retourner une valeur de type int. Toute valeur non négative est traitée comme une exécution réussie de la fonction d'appel système, et donc de l'appel système lui-même. Une valeur inférieure à zéro indique une erreur et contient également un code d'erreur (les codes d'erreur sont définis dans les en-têtes include/asm-generic/errno-base.h et include/asm-generic/errno.h). Sous Linux, jusqu'à récemment, l'interruption int 0x80 était la passerelle pour les appels système, tandis que sous Windows (jusqu'à XP Service Pack 2, si je ne me trompe pas), l'interruption 0x2e était une telle passerelle. Encore une fois, dans le noyau Linux, jusqu'à récemment, tous les appels système étaient gérés par la fonction system_call(). Cependant, comme il s'est avéré plus tard, le mécanisme classique de traitement des appels système via la passerelle 0x80 entraîne une baisse significative des performances des processeurs Intel Pentium 4. Par conséquent, le mécanisme classique a été remplacé par la méthode des objets partagés dynamiques virtuels (DSO - dynamique fichier objet partagé. Je ne peux pas garantir la traduction correcte, mais DSO est ce que les utilisateurs de Windows connaissent sous le nom de DLL (Dynamic Link Library) - VDSO. Quelle est la différence entre la nouvelle méthode et la classique ? Tout d'abord, abordons la méthode classique qui fonctionne à travers la porte 0x80.
Le mécanisme classique de gestion des appels système sous Linux.
Interruptions dans l'architecture x86.
Comme mentionné ci-dessus, la passerelle 0x80 (int 0x80) était auparavant utilisée pour traiter les demandes d'application des utilisateurs. Le fonctionnement d'un système basé sur l'architecture IA-32 est contrôlé par des interruptions (à proprement parler, cela s'applique à tous les systèmes basés sur x86 en général). Lorsqu'un événement se produit (un nouveau tick de minuterie, une activité sur un appareil, des erreurs - division par zéro, etc.), une interruption est générée. Une interruption est ainsi nommée car elle interrompt généralement le flux normal de code. Les interruptions sont généralement divisées en matériel et logiciel (interruptions matérielles et logicielles). Les interruptions matérielles sont des interruptions générées par le système et les périphériques. Lorsqu'un périphérique doit attirer l'attention du noyau du système d'exploitation, il (le périphérique) génère un signal sur sa ligne de demande d'interruption (IRQ - Interrupt ReQuest line). Cela conduit au fait qu'un signal correspondant est généré à certaines entrées du processeur, sur la base duquel le processeur décide d'interrompre l'exécution du flux d'instructions et de transférer le contrôle au gestionnaire d'interruption, qui découvre déjà ce qui s'est passé et ce qui doit être être terminé. Les interruptions matérielles sont de nature asynchrone. Cela signifie qu'une interruption peut se produire à tout moment. En plus des périphériques, le processeur lui-même peut générer des interruptions (ou, plus précisément, des exceptions matérielles - Hardware Exceptions - par exemple, la division par zéro déjà mentionnée). Ceci est fait afin d'informer le système d'exploitation de l'occurrence d'une situation anormale, de sorte que le système d'exploitation puisse prendre certaines mesures en réponse à l'occurrence d'une telle situation. Après traitement de l'interruption, le processeur revient à l'exécution du programme interrompu. L'interruption peut être déclenchée par l'application utilisateur. Une telle interruption est appelée une interruption logicielle. Les interruptions logicielles, contrairement aux interruptions matérielles, sont synchrones. Autrement dit, lorsqu'une interruption est appelée, le code qui l'a appelée est suspendu jusqu'à ce que l'interruption soit traitée. Lorsque le gestionnaire d'interruption se termine, il revient à l'adresse distante stockée plus tôt (lorsque l'interruption a été appelée) sur la pile, à l'instruction suivante après l'instruction d'appel d'interruption (int). Un gestionnaire d'interruptions est un morceau de code résident (en permanence en mémoire). En règle générale, il s'agit d'un petit programme. Bien que, si nous parlons du noyau Linux, le gestionnaire d'interruptions n'est pas toujours si petit. Le gestionnaire d'interruption est défini par un vecteur. Un vecteur n'est rien de plus que l'adresse (segment et décalage) du début du code qui doit gérer les interruptions à l'index donné. Travailler avec des interruptions diffère considérablement dans les modes de fonctionnement du processeur réel (mode réel) et protégé (mode protégé) (je vous rappelle que nous entendons ci-après les processeurs Intel et ceux compatibles). Dans le mode réel (non protégé) du processeur, les gestionnaires d'interruption sont définis par leurs vecteurs, qui sont toujours stockés au début de la mémoire. La sélection de l'adresse souhaitée dans la table des vecteurs s'effectue par index, qui est également le numéro d'interruption. En écrasant un vecteur avec un index spécifique, vous pouvez affecter votre propre gestionnaire d'interruption.
En mode protégé, les gestionnaires d'interruptions (passerelles, portes ou gates) ne sont plus définis à l'aide d'une table vectorielle. Au lieu de cette table, la table de porte ou, plus correctement, la table d'interruption - IDT (Interrupt Descriptors Table) est utilisée. Cette table est générée par le noyau et son adresse est stockée dans le registre du processeur idtr. Ce registre n'est pas directement accessible. Vous ne pouvez travailler avec lui qu'en suivant les instructions lidt/sidt. Le premier (lidt) charge dans le registre idtr la valeur spécifiée dans l'opérande, qui est l'adresse de base de la table des descripteurs d'interruption, le second (sidt) stocke l'adresse de la table dans idtr dans l'opérande spécifié. Tout comme les informations de segment sont extraites de la table de descripteurs par le sélecteur, le descripteur de segment servant l'interruption en mode protégé est également extrait. La protection de la mémoire est prise en charge par les processeurs Intel à partir du processeur i80286 (pas tout à fait sous la forme dans laquelle il est présenté maintenant, ne serait-ce que parce que 286 était un processeur 16 bits - donc Linux ne peut pas fonctionner sur ces processeurs) et i80386, et donc le processeur effectue lui-même toutes les sélections nécessaires et, par conséquent, nous n'approfondirons pas toutes les subtilités du mode protégé (à savoir, Linux fonctionne en mode protégé). Malheureusement, ni le temps ni les opportunités ne nous permettent de nous attarder longtemps sur le mécanisme de gestion des interruptions en mode protégé. Oui, ce n'était pas le but en écrivant cet article. Toutes les informations données ici concernant le fonctionnement de la famille de processeurs x86 sont assez superficielles et ne sont fournies que pour aider à comprendre un peu mieux le mécanisme des appels système du noyau. Certaines choses peuvent être apprises directement à partir du code du noyau, même si, pour bien comprendre ce qui se passe, il est toujours souhaitable de se familiariser avec les principes du mode protégé. La section de code qui initialise (mais ne définit pas !) l'IDT se trouve dans arch/i386/kernel/head.S : /* * setup_idt * * configure un idt avec 256 entrées pointant vers * ignore_int, portes d'interruption. Il ne charge pas réellement * idt - cela ne peut être fait qu'après que la pagination a été activée * et que le noyau a été déplacé vers PAGE_OFFSET. Les interruptions * sont activées ailleurs, lorsque nous pouvons être relativement * sûrs que tout va bien. * * Avertissement : %esi est actif sur cette fonction.*/ 1.setup_idt : 2.lea ignore_int,%edx 3.movl $(__KERNEL_CS<< 16),%eax 4. movw %dx,%ax /* selector = 0x0010 = cs */ 5. movw $0x8E00,%dx /* interrupt gate - dpl=0, present */ 6. lea idt_table,%edi 7. mov $256,%ecx 8.rp_sidt: 9. movl %eax,(%edi) 10. movl %edx,4(%edi) 11. addl $8,%edi 12. dec %ecx 13. jne rp_sidt 14..macro set_early_handler handler,trapno 15. lea \handler,%edx 16. movl $(__KERNEL_CS << 16),%eax 17. movw %dx,%ax 18. movw $0x8E00,%dx /* interrupt gate - dpl=0, present */ 19. lea idt_table,%edi 20. movl %eax,8*\trapno(%edi) 21. movl %edx,8*\trapno+4(%edi) 22..endm 23. set_early_handler handler=early_divide_err,trapno=0 24. set_early_handler handler=early_illegal_opcode,trapno=6 25. set_early_handler handler=early_protection_fault,trapno=13 26. set_early_handler handler=early_page_fault,trapno=14 28. ret Quelques notes sur le code : Le code ci-dessus est écrit dans une variante de l'assembleur AT&T, donc votre connaissance de l'assembleur dans sa notation Intel habituelle ne peut qu'être déroutante. La différence la plus fondamentale réside dans l'ordre des opérandes. Si l'ordre est défini pour la notation Intel - "accumulateur"< "источник", то для ассемблера AT&T порядок прямой. Регистры процессора, как правило, должны иметь префикс "%", непосредственные значения (константы) префиксируются символом доллара "$". Синтаксис AT&T традиционно используется в Un*x-системах.
Dans l'exemple ci-dessus, les lignes 2 à 4 définissent l'adresse du gestionnaire d'interruptions par défaut pour toutes les interruptions. Le gestionnaire par défaut est la fonction ignore_int, qui ne fait rien. La présence d'un tel stub est nécessaire pour le traitement correct de toutes les interruptions à ce stade, car il n'y en a tout simplement pas encore d'autres (bien que les traps soient installés un peu plus bas dans le code - pour les traps, voir Intel Architecture Manual Reference ou quelque chose de similaire, nous ne serons pas ici des pièges tactiles). La ligne 5 définit le type de vanne. A la ligne 6, nous chargeons l'adresse de notre table IDT dans le registre d'index. La table doit contenir 255 entrées de 8 octets chacune. Aux lignes 8 à 13, nous remplissons tout le tableau avec les mêmes valeurs définies précédemment dans les registres eax et edx - c'est-à-dire qu'il s'agit de la porte d'interruption faisant référence au gestionnaire ignore_int. Un peu plus bas, nous définissons une macro pour la pose de pièges - lignes 14-22. Aux lignes 23 à 26, en utilisant la macro ci-dessus, nous définissons des pièges pour les exceptions suivantes : early_divide_err - division par zéro (0), early_illegal_opcode - instruction de processeur inconnue (6), early_protection_fault - échec de protection de la mémoire (13), early_page_fault - traduction de page panne (14) . Entre parenthèses sont indiqués les nombres "d'interruptions" générées lorsque la situation anormale correspondante se produit. Avant de vérifier le type de processeur dans arch/i386/kernel/head.S, la table IDT est définie en appelant setup_idt : /* * démarre la configuration 32 bits du système. Nous devons refaire certaines des choses faites * en mode 16 bits pour les "vraies" opérations. */ 1. appelez setup_idt ... 2. appelez check_x87 3. lgdt early_gdt_descr 4. lidt idt_descr Après avoir trouvé le type de (co)processeur et effectué tout le travail préparatoire des lignes 3 et 4, nous chargeons les tables GDT et IDT, qui seront utilisées dans les toutes premières étapes du noyau.
Appels système et int 0x80.
Des interruptions, revenons aux appels système. Alors, que faut-il pour servir un processus qui demande un certain type de service ? Tout d'abord, vous devez passer de l'anneau 3 (niveau de privilège CPL=3) au niveau 0 le plus privilégié (Ring 0, CPL=0). le code du noyau se trouve dans le segment avec les privilèges les plus élevés. De plus, vous avez besoin du code du gestionnaire qui servira au processus. C'est à cela que sert la passerelle 0x80. Bien qu'il existe de nombreux appels système, ils utilisent tous un seul point d'entrée - int 0x80. Le gestionnaire lui-même est défini lors de l'appel de la fonction arch/i386/kernel/traps.c::trap_init() : void __init trap_init(void) ( ... set_system_gate(SYSCALL_VECTOR,&system_call); ... ) Nous sommes surtout intéressés par cette ligne dans trap_init(). Dans le même fichier ci-dessus, vous pouvez regarder le code de la fonction set_system_gate() : static void __init set_system_gate(unsigned int n, void *addr) ( _set_gate(n, DESCTYPE_TRAP | DESCTYPE_DPL3, addr, __KERNEL_CS); ) Ici, vous pouvez voir que la porte pour l'interruption 0x80 (à savoir, cette valeur est définie par la macro SYSCALL_VECTOR - vous pouvez prendre un mot pour cela :)) est définie comme un piège avec le niveau de privilège DPL=3 (Ring 3), c'est-à-dire cette interruption sera interceptée lorsqu'elle sera appelée depuis l'espace utilisateur. Le problème avec la transition de Ring 3 à Ring 0 donc. résolu. La fonction _set_gate() est définie dans le fichier d'en-tête include/asm-i386/desc.h. Pour ceux qui sont particulièrement curieux, voici le code, sans longues explications cependant : static inline void _set_gate(int gate, unsigned int type, void *addr, unsigned short seg) ( __u32 a, b; pack_gate(&a, &b, (unsigned long)addr, seg, type, 0); write_idt_entry(idt_table, gate , un B); ) Revenons à la fonction trap_init(). Il est appelé depuis la fonction start_kernel() dans init/main.c . Si vous regardez le code trap_init(), vous pouvez voir que cette fonction réécrit à nouveau certaines valeurs de table IDT - les gestionnaires qui ont été utilisés dans les premières étapes de l'initialisation du noyau (early_page_fault, early_divide_err, early_illegal_opcode, early_protection_fault) sont remplacés par ceux qui sera déjà utilisé dans le travail du noyau de processus. Donc, nous sommes presque arrivés au point et savons déjà que tous les appels système sont traités de la même manière - via la passerelle int 0x80. En tant que gestionnaire pour int 0x80, comme le montre à nouveau le morceau de code ci-dessus arch/i386/kernel/traps.c::trap_init(), la fonction system_call() est définie.
appel_système().
Le code de la fonction system_call() se trouve dans le fichier arch/i386/kernel/entry.S et ressemble à ceci : # stub du gestionnaire d'appels système ENTRY(system_call) RING0_INT_FRAME # ne peut pas se dérouler dans l'espace utilisateur pushl %eax # de toute façon enregistrer orig_eax CFI_ADJUST_CFA_OFFSET 4 SAVE_ALL GET_THREAD_INFO(%ebp) # traçage des appels système en opération / émulation /* Remarque, _TIF_SECCOMP est le bit numéro 8 , et donc il a besoin de testw et non de testb */ testw $(_TIF_SYSCALL_EMU|_TIF_SYSCALL_TRACE|_TIF_SECCOMP|_TIF_SYSCALL_AUDIT),TI_flags(%ebp) jnz syscall_trace_entry cmpl $(nr_syscalls), %eax jae syscall_badsys syscall_call: call *sys_call_table(,% 4) movl %eax,PT_EAX(%esp) # stocke la valeur de retour ... Le code n'est pas complet. Comme vous pouvez le voir, system_call() configure d'abord la pile pour qu'elle fonctionne dans Ring 0, enregistre la valeur qui lui est transmise via eax dans la pile, enregistre également tous les registres dans la pile, obtient des données sur le thread appelant et vérifie si la valeur qui lui est transmise, le numéro de l'appel système, est hors limites. en dehors de la table des appels système, puis enfin en utilisant la valeur transmise à eax comme argument, system_call() saute au gestionnaire de sortie système réel en fonction de à quelle entrée de table l'index dans eax fait référence. Rappelez-vous maintenant la bonne vieille table de vecteurs d'interruption du mode réel. Cela ne vous rappelle rien ? En réalité, bien sûr, tout est un peu plus compliqué. En particulier, l'appel système doit copier les résultats de la pile du noyau vers la pile de l'utilisateur, transmettre un code de retour et quelques autres choses. Dans le cas où l'argument spécifié dans eax ne fait pas référence à un appel système existant (la valeur est hors limites), le saut vers l'étiquette syscall_badsys se produit. Ici, la valeur -ENOSYS est poussée sur la pile à l'offset auquel la valeur de eax doit être située - l'appel système n'est pas implémenté. Ceci termine l'exécution de system_call().
La table des appels système se trouve dans le fichier arch/i386/kernel/syscall_table.S et a une forme assez simple : ENTRY(sys_call_table) .long sys_restart_syscall /* 0 - ancien appel système "setup()", utilisé pour redémarrer */ .long sys_exit .long sys_fork .long sys_read .long sys_write .long sys_open /* 5 */ .long sys_close .long sys_waitpid .long sys_creat ... En d'autres termes, la table entière n'est rien de plus qu'un tableau d'adresses de fonctions, organisées dans l'ordre des numéros d'appels système que ces fonctions servent. La table est un tableau ordinaire de mots machine doubles (ou mots 32 bits - comme vous voulez). Le code d'une partie des fonctions traitant les appels système se trouve dans la partie spécifique à la plate-forme - arch/i386/kernel/sys_i386.c, et la partie indépendante de la plate-forme - dans kernel/sys.c .
C'est le cas des appels système et de la porte 0x80.
Un nouveau mécanisme de gestion des appels système sous Linux. sysenter/sysexit.
Comme mentionné, il est rapidement devenu évident que l'utilisation de la méthode traditionnelle de traitement des appels système basée sur la porte 0x80 entraînait une perte de performances sur les processeurs Intel Pentium 4. Par conséquent, Linus Torvalds a implémenté un nouveau mécanisme dans le noyau basé sur les instructions sysenter / sysexit et conçu pour augmenter les performances du noyau sur les machines, équipées d'un processeur Pentium II et supérieur (c'est avec les Pentium II+ que les processeurs Intel supportent les instructions sysenter/sysexit mentionnées). Quelle est l'essence du nouveau mécanisme? Curieusement, mais l'essence est restée la même. L'exécution a changé. Selon la documentation d'Intel, l'instruction sysenter fait partie du mécanisme des "appels système rapides". En particulier, cette instruction est optimisée pour passer rapidement d'un niveau de privilège à un autre. Plus précisément, il accélère le passage à l'anneau 0 (Ring 0, CPL=0). Dans ce cas, le système d'exploitation doit préparer le processeur à utiliser l'instruction sysenter. Ce réglage est effectué une fois lors du chargement et de l'initialisation du noyau du système d'exploitation. Lorsque sysenter est appelé, il définit les registres du processeur en fonction des registres dépendant de la machine précédemment définis par le système d'exploitation. En particulier, le registre de segment et le registre de pointeur d'instruction - cs:eip, ainsi que le segment de pile et le pointeur de sommet de pile - ss, esp sont définis. Le passage à un nouveau segment de code et le passage s'effectuent de l'anneau 3 à 0.
L'instruction sysexit fait le contraire. Il effectue une transition rapide du niveau de privilège 0 au niveau de privilège 3 (CPL=3). Dans ce cas, le registre de segment de code est mis à 16 + la valeur du segment cs stocké dans le registre de processeur dépendant de la machine. Le registre eip est rempli avec le contenu du registre edx. La somme de 24 et la valeur de cs, entrée précédemment par le système d'exploitation dans le registre dépendant de la machine du processeur, est entrée dans ss lors de la préparation du contexte pour le fonctionnement de l'instruction sysenter. esp est rempli avec le contenu du registre ecx. Les valeurs requises pour que les instructions sysenter/sysexit fonctionnent sont stockées aux emplacements suivants :
- SYSENTER_CS_MSR 0x174 - segment de code où la valeur du segment contenant le code du gestionnaire d'appels système est entrée.
- SYSENTER_ESP_MSR 0x175 - pointeur vers le haut de la pile pour le gestionnaire d'appels système.
- SYSENTER_EIP_MSR 0x176 - pointeur vers le décalage dans le segment de code. Pointe vers le début du code du gestionnaire d'appels système.
Même si Linux n'utilise pas la commutation de contexte de tâche matérielle, le noyau est obligé d'allouer une entrée TSS pour chaque processeur installé sur le système. En effet, lorsque le processeur passe du mode utilisateur au mode noyau, il récupère l'adresse de la pile du noyau à partir de TSS. De plus, TSS est nécessaire pour contrôler l'accès aux ports d'E/S. Le TSS contient une carte des autorisations de port. Sur la base de cette carte, il devient possible de contrôler l'accès aux ports pour chaque processus à l'aide d'instructions d'entrée / sortie. Ici tss->x86_tss.esp1 pointe vers la pile du noyau. __KERNEL_CS pointe naturellement vers le segment de code du noyau. L'offset-eip est l'adresse de la fonction sysenter_entry().
La fonction sysenter_entry() est définie dans le fichier arch/i386/kernel/entry.S et ressemble à ceci : /* SYSENTER_RETURN pointe après l'instruction "sysenter" dans la page vsyscall. Voir vsyscall-sysentry.S, qui définit le symbole. */ # sysenter call handler stub ENTRY(sysenter_entry) CFI_STARTPROC simple CFI_SIGNAL_FRAME CFI_DEF_CFA esp, 0 CFI_REGISTER esp, ebp movl TSS_sysenter_esp0(%esp),%esp sysenter_past_esp: /* * Pas besoin de suivre cette section irqs on/off : le syscall * irqs désactivés et ici nous l'activons juste après l'entrée : */ ENABLE_INTERRUPTS(CLBR_NONE) pushl $(__USER_DS) CFI_ADJUST_CFA_OFFSET 4 /*CFI_REL_OFFSET ss, 0*/ pushl %ebp CFI_ADJUST_CFA_OFFSET 4 CFI_REL_OFFSET esp, 0 pushfl CFI_ADJUST_COFFFA_OFFSET 4 pushl 4 /*CFI_REL_OFFSET , 0*/ /* * Poussez current_thread_info()->sysenter_return vers la pile. * Un tout petit peu de correction de décalage est nécessaire - 4*4 signifie les 4 mots * poussés ci-dessus ; +8 correspond au paramètre esp0 de copy_thread. */ pushl (TI_sysenter_return-THREAD_SIZE+8+4*4)(%esp) CFI_ADJUST_CFA_OFFSET 4 CFI_REL_OFFSET eip, 0 /* * Charge le sixième argument potentiel de la pile utilisateur. * Attention à la sécurité .*/ cmpl $__PAGE_OFFSET-3,%ebp jae syscall_fault 1 : movl (%ebp),%ebp .section __ex_table,"a" .align 4 .long 1b,syscall_fault .previous pushl %eax CFI_ADJUST_CFA_OFFSET 4 SAVE_ALL GET_THREAD_INFO(%ebp ) /* Remarque, _TIF_SECCOMP est le bit numéro 8, et il a donc besoin de testw et non de testb */ testw $(_TIF_SYSCALL_EMU|_TIF_SYSCALL_TRACE|_TIF_SECCOMP|_TIF_SYSCALL_AUDIT),TI_flags(%ebp) jnz syscall_trace_entry cmpl $(nr_syscalls), %eax jae syscall_badsys call *sys_call_table(,%eax,4) movl %eax,PT_EAX(%esp) DISABLE_INTERRUPTS(CLBR_ANY) TRACE_IRQS_OFF movl TI_flags(%ebp), %ecx testw $_TIF_ALLWORK_MASK, %cx jne syscall_exit_work /* si quelque chose modifie les registres, il doit aussi désactiver sysexit */ movl PT_EIP(%esp), %edx movl PT_OLDESP(%esp), %ecx xorl % ebp,%ebp TRACE_IRQS_ON 1 : mov PT_FS(%esp), %fs ENABLE_INTERRUPTS_SYSEXIT CFI_ENDPROC .pushsection .fixup,"ax" 2 : movl $0,PT_FS(%esp) jmp 1b .section __ex_table,"a" .align 4 .long 1b,2b .popsection ENDPROC(entrée_système) Comme avec system_call() , le travail principal est effectué dans la ligne call *sys_call_table(,%eax,4). C'est là que le gestionnaire d'appels système spécifique est appelé. Donc, force est de constater que peu de choses ont fondamentalement changé. Le fait que le vecteur d'interruption soit maintenant entassé dans le matériel et le processeur nous aide à passer rapidement d'un niveau de privilège à un autre ne change que quelques détails de l'exécution avec le même contenu. Certes, les changements ne s'arrêtent pas là. Rappelez-vous comment l'histoire a commencé. Au tout début, j'ai déjà évoqué les objets partagés virtuels. Donc, si auparavant l'implémentation d'un appel système, disons, à partir de la bibliothèque système libc ressemblait à un appel d'interruption (malgré le fait que la bibliothèque a repris certaines fonctions pour réduire le nombre de changements de contexte), maintenant grâce à VDSO un appel système peut être fait presque directement, sans l'implication de la libc. Cela aurait pu être fait directement avant, encore une fois, comme une interruption. Mais maintenant, l'appel peut être demandé comme une fonction régulière exportée à partir d'une bibliothèque liée dynamiquement (DSO). Au démarrage, le noyau détermine quel mécanisme doit et peut être utilisé pour une plate-forme donnée. Selon les circonstances, le noyau définit le point d'entrée sur la fonction qui exécute l'appel système. Ensuite, la fonction est exportée vers l'espace utilisateur en tant que bibliothèque linux-gate.so.1. La bibliothèque linux-gate.so.1 n'existe pas physiquement sur le disque. Il est, pour ainsi dire, émulé par le noyau et n'existe que tant que le système fonctionne. Si vous effectuez un arrêt du système, montez le FS racine à partir d'un autre système, vous ne trouverez pas ce fichier sur le FS racine du système arrêté. En fait, vous ne pourrez pas le trouver même sur un système en cours d'exécution. Physiquement, ça n'existe tout simplement pas. C'est pourquoi linux-gate.so.1 est autre chose que VDSO - c'est-à-dire Objet virtuel partagé dynamiquement. Le noyau mappe la bibliothèque dynamique émulée de cette manière dans l'espace d'adressage de chaque processus. Vous pouvez facilement le vérifier en exécutant la commande suivante : [courriel protégé]:~$ cat /proc/self/maps 08048000-0804c000 r-xp 00000000 08:01 46 0 ... b7fdf000-b7fe1000 rw-p 00019000 08:01 2066 /lib/ld-2.5.so bffd2000-bffe8000 rw-p bffd2000 00:00 0 fffe000-fffff000 r-xp 00000000 00:00 0 Ici la toute dernière ligne est l'objet qui nous intéresse : fffe000-ffff000 r-xp 00000000 00:00 0 De l'exemple ci-dessus, on peut voir que l'objet occupe exactement une page en mémoire - 4096 octets, pratiquement à la périphérie de l'espace d'adressage. Faisons une autre expérience : [courriel protégé]:~$ ldd `quel chat` linux-gate.so.1 => (0xffffe000) libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7e87000) /lib/ld-linux .so.2 (0xb7fdf000) [courriel protégé]:~$ ldd `quel gcc` linux-gate.so.1 => (0xffffe000) libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7e3c000) /lib/ld-linux .so.2 (0xb7f94000) [courriel protégé]:~$ Ici, nous avons simplement pris deux applications. On peut voir que la bibliothèque est mappée à l'espace d'adressage du processus à la même adresse permanente - 0xffffe000. Essayons maintenant de voir ce qui est réellement stocké sur cette page mémoire...
Vous pouvez vider la page mémoire où le code partagé VDSO est stocké à l'aide du programme suivant : #include #include #include int main () ( char* vdso = 0xffffe000; char* buffer; FILE* f; buffer = malloc(4096); if (!buffer) exit(1); memcpy(buffer, vdso, 4096) ; if (!(f = fopen ("test.dump", "w+b"))) ( libre (tampon); exit (1); ) fwrite (tampon, 4096, 1, f); fclose (f) ; libre (tampon); renvoie 0; ) Strictement parlant, auparavant cela pouvait se faire plus simplement avec la commande jj if=/proc/self/mem of=test.dump bs=4096 skip=1048574 count=1, mais les noyaux depuis la version 2.6.22 ou peut-être même plus tôt ne mappent plus la mémoire du processus sur /proc/`pid`/mem. Ce fichier, apparemment conservé par compatibilité, ne contient plus d'informations.
Compilez et exécutez le programme ci-dessus. Essayons de désassembler le code résultant : F [courriel protégé]:~/tmp$ objdump --disassemble ./test.dump ./test.dump : format de fichier elf32-i386 Désassemblage de la section .text : ffffe400<__kernel_vsyscall>: ffffe400 : 51 push %ecx ffffe401 : 52 push %edx ffffe402 : 55 push %ebp ffffe403 : 89 e5 mov %esp,%ebp ffffe405 : 0f 34 sysenter ... ffffe40e : eb f3 jmp ffffe403<__kernel_vsyscall+0x3>ffffe410 : 5d pop %ebp ffffe411 : 5a pop %edx ffffe412 : 59 pop %ecx ffffe413 : c3 ret ... [courriel protégé]:~/tmp$ Ici, c'est notre passerelle pour les appels système, le tout en pleine vue. Le processus (ou la bibliothèque système libc), appelant la fonction __kernel_vsyscall, arrive à l'adresse 0хffffe400 (dans notre cas). De plus, __kernel_vsyscall enregistre le contenu des registres ecx, edx, ebp sur la pile du processus utilisateur.Nous avons déjà parlé du but des registres ecx et edx plus tôt, dans ebp il est utilisé plus tard pour restaurer la pile utilisateur. L'instruction sysenter est exécutée, "interceptant l'interruption" et, par conséquent, la transition suivante vers sysenter_entry (voir ci-dessus). L'instruction jmp à 0xffffe40e est insérée pour redémarrer l'appel système avec 6 arguments (voir http://lkml.org/lkml/2002/12/18/). Le code placé sur la page se trouve dans le fichier arch/i386/kernel/vsyscall-enter.S (ou arch/i386/kernel/vsyscall-int80.S pour le hook 0x80). Bien que j'ai trouvé que l'adresse de la fonction __kernel_vsyscall est constante, mais il y a une opinion que ce n'est pas le cas. Habituellement, la position du point d'entrée dans __kernel_vsyscall() peut être trouvée à partir du vecteur ELF-auxv en utilisant le paramètre AT_SYSINFO. Le vecteur ELF-auxv contient des informations transmises au processus via la pile au démarrage et contient diverses informations nécessaires lors de l'exécution du programme. Ce vecteur contient spécifiquement les variables d'environnement, les arguments, etc. du processus.
Voici un petit exemple en C montrant comment appeler directement la fonction __kernel_vsyscall : #comprendre
Résultats.
En fait, dans la pratique, il n'est pas toujours possible d'obtenir une augmentation spéciale des performances même avec le support FSCF (Fast System Call Facility) sur cette plate-forme. Le problème est que d'une manière ou d'une autre, le processus accède rarement directement au noyau. Et il y a de bonnes raisons à cela. L'utilisation de la bibliothèque libc permet de garantir la portabilité du programme, quelle que soit la version du noyau. Et c'est par la bibliothèque système standard que passent la plupart des appels système. Même si vous construisez et installez le dernier noyau compilé pour une plate-forme qui prend en charge FSCF, ce n'est pas une garantie de gains de performances. Le fait est que votre bibliothèque système libc.so utilisera toujours int 0x80 et ne pourra être traitée qu'en reconstruisant glibc. Que l'interface VDSO et __kernel_vsyscall soient généralement pris en charge dans la glibc, j'ai franchement du mal à répondre pour le moment.
Liens.
La page de Manu Garg, http://www.manugarg.com
Scatter/Rassembler les pensées par Johan Petersson, http://www.trilithium.com/johan/2005/08/linux-gate/
Bon vieux Comprendre le noyau Linux Où sans lui :)
Et bien sûr, les sources Linux (2.6.22)
Appels système
Jusqu'à présent, tous les programmes que nous avons créés devaient utiliser des mécanismes de noyau bien définis pour enregistrer les fichiers /proc et les pilotes de périphériques. C'est très bien si vous voulez faire quelque chose déjà fourni par les programmeurs du noyau, comme écrire un pilote de périphérique. Mais que se passe-t-il si vous voulez faire quelque chose de fantaisiste, modifier le comportement du système d'une manière ou d'une autre ?
C'est exactement là que la programmation du noyau devient dangereuse. En écrivant l'exemple ci-dessous, j'ai détruit l'appel système ouvert. Cela signifiait que je ne pouvais ouvrir aucun fichier, exécuter aucun programme et éteindre le système avec la commande shutdown. Je dois couper le courant pour l'arrêter. Heureusement, aucun fichier n'a été détruit. Pour vous assurer de ne perdre aucun fichier non plus, veuillez effectuer une synchronisation avant d'exécuter les commandes insmod et rmmod.
Oubliez les fichiers /proc et les fichiers de périphérique. Ce ne sont que de petits détails. Le véritable processus de communication du noyau utilisé par tous les processus est les appels système. Lorsqu'un processus demande un service au noyau (comme l'ouverture d'un fichier, le démarrage d'un nouveau processus ou la demande de plus de mémoire), ce mécanisme est utilisé. Si vous souhaitez modifier le comportement du noyau de manière intéressante, vous êtes au bon endroit. Au fait, si vous voulez voir quels appels système un programme a utilisés, exécutez : strace
En général, un processus ne peut pas accéder au noyau. Il ne peut pas accéder à la mémoire du noyau et ne peut pas appeler les fonctions du noyau. Le matériel du processeur rend obligatoire cet état de choses (il est appelé "mode protégé" pour une raison). Les appels système sont une exception à cette règle générale. Le processus remplit les registres avec les valeurs appropriées, puis appelle une instruction spéciale qui saute à un emplacement prédéfini dans le noyau (bien sûr, il est lu par les processus utilisateur, mais pas écrasé par eux.) Sous les processeurs Intel, cela se fait via l'interruption 0x80. Le matériel sait qu'une fois que vous accédez à cet emplacement, vous n'exécutez plus en mode utilisateur restreint. Au lieu de cela, vous exécutez en tant que noyau du système d'exploitation, et vous êtes donc autorisé à faire ce que vous voulez faire.
L'endroit du noyau auquel un processus peut accéder s'appelle system_call . La procédure qui y réside vérifie le numéro d'appel système, qui indique exactement au noyau ce que le processus veut. Ensuite, il consulte la table des appels système (sys_call_table) pour trouver l'adresse de la fonction noyau à appeler. Ensuite, la fonction souhaitée est appelée et après avoir renvoyé une valeur, plusieurs vérifications sont effectuées sur le système. Le résultat est ensuite renvoyé au processus (ou à un autre processus si le processus s'est terminé). Si vous voulez voir le code qui fait tout cela, c'est dans le fichier source arch/< architecture >/kernel/entry.S , après la ligne ENTRY(system_call).
Donc, si nous voulons changer le fonctionnement de certains appels système, la première chose que nous devons faire est d'écrire notre propre fonction pour faire la chose appropriée (généralement en ajoutant une partie de notre propre code puis en appelant la fonction d'origine), puis changez le pointeur à sys_call_table pour pointer vers notre fonction. Étant donné que nous pourrions être supprimés plus tard et que nous ne voulons pas laisser le système dans un état volatil, il est important que cleanup_module restaure la table à son état d'origine.
Le code source fourni ici est un exemple d'un tel module. Nous voulons "espionner" un utilisateur et envoyer un message via printk chaque fois que cet utilisateur ouvre un fichier. Nous remplaçons l'appel système d'ouverture de fichier par notre propre fonction appelée our_sys_open . Cette fonction vérifie l'uid (user id) du processus en cours, et s'il est égal à l'uid que nous espionnons, appelle printk pour afficher le nom du fichier à ouvrir. Il appelle ensuite la fonction d'ouverture d'origine avec les mêmes paramètres, ouvrant en fait le fichier.
La fonction init_module modifie l'emplacement approprié dans sys_call_table et stocke le pointeur d'origine dans une variable. La fonction cleanup_module utilise cette variable pour tout restaurer à la normale. Cette approche est dangereuse, en raison de la possibilité que deux modules modifient le même appel système. Imaginez que nous ayons deux modules, A et B. L'appel système ouvert du module A s'appellera A_open, et le même appel du module B s'appellera B_open. Maintenant que le syscall injecté par le noyau a été remplacé par A_open, qui appellera le sys_open d'origine lorsqu'il aura fait ce qu'il doit faire. Ensuite, B s'insérera dans le noyau et remplacera l'appel système par B_open, qui invoquera ce qu'il pense être l'appel système d'origine, mais qui est en fait A_open.
Maintenant, si B est supprimé en premier, tout ira bien : cela restaurera simplement l'appel système sur A_open qui appelle l'original. Cependant, si A est supprimé puis B est supprimé, le système s'effondrera. La suppression de A restaurera l'appel système à l'original, sys_open, coupant B de la boucle. Ensuite, lorsque B sera supprimé, il restaurera l'appel système à ce qu'il pense être l'original.En fait, l'appel sera dirigé vers A_open, qui n'est plus en mémoire. À première vue, il semble que nous pourrions résoudre ce problème particulier en vérifiant si l'appel système est égal à notre fonction ouverte et, si c'est le cas, en ne modifiant pas la valeur de cet appel (afin que B ne modifie pas l'appel système lorsqu'il est supprimé), mais cela appellerait toujours le pire problème. Lorsque A est supprimé, il voit que l'appel système a été modifié en B_open afin qu'il ne pointe plus vers A_open, il ne restaurera donc pas le pointeur vers sys_open avant d'être supprimé de la mémoire. Malheureusement, B_open essaiera toujours d'appeler A_open, qui n'est plus en mémoire, donc même sans supprimer B, le système plantera toujours.
Je vois deux façons d'éviter ce problème. Tout d'abord : restaurez l'appel à la valeur d'origine de sys_open. Malheureusement, sys_open ne fait pas partie de la table du noyau du système dans /proc/ksyms , nous ne pouvons donc pas y accéder. Une autre solution consiste à utiliser un compteur de liens pour empêcher le déchargement du module. C'est bon pour les modules réguliers, mais mauvais pour les modules "éducatifs".
/* syscall.c * * Exemple de "vol" d'appel système */ /* Copyright (C) 1998-99 par Ori Pomerantz */ /* Les fichiers d'en-tête nécessaires */ /* Standard dans les modules du noyau */ #include
Ce matériel est une modification de l'article du même nom de Vladimir Meshkov, publié dans la revue "System Administrator"
Ce matériel est une copie des articles de Vladimir Meshkov du magazine "System Administrator". Ces articles sont disponibles sur les liens ci-dessous. En outre, certains exemples de textes sources de programmes ont été modifiés - améliorés, finalisés. (L'exemple 4.2 a été fortement modifié, car un appel système légèrement différent devait être intercepté) URL : http://www.samag.ru/img/uploaded/p.pdf http://www.samag.ru/img/ uploadé/a3.pdf
Avoir des questions? Alors vous êtes ici : [courriel protégé]
- 2. Module de noyau chargeable
- 4. Exemples d'interception d'appels système basés sur LKM
- 4.1 Désactiver la création de répertoire
1. Vue générale de l'architecture Linux
La vue la plus générale nous permet de voir un modèle à deux niveaux du système. noyau<=>progs Au centre (à gauche) se trouve le noyau du système. Le noyau interagit directement avec le matériel informatique, isolant les programmes d'application des fonctionnalités architecturales. Le noyau dispose d'un ensemble de services fournis aux programmes d'application. Les services du noyau incluent les opérations d'E/S (ouverture, lecture, écriture et gestion des fichiers), la création et la gestion des processus, leur synchronisation et la communication interprocessus. Toutes les applications demandent les services du noyau via des appels système.Le deuxième niveau est composé d'applications ou de tâches, à la fois celles du système, qui déterminent la fonctionnalité du système, et celles de l'application, qui fournissent l'interface utilisateur Linux. Cependant, malgré l'hétérogénéité externe des applications, les schémas d'interaction avec le cœur sont les mêmes.
L'interaction avec le noyau se produit via l'interface d'appel système standard. L'interface d'appel système est un ensemble de services du noyau et définit le format des demandes de services. Un processus demande un service en effectuant un appel système à une procédure spécifique du noyau, qui ressemble à un appel de fonction de bibliothèque ordinaire. Le noyau exécute la demande au nom du processus et renvoie les données requises au processus.
Dans l'exemple ci-dessus, le programme ouvre un fichier, en lit les données et ferme le fichier. Dans ce cas, l'opération d'ouverture (open), de lecture (read) et de fermeture (close) d'un fichier est effectuée par le noyau à la demande de la tâche, et les opérations open (2), read (2) et close (2 ) les fonctions sont des appels système.
/* Source 1.0 */ #include
- au registre EAX - le numéro de l'appel système. Ainsi, pour notre cas, le numéro d'appel système est le 5 (voir __NR_open).
- au registre EBX - le premier paramètre de la fonction (pour open() c'est un pointeur vers une chaîne contenant le nom du fichier en cours d'ouverture.
- au registre ECX - le deuxième paramètre (droits d'accès au fichier)
Pour nous assurer que nous sommes sur la bonne voie, examinons le code de la fonction open() dans la bibliothèque système libc :
# gdb -q /lib/libc.so.6 (gdb) disas open Vidage du code assembleur pour la fonction open : 0x000c8080
Revenons maintenant au mécanisme d'appel système. Ainsi, le noyau appelle le gestionnaire d'interruptions 0x80 - la fonction system_call. System_call pousse des copies des registres contenant les paramètres d'appel sur la pile à l'aide de la macro SAVE_ALL et appelle la fonction système souhaitée avec la commande call. La table des pointeurs vers les fonctions du noyau qui implémentent les appels système est située dans le tableau sys_call_table (voir fichier arch/i386/kernel/entry.S). Le numéro d'appel système qui réside dans le registre EAX est l'index dans ce tableau. Ainsi, si EAX contient la valeur 5, la fonction noyau sys_open() sera appelée. Pourquoi la macro SAVE_ALL est-elle nécessaire ? L'explication ici est très simple. Puisque presque toutes les fonctions système du noyau sont écrites en C, elles recherchent leurs paramètres sur la pile. Et les paramètres sont poussés sur la pile avec SAVE_ALL ! La valeur de retour de l'appel système est stockée dans le registre EAX.
Voyons maintenant comment intercepter l'appel système. Le mécanisme des modules de noyau chargeables nous y aidera.
2. Module de noyau chargeable
Loadable Kernel Module (LKM - Loadable Kernel Module) est un code qui s'exécute dans l'espace du noyau. La principale caractéristique de LKM est la capacité de charger et de décharger dynamiquement sans avoir besoin de redémarrer l'ensemble du système ou de recompiler le noyau.Chaque LKM se compose de deux fonctions principales (minimum) :
- fonction d'initialisation du module. Appelé lorsque LKM est chargé en mémoire : int init_module(void) ( ... )
- fonction de déchargement du module : void cleanup_module(void) ( ... )
3. Algorithme d'interception d'un appel système basé sur LKM
Pour implémenter un module qui intercepte un appel système, il est nécessaire de définir un algorithme d'interception. L'algorithme est le suivant :- enregistrer un pointeur vers l'appel d'origine (original) afin qu'il puisse être restauré
- créer une fonction qui implémente le nouvel appel système
- remplacer les appels dans la table des appels système sys_call_table, c'est-à-dire définir le pointeur correspondant sur un nouvel appel système
- en fin de travail (lorsque le module est déchargé), restaurer l'appel système d'origine à l'aide du pointeur précédemment enregistré
4. Exemples d'interception d'appels système basés sur LKM
4.1 Désactiver la création de répertoire
Lorsqu'un répertoire est créé, la fonction noyau sys_mkdir est appelée. Le paramètre est une chaîne contenant le nom du répertoire à créer. Considérez le code qui intercepte l'appel système correspondant. /* Source 4.1 */ #include4.2 Masquer une entrée de fichier dans un répertoire
Déterminons quel appel système est responsable de la lecture du contenu du répertoire. Pour ce faire, nous allons écrire un autre fragment de test qui lit le répertoire courant : /* Source 4.2.1 */ #include- d_reclen - taille d'enregistrement
- d_name - nom du fichier
5. Méthode d'accès direct à l'espace d'adressage du noyau /dev/kmem
Considérons d'abord théoriquement comment l'interception est effectuée par la méthode d'accès direct à l'espace d'adressage du noyau, puis procédons à la mise en œuvre pratique.L'accès direct à l'espace d'adressage du noyau est fourni par le fichier de périphérique /dev/kmem. Ce fichier affiche tout l'espace d'adressage virtuel disponible, y compris la partition d'échange (swap-area). Pour travailler avec un fichier kmem, des fonctions système standard sont utilisées - open(), read(), write(). En ouvrant /dev/kmem de manière standard, nous pouvons faire référence à n'importe quelle adresse du système en la définissant comme un décalage dans ce fichier. Cette méthode a été développée par Silvio Cesare.
Les fonctions système sont accessibles en chargeant les paramètres de fonction dans les registres du processeur, puis en appelant l'interruption logicielle 0x80. Le gestionnaire de cette interruption, la fonction system_call, pousse les paramètres d'appel sur la pile, récupère l'adresse de la fonction système appelée à partir de la table sys_call_table et transfère le contrôle à cette adresse.
Avec un accès complet à l'espace d'adressage du noyau, nous pouvons obtenir tout le contenu de la table des appels système, c'est-à-dire adresses de toutes les fonctions du système. En changeant l'adresse de tout appel système, nous l'interceptons ainsi. Mais pour cela, vous devez connaître l'adresse de la table, ou, en d'autres termes, le décalage dans le fichier /dev/kmem où se trouve cette table.
Pour déterminer l'adresse de la table sys_call_table, vous devez d'abord calculer l'adresse de la fonction system_call. Puisque cette fonction est un gestionnaire d'interruptions, regardons comment les interruptions sont gérées en mode protégé.
En mode réel, lors de l'enregistrement d'une interruption, le processeur accède à la table des vecteurs d'interruption, qui se trouve toujours au tout début de la mémoire et contient les adresses à deux mots des gestionnaires d'interruption. En mode protégé, l'analogue de la table des vecteurs d'interruption est la table des descripteurs d'interruption (IDT), située dans le système d'exploitation en mode protégé. Pour que le processeur accède à cette table, son adresse doit être chargée dans le registre de la table des descripteurs d'interruption (IDTR). La table IDT contient des descripteurs de gestionnaire d'interruption, qui, en particulier, incluent leurs adresses. Ces descripteurs sont appelés passerelles (gates). Le processeur, ayant enregistré une interruption, récupère la passerelle de l'IDT par son numéro, détermine l'adresse du gestionnaire et lui transfère le contrôle.
Pour calculer l'adresse de la fonction system_call à partir de la table IDT, il est nécessaire d'extraire la porte d'interruption int $0x80, et de celle-ci l'adresse du gestionnaire correspondant, c'est-à-dire adresse de la fonction system_call. Dans la fonction system_call, la table system_call_table est accessible par la commande call<адрес_таблицы>(,%eax,4). Après avoir trouvé l'opcode (signature) de cette commande dans le fichier /dev/kmem, nous trouverons également l'adresse de la table des appels système.
Pour déterminer l'opcode, utilisons le débogueur et démontons la fonction system_call :
# gdb -q /usr/src/linux/vmlinux (gdb) disas system_call Vidage du code assembleur pour la fonction system_call : 0xc0194cbc
Considérez le pseudocode qui effectue l'opération d'interception :
readaddr(old_syscall, scr + SYS_CALL*4, 4); writeaddr(new_syscall, scr + SYS_CALL*4, 4); La fonction readaddr lit l'adresse d'appel système à partir de la table des appels système et la stocke dans la variable old_syscall. Chaque entrée de la table sys_call_table prend 4 octets. L'adresse requise est située à l'offset sct + SYS_CALL*4 dans le fichier /dev/kmem (ici sct est l'adresse de la table sys_call_table, SYS_CALL est le numéro de série de l'appel système). La fonction writeaddr écrase l'adresse de l'appel système SYS_CALL avec l'adresse de la fonction new_syscall, et tous les appels à l'appel système SYS_CALL seront traités par cette fonction.
Il semble que tout soit simple et que l'objectif soit atteint. Cependant, rappelons-nous que nous travaillons dans l'espace d'adressage de l'utilisateur. Si nous plaçons une nouvelle fonction système dans cet espace d'adressage, alors lorsque nous appellerons cette fonction, nous obtiendrons un beau message d'erreur. D'où la conclusion - un nouvel appel système doit être placé dans l'espace d'adressage du noyau. Pour ce faire, vous devez : obtenir un bloc de mémoire dans l'espace noyau, placer un nouvel appel système dans ce bloc.
Vous pouvez allouer de la mémoire dans l'espace du noyau à l'aide de la fonction kmalloc. Mais vous ne pouvez pas appeler directement une fonction du noyau à partir de l'espace d'adressage de l'utilisateur, nous utilisons donc l'algorithme suivant :
- connaissant l'adresse de la table sys_call_table, nous obtenons l'adresse d'un appel système (par exemple, sys_mkdir)
- nous définissons une fonction qui effectue un appel à la fonction kmalloc. Cette fonction renvoie un pointeur vers un bloc de mémoire dans l'espace d'adressage du noyau. Appelons cette fonction get_kmalloc
- stocker les N premiers octets de l'appel système sys_mkdir, où N est la taille de la fonction get_kmalloc
- écraser les N premiers octets de l'appel sys_mkdir avec la fonction get_kmalloc
- nous exécutons l'appel à l'appel système sys_mkdir, lançant ainsi la fonction get_kmalloc pour exécution
- restaurer les N premiers octets de l'appel système sys_mkdir
Mais pour implémenter cet algorithme, nous avons besoin de l'adresse de la fonction kmalloc. Vous pouvez le trouver de plusieurs manières. Le plus simple est de lire cette adresse dans le fichier System.map ou de la déterminer à l'aide du débogueur gdb (print &kmalloc). Si le noyau a la prise en charge des modules activée, l'adresse kmalloc peut être déterminée à l'aide de la fonction get_kernel_syms(). Cette option sera discutée plus loin. S'il n'y a pas de support pour les modules du noyau, alors l'adresse de la fonction kmalloc devra être recherchée par l'opcode de la commande d'appel kmalloc - similaire à ce qui a été fait pour la table sys_call_table.
La fonction kmalloc prend deux paramètres : la taille de la mémoire demandée et le spécificateur GFP. Pour trouver l'opcode, nous allons utiliser le débogueur et désassembler toute fonction du noyau contenant un appel à la fonction kmalloc.
# gdb -q /usr/src/linux/vmlinux (gdb) disas inter_module_register Vidage du code assembleur pour la fonction inter_module_register : 0xc01a57b4
Ceci conclut les calculs théoriques et, en utilisant la technique ci-dessus, nous intercepterons l'appel système sys_mkdir.
6. Un exemple d'interception avec /dev/kmem
/* source 6.0 */ #includeFin de papier/EOP
Les performances du code de toutes les sections ont été testées sur le noyau 2.4.22. Lors de la préparation du rapport, des matériaux du site ont été utilisésLe plus souvent, le code de l'appel système numéroté __NR_xxx, défini dans /usr/include/asm/unistd.h, se trouve dans le code source du noyau Linux dans la fonction sys_xxx(). (La table des appels pour i386 se trouve dans /usr/src/linux/arch/i386/kernel/entry.S.) Il existe de nombreuses exceptions à cette règle, principalement dues au fait que la plupart des anciens appels système sont remplacés par de nouveaux, et sans aucun système. Sur les plates-formes avec émulation de système d'exploitation propriétaire, telles que parisc, sparc, sparc64 et alpha, il existe de nombreux appels système supplémentaires ; mips64 dispose également d'un ensemble complet d'appels système 32 bits.
Au fil du temps, des modifications ont été apportées à l'interface de certains appels système selon les besoins. L'une des raisons de ces changements était la nécessité d'augmenter la taille des structures ou des valeurs scalaires transmises à un appel système. En raison de ces changements, sur certaines architectures (notamment sur l'ancien i386 32 bits), divers groupes d'appels système similaires sont apparus (par exemple, tronquer(2) et tronquer64(2)), qui effectuent les mêmes tâches mais diffèrent par la taille de leurs arguments. (Comme indiqué, les applications ne sont pas affectées : les wrappers de la glibc font un certain travail pour déclencher l'appel système correct, ce qui garantit la compatibilité ABI pour les anciens binaires.) Exemples d'appels système qui ont plusieurs versions :
* Actuellement, il existe trois versions différentes statistique(2): sys_stat() (endroit __NR_oldstat), sys_newstat() (endroit __NR_stat) et sys_stat64() (endroit __NR_stat64), ce dernier est actuellement utilisé. Une situation similaire avec lstat(2) et fstat(2). * De même défini __NR_oldolduname, __NR_olduname et __NR_uname pour les appels sys_olduname(), sys_uname() et sys_newuname(). * Linux 2.0 a une nouvelle version vm86(2), les nouvelles et anciennes versions des procédures nucléaires sont appelées sys_vm86old() et sys_vm86(). * Linux 2.4 a une nouvelle version getrlimit(2) les nouvelles et anciennes versions des procédures nucléaires sont appelées sys_old_getrlimit() (endroit __NR_getrlimit) et sys_getrlimit() (endroit __NR_ugetrlimit). * Dans Linux 2.4, la taille du champ ID utilisateur et groupe a été augmentée de 16 à 32 bits. Plusieurs appels système ont été ajoutés pour prendre en charge ce changement (par exemple, chown32(2), getuid32(2), getgroups32(2), setresuid32(2)), désapprouvant les appels antérieurs avec les mêmes noms mais sans le suffixe "32". * Linux 2.4 a ajouté la prise en charge de l'accès aux fichiers volumineux (dont les tailles et les décalages ne rentrent pas dans 32 bits) dans les applications sur les architectures 32 bits. Cela a nécessité des modifications des appels système qui fonctionnent avec les tailles et les décalages de fichiers. Les appels système suivants ont été ajoutés : fcntl64(2), getdents64(2), stat64(2), statfs64(2), tronquer64(2) et leurs homologues qui gèrent les descripteurs de fichiers ou les liens symboliques. Ces appels système suppriment les anciens appels système qui, à l'exception des appels "stat", portent le même nom mais n'ont pas le suffixe "64".
Sur les plates-formes plus récentes qui n'ont qu'un accès aux fichiers 64 bits et un UID/GID 32 bits (par exemple, alpha, ia64, s390x, x86-64), il n'y a qu'une seule version des appels système pour l'UID/GID et l'accès aux fichiers. Sur les plates-formes (généralement des plates-formes 32 bits) qui ont des appels *64 et *32, les autres versions sont obsolètes.
* Défis rt_sig* ajouté au noyau 2.2 pour prendre en charge des signaux en temps réel supplémentaires (voir signal(7)). Ces appels système rendent obsolètes les anciens appels système portant le même nom mais sans le préfixe "rt_". * Dans les appels système sélectionner(2) et mmap(2) cinq arguments ou plus sont utilisés, ce qui a causé des problèmes pour déterminer comment les arguments ont été transmis sur le i386. Par conséquent, alors que sur d'autres architectures, les appels sys_select() et sys_mmap() correspondre __NR_select et __NR_mmap, sur i386 ils correspondent à old_select() et old_mmap() (procédures utilisant un pointeur sur un bloc d'arguments). Actuellement, il n'y a plus de problème avec le passage de plus de cinq arguments et il y a __NR__nouveauselect, ce qui correspond exactement sys_select(), et la même situation avec __NR_mmap2.