C / C ++ con GCC: agregue estáticamente archivos de recursos al ejecutable / biblioteca

94

¿Alguien tiene una idea de cómo compilar estáticamente cualquier archivo de recursos directamente en el ejecutable o en el archivo de biblioteca compartida usando GCC?

Por ejemplo, me gustaría agregar archivos de imagen que nunca cambian (y si lo hacen, tendría que reemplazar el archivo de todos modos) y no querría que se quedaran en el sistema de archivos.

Si esto es posible (y creo que es porque Visual C ++ para Windows también puede hacer esto), ¿cómo puedo cargar los archivos que están almacenados en el propio binario? ¿El ejecutable se analiza a sí mismo, encuentra el archivo y extrae los datos?

Tal vez haya una opción para GCC que aún no he visto. El uso de motores de búsqueda realmente no arrojó las cosas correctas.

Necesitaría que esto funcione para bibliotecas compartidas y ejecutables ELF normales.

Se agradece cualquier ayuda

Atmocreaciones
fuente
3
Posible duplicado de stackoverflow.com/questions/1997172/…
blueberryfields
El enlace objcopy en la pregunta que señaló blueberryfields es una buena solución genérica para esto también
Flexo
@blueberryfields: perdón por duplicar. Tienes razón. Normalmente votaría por cerrar como duplicado. Pero debido a que todos publicaron respuestas tan bonitas, solo acepto una.
Atmocreations
Puedo agregar que el método de John Ripley es probablemente el mejor aquí por una gran razón: la alineación. Si hace una copia de obj estándar o "ld -r -b binary -o foo.o foo.txt" y luego mira el objeto resultante con objdump -x, parece que la alineación del bloque está establecida en 0. Si lo desea la alineación sea correcta para los datos binarios que no sean char, no puedo imaginar que esto sea algo bueno.
carveone
1
posible duplicado de los recursos
jww

Respuestas:

49

Con imagemagick :

convert file.png data.h

Da algo como:

/*
  data.h (PNM).
*/
static unsigned char
  MagickImage[] =
  {
    0x50, 0x36, 0x0A, 0x23, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x20, 
    0x77, 0x69, 0x74, 0x68, 0x20, 0x47, 0x49, 0x4D, 0x50, 0x0A, 0x32, 0x37, 
    0x37, 0x20, 0x31, 0x36, 0x32, 0x0A, 0x32, 0x35, 0x35, 0x0A, 0xFF, 0xFF, 
    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 

....

Por compatibilidad con otro código, puede usar fmemopenpara obtener un FILE *objeto "normal" o, alternativamente, std::stringstreampara crear un iostream. std::stringstreamSin embargo, no es bueno para esto y, por supuesto, puede usar un puntero en cualquier lugar donde pueda usar un iterador.

Si está usando esto con automake, no olvide configurar BUILT_SOURCES apropiadamente.

Lo bueno de hacerlo de esta manera es:

  1. Obtienes texto, por lo que puede estar en control de versiones y parches de manera sensata
  2. Es portátil y está bien definido en todas las plataformas.
Flexo
fuente
2
¡Bleahg! Esa es la solución en la que también pensé. No entiendo por qué alguien querría hacer esto. El almacenamiento de datos en un espacio de nombres bien definido es para lo que sirven los sistemas de archivos.
Omnifarious
35
De vez en cuando, tiene un ejecutable que se ejecuta donde no hay sistema de archivos, o incluso sin sistema operativo. O su algoritmo necesita alguna tabla precalculada para búsquedas. Y estoy seguro de que hay muchos más casos en los que almacenar datos en el programa tiene mucho sentido.
ndim
15
Este uso de convertir es exactamente el mismo quexxd -i infile.bin outfile.h
greyfade
5
Una desventaja de este enfoque es que algunos compiladores no pueden manejar matrices estáticas tan enormes, si sus imágenes son particularmente grandes; la forma de solucionarlo es, como sugiere ndim , utilizar objcopypara convertir los datos binarios directamente en un archivo objeto; sin embargo, esto rara vez es motivo de preocupación.
Adam Rosenfield
3
Tenga en cuenta que definirlo en un encabezado como este significa que cada archivo que lo incluya obtendrá su propia copia. Es mejor declararlo en el encabezado como extern y luego definirlo en un cpp. Ejemplo aquí
Nicholas Smith
90

Actualización He llegado a preferir el control que ofrece la solución basada en ensamblaje de John Ripley.incbin y ahora uso una variante de eso.

He usado objcopy (GNU binutils) para vincular los datos binarios de un archivo foo-data.bin a la sección de datos del ejecutable:

objcopy -B i386 -I binary -O elf32-i386 foo-data.bin foo-data.o

Esto le proporciona un foo-data.oarchivo de objeto que puede vincular a su ejecutable. La interfaz C se parece a

/** created from binary via objcopy */
extern uint8_t foo_data[]      asm("_binary_foo_data_bin_start");
extern uint8_t foo_data_size[] asm("_binary_foo_data_bin_size");
extern uint8_t foo_data_end[]  asm("_binary_foo_data_bin_end");

para que puedas hacer cosas como

for (uint8_t *byte=foo_data; byte<foo_data_end; ++byte) {
    transmit_single_byte(*byte);
}

o

size_t foo_size = (size_t)((void *)foo_data_size);
void  *foo_copy = malloc(foo_size);
assert(foo_copy);
memcpy(foo_copy, foo_data, foo_size);

Si su arquitectura de destino tiene restricciones especiales en cuanto a dónde se almacenan los datos constantes y variables, o si desea almacenar esos datos en el .textsegmento para que quepan en el mismo tipo de memoria que su código de programa, puede jugar con los objcopyparámetros un poco más.

ndim
fuente
¡buena idea! En mi caso no es muy útil. Pero esto es algo que realmente voy a poner en mi colección de fragmentos. ¡Gracias por compartir esto!
Atmocreations
2
Es un poco más fácil de usar ldya que el formato de salida está implícito allí, consulte stackoverflow.com/a/4158997/201725 .
Jan Hudec
52

Puede incrustar archivos binarios en ejecutables mediante el ldvinculador. Por ejemplo, si tiene un archivo foo.bar, puede incrustarlo en un ejecutable agregando los siguientes comandos ald

--format=binary foo.bar --format=default

Si está invocando a ldtravés gcc, deberá agregar-Wl

-Wl,--format=binary -Wl,foo.bar -Wl,--format=default

Aquí --format=binaryle dice al enlazador que el siguiente archivo es binario y --format=defaultvuelve al formato de entrada predeterminado (esto es útil si especificará otros archivos de entrada después foo.bar).

Luego puede acceder al contenido de su archivo desde el código:

extern uint8_t data[]     asm("_binary_foo_bar_start");
extern uint8_t data_end[] asm("_binary_foo_bar_end");

También hay un símbolo llamado "_binary_foo_bar_size". Creo que es de tipo uintptr_tpero no lo comprobé.

Simón
fuente
Comentario muy interesante. ¡Gracias por compartir esto!
Atmocreations
1
¡Buena esa! Solo una pregunta: ¿por qué es data_enduna matriz, no un puntero? (¿O es C idiomática?)
xtofl
2
@xtofl, si data_endserá un puntero, el compilador pensará que hay un puntero almacenado después del contenido del archivo. De manera similar, si cambia el tipo de dataa un puntero, obtendrá un puntero que consta de los primeros bytes de un archivo en lugar de un puntero al inicio. Creo que sí.
Simon
1
+1: Su respuesta me permite incrustar un cargador de clases de Java y un Jar en un archivo ejecutable para construir un lanzador de Java personalizado
Aubin
2
@xtofl: si va a convertirlo en un puntero, conviértalo en un const pointer. El compilador le permite cambiar el valor de punteros no constantes, no le permite cambiar el valor si es una matriz. Así que quizás sea menos tipeado usar la sintaxis de matriz.
Jesse Chisholm
40

Puede poner todos sus recursos en un archivo ZIP y agregarlo al final del archivo ejecutable :

g++ foo.c -o foo0
zip -r resources.zip resources/
cat foo0 resources.zip >foo

Esto funciona porque a) a la mayoría de los formatos de imagen ejecutables no les importa si hay datos adicionales detrás de la imagen yb) zip almacena la firma del archivo al final del archivo zip . Esto significa que su ejecutable es un archivo zip normal después de esto (excepto su ejecutable inicial, que zip puede manejar), que se puede abrir y leer con libzip.

Mainframe nórdico
fuente
7
Si quiero unir foo0 y resources.zip en foo, entonces necesito> si doy ambas entradas en la línea de comando de cat. (porque no quiero agregar a lo que ya está en foo)
Nordic Mainframe
1
ah si, mi error. No vi el 0 en el nombre correctamente en mi primera lectura completa
Flexo
Esto es muy inteligente. +1.
Linuxios
1
+1 Maravilloso, especialmente cuando se combina con miniz
mvp
Esto producirá un binario no válido (al menos en Mac y Linux), que no puede ser procesado por herramientas como install_name_tool. Además de eso, el binario todavía funciona como ejecutable.
Andy Li
36

De http://www.linuxjournal.com/content/embedding-file-executable-aka-hello-world-version-5967 :

Recientemente tuve la necesidad de incrustar un archivo en un ejecutable. Como estoy trabajando en la línea de comandos con gcc, et al, y no con una elegante herramienta RAD que hace que todo suceda mágicamente, no me fue inmediatamente obvio cómo hacer que esto sucediera. Un poco de búsqueda en la red encontró un truco que esencialmente lo capturó al final del ejecutable y luego descifró dónde estaba basado en un montón de información que no quería conocer. Parecía que debería haber una mejor manera ...

Y lo hay, es obvio al rescate. objcopy convierte archivos objeto o ejecutables de un formato a otro. Uno de los formatos que entiende es "binario", que es básicamente cualquier archivo que no esté en uno de los otros formatos que entiende. Entonces, probablemente haya imaginado la idea: convertir el archivo que queremos incrustar en un archivo de objeto, luego simplemente se puede vincular con el resto de nuestro código.

Digamos que tenemos un nombre de archivo data.txt que queremos incrustar en nuestro ejecutable:

# cat data.txt
Hello world

Para convertir esto en un archivo de objeto que podamos vincular con nuestro programa, simplemente usamos objcopy para producir un archivo ".o":

# objcopy --input binary \
--output elf32-i386 \
--binary-architecture i386 data.txt data.o

Esto le dice a objcopy que nuestro archivo de entrada está en formato "binario", que nuestro archivo de salida debe estar en formato "elf32-i386" (archivos de objeto en el x86). La opción --binary-architecture le dice a objcopy que el archivo de salida está destinado a "ejecutarse" en un x86. Esto es necesario para que ld acepte el archivo para vincularlo con otros archivos para x86. Uno pensaría que especificar el formato de salida como "elf32-i386" implicaría esto, pero no es así.

Ahora que tenemos un archivo de objeto, solo necesitamos incluirlo cuando ejecutamos el enlazador:

# gcc main.c data.o

Cuando ejecutamos el resultado, obtenemos el resultado pedido:

# ./a.out
Hello world

Por supuesto, aún no he contado toda la historia, ni les he mostrado el main.c. Cuando objcopy realiza la conversión anterior, agrega algunos símbolos de "vinculador" al archivo de objeto convertido:

_binary_data_txt_start
_binary_data_txt_end

Después de vincular, estos símbolos especifican el inicio y el final del archivo incrustado. Los nombres de los símbolos se forman anteponiendo binario y añadiendo _start o _end al nombre del archivo. Si el nombre del archivo contiene caracteres que no serían válidos en un nombre de símbolo, se convierten en guiones bajos (por ejemplo, data.txt se convierte en data_txt). Si obtiene nombres sin resolver al vincular usando estos símbolos, haga un hexdump -C en el archivo de objeto y busque al final del volcado los nombres que eligió objcopy.

El código para usar realmente el archivo incrustado ahora debería ser razonablemente obvio:

#include <stdio.h>

extern char _binary_data_txt_start;
extern char _binary_data_txt_end;

main()
{
    char*  p = &_binary_data_txt_start;

    while ( p != &_binary_data_txt_end ) putchar(*p++);
}

Una cosa importante y sutil a tener en cuenta es que los símbolos agregados al archivo de objeto no son "variables". No contienen ningún dato, más bien, su dirección es su valor. Los declaro como tipo char porque es conveniente para este ejemplo: los datos incrustados son datos de caracteres. Sin embargo, puede declararlos como cualquier cosa, como int si los datos son una matriz de enteros, o como struct foo_bar_t si los datos fueran cualquier matriz de barras foo. Si los datos incrustados no son uniformes, entonces char es probablemente lo más conveniente: tome su dirección y envíe el puntero al tipo adecuado a medida que recorre los datos.

Hazok
fuente
36

Si desea controlar el nombre exacto del símbolo y la ubicación de los recursos, puede usar (o script) el ensamblador GNU (que no es realmente parte de gcc) para importar archivos binarios completos. Prueba esto:

Montaje (x86 / brazo):

    .section .rodata

    .global thing
    .type   thing, @object
    .balign 4
thing:
    .incbin "meh.bin"
thing_end:

    .global thing_size
    .type   thing_size, @object
    .balign 4
thing_size:
    .int    thing_end - thing

C:

#include <stdio.h>

extern const char thing[];
extern const unsigned thing_size;

int main() {
  printf("%p %u\n", thing, thing_size);
  return 0;
}

Independientemente de lo que use, probablemente sea mejor hacer un script para generar todos los recursos y tener nombres de símbolo agradables / uniformes para todo.

Dependiendo de sus datos y las especificaciones del sistema, es posible que deba usar diferentes valores de alineación (preferiblemente con .balignpara la portabilidad) o tipos enteros de un tamaño diferente thing_sizeo un tipo de elemento diferente para la thing[]matriz.

John Ripley
fuente
¡gracias por compartir! definitivamente parece interesante, pero esta vez no es lo que estoy buscando =) saludos
Atmocreations
1
Exactamente lo que estaba buscando. Tal vez pueda verificar que también está bien para archivos con tamaños no visibles en 4. Parece que thing_size incluirá los bytes de relleno adicionales.
Pavel P
¿Qué pasa si quiero que algo sea un símbolo local? Probablemente pueda capturar la salida del compilador junto con mi propio ensamblado, pero ¿hay una manera mejor?
user877329
Para el registro: Mi edición aborda el problema de los bytes de relleno adicionales que @Pavel señaló.
ndim
4

Leyendo todas las publicaciones aquí y en Internet he llegado a la conclusión de que no existe una herramienta para los recursos, que es:

1) Fácil de usar en código.

2) Automatizado (para facilitar su inclusión en cmake / make).

3) multiplataforma.

He decidido escribir la herramienta yo mismo. El código está disponible aquí. https://github.com/orex/cpp_rsc

Usarlo con cmake es muy fácil.

Debe agregar dicho código a su archivo CMakeLists.txt.

file(DOWNLOAD https://raw.github.com/orex/cpp_rsc/master/cmake/modules/cpp_resource.cmake ${CMAKE_BINARY_DIR}/cmake/modules/cpp_resource.cmake) 

set(CMAKE_MODULE_PATH ${CMAKE_BINARY_DIR}/cmake/modules)

include(cpp_resource)

find_resource_compiler()
add_resource(pt_rsc) #Add target pt_rsc
link_resource_file(pt_rsc FILE <file_name1> VARIABLE <variable_name1> [TEXT]) #Adds resource files
link_resource_file(pt_rsc FILE <file_name2> VARIABLE <variable_name2> [TEXT])

...

#Get file to link and "resource.h" folder
#Unfortunately it is not possible with CMake add custom target in add_executable files list.
get_property(RSC_CPP_FILE TARGET pt_rsc PROPERTY _AR_SRC_FILE)
get_property(RSC_H_DIR TARGET pt_rsc PROPERTY _AR_H_DIR)

add_executable(<your_executable> <your_source_files> ${RSC_CPP_FILE})

El ejemplo real, usando el enfoque se puede descargar aquí, https://bitbucket.org/orex/periodic_table

usuario2794512
fuente
Creo que su respuesta necesita una mejor explicación para que sea útil para más personas.
kyb