Introdução a montagens determinísticas em C / C ++. Parte 1

A tradução do artigo foi preparada especialmente para os alunos do curso "Desenvolvedor C ++" .




O que é uma montagem determinística?


Um assembly determinístico é o processo de construção do mesmo código-fonte com o mesmo ambiente e instruções de montagem, no qual os mesmos arquivos binários são criados em qualquer caso, mesmo que sejam feitos em máquinas diferentes, em diretórios diferentes e com nomes diferentes . Às vezes, esses assemblies também são chamados de assemblies reproduzíveis ou selados, se for garantido que eles criarão os mesmos binários, mesmo ao compilar a partir de pastas diferentes.

Assembléias determinísticas não são algo que acontece por si só. Eles não são criados em projetos comuns e os motivos pelos quais isso não ocorre podem ser diferentes para cada sistema operacional ou compilador.

As montagens determinísticas devem ser garantidas para um determinado ambiente de montagem . Isso significa que algumas variáveis, como sistema operacional, versões do sistema de compilação e arquitetura de destino , presumivelmente permanecem as mesmas em diferentes compilações.

Nos últimos anos, várias organizações, como Chromium , Reprodutible builds ou Yocto , fizeram grandes esforços para obter assembléias determinísticas.

A importância das assembléias determinísticas


Há duas razões principais pelas quais assembléias determinísticas são tão importantes:

  • Segurança Alterar binários em vez de código fonte pode tornar as alterações invisíveis para os autores originais. Isso pode ser fatal em ambientes críticos para a segurança, como medicina, aviação e espaço. Resultados potencialmente idênticos para esses materiais permitem que terceiros cheguem a um consenso sobre o resultado correto.
  • Rastreabilidade e controle binário . Se você deseja ter um repositório para armazenar seus arquivos binários, provavelmente não deseja criar arquivos binários com somas de verificação aleatórias de fontes na mesma revisão. Isso pode fazer com que o sistema de repositório armazene binários diferentes como versões diferentes quando eles devem ser os mesmos. Por exemplo, se você trabalha no Windows ou MacOS, a biblioteca possui campos com o horário de criação / modificação dos arquivos de objeto incluídos, o que levará a diferenças nos arquivos binários.

Arquivos binários envolvidos no processo de construção em C / C ++


Existem vários tipos de binários criados durante o processo de compilação no C / C ++, dependendo do sistema operacional.

Microsoft Windows Os mais importantes são os arquivos com as extensões .obj , .lib , .lib dll e .exe . Todos eles estão em conformidade com a especificação de formato executável portátil (PE). Esses arquivos podem ser analisados ​​com ferramentas como dumpbin .
Linux Arquivos com as extensões .o , .a , .so e sem extensões (para arquivos binários executáveis) correspondem ao formato dos arquivos executáveis ​​e vinculáveis ​​(ELF). O conteúdo dos arquivos ELF pode ser analisado usando-se readelf .
Mac OS Arquivos com as extensões .o , .a , .dylib e sem extensões (para arquivos binários executáveis) estão em conformidade com a especificação de formato Mach-O. Esses arquivos podem ser verificados usando o aplicativo otool , que faz parte do kit de ferramentas Xcode no MacOS.

Fontes de Variações


Muitos fatores diferentes podem tornar suas montagens não determinísticas . Os fatores variam para diferentes sistemas operacionais e compiladores. Cada compilador possui certos parâmetros para corrigir as fontes de variação. Até o momento, gcc e clang são os compiladores que contêm mais opções para correção. Existem algumas opções não documentadas para o msvc que você pode tentar, mas no final, você provavelmente precisará corrigir os binários para obter montagens determinísticas.

Registros de data e hora adicionados pelo compilador / vinculador


Há dois motivos principais pelos quais nossos binários podem conter informações de tempo que os tornam impraticáveis:

  • Usando as __TIME__ ou __TIME__ na origem.
  • Quando um formato de arquivo obriga a armazenar informações de tempo em arquivos de objetos. É o caso do formato Portable Executable no Windows e do Mach-O no MacOS. No Linux, os arquivos ELF não codificam nenhum registro de data e hora.

Vejamos um exemplo em que essas informações terminam com a compilação de uma biblioteca estática do projeto hello world base no MacOS.

 . ├── CMakeLists.txt ├── hello_world.cpp ├── hello_world.hpp ├── main.cpp └── run_build.sh 

A biblioteca exibe uma mensagem no terminal:

 #include "hello_world.hpp" #include <iostream> void HelloWorld::PrintMessage(const std::string & message) { std::cout << message << std::endl; } 

E o aplicativo usará isso para exibir a mensagem "Hello World!":

 #include <iostream> #include "hello_world.hpp" int main(int argc, char** argv) { HelloWorld hello; hello.PrintMessage("Hello World!"); return 0; } 

Usaremos o CMake para criar o projeto:

 cmake_minimum_required(VERSION 3.0) project(HelloWorld) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_library(HelloLibA hello_world.cpp) add_library(HelloLibB hello_world.cpp) add_executable(helloA main.cpp) add_executable(helloB main.cpp) target_link_libraries(helloA HelloLibA) target_link_libraries(helloB HelloLibB) 

Criaremos duas bibliotecas diferentes com o mesmo código fonte, bem como dois arquivos binários com as mesmas fontes. Crie o projeto e execute o md5sum para ver as somas de verificação de todos os arquivos binários:

 mkdir build && cd build cmake .. make md5sum helloA md5sum helloB md5sum CMakeFiles/HelloLibA.dir/hello_world.cpp.o md5sum CMakeFiles/HelloLibB.dir/hello_world.cpp.o md5sum libHelloLibA.a md5sum libHelloLibB.a 

Temos uma conclusão como esta:

 b5dce09c593658ee348fd0f7fae22c94 helloA b5dce09c593658ee348fd0f7fae22c94 helloB 0a4a0de3df8cc7f053f2fcb6d8b75e6d CMakeFiles/HelloLibA.dir/hello_world.cpp.o 0a4a0de3df8cc7f053f2fcb6d8b75e6d CMakeFiles/HelloLibB.dir/hello_world.cpp.o adb80234a61bb66bdc5a3b4b7191eac7 libHelloLibA.a 5ac3c70d28d9fdd9c6571e077131545e libHelloLibB.a 

Isso é interessante porque os helloB e helloB têm as mesmas somas de verificação, assim como os arquivos de objetos intermediários do Mach-O hello_world.cpp.o , mas isso não pode ser dito para arquivos com a extensão .a . Isso ocorre porque eles armazenam informações sobre arquivos de objetos intermediários em um formato de arquivo morto. O cabeçalho desse formato inclui um campo chamado st_time definido pela chamada do sistema stat . Verifique libHelloLibA.a e libHelloLibB.a usando otool para mostrar os cabeçalhos:

 > otool -a libHelloLibA.a Archive : libHelloLibA.a 0100644 503/20 612 1566927276 #1/20 0100644 503/20 13036 1566927271 #1/28 > otool -a libHelloLibB.a Archive : libHelloLibB.a 0100644 503/20 612 1566927277 #1/20 0100644 503/20 13036 1566927272 #1/28 

Vemos que o arquivo contém vários campos temporários que tornam nossa montagem não determinística. Observe que esses campos não se aplicam ao arquivo executável final porque eles têm a mesma soma de verificação. Esse problema também pode ocorrer ao criar no Windows com o Visual Studio, mas com um arquivo PE em vez do Mach-O.

Nesse ponto, podemos tentar piorar as coisas e fazer com que nossos binários também não sejam deterministas. Altere o arquivo main.cpp para incluir a macro __TIME__ :

 #include <iostream> #include "hello_world.hpp" int main(int argc, char** argv) { HelloWorld hello; hello.PrintMessage("Hello World!"); std::cout << "At time: " << __TIME__ << std::endl; return 0; } 

Verifique as somas de verificação dos arquivos novamente:

 625ecc7296e15d41e292f67b57b04f15 helloA 20f92d2771a7d2f9866c002de918c4da helloB 0a4a0de3df8cc7f053f2fcb6d8b75e6d CMakeFiles/HelloLibA.dir/hello_world.cpp.o 0a4a0de3df8cc7f053f2fcb6d8b75e6d CMakeFiles/HelloLibB.dir/hello_world.cpp.o b7801c60d3bc4f83640cadc1183f43b3 libHelloLibA.a 4ef6cae3657f2a13ed77830953b0aee8 libHelloLibB.a 

Vemos que agora temos binários diferentes. Poderíamos analisar o executável com uma ferramenta como um difoscópio , que mostra a diferença entre dois arquivos binários:

 > diffoscope helloA helloB --- helloA +++ helloB ├── otool -arch x86_64 -tdvV {} │┄ Code for architecture x86_64 │ @@ -16,15 +16,15 @@ │ 00000001000018da jmp 0x1000018df00000001000018df leaq -0x30(%rbp), %rdi │ 00000001000018e3 callq 0x100002d54 ## symbol stub for: __ZNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEED1Ev │ 00000001000018e8 movq 0x1721(%rip), %rdi ## literal pool symbol address: __ZNSt3__14coutE │ 00000001000018ef leaq 0x162f(%rip), %rsi ## literal pool for: "At time: " │ 00000001000018f6 callq 0x100002d8a ## symbol stub for: __ZNSt3__1lsINS_11char_traitsIcEEEERNS_13basic_ostreamIcT_EES6_PKc │ 00000001000018fb movq %rax, %rdi │ -00000001000018fe leaq 0x162a(%rip), %rsi ## literal pool for: "19:40:47" │ +00000001000018fe leaq 0x162a(%rip), %rsi ## literal pool for: "19:40:48" │ 0000000100001905 callq 0x100002d8a ## symbol stub for: __ZNSt3__1lsINS_11char_traitsIcEEEERNS_13basic_ostreamIcT_EES6_PKc │ 000000010000190a movq %rax, %rdi │ 000000010000190d leaq __ZNSt3__1L4endlIcNS_11char_traitsIcEEEERNS_13basic_ostreamIT_T0_EES7_(%rip), %rsi # 

Isso mostra que __TIME__ informações __TIME__ foram coladas no binário, o que as torna não determinísticas. Vamos ver o que pode ser feito para evitar isso.

Soluções possíveis para o Microsoft Visual Studio


O Microsoft Visual Studio possui um sinalizador / sinalizador Brepro que não está documentado pela Microsoft. Esse sinalizador define os carimbos de data e hora do formato Portable Executable como -1, como pode ser visto na figura abaixo.



Para ativar esse sinalizador com o CMake, precisamos adicionar as seguintes linhas ao criar o .exe :

 add_link_options("/Brepro") 

ou essas linhas para .lib

 set_target_properties( TARGET PROPERTIES STATIC_LIBRARY_OPTIONS "/Brepro" ) 

O problema é que esse sinalizador torna os binários reproduzíveis (em relação aos carimbos de data e hora no formato de arquivo) em nosso arquivo .exe final binário, mas não remove todos os carimbos de hora do .lib (o mesmo problema dos arquivos de objeto do Mach-O, sobre o qual falamos acima). O campo TimeDateStamp do arquivo de cabeçalho COFF para arquivos .lib permanecerá. A única maneira de remover essas informações do arquivo .lib binário é corrigi-lo, substituindo os bytes correspondentes ao campo TimeDateStamp por qualquer valor conhecido.

Possíveis soluções para GCC e CLANG


  • O gcc detecta a existência da variável de ambiente SOURCE_DATE_EPOCH. Se essa variável for configurada, seu valor indicará o registro de data e hora do UNIX que será usado para substituir a data e hora __DATE__ __TIME__ macros __TIME__ e __TIME__ para que os registros de data e hora __DATE__ se tornem reproduzíveis. O valor pode ser definido como um carimbo de data / hora conhecido, como a hora da última alteração nos arquivos ou pacote de origem.
  • O clang usa ZERO_AR_DATE , que, se definido, redefine o ZERO_AR_DATE data e ZERO_AR_DATE fornecido nos arquivos, configurando-o como 0. Observe que isso não corrigirá as __TIME__ ou __TIME__ . Se queremos corrigir o efeito dessa macro, precisamos corrigir os binários ou falsificar a hora do sistema.

Vamos continuar com o nosso projeto de amostra para o MacOS e ver quais serão os resultados ao definir a ZERO_AR_DATE ambiente ZERO_AR_DATE .

 export ZERO_AR_DATE=1 

Agora, se compilarmos nosso arquivo executável e bibliotecas (removendo a macro __DATE__ nas fontes), obteremos:

 b5dce09c593658ee348fd0f7fae22c94 helloA b5dce09c593658ee348fd0f7fae22c94 helloB 0a4a0de3df8cc7f053f2fcb6d8b75e6d CMakeFiles/HelloLibA.dir/hello_world.cpp.o 0a4a0de3df8cc7f053f2fcb6d8b75e6d CMakeFiles/HelloLibB.dir/hello_world.cpp.o 9f9a9af4bb3e220e7a22fb58d708e1e5 libHelloLibA.a 9f9a9af4bb3e220e7a22fb58d708e1e5 libHelloLibB.a 

Todas as somas de verificação agora são as mesmas. .a analisar os cabeçalhos dos arquivos com a extensão .a :

 > otool -a libHelloLibA.a Archive : libHelloLibA.a 0100644 503/20 612 0 #1/20 0100644 503/20 13036 0 #1/28 > otool -a libHelloLibB.a Archive : libHelloLibB.a 0100644 503/20 612 0 #1/20 0100644 503/20 13036 0 #1/28 

Podemos ver que o campo de timestamp de timestamp e timestamp do cabeçalho da biblioteca foi definido como zero.

Chegamos sem problemas ao final da primeira parte do artigo. A continuação do material pode ser lida aqui .

Source: https://habr.com/ru/post/pt467741/


All Articles