La recompilation d'un programme produit-elle un binaire identique bit par bit?

25

Si je devais compiler un programme en un seul binaire, faire une somme de contrôle, puis le recompiler sur la même machine avec le même compilateur et les mêmes paramètres de compilation et la somme de contrôle du programme recompilé, la somme de contrôle échouerait-elle?

Si oui, pourquoi? Sinon, le fait d'avoir un processeur différent entraînerait-il un binaire non identique?

David
la source
8
Cela dépend du compilateur. Certains d'entre eux intègrent des horodatages, donc la réponse est «non» pour ceux-là.
ta.speot.is
En fait, cela dépend du format exécutable , pas du compilateur. Certains formats exécutables comme le format PE de Windows incluent un horodatage qui est touché à l'heure et à la date de compilation, contrairement à d'autres formats comme le format ELF de Linux. Quoi qu'il en soit, cette question repose sur la définition de «binaire identique». L'image elle-même sera / devrait être identique au niveau du bit si le même fichier source est compilé avec le même compilateur et les mêmes bibliothèques et commutateurs et tout, mais l'en-tête et les autres métadonnées peuvent varier.
Synetech

Réponses:

19
  1. Compilez le même programme avec les mêmes paramètres sur la même machine:

    Bien que la réponse définitive soit «cela dépend», il est raisonnable de s'attendre à ce que la plupart des compilateurs soient déterministes la plupart du temps et que les binaires produits soient identiques. En effet, certains systèmes de contrôle de version en dépendent. Pourtant, il y a toujours des exceptions; il est tout à fait possible que certains quelque part du compilateur décide d'insérer un horodatage ou d' un tel (IIRC, Delphi fait, par exemple). Ou le processus de construction lui-même pourrait le faire; J'ai vu des makefiles pour les programmes C qui définissaient une macro de préprocesseur sur l'horodatage actuel. (Je suppose que cela compterait comme étant un paramètre de compilateur différent, cependant.)

    Sachez également que si vous liez statiquement le binaire, vous incorporez effectivement l'état de toutes les bibliothèques pertinentes sur votre machine, et tout changement dans l'une d'entre elles affectera également votre binaire. Ce ne sont donc pas seulement les paramètres du compilateur qui sont pertinents.

  2. Compilez le même programme sur une machine différente avec un processeur différent.

    Ici, tous les paris sont désactivés. La plupart des compilateurs modernes sont capables de faire des optimisations spécifiques à la cible; si cette option est activée, les binaires sont susceptibles de différer, sauf si les CPU sont similaires (et même alors, c'est possible). Voir également la note ci-dessus sur la liaison statique: l'environnement de configuration va bien au-delà des paramètres du compilateur. Sauf si vous avez un contrôle de configuration très strict, il est extrêmement probable que quelque chose diffère entre les deux machines.

rici
la source
1
Disons que j'utilisais GCC, et je n'utilisais pas l'option march (l'option qui optimise le binaire pour une famille spécifique de CPU), et je devais compiler un binaire avec un CPU, puis avec un autre CPU y aurait-il un différence?
David
1
@David: Cela dépend toujours. Tout d'abord, les bibliothèques auxquelles vous vous connectez peuvent avoir des builds spécifiques à l'architecture. La sortie de gcc -cpeut donc être identique, mais les versions liées diffèrent. De plus, ce n'est pas seulement -march; il y en a aussi -mtune/-mcpu et -mfpmatch(et peut-être d'autres). Certains d'entre eux peuvent avoir des valeurs par défaut différentes sur différentes installations, vous devrez donc forcer explicitement le pire des cas pour vos machines; cela pourrait réduire considérablement les performances, en particulier si vous revenez à i386 sans sse. Et, bien sûr, si l'un de vos processeurs est un ARM et l'autre un i686 ...
rici
1
De plus, GCC est-il l'un des compilateurs en question qui ajoute un horodatage aux binaires?
David
@david: afaik, non.
rici
8

Ce que vous demandez, c'est "est la sortie déterministe ". Si vous avez compilé le programme une fois, le compilé à nouveau immédiatement, vous vous retrouverez probablement avec le même fichier de sortie. Cependant, si quelque chose a changé - même un petit changement - en particulier dans un composant utilisé par le programme compilé, la sortie du compilateur peut également changer.

headkase
la source
2
Très bon point en effet. Cet article contient quelques observations très intéressantes. En particulier, la compilation avec GCC peut ne pas être déterministe en ce qui concerne les entrées dans certains cas, par exemple dans la façon dont elle gère les fonctions dans les espaces de noms anonymes, pour lesquels elle utilise un générateur de nombres aléatoires en interne. Pour obtenir le déterminisme dans ce cas particulier, fournissez une graine aléatoire initiale en spécifiant l'option -frandom-seed=string.
ack
7

La recompilation d'un programme produit-elle un binaire identique bit par bit?

Pour tous les compilateurs? Non. Le compilateur C #, au moins, n'est pas autorisé à le faire.

Eric Lippert a une ventilation très approfondie des raisons pour lesquelles la sortie du compilateur n'est pas déterministe .

[Le] compilateur C # par conception ne produit jamais deux fois le même binaire. Le compilateur C # incorpore un GUID fraîchement généré dans chaque assembly, chaque fois que vous l'exécutez, garantissant ainsi qu'aucun assemblage n'est jamais bit à bit identique. Pour citer la spécification CLI:

La colonne Mvid doit indexer un GUID [...] unique qui identifie cette instance du module. [...] Le Mvid devrait être nouvellement généré pour chaque module [...] Bien que le [runtime] lui-même n'utilise pas le Mvid, d'autres outils (tels que les débogueurs [...]) reposent sur le fait que le Mvid diffère presque toujours d'un module à l'autre.

Bien qu'il soit spécifique à une version du compilateur C #, de nombreux points de l'article peuvent être appliqués à n'importe quel compilateur.

Tout d'abord, nous supposons que nous obtenons toujours la même liste de fichiers à chaque fois, dans le même ordre. Mais c'est dans certains cas jusqu'au système d'exploitation. Lorsque vous dites "csc * .cs", l'ordre dans lequel le système d'exploitation profère la liste des fichiers correspondants est un détail d'implémentation du système d'exploitation; le compilateur ne trie pas cette liste dans un ordre canonique.

ta.speot.is
la source
Il ne devrait pas être difficile de rendre le build reproductible (à part quelques champs facilement jetables comme le temps de compilation et le GUID d'assemblage). Par exemple, le tri des fichiers d'entrée dans un ordre canonique est une ligne unique. Même ce GUID pourrait être un hachage du reste de l'assembly au lieu d'être nouvellement généré.
CodesInChaos
Je suppose que vous voulez dire le compilateur Microsoft C #, ou est-ce une exigence de la spécification?
David
@David La spécification CLI l'exige. Le compilateur C # de Mono devrait faire de même. Idem pour tout compilateur VB .NET.
ta.speot.is
4
La norme ECMA ne doit pas avoir d'horodatage ni de différence MVID. Sans ceux-ci, il est au moins possible pour des binaires identiques en C #. Ainsi, la raison principale est une décision de conception discutable et non une réelle contrainte technique.
Shiv
7
  • -frandom-seed=123contrôle certains aléas internes du GCC. man gccdit:

    Cette option fournit une graine que GCC utilise à la place de nombres aléatoires pour générer certains noms de symboles qui doivent être différents dans chaque fichier compilé. Il est également utilisé pour placer des tampons uniques dans les fichiers de données de couverture et les fichiers objets qui les produisent. Vous pouvez utiliser l'option -frandom-seed pour produire des fichiers d'objets reproductibles identiques.

  • __FILE__: mettre la source dans un dossier fixe (par exemple /tmp/build)

  • pour __DATE__, __TIME__, __TIMESTAMP__:
    • libfaketime: https://github.com/wolfcw/libfaketime
    • remplacer ces macros avec -D
    • -Wdate-timeou -Werror=date-time: avertir ou échouer si l' __TIME__, __DATE__ou __TIMESTAMP__sont est utilisé. Le noyau Linux 4.4 l'utilise par défaut.
  • utilisez le Ddrapeau avec arou utilisez https://github.com/nh2/ar-timestamp-wiper/tree/master pour essuyer les tampons
  • -fno-guess-branch-probability: les anciennes versions manuelles disent que c'est une source de non-déterminisme, mais plus maintenant . Je ne sais pas si cela est couvert -frandom-seedou non.

Le projet de construction Debian Reproductible tente de standardiser les paquets Debian octet par octet et a récemment obtenu une subvention de la Fondation Linux . Cela comprend plus qu'une simple compilation, mais cela devrait être intéressant.

Buildroot a une BR2_REPRODUCIBLEoption qui peut donner quelques idées au niveau du paquet, mais elle est loin d'être complète à ce stade.

Sujets associés:

Ciro Santilli 新疆 改造 中心 法轮功 六四 事件
la source
3

Le projet https://reproducible-builds.org/ est tout cela, et s'efforce de répondre à votre question "non, ils ne différeront pas" dans autant d'endroits que possible. NixOS et Debian ont désormais plus de 90% de reproductibilité pour leurs packages.

Si vous compilez un binaire, et je compile un binaire, et ils sont identiques bit à bit, alors je peux être rassuré que le code source et les outils sont ce qui détermine la sortie, et que vous n'avez pas faufilé certains code de Troie en cours de route.

Si nous combinons la reproductibilité avec la bootstrappabilité à partir d'une source lisible par l'homme, comme le fait http://bootstrappable.org/ , nous obtenons un système déterminé à partir de zéro par une source lisible par l'homme, et alors seulement nous en sommes à un point où nous pouvons avoir confiance que nous savons ce que fait le système.

clacke
la source
1
Liens sympas. Je suis un fan de Buildroot, mais si quelqu'un me donne une configuration d'arc croisé Nix ARM qui démarre sur QEMU, je serai heureux :-)
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件
Je n'ai pas mentionné Guix parce que je ne sais pas où trouver leurs numéros, mais ils étaient avant NixOS dans le train de reproductibilité avec des outils de vérification et autres, donc je suis sûr qu'ils sont sur un pied d'égalité ou mieux.
clacke
2

Je dirais NON, ce n'est pas 100% déterministe. J'ai précédemment travaillé avec une version de GCC qui génère des binaires cibles pour le processeur Hitachi H8.

Ce n'est pas un problème d'horodatage. Même si le problème d'horodatage est ignoré, l'architecture de processeur spécifique peut permettre à la même instruction d'être codée de 2 façons légèrement différentes où certains bits peuvent être 1 ou 0. Mon expérience précédente montre que les binaires générés étaient les mêmes la plupart du temps mais de temps en temps le gcc produirait des binaires de taille identique mais certains des octets différents par seulement 1 bit par exemple 0XE0 deviennent 0XE1.

JavaMan
la source
Et cela a-t-il conduit à des comportements différents ou à de "graves problèmes"?
Florian Straub
1

En général, non. Les compilateurs les plus sophistiqués incluent le temps de compilation dans le module objet. Même si vous deviez réinitialiser l'horloge, vous devriez être très précis en ce qui concerne le moment où vous avez lancé la compilation (et ensuite espérer que les accès au disque, etc., étaient à la même vitesse qu'auparavant).

Daniel R Hicks
la source