C / C ++ avec GCC: ajouter statiquement des fichiers de ressources à l'exécutable / bibliothèque


94

Quelqu'un a-t-il une idée de la façon de compiler statiquement n'importe quel fichier de ressources directement dans l'exécutable ou le fichier de bibliothèque partagé en utilisant GCC?

Par exemple, je voudrais ajouter des fichiers image qui ne changent jamais (et s'ils le font, je devrais de toute façon remplacer le fichier) et je ne voudrais pas qu'ils traînent dans le système de fichiers.

Si cela est possible (et je pense que c'est parce que Visual C ++ pour Windows peut également le faire), comment puis-je charger les fichiers qui sont stockés dans le propre binaire? L'exécutable s'analyse-t-il, trouve-t-il le fichier et en extrait les données?

Peut-être qu'il y a une option pour GCC que je n'ai pas encore vue. L'utilisation des moteurs de recherche n'a pas vraiment craché les bonnes choses.

J'aurais besoin de cela pour fonctionner pour les bibliothèques partagées et les exécutables ELF normaux.

Toute aide est appréciée



Le lien objcopy dans la question pointée par blueberryfields est une bonne solution générique à cela aussi
Flexo

@blueberryfields: désolé pour la duplication. Vous avez raison. Normalement, je voterais pour la fermeture en double. Mais comme ils ont tous publié de si belles réponses, je n'en accepterai qu'une.
Atmocreations

Puis-je ajouter que la méthode de John Ripley est probablement la meilleure ici pour une raison énorme: l'alignement. Si vous faites une objcopy standard ou "ld -r -b binary -o foo.o foo.txt" et que vous regardez l'objet résultant avec objdump -x, il semble que l'alignement du bloc est défini sur 0. Si vous le souhaitez l'alignement pour être correct pour les données binaires autres que char, je ne peux pas imaginer que ce soit une bonne chose.
carveone

Réponses:


49

Avec imagemagick :

convert file.png data.h

Donne quelque chose comme:

/*
  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, 

....

Pour la compatibilité avec d'autres codes, vous pouvez ensuite utiliser soit fmemopenpour obtenir un FILE *objet "normal" , soit std::stringstreampour créer un fichier iostream. std::stringstreamce n'est pas génial pour cela et vous pouvez bien sûr simplement utiliser un pointeur partout où vous pouvez utiliser un itérateur.

Si vous l'utilisez avec automake, n'oubliez pas de définir BUILT_SOURCES de manière appropriée.

La bonne chose à faire de cette façon est:

  1. Vous obtenez du texte, donc il peut être dans le contrôle de version et les correctifs de manière raisonnable
  2. Il est portable et bien défini sur chaque plateforme

2
Bleahg! C'est aussi la solution à laquelle j'ai pensé. Pourquoi quelqu'un voudrait-il jamais faire cela me dépasse. Stocker des éléments de données dans un espace de noms bien défini est à quoi servent les systèmes de fichiers.
Omnifarious le

35
Parfois, vous avez un exécutable qui s'exécute là où il n'y a pas de système de fichiers, voire pas de système d'exploitation. Ou votre algorithme a besoin d'une table précalculée pour les recherches. Et je suis sûr qu'il y a beaucoup plus de cas où le stockage de données dans le programme a beaucoup de sens.
ndim

15
Cette utilisation de convert est exactement la même quexxd -i infile.bin outfile.h
greyfade

5
Un inconvénient de cette approche est que certains compilateurs ne peuvent pas gérer des tableaux statiques aussi énormes, si vos images sont particulièrement grandes; le moyen de contourner cela est, comme le suggère ndim , d'utiliser objcopypour convertir les données binaires directement en un fichier objet; cependant, c'est rarement un problème.
Adam Rosenfield

3
Gardez à l'esprit que le définir dans un en-tête comme celui-ci signifie que chaque fichier qui l'inclut recevra sa propre copie. Il est préférable de le déclarer dans l'en-tête comme extern et de le définir ensuite dans un cpp. Exemple ici
Nicholas Smith

90

Mise à jour J'ai grandi pour préférer la solution de contrôle basée sur l' assemblage de John Ripley.incbin et j'utilise maintenant une variante à ce sujet.

J'ai utilisé objcopy (GNU binutils) pour lier les données binaires d'un fichier foo-data.bin dans la section data de l'exécutable:

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

Cela vous donne un foo-data.ofichier objet que vous pouvez lier à votre exécutable. L'interface C ressemble à quelque chose comme

/** 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");

pour que tu puisses faire des trucs comme

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

ou

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 votre architecture cible a des contraintes spéciales quant à l'emplacement de stockage des données constantes et variables, ou si vous souhaitez stocker ces données dans le .textsegment pour qu'elles tiennent dans le même type de mémoire que votre code de programme, vous pouvez jouer un objcopypeu plus avec les paramètres.


bonne idée! Dans mon cas, ce n'est pas très utile. Mais c'est quelque chose que je vais vraiment mettre dans ma collection d'extraits. Merci d'avoir partagé ça!
Atmocreations

2
C'est un peu plus facile à utiliser ldcar le format de sortie y est implicite, voir stackoverflow.com/a/4158997/201725 .
Jan Hudec

52

Vous pouvez incorporer des fichiers binaires dans un exécutable à l'aide de l' ldéditeur de liens. Par exemple, si vous avez un fichier, foo.barvous pouvez l'intégrer dans l'exécutable en ajoutant les commandes suivantes àld

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

Si vous invoquez ldvia, gccvous devrez ajouter-Wl

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

Ici, il --format=binaryindique à l'éditeur de liens que le fichier suivant est binaire et --format=defaultrevient au format d'entrée par défaut (cela est utile si vous spécifiez d'autres fichiers d'entrée après foo.bar).

Ensuite, vous pouvez accéder au contenu de votre fichier à partir du code:

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

Il y a aussi un symbole nommé "_binary_foo_bar_size". Je pense que c'est du type uintptr_tmais je ne l'ai pas vérifié.


Commentaire très intéressant. Merci d'avoir partagé ça!
Atmocreations

1
Joli! Juste une question: pourquoi data_endun tableau, pas un pointeur? (Ou est-ce idiomatique C?)
xtofl

2
@xtofl, si data_endsera un pointeur, le compilateur pensera qu'il y a un pointeur stocké après le contenu du fichier. De même, si vous changez le type de dataen pointeur, vous obtiendrez un pointeur composé des premiers octets d'un fichier au lieu d'un pointeur vers son début. Je le pense.
Simon

1
+1: Votre réponse me permet d'intégrer un chargeur de classe java et un Jar dans un exe pour créer un lanceur java personnalisé
Aubin

2
@xtofl - Si vous voulez en faire un pointeur, faites-en un const pointer. Le compilateur vous permet de changer la valeur des pointeurs non const, il ne vous permet pas de changer la valeur s'il s'agit d'un tableau. Il est donc peut-être moins difficile d'utiliser la syntaxe du tableau.
Jesse Chisholm

40

Vous pouvez mettre toutes vos ressources dans un fichier ZIP et l' ajouter à la fin du fichier exécutable :

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

Cela fonctionne, car a) La plupart des formats d'image exécutables ne se soucient pas de savoir s'il y a des données supplémentaires derrière l'image et b) zip stocke la signature du fichier à la fin du fichier zip . Cela signifie que votre exécutable est un fichier zip normal après cela (à l'exception de votre exécutable initial, que zip peut gérer), qui peut être ouvert et lu avec libzip.


7
Si je veux joindre foo0 et resources.zip dans foo, alors j'ai besoin de> si je donne les deux entrées sur la ligne de commande de cat. (parce que je ne veux pas ajouter à ce qui est déjà dans foo)
Nordic Mainframe

1
ah oui, mon erreur. Je n'ai pas repéré le 0 dans le nom correctement lors de ma première lecture
Flexo

C'est très intelligent. +1.
Linuxios

1
+1 Merveilleux, surtout lorsqu'il est associé à miniz
mvp

Cela produira un binaire invalide (au moins sur Mac et Linux), qui ne peut pas être traité par des outils comme install_name_tool. À côté de cela, le binaire fonctionne toujours comme exécutable.
Andy Li

36

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

J'ai récemment eu le besoin d'intégrer un fichier dans un exécutable. Puisque je travaille en ligne de commande avec gcc, et al et non avec un outil RAD sophistiqué qui fait que tout se passe comme par magie, il ne m'a pas été immédiatement évident de savoir comment y arriver. Un peu de recherche sur le net a trouvé un hack pour le mettre essentiellement à la fin de l'exécutable, puis déchiffrer où il était basé sur un tas d'informations que je ne voulais pas connaître. Il semblait qu'il devrait y avoir un meilleur moyen ...

Et il y a, c'est objcopy à la rescousse. objcopy convertit les fichiers objets ou exécutables d'un format à un autre. L'un des formats qu'il comprend est "binaire", qui est essentiellement tout fichier qui n'est pas dans l'un des autres formats qu'il comprend. Vous avez donc probablement envisagé l'idée: convertir le fichier que nous voulons incorporer en un fichier objet, puis il peut simplement être lié au reste de notre code.

Disons que nous avons un nom de fichier data.txt que nous voulons intégrer dans notre exécutable:

# cat data.txt
Hello world

Pour le convertir en un fichier objet que nous pouvons lier avec notre programme, nous utilisons simplement objcopy pour produire un fichier ".o":

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

Cela indique à objcopy que notre fichier d'entrée est au format "binaire", que notre fichier de sortie doit être au format "elf32-i386" (fichiers objets sur le x86). L'option --binary-architecture indique à objcopy que le fichier de sortie est destiné à "s'exécuter" sur un x86. Ceci est nécessaire pour que ld accepte le fichier pour la liaison avec d'autres fichiers pour le x86. On pourrait penser que spécifier le format de sortie comme "elf32-i386" impliquerait cela, mais ce n'est pas le cas.

Maintenant que nous avons un fichier objet, nous n'avons besoin de l'inclure que lorsque nous exécutons l'éditeur de liens:

# gcc main.c data.o

Lorsque nous exécutons le résultat, nous obtenons la sortie priée:

# ./a.out
Hello world

Bien sûr, je n'ai pas encore raconté toute l'histoire, ni montré main.c. Lorsque objcopy effectue la conversion ci-dessus, il ajoute des symboles "linker" au fichier objet converti:

_binary_data_txt_start
_binary_data_txt_end

Après la liaison, ces symboles spécifient le début et la fin du fichier incorporé. Les noms de symboles sont formés en ajoutant binaire au début et en ajoutant _start ou _end au nom de fichier. Si le nom de fichier contient des caractères qui ne seraient pas valides dans un nom de symbole, ils sont convertis en traits de soulignement (par exemple, data.txt devient data_txt). Si vous obtenez des noms non résolus lors de la liaison à l'aide de ces symboles, effectuez un hexdump -C sur le fichier objet et recherchez à la fin du vidage les noms choisis par objcopy.

Le code pour utiliser réellement le fichier incorporé devrait maintenant être raisonnablement évident:

#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++);
}

Une chose importante et subtile à noter est que les symboles ajoutés au fichier objet ne sont pas des «variables». Ils ne contiennent aucune donnée, mais leur adresse est leur valeur. Je les déclare en tant que type char car c'est pratique pour cet exemple: les données incorporées sont des données de caractères. Cependant, vous pouvez les déclarer comme n'importe quoi, comme int si les données sont un tableau d'entiers, ou comme struct foo_bar_t si les données étaient n'importe quel tableau de foo bars. Si les données incorporées ne sont pas uniformes, alors char est probablement le plus pratique: prenez son adresse et transtypez le pointeur vers le type approprié lorsque vous parcourez les données.


36

Si vous voulez contrôler le nom exact du symbole et le placement des ressources, vous pouvez utiliser (ou script) l'assembleur GNU (qui ne fait pas vraiment partie de gcc) pour importer des fichiers binaires entiers. Essaye ça:

Assemblage (x86 / bras):

    .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;
}

Quoi que vous utilisiez, il est probablement préférable de créer un script pour générer toutes les ressources et d'avoir des noms de symboles sympas / uniformes pour tout.

En fonction de vos données et des spécificités du système, vous devrez peut-être utiliser des valeurs d'alignement différentes (de préférence avec .balignpour la portabilité), ou des types entiers d'une taille différente pour thing_sizeou un type d'élément différent pour le thing[]tableau.


Merci d'avoir partagé! semble vraiment intéressant, mais cette fois ce n'est pas ce que je cherche =) salutations
Atmocreations

1
Exactement ce que je cherchais. Peut-être que vous pouvez vérifier que c'est également correct pour les fichiers dont les tailles ne sont pas divisibles par 4. On dirait que thing_size comprendra les octets de remplissage supplémentaires.
Pavel P

Et si je veux que quelque chose soit un symbole local? Je peux probablement associer la sortie du compilateur à mon propre assemblage, mais y a-t-il un meilleur moyen?
user877329

Pour mémoire: Ma modification résout le problème des octets de remplissage supplémentaires notés par @Pavel.
ndim

4

En lisant tous les articles ici et sur Internet, j'ai conclu qu'il n'y avait pas d'outil pour les ressources, à savoir:

1) Facile à utiliser dans le code.

2) Automatisé (pour être facilement inclus dans cmake / make).

3) multiplateforme.

J'ai décidé d'écrire l'outil moi-même. Le code est disponible ici. https://github.com/orex/cpp_rsc

L'utiliser avec cmake est très simple.

Vous devez ajouter à votre fichier CMakeLists.txt un tel code.

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})

L'exemple réel, utilisant l'approche peut être téléchargé ici, https://bitbucket.org/orex/periodic_table


Je pense que votre réponse nécessite une meilleure explication pour devenir utile à plus de gens.
kyb
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.