Modification du binaire pendant l'exécution

10

Je rencontre souvent la situation lors du développement, où j'exécute un fichier binaire, disons a.outen arrière-plan car cela fait un travail long. Pendant ce temps, j'apporte des modifications au code C qui a produit a.outet compilé à a.outnouveau. Jusqu'à présent, je n'ai eu aucun problème avec cela. Le processus en cours d'exécution se a.outpoursuit normalement, ne se bloque jamais et exécute toujours l'ancien code à partir duquel il a démarré à l'origine.

Cependant, disons que a.outc'était un énorme fichier, peut-être comparable à la taille de la RAM. Que se passerait-il dans ce cas? Et dites-le lié à un fichier d'objet partagé, libblas.soet si je le modifiais libblas.sopendant l'exécution? Ce qui se passerait?

Ma question principale est - le système d'exploitation garantit-il que lorsque j'exécute a.out, le code d'origine fonctionnera toujours normalement, selon le binaire d' origine , quelle que soit la taille du binaire ou des .sofichiers auxquels il est lié, même lorsque ceux-ci .oet les .sofichiers sont modifiés pendant Durée?

Je sais qu'il y a ces questions qui abordent des problèmes similaires: /programming/8506865/when-a-binary-file-runs-does-it-copy-its-entire-binary-data-into-memory -en-une fois Que se passe - t-il si vous modifiez un script pendant l'exécution? Comment est-il possible de faire une mise à jour en direct pendant l'exécution d'un programme?

Ce qui m'a aidé à comprendre un peu plus à ce sujet mais je ne pense pas qu'ils demandent exactement ce que je veux, ce qui est une règle générale pour les conséquences de la modification d'un binaire pendant l'exécution

Texasflood
la source
Pour moi, les questions que vous avez liées (en particulier celle de Stack Overflow) fournissent déjà une aide significative pour comprendre ces conséquences (ou leur absence). Étant donné que le noyau charge votre programme dans des régions / segments de texte en mémoire , il ne devrait pas être affecté par les modifications apportées via le sous-système de fichiers.
John WH Smith
@JohnWHSmith Sur Stackoverflow, la première réponse dit if they are read-only copies of something already on disc (like an executable, or a shared object file), they just get de-allocated and are reloaded from their source, donc j'ai l'impression que si votre binaire est énorme, alors si une partie de votre binaire sort de la RAM, mais est à nouveau nécessaire, il est "rechargé à partir de la source" - donc tout changement dans le .(s)ofichier sera reflété pendant l'exécution. Mais bien sûr, j'ai peut-être mal compris - c'est pourquoi je pose cette question plus spécifique
texasflood
@JohnWHSmith De plus, la deuxième réponse dit que No, it only loads the necessary pages into memory. This is demand paging.j'avais en fait l'impression que ce que j'avais demandé ne pouvait pas être garanti.
texasflood

Réponses:

11

Alors que la question Stack Overflow semblait être suffisante au début, je comprends, d'après vos commentaires, pourquoi vous avez peut-être encore un doute à ce sujet. Pour moi, c'est exactement le genre de situation critique impliquée lorsque les deux sous-systèmes UNIX (processus et fichiers) communiquent.

Comme vous le savez peut-être, les systèmes UNIX sont généralement divisés en deux sous-systèmes: le sous-système de fichiers et le sous-système de processus. Maintenant, sauf indication contraire via un appel système, le noyau ne devrait pas avoir ces deux sous-systèmes interagissent l'un avec l'autre. Il existe cependant une exception: le chargement d'un fichier exécutable dans les régions de texte d' un processus . Bien sûr, on peut faire valoir que cette opération est également déclenchée par un appel système ( execve), mais il est généralement connu que c'est le seul cas où le sous-système de processus fait une demande implicite au sous-système de fichiers.

Parce que le sous-système de processus n'a naturellement aucun moyen de gérer les fichiers (sinon il ne servirait à rien de diviser le tout en deux), il doit utiliser tout ce que le sous-système de fichiers fournit pour accéder aux fichiers. Cela signifie également que le sous-système de processus est soumis à toute mesure prise par le sous-système de fichiers concernant l'édition / la suppression de fichiers. Sur ce point, je recommanderais de lire la réponse de Gilles à cette question U&L . Le reste de ma réponse est basé sur celle plus générale de Gilles.

La première chose à noter est qu'en interne, les fichiers ne sont accessibles que via des inodes . Si le noyau reçoit un chemin, sa première étape sera de le traduire en un inode à utiliser pour toutes les autres opérations. Lorsqu'un processus charge un exécutable en mémoire, il le fait via son inode, qui a été fourni par le sous-système de fichiers après la traduction d'un chemin. Les inodes peuvent être associés à plusieurs chemins (liens) et les programmes ne peuvent supprimer que des liens. Afin de supprimer un fichier et son inode, l'espace utilisateur doit supprimer tous les liens existants vers cet inode et s'assurer qu'il est complètement inutilisé. Lorsque ces conditions sont remplies, le noyau supprimera automatiquement le fichier du disque.

Si vous regardez la partie remplaçable des exécutables de la réponse de Gilles, vous verrez que selon la façon dont vous éditez / supprimez le fichier, le noyau réagira / s'adaptera différemment, toujours via un mécanisme implémenté dans le sous-système de fichiers.

  • Si vous essayez la première stratégie ( ouvrir / tronquer à zéro / écrire ou ouvrir / écrire / tronquer à une nouvelle taille ), vous verrez que le noyau ne prendra pas la peine de traiter votre demande. Vous obtiendrez une erreur 26: fichier texte occupé ( ETXTBSY). Aucune conséquence que ce soit.
  • Si vous essayez la stratégie deux, la première étape consiste à supprimer votre exécutable. Cependant, étant donné qu'il est utilisé par un processus, le sous-système de fichiers démarrera et empêchera le fichier (et son inode) d'être réellement supprimé du disque. À partir de là, le seul moyen d'accéder au contenu de l'ancien fichier est de le faire via son inode, ce que fait le sous-système de processus chaque fois qu'il a besoin de charger de nouvelles données dans des sections de texte (en interne, il est inutile d'utiliser des chemins, sauf lors de leur traduction en inodes). Même si vous avez dissociéle fichier (supprimé tous ses chemins), le processus peut toujours l'utiliser comme si vous n'aviez rien fait. La création d'un nouveau fichier avec l'ancien chemin ne change rien: le nouveau fichier recevra un inode complètement nouveau, dont le processus en cours n'a aucune connaissance.

Les stratégies 2 et 3 sont également sans danger pour les exécutables: bien que l'exécution d'exécutables (et de bibliothèques chargées dynamiquement) ne soit pas des fichiers ouverts dans le sens d'avoir un descripteur de fichier, ils se comportent de manière très similaire. Tant qu'un programme exécute le code, le fichier reste sur le disque même sans entrée de répertoire.

  • La troisième stratégie est assez similaire car l' mvopération est atomique. Cela nécessitera probablement l'utilisation de l' renameappel système, et comme les processus ne peuvent pas être interrompus en mode noyau, rien ne peut interférer avec cette opération tant qu'elle n'est pas terminée (avec succès ou non). Encore une fois, il n'y a pas d'altération de l'inode de l'ancien fichier: un nouveau est créé et les processus déjà en cours n'en auront aucune connaissance, même s'il est associé à l'un des liens de l'ancien inode.

Avec la stratégie 3, l'étape de déplacement du nouveau fichier vers le nom existant supprime l'entrée de répertoire menant à l'ancien contenu et crée une entrée de répertoire menant au nouveau contenu. Cela se fait en une seule opération atomique, donc cette stratégie a un avantage majeur: si un processus ouvre le fichier à tout moment, il verra l'ancien contenu ou le nouveau contenu - il n'y a aucun risque d'obtenir du contenu mixte ou du fichier non existant.

Recompilation d'un fichier : lorsque vous utilisez gcc(et le comportement est probablement similaire pour de nombreux autres compilateurs), vous utilisez la stratégie 2. Vous pouvez le voir en exécutant l'un stracedes processus de votre compilateur:

stat("a.out", {st_mode=S_IFREG|0750, st_size=8511, ...}) = 0
unlink("a.out") = 0
open("a.out", O_RDWR|O_CREAT|O_TRUNC, 0666) = 3
chmod("a.out", 0750) = 0
  • Le compilateur détecte que le fichier existe déjà via les appels système statet lstat.
  • Le fichier n'est pas lié . Ici, bien qu'il ne soit plus accessible via le nom a.out, son inode et son contenu restent sur le disque, tant qu'ils sont utilisés par des processus déjà en cours d'exécution.
  • Un nouveau fichier est créé et rendu exécutable sous le nom a.out. Il s'agit d'un tout nouvel inode et de nouveaux contenus dont les processus déjà en cours ne se soucient pas.

Maintenant, en ce qui concerne les bibliothèques partagées, le même comportement s'applique. Tant qu'un objet de bibliothèque est utilisé par un processus, il ne sera pas supprimé du disque, quelle que soit la façon dont vous modifiez ses liens. Chaque fois que quelque chose doit être chargé en mémoire, le noyau le fera via l'inode du fichier, et ignorera donc les modifications que vous avez apportées à ses liens (comme les associer à de nouveaux fichiers).

John WH Smith
la source
Réponse fantastique et détaillée. Cela explique ma confusion. Donc, ai-je raison de supposer que parce que l'inode est toujours disponible, les données du fichier binaire d'origine sont toujours sur le disque et donc utiliser dfpour calculer le nombre d'octets libres sur le disque est incorrect car il ne prend pas d'inodes qui tous les liens du système de fichiers supprimés ont-ils été pris en compte? Je devrais donc utiliser df -i? (C'est juste une curiosité technique, je n'ai pas vraiment besoin de connaître l'utilisation exacte du disque!)
texasflood
1
Juste pour clarifier pour les futurs lecteurs - ma confusion était que je pensais à l'exécution, tout le binaire serait chargé dans la RAM, donc si la RAM était petite, alors une partie du binaire quitterait la RAM et devrait être rechargée à partir du disque - ce qui provoquer des problèmes si vous avez modifié le fichier. Mais la réponse a clairement indiqué que le binaire n'est jamais vraiment supprimé du disque même si vous rmou mvlui en tant qu'inode au fichier d'origine n'est pas supprimé jusqu'à ce que tous les processus suppriment leur lien vers cet inode.
texasflood
@texasflood Exactement. Une fois que tous les chemins ont été supprimés, aucun nouveau processus ( dfinclus) ne peut obtenir d'informations sur l'inode. Les nouvelles informations que vous trouvez sont liées au nouveau fichier et au nouvel inode. Le point principal ici est que le sous-système de processus ne s'intéresse pas à ce problème, donc les notions de gestion de la mémoire (pagination de la demande, échange de processus, défauts de page, ...) sont complètement hors de propos. Il s'agit d'un problème de sous-système de fichiers et il est pris en charge par le sous-système de fichiers. Le sous-système de processus ne se soucie pas de cela, ce n'est pas pour cela qu'il est là.
John WH Smith
@texasflood Une note à propos de df -i: cet outil récupère probablement des informations du superbloc fs, ou de son cache, ce qui signifie qu'il peut inclure l'inode de l'ancien binaire (pour lequel tous les liens ont été supprimés). Cela ne signifie pas pour autant que les nouveaux processus sont libres d'utiliser ces anciennes données.
John WH Smith
2

Ma compréhension est qu'en raison du mappage de la mémoire d'un processus en cours, le noyau ne permettrait pas de mettre à jour une partie réservée du fichier mappé. Je suppose que dans le cas où un processus est en cours d'exécution, tout son fichier est réservé, ce qui le met à jour car vous avez compilé une nouvelle version de votre source entraîne en fait la création d'un nouvel ensemble d'inodes. En bref, les anciennes versions de votre exécutable restent accessibles sur le disque via des événements de défaut de page. Ainsi, même si vous mettez à jour un énorme fichier, il doit rester accessible et le noyau doit toujours voir la version intacte aussi longtemps que le processus est en cours d'exécution. Les inodes du fichier d'origine ne doivent pas être réutilisés tant que le processus est en cours d'exécution.

Cela doit bien sûr être confirmé.


la source
2

Ce n'est pas toujours le cas lors du remplacement d'un fichier .jar. Les ressources Jar et certains chargeurs de classe de réflexion d'exécution ne sont pas lus à partir du disque tant que le programme ne demande pas explicitement les informations.

Ce n'est qu'un problème car un jar est simplement une archive plutôt qu'un seul exécutable qui est mappé en mémoire. C'est un peu décalé mais c'est toujours une ramification de votre question et quelque chose avec laquelle je me suis tiré une balle dans le pied.

Donc pour les exécutables: oui. Pour les fichiers jar: peut-être (selon l'implémentation).

Zhro
la source