Czy rekompilacja programu daje identyczny plik binarny?

25

Gdybym miał skompilować program do pojedynczego pliku binarnego, zrobić sumę kontrolną, a następnie ponownie skompilować na tym samym komputerze z tymi samymi ustawieniami kompilatora i kompilatora oraz sumą kontrolną zrekompilowanego programu, czy suma kontrolna się nie powiedzie?

Jeśli tak, to dlaczego? Jeśli nie, czy posiadanie innego procesora skutkowałoby nieidentycznym plikiem binarnym?

David
źródło
8
To zależy od kompilatora. Niektóre z nich zawierają znaczniki czasu, więc odpowiedź brzmi „nie” dla nich.
ta.speot.is
W rzeczywistości zależy to od formatu wykonywalnego , a nie kompilatora. Niektóre formaty plików wykonywalnych, takie jak PE systemu Windows, zawierają znacznik czasu, który jest dostosowywany do godziny i daty kompilacji, podczas gdy inne formaty, takie jak ELF Linuksa, nie. Tak czy inaczej, pytanie to opiera się na definicji „identycznego pliku binarnego”. Sam obraz będzie / powinien być bitowo identyczny, jeśli ten sam plik źródłowy zostanie skompilowany z tym samym kompilatorem, bibliotekami i przełącznikami i wszystkim innym, ale nagłówek i inne metadane mogą się różnić.
Synetech

Odpowiedzi:

19
  1. Skompiluj ten sam program z tymi samymi ustawieniami na tym samym komputerze:

    Chociaż ostateczna odpowiedź brzmi „zależy”, uzasadnione jest oczekiwanie, że większość kompilatorów będzie deterministyczna przez większość czasu i że utworzone pliki binarne powinny być identyczne. Rzeczywiście, niektóre systemy kontroli wersji zależą od tego. Jednak zawsze są wyjątki; jest całkiem możliwe, że jakiś kompilator gdzieś zdecyduje się wstawić znacznik czasu lub coś takiego (na przykład iirc, Delphi). Lub sam proces kompilacji może to zrobić; Widziałem makefile dla programów w C, które ustawiają makro preprocesora na bieżący znacznik czasu. (Wydaje mi się, że byłoby to inne ustawienie kompilatora.)

    Pamiętaj również, że jeśli statycznie podłączysz plik binarny, wówczas skutecznie uwzględnisz stan wszystkich odpowiednich bibliotek na komputerze, a wszelkie zmiany w dowolnej z nich wpłyną również na twój plik binarny. Tak więc istotne są nie tylko ustawienia kompilatora.

  2. Skompiluj ten sam program na innym komputerze z innym procesorem.

    Tutaj wszystkie zakłady są wyłączone. Większość nowoczesnych kompilatorów jest w stanie dokonywać optymalizacji specyficznych dla celu; jeśli ta opcja jest włączona, pliki binarne mogą się różnić, chyba że procesory są podobne (i nawet wtedy jest to możliwe). Zobacz także powyższą uwagę na temat łączenia statycznego: środowisko konfiguracyjne wykracza daleko poza ustawienia kompilatora. O ile nie masz bardzo ścisłej kontroli konfiguracji, jest bardzo prawdopodobne, że coś różni się między tymi dwiema maszynami.

rici
źródło
1
Powiedzmy, że korzystam z GCC i nie używałem opcji march (opcja optymalizująca pliki binarne dla konkretnej rodziny procesorów), i miałem skompilować plik binarny z jednym procesorem, a następnie z innym procesorem różnica?
David
1
@David: To wciąż zależy. Po pierwsze, biblioteki, z którymi się łączysz, mogą mieć kompilacje specyficzne dla architektury. Wynik gcc -cmoże być identyczny, ale powiązane wersje różnią się. Poza tym to nie tylko -march; są też -mtune/-mcpu i -mfpmatch(i ewentualnie inne). Niektóre z nich mogą mieć różne ustawienia domyślne w różnych instalacjach, więc może być konieczne jawne wymuszenie najgorszego możliwego przypadku dla twoich komputerów; może to znacznie obniżyć wydajność, szczególnie jeśli powrócisz do i386 bez sse. I oczywiście, jeśli jeden z twoich procesorów jest ARM, a drugi i686 ...
rici
1
Czy GCC jest jednym z omawianych kompilatorów, które dodają znaczniki czasu do plików binarnych?
David
@david: afaik, no.
rici
8

To, o co pytasz, to „ deterministyczny wynik ”. Jeśli skompilowałeś program raz, natychmiast skompiluj go ponownie, prawdopodobnie uzyskasz ten sam plik wyjściowy. Jednak jeśli cokolwiek się zmieniło - nawet niewielka zmiana - szczególnie w komponencie, którego używa skompilowany program, to wyjście kompilatora może się również zmienić.

headkase
źródło
2
Rzeczywiście bardzo dobra uwaga. Ten artykuł zawiera kilka bardzo interesujących spostrzeżeń. W szczególności kompilacja z GCC może nie być deterministyczna w odniesieniu do danych wejściowych w niektórych przypadkach, na przykład w zakresie sposobu zarządzania funkcjami w anonimowych przestrzeniach nazw, w których wewnętrznie wykorzystuje generator liczb losowych. Aby uzyskać determinizm w tym konkretnym przypadku, podaj początkowe losowe ziarno, określając opcję -frandom-seed=string.
ack
7

Czy rekompilacja programu daje identyczny plik binarny?

Dla wszystkich kompilatorów? Nie. Kompilator C # przynajmniej nie jest dozwolony.

Eric Lippert szczegółowo analizuje, dlaczego dane wyjściowe kompilatora nie są deterministyczne .

[T] Kompilator C # z założenia nigdy nie produkuje tego samego pliku binarnego dwa razy. Kompilator C # osadza świeżo wygenerowany identyfikator GUID w każdym zestawie za każdym razem, gdy go uruchamiasz, dzięki czemu nie ma dwóch identycznych zestawów. Cytat ze specyfikacji CLI:

Kolumna Mvid indeksuje unikalny identyfikator GUID [...] identyfikujący to wystąpienie modułu. [...] Mvid powinien być nowo wygenerowany dla każdego modułu [...] Podczas gdy sam [środowisko wykonawcze] nie korzysta z Mvid, inne narzędzia (takie jak debuggery [...]) polegają na tym, że Mvid prawie zawsze różni się w zależności od modułu.

Chociaż jest to specyficzne dla wersji kompilatora C #, wiele punktów w artykule można zastosować do dowolnego kompilatora.

Po pierwsze, zakładamy, że za każdym razem otrzymujemy tę samą listę plików, w tej samej kolejności. Ale w niektórych przypadkach dotyczy to systemu operacyjnego. Kiedy mówisz „csc * .cs”, kolejność, w jakiej system operacyjny wyświetla listę pasujących plików, jest szczegółem implementacji systemu operacyjnego; kompilator nie sortuje tej listy w kolejności kanonicznej.

ta.speot.is
źródło
Nie powinno być trudne, aby zbudowana odtwarzalność była powtarzalna (poza kilkoma łatwymi do odrzucenia polami, takimi jak czas kompilacji i GUID zestawu). Na przykład sortowanie plików wejściowych w porządku kanonicznym jest jedno-liniowe. Nawet ten identyfikator GUID może być skrótem pozostałej części zestawu, a nie nowo wygenerowanym.
CodesInChaos
Zakładam, że masz na myśli kompilator Microsoft C #, czy jest to wymóg specyfikacji?
David
@David Specyfikacja CLI tego wymaga. Kompilator C # Mono musiałby zrobić to samo. To samo dotyczy dowolnego kompilatora VB .NET.
ta.speot.is
4
Standard ECMA nie musi zawierać znaczników czasu ani różnic MVID. Bez nich możliwe jest co najmniej identyczne pliki binarne w języku C #. Dlatego głównym powodem jest wątpliwa decyzja projektowa, a nie rzeczywiste ograniczenie techniczne.
Shiv
7
  • -frandom-seed=123kontroluje losowość wewnętrzną GCC. man gccmówi:

    Ta opcja zapewnia ziarno, którego GCC używa zamiast liczb losowych do generowania niektórych nazw symboli, które muszą być różne w każdym skompilowanym pliku. Służy również do umieszczania unikatowych znaczków w plikach danych zasięgu i plikach obiektów, które je wytwarzają. Możesz użyć opcji -frandom-seed, aby utworzyć odtwarzalnie identyczne pliki obiektowe.

  • __FILE__: umieść źródło w stałym folderze (np. /tmp/build)

  • na __DATE__, __TIME__, __TIMESTAMP__:
    • libfaketime: https://github.com/wolfcw/libfaketime
    • zastąp te makra za pomocą -D
    • -Wdate-timelub -Werror=date-time: ostrzegaj lub zawieść, jeśli albo __TIME__, __DATE__albo __TIMESTAMP__są używane. Jądro Linux 4.4 używa go domyślnie.
  • użyj Dflagi z arlub użyj https://github.com/nh2/ar-timestamp-wiper/tree/master do wycierania znaczków
  • -fno-guess-branch-probability: starsze wersje podręczników mówią, że jest to źródło niedeterminizmu, ale już nie . Nie jestem pewien, czy jest to objęte, -frandom-seedczy nie.

Debian Reproducible buduje próby standaryzacji pakietów Debiana bajt po bajcie, a ostatnio otrzymał grant Linux Foundation . Obejmuje to coś więcej niż tylko kompilację, ale powinno być interesujące.

Buildroot ma BR2_REPRODUCIBLEopcję, która może dać pewne pomysły na poziomie pakietu, ale na tym etapie nie jest jeszcze ukończona.

Powiązane wątki:

Ciro Santilli
źródło
3

Projekt https://reproducible-builds.org/ dotyczy właśnie tego i stara się znaleźć odpowiedź na pytanie „nie, nie będą się różnić” w jak największej liczbie miejsc. NixOS i Debian mają teraz ponad 90% odtwarzalności swoich pakietów.

Jeśli skompilujesz plik binarny, a ja skompiluję plik binarny, a są one identyczne bit po bicie, to mogę być pewny, że kod źródłowy i narzędzia są tym, co określa dane wyjściowe, i że nie zakradłeś się po drodze kod trojana.

Jeśli połączymy odtwarzalność z możliwością rozruchu ze źródła czytelnego dla człowieka, jak działa http://bootstrappable.org/ , otrzymujemy system określony od podstaw przez źródło czytelne dla człowieka, i dopiero wtedy jesteśmy w punkcie, w którym możemy ufać, że wiemy, co robi system.

clacke
źródło
1
Fajne linki. Jestem fanboyem Buildroot, ale jeśli ktoś da mi konfigurację krzyżową Nix ARM, która uruchamia się na QEMU, będę szczęśliwy :-)
Ciro Santilli 事件 改造 中心 法轮功 六四 事件
Nie wspomniałem o Guixie, ponieważ nie wiem, gdzie znaleźć ich numery, ale byli przed NixOSem w pociągu odtwarzalności z narzędziami weryfikacyjnymi i tak dalej, więc jestem pewien, że są na równi lub lepsi.
clacke
2

Powiedziałbym NIE, to nie jest w 100% deterministyczne. Wcześniej współpracowałem z wersją GCC, która generuje docelowe pliki binarne dla procesora Hitachi H8.

Nie ma problemu ze znacznikiem czasu. Nawet jeśli kwestia znacznika czasu zostanie zignorowana, określona architektura procesora może pozwolić na zakodowanie tej samej instrukcji na 2 nieco inne sposoby, przy czym niektóre bity mogą mieć wartość 1 lub 0. Moje wcześniejsze doświadczenia pokazują, że wygenerowane pliki binarne były w tej samej chwili WIĘCEJ ale czasami gcc generuje pliki binarne o identycznym rozmiarze, ale niektóre bajty różnią się tylko 1 bitem, np. 0XE0 staje się 0XE1.

JavaMan
źródło
Czy doprowadziło to do różnych zachowań lub „poważnych problemów”?
Florian Straub,
1

Ogólnie nie. Najbardziej rozsądne kompilatory uwzględnią czas kompilacji w module obiektowym. Nawet jeśli zresetujesz zegar, musisz być bardzo dokładny w odniesieniu do momentu rozpoczęcia kompilacji (a następnie mieć nadzieję, że dostęp do dysku itp. Był taki sam jak wcześniej).

Daniel R. Hicks
źródło