Existe-t-il des cas intelligents de modification du code d'exécution?

119

Pouvez-vous penser à des utilisations légitimes (intelligentes) pour la modification du code d'exécution (programme modifiant son propre code au moment de l'exécution)?

Les systèmes d'exploitation modernes semblent désapprouver les programmes qui font cela puisque cette technique a été utilisée par des virus pour éviter la détection.

Tout ce à quoi je peux penser, c'est une sorte d'optimisation d'exécution qui supprimerait ou ajouterait du code en connaissant quelque chose au moment de l'exécution qui ne peut pas être connu au moment de la compilation.

déo
la source
8
Sur les architectures modernes, cela interfère gravement avec la mise en cache et le pipeline d'instructions: le code auto-modifiable finirait par ne pas modifier le cache, vous auriez donc besoin de barrières, ce qui ralentirait probablement votre code. Et vous ne pouvez pas modifier le code qui est déjà dans le pipeline d'instructions. Ainsi, toute optimisation basée sur un code auto-modifiable doit être effectuée bien avant que le code ne soit exécuté pour avoir un impact sur les performances supérieur, par exemple, à une vérification à l'exécution.
Alexandre C.
7
@Alexandre: il est courant que le code auto-modifiable fasse rarement des modifications (par exemple une, deux fois) malgré le fait d'être exécuté un nombre arbitraire de fois, donc le coût ponctuel peut être insignifiant.
Tony Delroy le
7
Je ne sais pas pourquoi c'est étiqueté C ou C ++, car aucun des deux n'a de mécanisme pour cela.
MSalters le
4
@Alexandre: Microsoft Office est connu pour faire exactement cela. En conséquence (?) Tous les processeurs x86 ont un excellent support pour le code auto-modifiable. Sur d'autres processeurs, une synchronisation coûteuse est nécessaire, ce qui rend l'ensemble moins attractif.
Mackie Messer le
3
@Cawas: Habituellement, les logiciels de mise à jour automatique téléchargent de nouveaux assemblages et / ou exécutables et écrasent ceux existants. Ensuite, il redémarrera le logiciel. C'est ce que font Firefox, Adobe, etc. L'auto-modification signifie généralement que pendant l'exécution, le code est réécrit en mémoire par l'application en raison de certains paramètres et n'est pas nécessairement conservé sur le disque. Par exemple, il pourrait optimiser les chemins de code entiers s'il peut détecter intelligemment que ces chemins ne seraient pas exercés pendant cette exécution particulière afin d'accélérer l'exécution.
NotMe

Réponses:

117

Il existe de nombreux cas valides pour la modification de code. La génération de code au moment de l'exécution peut être utile pour:

  • Certaines machines virtuelles utilisent la compilation JIT pour améliorer les performances.
  • La création de fonctions spécialisées à la volée est depuis longtemps courante en infographie. Voir par exemple Rob Pike et Bart Locanthi et John Reiser Hardware Software Tradeoffs for Bitmap Graphics on the Blit (1984) ou cet article (2006) de Chris Lattner sur l'utilisation par Apple de LLVM pour la spécialisation du code d'exécution dans leur pile OpenGL.
  • Dans certains cas, le logiciel recourt à une technique connue sous le nom de trampoline qui implique la création dynamique de code sur la pile (ou à un autre endroit). Des exemples sont les fonctions imbriquées de GCC et le mécanisme de signal de certains Unices.

Parfois, le code est traduit en code au moment de l'exécution (c'est ce qu'on appelle la traduction binaire dynamique ):

  • Les émulateurs comme Rosetta d'Apple utilisent cette technique pour accélérer l'émulation. Un autre exemple est le logiciel de morphing de code de Transmeta .
  • Des débogueurs et des profileurs sophistiqués comme Valgrind ou Pin l' utilisent pour instrumenter votre code pendant son exécution.
  • Avant que des extensions ne soient apportées au jeu d'instructions x86, les logiciels de virtualisation comme VMWare ne pouvaient pas exécuter directement du code x86 privilégié dans les machines virtuelles. Au lieu de cela, il a dû traduire toutes les instructions problématiques à la volée en un code personnalisé plus approprié.

La modification du code peut être utilisée pour contourner les limitations du jeu d'instructions:

  • Il fut un temps (il y a longtemps, je sais), où les ordinateurs n'avaient pas d'instructions pour revenir d'un sous-programme ou adresser indirectement la mémoire. Le code auto-modifiable était le seul moyen d' implémenter des sous-programmes, des pointeurs et des tableaux .

Plus de cas de modification de code:

  • De nombreux débogueurs remplacent les instructions pour implémenter des points d'arrêt .
  • Certains éditeurs de liens dynamiques modifient le code au moment de l'exécution. Cet article fournit des informations générales sur le déplacement des DLL Windows à l'exécution, qui est en fait une forme de modification de code.
Mackie Messer
la source
10
Cette liste semble mélanger des exemples de code qui se modifie et du code qui modifie d'autres codes, comme les linkers.
AShelly
6
@AShelly: Eh bien, si vous considérez que l'éditeur de liens / chargeur dynamique fait partie du code, alors il se modifie. Ils vivent dans le même espace d'adressage, donc je pense que c'est un point de vue valable.
Mackie Messer le
1
Ok, la liste fait maintenant la distinction entre les programmes et les logiciels système. J'espère que cela a du sens. En fin de compte, toute classification est discutable. Tout se résume à ce que vous incluez exactement dans la définition du programme (ou du code).
Mackie Messer
35

Cela a été fait dans l'infographie, en particulier les moteurs de rendu logiciels à des fins d'optimisation. Lors de l'exécution, l'état de nombreux paramètres est examiné et une version optimisée du code du rastériseur est générée (éliminant potentiellement beaucoup de conditions), ce qui permet de rendre les primitives graphiques, par exemple les triangles, beaucoup plus rapidement.

Trenki
la source
5
Une lecture intéressante est les articles Pixomatic en 3 parties de Michael Abrash sur DDJ: drdobbs.com/architecture-and-design/184405765 , drdobbs.com/184405807 , drdobbs.com/184405848 . Le deuxième lien (Part2) parle du soudeur de code Pixomatic pour le pipeline de pixels.
typo.pl
1
Un très bel article sur le sujet. De 1984, mais toujours une bonne lecture: Rob Pike et Bart Locanthi et John Reiser. Compromis matériels et logiciels pour les graphiques Bitmap sur Blit .
Mackie Messer le
5
Charles Petzold explique un exemple de ce genre dans un livre intitulé "Beautiful Code": amazon.com/Beautiful-Code-Leading-Programmers-Practice/dp/…
Nawaz
3
Cette réponse parle de générer du code, mais la question se pose sur la modification du code ...
Timwi
3
@Timwi - il a modifié le code. Plutôt que de gérer une grande chaîne de if, il a analysé la forme une fois et réécrit le moteur de rendu pour qu'il soit configuré pour le type de forme correct sans avoir à vérifier à chaque fois. Fait intéressant, cela est maintenant courant avec le code opencl - puisqu'il est compilé à la volée, vous pouvez le réécrire pour le cas spécifique au moment de l'exécution
Martin Beckett
23

Une raison valable est que le jeu d'instructions asm ne dispose pas des instructions nécessaires, que vous pouvez créer vous-même. Exemple: Sur x86, il n'y a aucun moyen de créer une interruption vers une variable dans un registre (par exemple, faire une interruption avec un numéro d'interruption dans ax). Seuls les numéros const codés dans l'opcode étaient autorisés. Avec un code auto-modifiable, on pourrait émuler ce comportement.

flolo
la source
C'est suffisant. Y a-t-il une utilisation de cette technique? Cela semble dangereux.
Alexandre C.
4
@Alexandre C.: Si je me souviens bien, de nombreuses bibliothèques d'exécution (C, Pascal, ...) devaient multiplier par DOS une fonction pour effectuer des appels d'interruption. En tant que telle fonction obtient le numéro d'interruption comme paramètre que vous deviez fournir une telle fonction (bien sûr, si le nombre était constant, vous auriez pu générer le bon code, mais ce n'était pas garanti). Et toutes les bibliothèques l'ont implémenté avec un code auto-modifiable.
flolo
Vous pouvez utiliser une casse de commutation pour le faire sans modification du code. La taille réduite est que le code de sortie sera plus grand
phuclv
17

Certains compilateurs l'utilisaient pour l'initialisation de variables statiques, évitant ainsi le coût d'un conditionnel pour les accès ultérieurs. En d'autres termes, ils implémentent "n'exécuter ce code qu'une seule fois" en écrasant ce code par des no-ops la première fois qu'il est exécuté.

JoeG
la source
1
Très bien, surtout si cela évite les verrouillages / déverrouillages mutex.
Tony Delroy le
2
Vraiment? Comment cela se passe-t-il pour le code ROM ou pour le code exécuté dans le segment de code protégé en écriture?
Ira Baxter le
1
@Ira Baxter: tout compilateur qui émet du code relocalisable sait que le segment de code est accessible en écriture, au moins au démarrage. Ainsi, l'affirmation «certains compilateurs l'ont utilisé» est toujours possible.
MSalters le
17

Il existe de nombreux cas:

  • Les virus utilisent couramment du code auto-modifiable pour «désobfusquer» leur code avant l'exécution, mais cette technique peut également être utile pour contrecarrer l'ingénierie inverse, le cracking et le piratage indésirable.
  • Dans certains cas, il peut y avoir un moment particulier pendant l'exécution (par exemple immédiatement après la lecture du fichier de configuration) où l'on sait que - pour le reste de la durée de vie du processus - une branche particulière sera toujours ou jamais prise: plutôt qu'inutile en vérifiant une variable pour déterminer dans quelle direction brancher, l'instruction de branchement elle-même pourrait être modifiée en conséquence
    • Par exemple, il se peut que l'on sache qu'un seul des types dérivés possibles sera traité, de sorte que l'envoi virtuel peut être remplacé par un appel spécifique
    • Après avoir détecté quel matériel est disponible, l'utilisation d'un code correspondant peut être codée en dur
  • Le code inutile peut être remplacé par des instructions no-op ou un saut dessus, ou avoir le prochain bit de code décalé directement en place (plus facile si vous utilisez des opcodes indépendants de la position)
  • Le code écrit pour faciliter son propre débogage peut injecter une instruction d'interruption / signal / interruption attendue par le débogueur à un emplacement stratégique.
  • Certaines expressions de prédicat basées sur l'entrée de l'utilisateur peuvent être compilées en code natif par une bibliothèque
  • Inlining quelques opérations simples qui ne sont pas visibles avant l'exécution (par exemple à partir d'une bibliothèque chargée dynamiquement) ...
  • Ajout conditionnel d'étapes d'auto-instrumentation / profilage
  • Les fissures peuvent être implémentées en tant que bibliothèques qui modifient le code qui les charge (pas de modification «auto» exacte, mais qui nécessitent les mêmes techniques et autorisations).
  • ...

Les modèles de sécurité de certains systèmes d'exploitation signifient que le code auto-modifiable ne peut pas s'exécuter sans les privilèges root / admin, ce qui le rend impraticable pour une utilisation générale.

De Wikipedia:

Les logiciels d'application exécutés sous un système d'exploitation avec une sécurité W ^ X stricte ne peuvent pas exécuter les instructions dans les pages sur lesquelles ils sont autorisés à écrire. Seul le système d'exploitation lui-même est autorisé à écrire des instructions en mémoire et à les exécuter ultérieurement.

Sur de tels systèmes d'exploitation, même des programmes tels que la machine virtuelle Java ont besoin des privilèges root / admin pour exécuter leur code JIT. (Voir http://en.wikipedia.org/wiki/W%5EX pour plus de détails)

Tony Delroy
la source
2
Vous n'avez pas besoin de privilèges root pour le code auto-modifiable. La machine virtuelle Java non plus.
Mackie Messer le
Je ne savais pas que certains OS étaient si stricts. Mais cela a certainement du sens dans certaines applications. Je me demande cependant si l'exécution de Java avec les privilèges root augmente réellement la sécurité ...
Mackie Messer
@Mackie: Je pense que cela doit le diminuer, mais peut-être qu'il peut définir des autorisations de mémoire puis changer l'uid effectif en un compte utilisateur ...?
Tony Delroy
Oui, je m'attendrais à ce qu'ils aient un mécanisme fin pour accorder des autorisations pour accompagner le modèle de sécurité strict.
Mackie Messer
15

Le système d'exploitation Synthesis a essentiellement évalué partiellement votre programme par rapport aux appels d'API et a remplacé le code du système d'exploitation par les résultats. Le principal avantage est que de nombreuses vérifications d'erreurs ont disparu (car si votre programme ne demande pas au système d'exploitation de faire quelque chose de stupide, il n'a pas besoin de vérifier).

Oui, c'est un exemple d'optimisation d'exécution.

Ira Baxter
la source
Je ne vois pas le point. Si, par exemple, un appel système va être interdit par le système d'exploitation, vous obtiendrez probablement une erreur indiquant que vous devrez vérifier le code, n'est-ce pas? Il me semble que modifier l'exécutable au lieu de renvoyer un code d'erreur est une sorte de sur-ingénierie.
Alexandre C.
@Alexandre C.: vous pourrez peut-être éliminer les vérifications de pointeur nul de cette façon. Il est souvent trivial pour l'appelant qu'un argument est valide.
MSalters le
@Alexandre: Vous pouvez lire la recherche sur le lien. Je pense qu'ils ont eu des accélérations assez impressionnantes, et ce serait le point: -}
Ira Baxter
2
Pour les appels système relativement simples et non liés aux E / S, les économies sont importantes. Par exemple, si vous écrivez un démon pour Unix, il y a un tas d'appels système à chaud que vous faites pour déconnecter stdio, configurer divers gestionnaires de signaux, etc. Si vous savez que les paramètres d'un appel sont des constantes, et que le les résultats seront toujours les mêmes (fermeture de stdin, par exemple), une grande partie du code que vous exécutez dans le cas général est inutile.
Mark Bessey
1
Si vous lisez la thèse, le chapitre 8 contient des chiffres vraiment impressionnants sur les E / S temps réel non triviales pour l'acquisition de données. Vous vous souvenez qu'il s'agit d'une thèse du milieu des années 1980 et que la machine sur laquelle il fonctionnait avait 10 ans? Mhz 68000, il était capable dans le logiciel de capturer des données audio de qualité CD (44 000 échantillons par seconde) avec de vieux logiciels simples. Il a affirmé que les stations de travail Sun (Unix classique) ne pouvaient atteindre qu'environ 1/5 de ce taux. Je suis un ancien codeur en langage d'assemblage de l'époque, et c'est assez spectaculaire.
Ira Baxter le
9

Il y a de nombreuses années, j'ai passé une matinée à essayer de déboguer du code auto-modifiable, une instruction a changé l'adresse cible de l'instruction suivante, c'est-à-dire que je calculais une adresse de branche. Il a été écrit en langage d'assemblage et a parfaitement fonctionné lorsque j'ai parcouru le programme une instruction à la fois. Mais quand j'ai exécuté le programme, il a échoué. Finalement, j'ai réalisé que la machine récupérait 2 instructions de la mémoire et (comme les instructions étaient disposées en mémoire) l'instruction que je modifiais avait déjà été récupérée et que la machine exécutait donc la version non modifiée (incorrecte) de l'instruction. Bien sûr, lorsque je déboguais, je ne faisais qu'une instruction à la fois.

Mon point de vue, le code auto-modifiable peut être extrêmement désagréable à tester / déboguer et a souvent des hypothèses cachées quant au comportement de la machine (qu'elle soit matérielle ou virtuelle). De plus, le système ne pourrait jamais partager les pages de code entre les différents threads / processus s'exécutant sur les (maintenant) machines multicœurs. Cela annule de nombreux avantages de la mémoire virtuelle, etc. Cela invaliderait également les optimisations de branche effectuées au niveau matériel.

(Remarque - je n'inclus pas JIT dans la catégorie du code auto-modifiable. JIT traduit une représentation du code en une autre représentation, il ne modifie pas le code)

Dans l'ensemble, c'est juste une mauvaise idée - vraiment soignée, vraiment obscure, mais vraiment mauvaise.

bien sûr - si tout ce que vous avez est une mémoire de 8080 et ~ 512 octets, vous devrez peut-être recourir à de telles pratiques.

Geai
la source
1
Je ne sais pas, le bien et le mal ne semblent pas être les bonnes catégories pour y penser. Bien sûr, vous devez vraiment savoir ce que vous faites et pourquoi vous le faites. Mais le programmeur qui a écrit ce code ne voulait probablement pas que vous voyiez ce que faisait le programme. Bien sûr, c'est méchant si vous devez déboguer du code comme ça. Mais ce code était très probablement destiné à être ainsi.
Mackie Messer le
Les processeurs x86 modernes ont une détection SMC plus forte que nécessaire sur le papier: Observation de la récupération d'instructions obsolètes sur x86 avec un code auto-modifiable . Et sur la plupart des processeurs non x86 (comme ARM), le cache d'instructions n'est pas cohérent avec les caches de données, donc un vidage / synchronisation manuel est nécessaire avant que les octets nouvellement stockés puissent être exécutés de manière fiable en tant qu'instructions. community.arm.com/processors/b/blog/posts/… . Dans tous les cas, les performances SMC sont terribles sur les processeurs modernes, sauf si vous modifiez une fois et exécutez plusieurs fois.
Peter Cordes
7

Du point de vue du noyau d'un système d'exploitation, chaque Just In Time Compiler et Linker Runtime effectue une auto-modification du texte du programme. Un exemple frappant serait l'interpréteur de scripts V8 ECMA de Google.

datenwolf
la source
5

Une autre raison du code auto-modifiable (en fait un code "auto-générateur") est d'implémenter un mécanisme de compilation juste à temps pour les performances. Par exemple, un programme qui lit une expression algébrique et la calcule sur une plage de paramètres d'entrée peut convertir l'expression en code machine avant d'énoncer le calcul.

Giuseppe Guerrini
la source
5

Vous connaissez le vieux châtain qu'il n'y a pas de différence logique entre le matériel et le logiciel ... on peut aussi dire qu'il n'y a pas de différence logique entre le code et les données.

Qu'est-ce que le code auto-modifiable? Code qui place des valeurs dans le flux d'exécution afin qu'il puisse être interprété non pas en tant que données mais en tant que commande. Bien sûr, il y a le point de vue théorique dans les langages fonctionnels qu'il n'y a vraiment aucune différence. Je dis que sur e peut le faire d'une manière simple dans les langages impératifs et les compilateurs / interprètes sans présomption d'égalité de statut.

Ce à quoi je fais référence, c'est dans le sens pratique que les données peuvent modifier les chemins d'exécution du programme (dans un certain sens, c'est extrêmement évident). Je pense à quelque chose comme un compilateur-compilateur qui crée une table (un tableau de données) que l'on traverse en analysant, passant d'un état à l'autre (et modifiant également d'autres variables), tout comme la façon dont un programme passe d'une commande à l'autre , en modifiant les variables dans le processus.

Ainsi, même dans le cas habituel où un compilateur crée un espace de code et se réfère à un espace de données entièrement séparé (le tas), on peut toujours modifier les données pour changer explicitement le chemin d'exécution.

Mitch
la source
4
Aucune différence logique, c'est vrai. Cependant, je n'ai pas vu trop de circuits intégrés auto-modifiants.
Ira Baxter
@Mitch, IMO changer le chemin d'exécution n'a rien à voir avec une (auto-) modification du code. En outre, vous confondez les données avec les informations. Je ne peux pas répondre à mon commentaire à ma réponse en LSE car je suis banni là-bas, depuis février, pendant 3 ans (1000 jours) pour avoir exprimé en méta-LSE mon point de vue que les Américains et les Britanniques ne possèdent pas l'anglais.
Gennady Vanin Геннадий Ванин
4

J'ai implémenté un programme utilisant l'évolution pour créer le meilleur algorithme. Il a utilisé un code auto-modifiable pour modifier le plan d'ADN.

David
la source
2

Un cas d'utilisation est le fichier de test EICAR qui est un fichier COM exécutable DOS légitime pour tester les programmes antivirus.

X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*

Il doit utiliser la modification du code automatique car le fichier exécutable ne doit contenir que des caractères ASCII imprimables / typables dans la plage [21h-60h, 7Bh-7Dh], ce qui limite considérablement le nombre d'instructions encodables

Les détails sont expliqués ici


Il est également utilisé pour la distribution d'opérations en virgule flottante sous DOS

Certains compilateurs émettront CD xxavec xx allant de 0x34-0x3B à la place des instructions à virgule flottante x87. Comme CDc'est l'opcode pour l' intinstruction, il sautera dans l'interruption 34h-3Bh et émulera cette instruction dans le logiciel si le coprocesseur x87 n'est pas disponible. Sinon, le gestionnaire d'interruption remplacera ces 2 octets par9B Dx sorte que les exécutions ultérieures seront gérées directement par x87 sans émulation.

Quel est le protocole pour l'émulation en virgule flottante x87 dans MS-DOS?

phuclv
la source
1

Le noyau Linux a des modules de noyau chargeables qui font exactement cela.

Emacs a également cette capacité et je l'utilise tout le temps.

Tout ce qui prend en charge une architecture de plug-in dynamique modifie essentiellement son code au moment de l'exécution.

régime bouddha
la source
4
à peine. avoir une bibliothèque chargeable dynamiquement qui n'est pas toujours résidente a très peu à voir avec le code auto-modifiable.
Dov le
1

J'exécute des analyses statistiques sur une base de données constamment mise à jour. Mon modèle statistique est écrit et réécrit chaque fois que le code est exécuté pour accueillir les nouvelles données qui deviennent disponibles.

David LeBauer
la source
0

Le scénario dans lequel cela peut être utilisé est un programme d'apprentissage. En réponse à l'entrée de l'utilisateur, le programme apprend un nouvel algorithme:

  1. il recherche la base de code existante pour un algorithme similaire
  2. si aucun algorithme similaire n'est dans la base de code, le programme ajoute simplement un nouvel algorithme
  3. si un algorithme similaire existe, le programme (peut-être avec l'aide de l'utilisateur) modifie l'algorithme existant pour pouvoir servir à la fois l'ancien objectif et le nouvel objectif

Il y a une question comment faire cela en Java: Quelles sont les possibilités d'auto-modification du code Java?

Serge Rogatch
la source
-1

La meilleure version de ceci peut être les macros Lisp. Contrairement aux macros C qui ne sont qu'un préprocesseur Lisp vous permet d'avoir accès à tout le langage de programmation à tout moment. Il s'agit de la fonctionnalité la plus puissante de lisp et n'existe dans aucune autre langue.

Je ne suis en aucun cas un expert, mais faites en parler l'un des gars lisp! Il y a une raison pour laquelle ils disent que Lisp est le langage le plus puissant et les gens intelligents ne disent pas qu'ils ont probablement raison.

Zachary K
la source
2
Cela crée-t-il réellement du code auto-modifiable ou s'agit-il simplement d'un préprocesseur plus puissant (qui générera des fonctions)?
Brendan Long le
@Brendan: en effet, mais il est la bonne façon de faire prétraiter. Il n'y a pas de modification du code d'exécution ici.
Alexandre C.