Introducción a los ensambles deterministas en C / C ++. Parte 1

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 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 # 

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í .

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


All Articles