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

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



Leia a primeira parte



As informações da pasta Assembly são distribuídas em arquivos binários


Se os mesmos arquivos de origem forem compilados em pastas diferentes, às vezes as informações da pasta são transferidas para arquivos binários. Isso pode acontecer principalmente por dois motivos:

  • Usando macros que contêm informações sobre o arquivo atual, como a macro __FILE__ .
  • Crie binários de depuração que armazenam informações sobre onde estão as fontes.

Continuando nosso exemplo de olá mundo no MacOS, vamos dividir a fonte para que possamos mostrar o efeito da localização nos binários finais. A estrutura do projeto será semelhante à abaixo.

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

Vamos coletar nossos arquivos binários no modo de depuração.

 cd srcA/build cmake -DCMAKE_BUILD_TYPE=Debug .. make cd .. && cd .. cd srcB/build cmake -DCMAKE_BUILD_TYPE=Debug .. make cd .. && cd .. md5sum srcA/build/hello md5sum srcB/build/hello md5sum srcA/build/CMakeFiles/HelloLib.dir/hello_world.cpp.o md5sum srcB/build/CMakeFiles/HelloLib.dir/hello_world.cpp.o md5sum srcA/build/libHelloLib.a md5sum srcB/build/libHelloLib.a     : 3572a95a8699f71803f3e967f92a5040 srcA/build/hello 7ca693295e62de03a1bba14853efa28c srcB/build/hello 76e0ae7c4ef79ec3be821ccf5752730f srcA/build/CMakeFiles/HelloLib.dir/hello_world.cpp.o 5ef044e6dcb73359f46d48f29f566ae5 srcB/build/CMakeFiles/HelloLib.dir/hello_world.cpp.o dc941156608b578c91e38f8ecebfef6d srcA/build/libHelloLib.a 1f9697ef23bf70b41b39ef3469845f76 srcB/build/libHelloLib.a 

As informações sobre a pasta são transferidas dos arquivos de objeto para os arquivos executáveis ​​finais, o que torna nossos assemblies improdutíveis. Podemos ver as diferenças entre os binários usando um difoscópio para ver onde as informações da pasta estão incorporadas.

 > diffoscope helloA helloB --- srcA/build/hello +++ srcB/build/hello @@ -1282,20 +1282,20 @@ ... 00005070: 5f77 6f72 6c64 5f64 6562 7567 2f73 7263 _world_debug/src -00005080: 412f 006d 6169 6e2e 6370 7000 2f55 7365 A/.main.cpp./Use +00005080: 422f 006d 6169 6e2e 6370 7000 2f55 7365 B/.main.cpp./Use 00005090: 7273 2f63 6172 6c6f 732f 446f 6375 6d65 rs/carlos/Docume 000050a0: 6e74 732f 6465 7665 6c6f 7065 722f 7265 nts/developer/re 000050b0: 7072 6f64 7563 6962 6c65 2d62 7569 6c64 producible-build 000050c0: 732f 7361 6e64 626f 782f 6865 6c6c 6f5f s/sandbox/hello_ -000050d0: 776f 726c 645f 6465 6275 672f 7372 6341 world_debug/srcA +000050d0: 776f 726c 645f 6465 6275 672f 7372 6342 world_debug/srcB 000050e0: 2f62 7569 6c64 2f43 4d61 6b65 4669 6c65 /build/CMakeFile 000050f0: 732f 6865 6c6c 6f2e 6469 722f 6d61 696e s/hello.dir/main 00005100: 2e63 7070 2e6f 005f 6d61 696e 005f 5f5a .cpp.o._main.__Z ... @@ -1336,15 +1336,15 @@ ... 000053c0: 6962 6c65 2d62 7569 6c64 732f 7361 6e64 ible-builds/sand 000053d0: 626f 782f 6865 6c6c 6f5f 776f 726c 645f box/hello_world_ -000053e0: 6465 6275 672f 7372 6341 2f62 7569 6c64 debug/srcA/build +000053e0: 6465 6275 672f 7372 6342 2f62 7569 6c64 debug/srcB/build 000053f0: 2f6c 6962 4865 6c6c 6f4c 6962 2e61 2868 /libHelloLib.a(h 00005400: 656c 6c6f 5f77 6f72 6c64 2e63 7070 2e6f ello_world.cpp.o 00005410: 2900 5f5f 5a4e 3130 4865 6c6c 6f57 6f72 ).__ZN10HelloWor ... 

Possíveis soluções


Novamente, a decisão dependerá do compilador usado:

  • O msvc não pode definir parâmetros para evitar adicionar essas informações aos arquivos binários. A única maneira de obter binários reproduzíveis é usar a ferramenta de reparo novamente para remover essas informações durante a fase de construção. Observe que, como corrigimos binários para produzir binários reproduzíveis, as pastas usadas para diferentes montagens devem ter o mesmo comprimento em caracteres.
  • gcc possui três sinalizadores de compilador para contornar esse problema:
    • -fdebug-prefix-map=OLD=NEW pode remover prefixos de diretório das informações de depuração.
    • -fmacro-prefix-map=OLD=NEW disponível desde o gcc 8 e resolve o problema da irreprodutibilidade usando a macro __FILE__.
    • -ffile-prefix-map=OLD=NEW está disponível desde o gcc 8 e é uma união de -fdebug-prefix-map e -fmacro-prefix-map
  • clang suporta -fdebug-prefix-map=OLD=NEW desde a versão 3.8 e está trabalhando no suporte de outros dois sinalizadores para versões futuras.

A melhor maneira de resolver esse problema é adicionar sinalizadores às opções do compilador. Ao usar o CMake:

 target_compile_options(target PUBLIC "-ffile-prefix-map=${CMAKE_SOURCE_DIR}=.") 

A ordem dos arquivos no sistema de construção


A ordem dos arquivos pode ser um problema se os diretórios forem lidos para fazer uma lista de seus arquivos. Por exemplo, o Unix não possui uma ordem determinística na qual readdir () e listdir () devem retornar o conteúdo de um diretório, portanto, confiar nessas funções para alimentar o sistema de montagem pode levar a montagens não determinísticas.

O mesmo problema ocorre, por exemplo, se o sistema de construção armazena arquivos para o vinculador em um contêiner (por exemplo, em um dicionário python comum), que pode retornar elementos em uma ordem não determinística. Isso fará com que os arquivos sejam vinculados em uma ordem diferente a cada vez, e diferentes arquivos binários serão criados.

Podemos simular esse problema reorganizando os arquivos no CMake. Se modificarmos o exemplo anterior para ter mais de um arquivo de origem para a biblioteca:

 . ├── CMakeLists.txt ├── CMakeListsA.txt ├── CMakeListsB.txt ├── hello_world.cpp ├── hello_world.hpp ├── main.cpp ├── sources0.cpp ├── sources0.hpp ├── sources1.cpp ├── sources1.hpp ├── sources2.cpp └── sources2.hpp 

Podemos ver que os resultados da compilação são diferentes se CMakeLists.txt a ordem dos arquivos em CMakeLists.txt :

 cmake_minimum_required(VERSION 3.0) project(HelloWorld) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_library(HelloLib hello_world.cpp sources0.cpp sources1.cpp sources2.cpp) add_executable(hello main.cpp) target_link_libraries(hello HelloLib) 

Se fizermos dois assemblies consecutivos com os nomes A e B, trocando o sources0.cpp e o sources0.cpp na lista de arquivos, obteremos as seguintes somas de verificação:

 30ab264d6f8e1784282cd1a415c067f2 helloA cdf3c9dd968f7363dc9e8b40918d83af helloB 707c71bc2a8def6885b96fb67b84d79c hello_worldA.cpp.o 707c71bc2a8def6885b96fb67b84d79c hello_worldB.cpp.o 694ff3765b688e6faeebf283052629a3 sources0A.cpp.o 694ff3765b688e6faeebf283052629a3 sources0B.cpp.o 0db24dc6a94da1d167c68b96ff319e56 sources1A.cpp.o 0db24dc6a94da1d167c68b96ff319e56 sources1B.cpp.o fd0754d9a4a44b0fcc4e4f3c66ad187c sources2A.cpp.o fd0754d9a4a44b0fcc4e4f3c66ad187c sources2B.cpp.o baba9709d69c9e5fd51ad985ee328172 libHelloLibA.a 72641dc6fc4f4db04166255f62803353 libHelloLibB.a 

Os arquivos .o objeto são idênticos, mas as bibliotecas e executáveis ​​.a não são. Isso ocorre porque a ordem de inserção na biblioteca depende da ordem em que os arquivos estão listados.

Compilador criado aleatoriedade


Esse problema ocorre, por exemplo, no gcc quando as otimizações de tempo do link (sinalizador -flto ) estão -flto . Esta opção introduz nomes gerados aleatoriamente em arquivos binários. A única maneira de evitar esse problema é usar a flag - frandom-seed . Esta opção fornece uma semente que o gcc usa em vez de números aleatórios. É usado para gerar nomes de símbolos específicos, que devem ser diferentes em cada arquivo compilado. Também é usado para colocar carimbos exclusivos em arquivos de cobertura de dados e arquivos de objetos que os produzem. Este parâmetro deve ser diferente para cada arquivo de origem. Uma opção é definir a soma de verificação do arquivo para que a probabilidade de colisão seja muito baixa. Por exemplo, no CMake, isso pode ser feito usando uma função como esta:

 set(LIB_SOURCES ./src/source1.cpp ./src/source2.cpp ./src/source3.cpp) foreach(_file ${LIB_SOURCES}) file(SHA1 ${_file} checksum) string(SUBSTRING ${checksum} 0 8 checksum) set_property(SOURCE ${_file} APPEND_STRING PROPERTY COMPILE_FLAGS "-frandom-seed=0x${checksum}") endforeach() 

Algumas dicas para usar o Conan


Os ganchos Conan podem nos ajudar a tornar nossas construções reproduzíveis. Esse recurso permite que você personalize o comportamento do cliente em determinados pontos.

Uma maneira de usar ganchos pode ser definir variáveis ​​de ambiente no estágio pre_build . No exemplo abaixo, a função set_environment é set_environment e, em seguida, o ambiente é restaurado na etapa reset_environment usando reset_environment .

 def set_environment(self): if self._os == "Linux": self._old_source_date_epoch = os.environ.get("SOURCE_DATE_EPOCH") timestamp = "1564483496" os.environ["SOURCE_DATE_EPOCH"] = timestamp self._output.info( "set SOURCE_DATE_EPOCH: {}".format(timestamp)) elif self._os == "Macos": os.environ["ZERO_AR_DATE"] = "1" self._output.info( "set ZERO_AR_DATE: {}".format(timestamp)) def reset_environment(self): if self._os == "Linux": if self._old_source_date_epoch is None: del os.environ["SOURCE_DATE_EPOCH"] else: os.environ["SOURCE_DATE_EPOCH"] = self._old_source_date_epoch elif self._os == "Macos": del os.environ["ZERO_AR_DATE"] 

Ganchos também podem ser úteis para corrigir binários no estágio post_build . Existem várias ferramentas para analisar e corrigir arquivos binários, como pefile , pefile , pe-parse ou strip-nondeterminism . Um exemplo de gancho para corrigir um binário PE usando o ducible pode ser:

 class Patcher(object): ... def patch(self): if self._os == "Windows" and self._compiler == "Visual Studio": for root, _, filenames in os.walk(self._conanfile.build_folder): for filename in filenames: filename = os.path.join(root, filename) if ".exe" in filename or ".dll" in filename: self._patch_pe(filename) def _patch_pe(self, filename): patch_tool_location = "C:/ducible/ducible.exe" if os.path.isfile(patch_tool_location): self._output.info("Patching {} with md5sum: {}".format(filename,md5sum(filename))) self._conanfile.run("{} {}".format(patch_tool_location, filename)) self._output.info("Patched file: {} with md5sum: {}".format(filename,md5sum(filename))) ... def pre_build(output, conanfile, **kwargs): lib_patcher.init(output, conanfile) lib_patcher.set_environment() def post_build(output, conanfile, **kwargs): lib_patcher.patch() lib_patcher.reset_environment() 

Conclusões


Assemblies determinísticos são uma tarefa complexa, intimamente relacionada ao sistema operacional e ao kit de ferramentas usado. Esta introdução deveria ajudar a entender as causas mais comuns de falta de determinismo e como abordá-las.

Referências


Informação geral



As ferramentas


Ferramentas de comparação binária


Ferramentas de reparo de arquivo


Ferramentas de análise de arquivo


Leia a primeira parte

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


All Articles