La traducción del artículo fue preparada especialmente para estudiantes del curso "Desarrollador C ++" .
¿Qué es una asamblea determinista?
Un ensamblaje determinista es el proceso de construir el mismo código fuente con el mismo entorno e instrucciones de ensamblaje, en el que se crean los mismos archivos binarios en cualquier caso, incluso si están hechos en diferentes máquinas, en diferentes directorios y con diferentes nombres . Tales ensamblajes también se denominan a veces ensamblables jugables o sellados si se garantiza que crearán los mismos binarios incluso cuando se compilan desde diferentes carpetas.
Las asambleas deterministas no son algo que sucede por sí solo. No se crean en proyectos ordinarios, y las razones por las que esto no sucede pueden ser diferentes para cada sistema operativo o compilador.
Los ensamblajes deterministas deben estar garantizados para un
entorno de ensamblaje dado. Esto significa que algunas variables, como
el sistema operativo, las versiones del sistema de compilación y la arquitectura de destino , presumiblemente permanecen iguales en diferentes compilaciones.
En los últimos años, varias organizaciones, como
Chromium ,
Construcciones reproducibles o
Yocto , han realizado grandes esfuerzos para lograr ensamblajes deterministas.
La importancia de las asambleas deterministas
Hay dos razones principales por las que los conjuntos deterministas son tan importantes:
- Seguridad Cambiar los binarios en lugar del código fuente puede hacer que los cambios sean invisibles para los autores originales. Esto puede ser fatal en entornos críticos para la seguridad, como la medicina, la aviación y el espacio. Resultados potencialmente idénticos para estos materiales permiten a terceros llegar a un consenso sobre el resultado correcto.
- Trazabilidad y control binario . Si desea tener un repositorio para almacenar sus archivos binarios, lo más probable es que no desee crear archivos binarios con sumas de verificación aleatorias de las fuentes en la misma revisión. Esto puede hacer que el sistema de repositorio almacene diferentes archivos binarios como versiones diferentes cuando deberían ser iguales. Por ejemplo, si trabaja en Windows o MacOS, la biblioteca tiene campos con el tiempo de creación / modificación de los archivos de objeto incluidos, lo que conducirá a diferencias en los archivos binarios.
Archivos binarios involucrados en el proceso de compilación en C / C ++
Existen varios tipos de archivos binarios que se crean durante el proceso de compilación en C / C ++, según el sistema operativo.
Microsoft Windows Los más importantes son los archivos con las extensiones
.obj
,
.lib
,
.lib
dll
y
.exe
. Todos cumplen con la especificación del formato ejecutable portátil (PE). Estos archivos se pueden analizar con herramientas como
dumpbin .
Linux Los archivos con las extensiones
.o
,
.a
,
.so
y sin extensiones (para archivos binarios ejecutables) corresponden al formato de archivos ejecutables y vinculables (ELF). El contenido de los archivos ELF se puede analizar con
readelf .
Mac OS Los archivos con las extensiones
.o
,
.a
,
.dylib
y sin extensiones (para archivos binarios ejecutables) cumplen con la especificación de formato Mach-O. Estos archivos se pueden verificar utilizando la aplicación
otool , que forma parte del kit de herramientas
Xcode en MacOS.
Fuentes de variaciones
Muchos factores diferentes pueden hacer que sus ensamblajes
no sean deterministas . Los factores variarán para los diferentes sistemas operativos y compiladores. Cada compilador tiene ciertos parámetros para corregir las fuentes de variación. Hasta la fecha,
gcc
y
clang
son los compiladores que contienen más opciones de reparación. Hay algunas opciones no documentadas para
msvc
que puede probar, pero al final, probablemente tenga que arreglar los binarios para obtener ensamblajes deterministas.
Marcas de tiempo agregadas por compilador / enlazador
Hay dos razones principales por las cuales nuestros archivos binarios pueden contener información de tiempo que los hace imposibles de reproducir:
- Usando las
__TIME__
__DATE__
o __TIME__
en la fuente. - Cuando un formato de archivo te obliga a almacenar información de tiempo en archivos de objetos. Este es el caso del formato ejecutable portátil en Windows y Mach-O en MacOS. En Linux, los archivos ELF no codifican ninguna marca de tiempo.
Veamos un ejemplo en el que esta información termina compilando una biblioteca estática del proyecto base hello world en MacOS.
. ├── CMakeLists.txt ├── hello_world.cpp ├── hello_world.hpp ├── main.cpp └── run_build.sh
La biblioteca muestra un mensaje en la terminal:
#include "hello_world.hpp" #include <iostream> void HelloWorld::PrintMessage(const std::string & message) { std::cout << message << std::endl; }
Y la aplicación usará esto para mostrar el mensaje "¡Hola Mundo!":
#include <iostream> #include "hello_world.hpp" int main(int argc, char** argv) { HelloWorld hello; hello.PrintMessage("Hello World!"); return 0; }
Usaremos CMake para construir el proyecto:
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)
Crearemos dos bibliotecas diferentes con el mismo código fuente, así como dos archivos binarios con las mismas fuentes. Compile el proyecto y ejecute
md5sum
para ver las sumas de comprobación de todos los archivos binarios:
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
Tenemos una conclusión 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
Esto es interesante porque los
helloB
helloA
y
helloB
tienen las mismas sumas de comprobación, así como los archivos de objetos intermedios de Mach-O
hello_world.cpp.o
, pero esto no se puede decir de los archivos con la extensión
.a
. Esto se debe a que almacenan información sobre archivos de objetos intermedios en un formato de archivo. El encabezado de este formato incluye un campo llamado
st_time
establecido por la llamada al sistema de
stat
. Verifique
libHelloLibA.a
y
libHelloLibB.a
usando
otool
para mostrar los encabezados:
> 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 el archivo contiene varios campos temporales que hacen que nuestro ensamblaje no sea determinista. Tenga en cuenta que estos campos no se aplican al archivo ejecutable final porque tienen la misma suma de verificación. Este problema también puede ocurrir al compilar en Windows con Visual Studio, pero con un archivo PE en lugar de Mach-O.
En este punto, podemos intentar empeorar las cosas y hacer que nuestros archivos binarios también sean no deterministas. Cambie el archivo
main.cpp
para que incluya la 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 nuevamente las sumas de verificación de los archivos:
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 ahora tenemos diferentes binarios. Podríamos analizar el ejecutable con una herramienta como un
difoscopio , que muestra la diferencia entre dos archivos binarios:
> diffoscope helloA helloB --- helloA +++ helloB ├── otool -arch x86_64 -tdvV {} │┄ Code for architecture x86_64 │ @@ -16,15 +16,15 @@ │ 00000001000018da jmp 0x1000018df │ 00000001000018df 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 #
Muestra que la información
__TIME__
ha pegado en el binario, lo que la hace no determinista. Veamos qué se puede hacer para evitar esto.
Posibles soluciones para Microsoft Visual Studio
Microsoft Visual Studio tiene un indicador de enlace / Brepro que no está documentado por Microsoft. Este indicador establece las marcas de tiempo del formato ejecutable portátil en -1, como se ve en la figura a continuación.

Para activar este indicador con CMake, debemos agregar las siguientes líneas al crear el
.exe
:
add_link_options("/Brepro")
o estas líneas para
.lib
set_target_properties( TARGET PROPERTIES STATIC_LIBRARY_OPTIONS "/Brepro" )
El problema es que este indicador hace que los binarios sean reproducibles (en relación con las marcas de tiempo en el formato de archivo) en nuestro .exe binario final, pero no elimina todas las marcas de tiempo de .lib (el mismo problema que con los archivos de objetos Mach-O, de lo que hablamos anteriormente). El campo TimeDateStamp del
archivo de encabezado COFF para archivos
.lib
permanecerá. La única forma de eliminar esta información del archivo binario
.lib
es arreglar el
.lib
reemplazando los bytes correspondientes al campo TimeDateStamp con cualquier valor conocido.
Posibles soluciones para GCC y CLANG
- gcc detecta la existencia de la variable de entorno SOURCE_DATE_EPOCH. Si se establece esta variable, su valor indica la marca de tiempo UNIX que se usará para reemplazar la fecha y hora actuales en las macros
__DATE__
y __TIME__
para que las marcas de tiempo incorporadas se vuelvan reproducibles. El valor se puede establecer en una marca de tiempo conocida, como la hora del último cambio en los archivos o paquetes fuente. - clang usa
ZERO_AR_DATE
, que, si está configurado, restablece la ZERO_AR_DATE
tiempo proporcionada en los archivos de almacenamiento, configurándola en 0. Tenga en cuenta que esto no solucionará las __TIME__
__DATE__
o __TIME__
. Si queremos corregir el efecto de esta macro, debemos corregir los binarios o simular la hora del sistema.
Continuemos con nuestro proyecto de muestra para MacOS y veamos cuáles serán los resultados al configurar la
ZERO_AR_DATE
entorno
ZERO_AR_DATE
.
export ZERO_AR_DATE=1
Ahora, si compilamos nuestro archivo ejecutable y nuestras bibliotecas (eliminando la macro
__DATE__
en las fuentes), obtenemos:
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 las sumas de verificación son ahora iguales.
.a
los encabezados de los archivos con la extensión
.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 el campo de
timestamp
de
timestamp
del encabezado de la biblioteca se estableció en cero.
Llegamos sin problemas al final de la primera parte del artículo. La continuación del material se puede leer aquí .