A recompilação de um programa produz um binário idêntico bit a bit?

25

Se eu fosse compilar um programa em um único binário, criar uma soma de verificação e recompilar na mesma máquina com as mesmas configurações de compilador e compilador e soma de verificação do programa recompilado, a soma de verificação falharia?

Se sim, por que isso? Caso contrário, ter uma CPU diferente resultaria em um binário não idêntico?

David
fonte
8
Depende do compilador. Alguns deles incorporam carimbos de data / hora, então a resposta é "não" para eles.
ta.speot.is
Na verdade, depende do formato executável , não do compilador. Alguns formatos executáveis, como o formato PE do Windows, incluem um carimbo de data / hora que é tocado na data e hora da compilação, enquanto outros formatos como o formato ELF do Linux não. De qualquer forma, esta questão depende da definição de "binário idêntico". A própria imagem será / deverá ser idêntica em bits se o mesmo arquivo de origem for compilado com o mesmo compilador e bibliotecas e opções e tudo mais, mas o cabeçalho e outros metadados podem variar.
Synetech

Respostas:

19
  1. Compile o mesmo programa com as mesmas configurações na mesma máquina:

    Embora a resposta definitiva seja "depende", é razoável esperar que a maioria dos compiladores seja determinística na maior parte do tempo e que os binários produzidos sejam idênticos. De fato, alguns sistemas de controle de versão dependem disso. Ainda assim, sempre há exceções; é bem possível que algum compilador em algum lugar decida inserir um carimbo de data / hora ou algo parecido (iirc, Delphi, por exemplo). Ou o próprio processo de compilação pode fazer isso; Eu já vi makefiles para programas C que definem uma macro de pré-processador para o timestamp atual. (Eu acho que isso contaria como sendo uma configuração diferente do compilador.)

    Além disso, esteja ciente de que se você vincular estaticamente o binário, estará incorporando efetivamente o estado de todas as bibliotecas relevantes em sua máquina, e qualquer alteração em qualquer uma delas também afetará seu binário. Portanto, não são apenas as configurações do compilador que são relevantes.

  2. Compile o mesmo programa em uma máquina diferente com uma CPU diferente.

    Aqui, todas as apostas estão fora. A maioria dos compiladores modernos são capazes de fazer otimizações específicas de destino; se essa opção estiver ativada, é provável que os binários sejam diferentes, a menos que as CPUs sejam semelhantes (e mesmo assim, é possível). Além disso, consulte a nota acima sobre links estáticos: o ambiente de configuração vai muito além das configurações do compilador. A menos que você tenha um controle de configuração muito rigoroso, é extremamente provável que algo difira entre as duas máquinas.

rici
fonte
1
Digamos que eu estivesse usando o GCC e não estivesse usando a opção march (a opção que otimiza o binário para uma família específica de CPUs), e eu deveria compilar um binário com uma CPU e, em seguida, com outra CPU haveria um diferença?
David
1
@ David: Ainda depende. Primeiro, as bibliotecas às quais você está vinculando podem ter construções específicas da arquitetura. Portanto, a saída de gcc -cpode muito bem ser idêntica, mas as versões vinculadas diferem. Além disso, não é apenas -march; existe também -mtune/-mcpu e -mfpmatch(e possivelmente outros). Alguns deles podem ter padrões diferentes em instalações diferentes; portanto, você pode precisar forçar explicitamente o pior caso possível para suas máquinas; isso pode reduzir significativamente o desempenho, principalmente se você reverter para o i386 sem a sse. E, claro, se um de seus CPUs é um braço eo outro um i686 ...
rici
1
Além disso, o GCC é um dos compiladores em questão que adiciona um carimbo de data / hora aos binários?
David
@ David: afaik, não.
rici 1/09/09
8

O que você está perguntando é "é a saída determinística ". Se você compilou o programa uma vez, imediatamente o compilou novamente, provavelmente terminaria com o mesmo arquivo de saída. No entanto, se alguma coisa mudar - mesmo uma pequena alteração - especialmente em um componente usado pelo programa compilado, a saída do compilador também poderá ser alterada.

headkase
fonte
2
Muito bom ponto de fato. Este artigo tem algumas observações muito interessantes. Em particular, a compilação com o GCC pode não ser determinística em relação a entradas em certos casos, por exemplo, como ele gerencia funções em namespaces anônimos, para os quais utiliza internamente um gerador de números aleatórios. Para obter o determinismo, neste caso particular, fornecer uma semente aleatória inicial especificando a opção -frandom-seed=string.
Ack
7

A recompilação de um programa produz um binário idêntico bit a bit?

Para todos os compiladores? Não. O compilador C #, pelo menos, não tem permissão.

Eric Lippert tem um detalhamento completo sobre por que a saída do compilador não é determinística .

[O] compilador C # por design nunca produz o mesmo binário duas vezes. O compilador C # incorpora um GUID recém-gerado em todos os assemblies, toda vez que você o executa, garantindo assim que nenhum dois assemblies sejam idênticos bit a bit. Para citar a especificação da CLI:

A coluna Mvid deve indexar um GUID [...] exclusivo que identifica esta instância do módulo. [...] O Mvid deve ser gerado recentemente para cada módulo [...] Enquanto o próprio [runtime] não faz uso do Mvid, outras ferramentas (como depuradores) [...] contam com o fato de que o Mvid quase sempre difere de um módulo para outro.

Embora seja específico para uma versão do compilador C #, muitos pontos do artigo podem ser aplicados a qualquer compilador.

Primeiro, estamos assumindo que sempre obtemos a mesma lista de arquivos sempre, na mesma ordem. Mas, em alguns casos, isso depende do sistema operacional. Quando você diz "csc * .cs", a ordem na qual o sistema operacional oferece a lista de arquivos correspondentes é um detalhe de implementação do sistema operacional; o compilador não classifica essa lista em uma ordem canônica.

ta.speot.is
fonte
Não deve ser difícil tornar o reprodutível compilado (além de alguns campos facilmente descartáveis, como tempo de compilação e o GUID do assembly). Por exemplo, classificar os arquivos de entrada em uma ordem canônica é uma linha. Até esse GUID pode ser um hash do restante do assembly, em vez de ser gerado recentemente.
CodesInChaos
Suponho que você queira dizer o compilador Microsoft C #, ou é um requisito da especificação?
David
@ David A especificação da CLI exige. O compilador C # da Mono teria que fazer o mesmo. O mesmo vale para qualquer compilador VB .NET.
ta.speot.is
4
O padrão ECMA não precisa ter registros de data e hora ou diferenças de MVID. Sem esses, é pelo menos possível para binários idênticos em C #. Portanto, o principal motivo é uma decisão de projeto questionável e não uma restrição técnica real.
Shiv
7
  • -frandom-seed=123controla alguma aleatoriedade interna do GCC. man gccdiz:

    Essa opção fornece uma semente que o GCC usa no lugar de números aleatórios na geração de certos nomes de símbolos que precisam ser diferentes em cada arquivo compilado. Também é usado para colocar carimbos exclusivos nos arquivos de dados de cobertura e nos arquivos de objetos que os produzem. Você pode usar a opção -frandom-seed para produzir arquivos de objeto reproduzíveis idênticos.

  • __FILE__: coloque a fonte em uma pasta fixa (por exemplo /tmp/build)

  • para __DATE__, __TIME__, __TIMESTAMP__:
    • libfaketime: https://github.com/wolfcw/libfaketime
    • substituir essas macros por -D
    • -Wdate-timeou -Werror=date-time: avisar ou falhar se quer __TIME__, __DATE__ou __TIMESTAMP__se é usado. O kernel do Linux 4.4 usa-o por padrão.
  • use a Dbandeira com arou https://github.com/nh2/ar-timestamp-wiper/tree/master para limpar carimbos
  • -fno-guess-branch-probability: versões manuais mais antigas dizem que é uma fonte de não-determinismo, mas não é mais . Não tenho certeza se isso é coberto -frandom-seedou não.

O projeto Debian Reproducible cria tentativas de padronizar pacotes Debian byte a byte, e recentemente recebeu uma concessão da Linux Foundation . Isso inclui mais do que apenas compilação, mas deve ser de interesse.

O Buildroot tem uma BR2_REPRODUCIBLEopção que pode fornecer algumas idéias no nível do pacote, mas ainda está longe de ser concluída.

Tópicos relacionados:

Ciro Santilli adicionou uma nova foto
fonte
3

O projeto https://reproducible-builds.org/ trata disso e está se esforçando para responder a sua pergunta "não, eles não diferem" no maior número possível de lugares. NixOS e Debian agora têm mais de 90% de reprodutibilidade para seus pacotes.

Se você compilar um binário, e eu compilar um binário, e eles forem bit a bit idênticos, posso ter certeza de que o código-fonte e as ferramentas são as que determinam a saída e que você não se esgueirou em alguns código de tróia ao longo do caminho.

Se combinarmos a reprodutibilidade com a capacidade de inicialização de fontes legíveis por humanos, como http://bootstrappable.org/ está trabalhando, obteremos um sistema determinado desde o início por fontes legíveis por humanos, e somente então estaremos em um ponto em que podemos confiar que sabemos o que o sistema está fazendo.

clacke
fonte
1
Links legais. Sou fanboy do Buildroot, mas se alguém me der uma configuração de arco cruzado Nix ARM que inicialize no QEMU, ficarei feliz :-)
Ciro Santilli 事件 改造 中心 法轮功 六四 事件
Eu não mencionei o Guix porque não sei onde encontrar seus números, mas eles estavam antes do NixOS no trem de reprodutibilidade com ferramentas de verificação e outras coisas, por isso tenho certeza de que estão em pé de igualdade ou melhor.
clacke 07/07
2

Eu diria que NÃO, não é 100% determinístico. Anteriormente, trabalhei com uma versão do GCC que gera binários de destino para o processador Hitachi H8.

Não é um problema com o carimbo de data / hora. Mesmo que o problema de registro de data e hora seja ignorado, a arquitetura específica do processador pode permitir que a mesma instrução seja codificada de duas maneiras ligeiramente diferentes, onde alguns bits podem ser 1 ou 0. Minha experiência anterior mostra que os binários gerados eram os mesmos na MAIORIA do tempo mas, ocasionalmente, o gcc geraria binários com tamanho idêntico, mas alguns bytes são diferentes em apenas 1 bit, por exemplo, 0XE0 se torna 0XE1.

JavaMan
fonte
E isso levou a comportamentos diferentes ou a "problemas sérios"?
Florian Straub
1

Em geral, não. A maioria dos compiladores razoavelmente sofisticados incluirá o tempo de compilação no módulo de objeto. Mesmo se você redefinisse o relógio, teria que ser muito preciso em relação ao momento em que iniciou a compilação (e então espere que os acessos ao disco, etc., tenham a mesma velocidade de antes).

Daniel R Hicks
fonte