Question:
Pourquoi les décompilateurs de code machine sont-ils moins performants que par exemple ceux du CLR et de la JVM?
Rolf Rolles
2013-03-27 15:12:24 UTC
view on stackexchange narkive permalink

Les décompilateurs Java et .NET peuvent (généralement) produire un code source presque parfait, souvent très proche de l'original.

Pourquoi ne peut-on pas faire de même pour le code natif? J'en ai essayé quelques-uns mais ils ne fonctionnent pas ou produisent un désordre de gotos et de cast avec des pointeurs.

C'est génial que vous ayez écrit ce post, mais il doit encore être sous la forme d'un Q&R. Si vous pouviez transformer cela en une série de questions, ce serait encore mieux :)
Est-ce mieux?
Expliquez-vous vraiment comment rendre la récupération de code de haut niveau difficile? Je sauterais cette partie de la question et je parlerais simplement de la décompilation. Votre réponse est très bonne cependant imo.
@IgorSkochinsky Vous venez d'appeler votre décompilateur Hex-Rays de merde avec cette modification? : P
Eh bien, j'allais avec le sentiment général que vous pouvez lire dans beaucoup de ces questions :)
J'ai essayé de le rendre plus agréable. Vous ne savez pas si cela capture encore l'esprit de la question Rolf?
Ouais, ça marche. Fondamentalement, je l'ai écrit pour pouvoir y faire référence à l'avenir, donc je me fiche de savoir quel est le titre. Cependant, votre titre reflète parfaitement l'esprit du questionnement et de la réponse, il me semble donc très bien.
Deux réponses:
#1
+40
Rolf Rolles
2013-03-27 15:12:24 UTC
view on stackexchange narkive permalink

TL; DR: les décompilateurs de code machine sont très utiles, mais ne vous attendez pas aux mêmes miracles qu'ils fournissent pour les langages gérés. Pour nommer plusieurs limitations: le résultat ne peut généralement pas être recompilé, manque de noms, de types et d'autres informations cruciales du code source d'origine, est susceptible d'être beaucoup plus difficile à lire que le code source d'origine moins les commentaires, et peut laisser bizarre artefacts spécifiques au processeur dans la liste de décompilation.

  1. Pourquoi les décompilateurs sont-ils si populaires?

    Les décompilateurs sont des outils de rétro-ingénierie très intéressants car ils ont le potentiel d'économiser beaucoup de travail. En fait, ils sont si déraisonnablement efficaces pour les langages gérés tels que Java et .NET que «l'ingénierie inverse Java et .NET» est pratiquement inexistante en tant que sujet. Cette situation amène de nombreux débutants à se demander s'il en va de même pour le code machine. Malheureusement, ce n'est pas le cas. Les décompilateurs de code machine existent et sont utiles pour gagner du temps à l'analyste. Cependant, ils ne sont qu'une aide à un processus très manuel. La raison pour laquelle cela est vrai est que les décompilateurs de langage bytecode et de code machine sont confrontés à un ensemble de défis différents.

  2. Vais-je voir les noms de variables d'origine dans la source décompilée code?

    Certains défis proviennent de la perte d'informations sémantiques tout au long du processus de compilation. Les langages gérés conservent souvent les noms des variables, tels que les noms des champs dans un objet. Par conséquent, il est facile de présenter à l'analyste humain des noms créés par le programmeur qui, espérons-le, sont significatifs. Cela améliore la vitesse de compréhension du code machine décompilé.

    D'un autre côté, les compilateurs pour les programmes de code machine détruisent généralement la plupart de toutes ces informations lors de la compilation du programme (peut-être en laissant une partie sous forme d'informations de débogage). Par conséquent, même si un décompilateur de code machine était parfait à tous égards, il restituerait toujours des noms de variables non informatifs (tels que "v11", "a0", "esi0", etc.) qui ralentiraient la vitesse de compréhension humaine .

  3. Puis-je recompiler le programme décompilé?

    Certains défis concernent le désassemblage du programme. Dans les langages de bytecode tels que Java et .NET, les métadonnées associées à l'objet compilé décriront généralement les emplacements de tous les octets de code dans l'objet. C'est-à-dire que toutes les fonctions auront une entrée dans une table dans un en-tête de l'objet.

    En langage machine par contre, prendre le démontage de Windows x86 par exemple, sans l'aide d'informations de débogage lourdes telles que un PDB le désassembleur ne sait pas où se trouve le code dans le binaire. Il est donné quelques indices tels que le point d'entrée du programme. En conséquence, les désassembleurs de code machine sont obligés d'implémenter leurs propres algorithmes pour découvrir les emplacements de code dans le binaire. Ils utilisent généralement deux algorithmes: le balayage linéaire (parcourez la section de texte à la recherche de séquences d'octets connues qui indiquent généralement le début d'une fonction) et le parcours récursif (lorsqu'une instruction d'appel vers un emplacement fixe est rencontrée, considérez cet emplacement comme contenant du code ).

    Cependant, ces algorithmes ne découvriront généralement pas tout le code dans le binaire, en raison des optimisations du compilateur telles que l'allocation interprocédurale de registres qui modifient les prologues de fonction provoquant l'échec du composant de balayage linéaire, et en raison du flux de contrôle indirect naturel ( c'est-à-dire appel via un pointeur de fonction) provoquant l'échec du parcours récursif. Par conséquent, même si un décompilateur de code machine ne rencontrait aucun problème autre que celui-là, il ne pouvait généralement pas produire une décompilation pour un programme entier, et par conséquent le résultat ne pourrait pas être recompilé.

    Le code / Le problème de séparation des données décrit ci-dessus tombe dans une catégorie spéciale de problèmes théoriques, appelés problèmes «indécidables», qu'il partage avec d'autres problèmes impossibles tels que le problème de l'arrêt. Par conséquent, abandonnez l'espoir de trouver un décompilateur de code machine automatisé qui produira une sortie qui pourra être recompilée pour obtenir un clone du binaire d'origine.

  4. Aurai-je des informations sur les objets utilisés par le programme décompilé?

    Il y a aussi des défis liés à la nature de la façon dont les langages tels que C et C ++ sont compilés par rapport aux langages gérés; Je vais discuter des informations de type ici. Dans le bytecode Java, il existe une instruction dédiée appelée «nouveau» pour allouer des objets. Il prend un argument entier qui est interprété comme une référence dans les métadonnées du fichier .class qui décrit l'objet à allouer. Ces métadonnées décrivent à leur tour la disposition de la classe, les noms et les types des membres, etc. Cela rend très facile la décompilation des références à la classe d'une manière qui plaît à l'inspecteur humain.

    Lorsqu'un programme C ++ est compilé, par contre, en l'absence d'informations de débogage telles que RTTI, la création d'objet n'est pas effectuée de manière ordonnée. Il appelle un allocateur de mémoire spécifiable par l'utilisateur, puis transmet le pointeur résultant en tant qu'argument à la fonction constructeur (qui peut également être insérée, et donc pas une fonction). Les instructions qui accèdent aux membres de la classe sont syntaxiquement indiscernables des références de variables locales, des références de tableau, etc. De plus, la disposition de la classe n'est stockée nulle part dans le binaire. En effet, la seule façon de découvrir les structures de données dans un fichier binaire dépouillé est l'analyse de flux de données. Par conséquent, un décompilateur doit implémenter sa propre reconstruction de type afin de faire face à la situation. En fait, le populaire décompilateur Hex-Rays laisse principalement cette tâche à l'analyste humain (bien qu'il offre également une assistance humaine utile).

  5. La décompilation va-t-elle fondamentalement ressemble au code source original en termes de structure de flux de contrôle?

    Certains défis découlent des optimisations du compilateur appliquées au binaire compilé. L'optimisation populaire connue sous le nom de "fusion de queue" provoque la mutilation du flux de contrôle du programme par rapport aux compilateurs moins agressifs, ce qui se manifeste généralement par de nombreuses instructions goto dans la décompilation. La compilation d'instructions de commutateur éparses peut provoquer des problèmes similaires. D'un autre côté, les langages gérés ont souvent des instructions d'instruction de commutation.

  6. Le décompilateur donnera-t-il une sortie significative lorsque des facettes obscures du processeur sont impliquées?

    Certains défis découlent des caractéristiques architecturales du processeur en question. Par exemple, l'unité à virgule flottante intégrée sur x86 est un cauchemar d'une épreuve. Il n'y a pas de "registres" en virgule flottante, il y a une "pile" en virgule flottante, et il faut le suivre précisément pour que le programme soit correctement décompilé. En revanche, les langages gérés ont souvent des instructions spécialisées pour traiter les valeurs à virgule flottante, qui sont elles-mêmes des variables. (Hex-Rays gère très bien l'arithmétique en virgule flottante.) Ou considérez le fait qu'il existe plusieurs centaines de types d'instructions juridiques sur x86, dont la plupart ne sont jamais produits par un compilateur régulier sans que l'utilisateur spécifie explicitement qu'il devrait le faire via un intrinsèque. Un décompilateur doit inclure un traitement spécial pour les instructions qu'il prend en charge nativement, et donc la plupart des décompilateurs incluent simplement la prise en charge de celles les plus couramment générées par les compilateurs, en utilisant l'assemblage en ligne ou (au mieux) les intrinsèques pour celles qu'il ne prend pas en charge.

Ce ne sont que quelques exemples accessibles de défis qui affligent les décompilateurs de code machine. Nous pouvons nous attendre à ce que des limitations subsistent dans un avenir prévisible. Par conséquent, ne cherchez pas une solution miracle aussi efficace que les décompilateurs de langage gérés.

préférez-vous une nouvelle réponse pour des aspects supplémentaires ou les éditer dans votre réponse? En général, je me sens mal à l'aise avec l'édition à ce niveau de représentant (peut-être que c'est différent pour les bêtas privés?), Car cela se termine dans une file d'attente et autres. Mais peu importe. Alors qu'est-ce que c'est? :)
Vous pouvez vous sentir libre de le modifier, ou suggérer de nouveaux sujets et je le modifierai.
On 6. Lorsque le code est passé par * l'optimisation du pipeline *, une séquence logique d'opérations uniques peut être mélangée avec le bloc logique précédent et / ou suivant d'opérations.
#2
+7
Ed McMan
2013-03-27 22:48:57 UTC
view on stackexchange narkive permalink

La décompilation est difficile car les décompilateurs doivent récupérer les abstractions de code source qui manquent dans la cible binaire / bytecode.

Il existe plusieurs types d'abstractions:

  • Fonctions: L'identification du code correspondant à une fonction haute, avec son entrée, ses arguments, sa (ses) valeur (s) de retour et sa sortie.
  • Variables: Les variables locales dans chaque fonction, et toutes les variables globales ou statiques. li>
  • Types: Le type de chaque variable, et les arguments et la valeur de retour de chaque fonction.
  • Flux de contrôle de haut niveau: Le schéma de flux de contrôle d'un programme, par exemple, while (. ..) {if (...) {...} else {...}}

La décompilation du code natif est difficile car aucune de ces abstractions n'est représentée explicitement dans le code natif. Ainsi, pour produire un bon code décompilé (c'est-à-dire ne pas utiliser les goto partout), les décompilateurs doivent réinférer ces abstractions en fonction du comportement du code natif. C'est un processus difficile, et de nombreux articles ont été écrits sur la façon de déduire ces abstractions. Voir Balakrishnan et Lee pour commencer.

En revanche, le bytecode est plus facile à décompiler car il contient généralement suffisamment d'informations pour permettre la vérification de type . Par conséquent, le bytecode contient généralement des abstractions explicites pour les fonctions (ou méthodes), les variables et le type de chaque variable. L'abstraction principale manquante dans le bytecode est le flux de contrôle de haut niveau.



Ce Q&R a été automatiquement traduit de la langue anglaise.Le contenu original est disponible sur stackexchange, que nous remercions pour la licence cc by-sa 3.0 sous laquelle il est distribué.
Loading...