La traducción del artículo fue preparada especialmente para estudiantes del curso "Desarrollador C ++" .
→
Leer la primera parte
La información de la carpeta de ensamblaje se distribuye en archivos binarios.
Si los mismos archivos fuente se compilan en diferentes carpetas, a veces la información de la carpeta se transfiere a archivos binarios. Esto puede suceder principalmente por dos razones:
- Usar macros que contienen información sobre el archivo actual, como la macro
__FILE__
. - Cree binarios de depuración que almacenen información sobre dónde están las fuentes.
Continuando con nuestro ejemplo de hello world en MacOS, dividamos la fuente para que podamos mostrar el efecto de la ubicación en los binarios finales. La estructura del proyecto será similar a la siguiente.
. ├── 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
Recopilemos nuestros archivos binarios en modo de depuración.
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
La información sobre la carpeta se transfiere de los archivos de objeto a los archivos ejecutables finales, lo que hace que nuestros ensamblajes sean irreproducibles. Podemos ver las diferencias entre los binarios usando un difoscopio para ver dónde está incrustada la información de la carpeta.
> 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 ...
Posibles soluciones
Nuevamente, la decisión dependerá del compilador utilizado:
- msvc no puede establecer parámetros para evitar agregar esta información a archivos binarios. La única forma de obtener archivos binarios reproducibles es usar la herramienta de reparación nuevamente para eliminar esta información durante la fase de compilación. Tenga en cuenta que, dado que arreglamos binarios para producir binarios reproducibles, las carpetas utilizadas para diferentes ensamblajes deben tener la misma longitud en caracteres.
gcc
tiene tres indicadores de compilación para solucionar este problema:
-fdebug-prefix-map=OLD=NEW
puede eliminar los prefijos de directorio de la información de depuración.-fmacro-prefix-map=OLD=NEW
disponible desde gcc 8 y resuelve el problema de irreproducibilidad utilizando la macro __FILE__.-ffile-prefix-map=OLD=NEW
está disponible desde gcc 8 y es una unión de -fdebug-prefix-map y -fmacro-prefix-map
clang
admitido -fdebug-prefix-map=OLD=NEW
desde la versión 3.8 y está trabajando para admitir otros dos indicadores para futuras versiones.
La mejor manera de resolver este problema es agregar banderas a las opciones del compilador. Cuando use CMake:
target_compile_options(target PUBLIC "-ffile-prefix-map=${CMAKE_SOURCE_DIR}=.")
El orden de los archivos en el sistema de compilación
El orden de los archivos puede ser un problema si los directorios se leen para hacer una lista de sus archivos. Por ejemplo, Unix no tiene un orden determinista en el que readdir () y listdir () deben devolver el contenido de un directorio, por lo que confiar en estas funciones para alimentar el sistema de ensamblaje puede conducir a ensamblajes no deterministas.
El mismo problema ocurre, por ejemplo, si su sistema de compilación almacena archivos para el enlazador en un contenedor (por ejemplo, en un diccionario de Python normal), que puede devolver elementos en un orden no determinista. Esto hará que los archivos se vinculen en un orden diferente cada vez, y se crearán diferentes archivos binarios.
Podemos simular este problema reorganizando los archivos en CMake. Si modificamos el ejemplo anterior para tener más de un archivo fuente para la 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 los resultados de la compilación son diferentes si cambiamos el orden de los archivos en
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)
Si hacemos dos ensamblajes consecutivos con los nombres A y B, intercambiando
sources0.cpp
y
sources0.cpp
en la lista de archivos, obtenemos las siguientes sumas de verificación:
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
Los archivos
.o
objeto son idénticos, pero las bibliotecas y ejecutables .a no lo son. Esto se debe a que el orden de inserción en la biblioteca depende del orden en que se enumeran los archivos.
Compilador creado aleatoriedad
Este problema ocurre, por ejemplo, en gcc cuando se
-flto
Optimizaciones de tiempo de enlace (indicador
-flto
). Esta opción introduce nombres generados aleatoriamente en archivos binarios. La única forma de evitar este problema es usar la bandera -
frandom-seed
. Esta opción proporciona una semilla que usa gcc en lugar de números aleatorios. Se utiliza para generar nombres de símbolos específicos, que deben ser diferentes en cada archivo compilado. También se usa para colocar sellos únicos en archivos de cobertura de datos y archivos de objetos que los producen. Este parámetro debe ser diferente para cada archivo fuente. Una opción es establecer la suma de verificación del archivo para que la probabilidad de colisión sea muy baja. Por ejemplo, en CMake, esto se puede hacer usando esta función:
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()
Algunos consejos para usar Conan
Conan
Hooks puede ayudarnos a hacer que nuestras compilaciones sean reproducibles. Esta característica le permite personalizar el comportamiento del cliente en ciertos puntos.
Una forma de usar ganchos puede ser establecer variables de entorno en la etapa
pre_build
. En el ejemplo a continuación, se
set_environment
función
set_environment
y luego se restaura el entorno en el paso
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"]
Los ganchos también pueden ser útiles para arreglar binarios en la etapa
post_build
. Existen varias herramientas para analizar y corregir archivos binarios, como
ducible
,
pefile
,
pe-parse
o
strip-nondeterminism
. Un gancho de ejemplo para arreglar un binario de PE usando
ducible
podría 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()
Conclusiones
Los ensamblajes deterministas son una tarea compleja, estrechamente relacionada con el sistema operativo y el kit de herramientas utilizado. Se suponía que esta introducción ayudaría a comprender las causas más comunes de falta de determinismo y cómo abordarlas.
Referencias
Información general
Las herramientas
Herramientas de comparación binariaHerramientas de reparación de archivosHerramientas de análisis de archivos→
Leer la primera parte