C / C ++ con GCC: aggiungi staticamente file di risorse all'eseguibile / libreria


94

Qualcuno ha un'idea di come compilare staticamente qualsiasi file di risorse direttamente nell'eseguibile o nel file della libreria condivisa utilizzando GCC?

Ad esempio, vorrei aggiungere file di immagine che non cambiano mai (e se lo fanno, dovrei sostituire il file comunque) e non vorrei che si trovassero nel file system.

Se questo è possibile (e penso sia perché anche Visual C ++ per Windows può farlo), come posso caricare i file che sono memorizzati nel proprio binario? L'eseguibile analizza se stesso, trova il file ed estrae i dati da esso?

Forse c'è un'opzione per GCC che non ho ancora visto. Usare i motori di ricerca non ha veramente sputato fuori la roba giusta.

Avrei bisogno di questo per funzionare con librerie condivise e normali eseguibili ELF.

Qualsiasi aiuto è apprezzato



Il link objcopy nella domanda a cui puntava blueberryfields è una buona soluzione generica anche a questo
Flexo

@blueberryfields: scusa per la duplicazione. Hai ragione. Normalmente voterei per chiudere come duplicato. Ma poiché hanno pubblicato tutte risposte così belle, ne accetterò una.
Atmocreations

Posso aggiungere che il metodo di John Ripley è probabilmente il migliore qui per un'enorme ragione: l'allineamento. Se esegui un objcopy standard o "ld -r -b binary -o foo.o foo.txt" e poi guardi l'oggetto risultante con objdump -x sembra che l'allineamento per il blocco sia impostato a 0. Se vuoi allineamento corretto per dati binari diversi da char, non riesco a immaginare che sia una buona cosa.
carveone

1
possibile duplicato delle risorse
jww

Risposte:


49

Con imagemagick :

convert file.png data.h

Fornisce qualcosa come:

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

....

Per compatibilità con altro codice puoi quindi utilizzare sia fmemopenper ottenere un FILE *oggetto "normale" , o in alternativa std::stringstreamper creare un file iostream. std::stringstreamnon è eccezionale per questo e ovviamente puoi semplicemente usare un puntatore ovunque tu possa usare un iteratore.

Se lo stai usando con automake, non dimenticare di impostare BUILT_SOURCES in modo appropriato.

La cosa bella di farlo in questo modo è:

  1. Ottieni il testo, quindi può essere in controllo della versione e patch in modo sensato
  2. È portatile e ben definito su ogni piattaforma

2
Bleahg! Questa è la soluzione a cui ho pensato anch'io. Perché qualcuno vorrebbe mai farlo è al di là di me. Memorizzare pezzi di dati in uno spazio dei nomi ben definito è ciò a cui servono i file system.
Omnifario

35
Occasionalmente, hai un eseguibile che gira dove non c'è un filesystem, o anche nessun sistema operativo. Oppure il tuo algoritmo necessita di una tabella precalcolata per le ricerche. E sono sicuro che ci sono molti più casi in cui la memorizzazione dei dati nel programma ha molto senso.
ndim

15
Questo uso della conversione è esattamente lo stesso dixxd -i infile.bin outfile.h
dissolvenza grigia

5
Uno svantaggio di questo approccio è che alcuni compilatori non sono in grado di gestire array statici così enormi, se le tue immagini sono particolarmente grandi; il modo per aggirare il problema è, come suggerisce ndim , utilizzare objcopyper convertire i dati binari direttamente in un file oggetto; tuttavia questo è raramente un problema.
Adam Rosenfield

3
Tieni presente che definirlo in un'intestazione come questa significa che ogni file che lo include riceverà la propria copia. È meglio dichiararlo nell'header come extern e poi definirlo in un cpp. Esempio qui
Nicholas Smith

90

Aggiornamento Sono cresciuto per preferire la soluzione basata su assembly di John Ripley di controllo.incbin offerto dalla e ora ne uso una variante.

Ho usato objcopy (GNU binutils) per collegare i dati binari da un file foo-data.bin nella sezione dati dell'eseguibile:

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

Questo ti dà un foo-data.ofile oggetto che puoi collegare al tuo eseguibile. L'interfaccia C assomiglia a qualcosa

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

così puoi fare cose come

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

Se l'architettura di destinazione ha vincoli speciali su dove vengono memorizzati i dati costanti e variabili, o si desidera memorizzare quei dati nel .textsegmento per adattarli allo stesso tipo di memoria del codice del programma, è possibile giocare ancora con i objcopyparametri.


buona idea! Nel mio caso non è molto utile. Ma questo è qualcosa che metterò davvero nella mia raccolta di frammenti. Grazie per aver condiviso questo!
Atmocreations

2
È un po 'più facile da usare ldpoiché il formato di output è implicito lì, vedi stackoverflow.com/a/4158997/201725 .
Jan Hudec

52

Puoi incorporare file binari in eseguibili usando ldlinker. Ad esempio, se hai un file foo.bar, puoi incorporarlo nell'eseguibile aggiungendo i seguenti comandi ald

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

Se stai invocando ldthru gcc, dovrai aggiungere-Wl

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

Qui --format=binarydice al linker che il seguente file è binario e --format=defaulttorna al formato di input predefinito (questo è utile se si specificheranno altri file di input dopo foo.bar).

Quindi puoi accedere al contenuto del tuo file dal codice:

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

C'è anche un simbolo denominato "_binary_foo_bar_size". Penso che sia di tipo uintptr_tma non l'ho controllato.


Commento molto interessante. Grazie per aver condiviso questo!
Atmocreations

1
Ben fatto! Solo una domanda: perché è data_endun array, non un puntatore? (O è idiomatico C?)
xtofl

2
@xtofl, se data_endsarà un puntatore, il compilatore penserà che ci sia un puntatore memorizzato dopo il contenuto del file. Allo stesso modo, se cambierai il tipo di datain un puntatore, otterrai un puntatore costituito dai primi byte di un file invece del puntatore al suo inizio. Credo di si.
Simon

1
+1: La tua risposta mi permette di incorporare un caricatore di classi Java e un Jar in un exe per creare un launcher Java personalizzato
Aubin

2
@xtofl - Se hai intenzione di renderlo un puntatore, rendilo un file const pointer. Il compilatore ti consente di modificare il valore dei puntatori non const, non ti consente di modificare il valore se è un array. Quindi è forse meno digitando usare la sintassi dell'array.
Jesse Chisholm

40

Puoi mettere tutte le tue risorse in un file ZIP e aggiungerlo alla fine del file eseguibile :

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

Funziona, perché a) la maggior parte dei formati di immagine eseguibili non si preoccupa se ci sono dati extra dietro l'immagine eb) zip memorizza la firma del file alla fine del file zip . Ciò significa che il tuo eseguibile è un normale file zip dopo questo (ad eccezione del tuo eseguibile iniziale, che zip può gestire), che può essere aperto e letto con libzip.


7
Se voglio unire foo0 e resources.zip in foo, allora ho bisogno di> se fornisco entrambi gli input sulla riga di comando di cat. (perché non voglio aggiungere a ciò che è già in foo)
Nordic Mainframe

1
ah si, errore mio. Non ho individuato correttamente lo 0 nel nome durante la mia prima lettura
Flexo

Questo è molto intelligente. +1.
Linuxios

1
+1 Meraviglioso, soprattutto se abbinato a miniz
mvp

Questo produrrà un binario non valido (almeno su Mac e Linux), che non può essere elaborato da strumenti come install_name_tool. Oltre a ciò, il binario funziona ancora come eseguibile.
Andy Li

36

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

Recentemente ho avuto la necessità di incorporare un file in un eseguibile. Dato che sto lavorando alla riga di comando con gcc, et al e non con un sofisticato strumento RAD che fa accadere tutto magicamente, non è stato immediatamente ovvio per me come farlo accadere. Un po 'di ricerca in rete ha trovato un trucco per essenzialmente catarlo alla fine dell'eseguibile e quindi decifrare dove era basato su un mucchio di informazioni che non volevo sapere. Sembrava che ci dovesse essere un modo migliore ...

E c'è, è una copia in soccorso. objcopy converte file oggetto o eseguibili da un formato a un altro. Uno dei formati che comprende è "binario", che è fondamentalmente qualsiasi file che non sia in uno degli altri formati che comprende. Quindi probabilmente hai immaginato l'idea: convertire il file che vogliamo incorporare in un file oggetto, quindi può essere semplicemente collegato con il resto del nostro codice.

Supponiamo di avere un nome file data.txt che vogliamo incorporare nel nostro eseguibile:

# cat data.txt
Hello world

Per convertirlo in un file oggetto che possiamo collegare al nostro programma usiamo semplicemente objcopy per produrre un file ".o":

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

Questo dice a objcopy che il nostro file di input è nel formato "binario", che il nostro file di output dovrebbe essere nel formato "elf32-i386" (file oggetto su x86). L'opzione --binary-architecture dice a objcopy che il file di output è pensato per "essere eseguito" su un x86. Ciò è necessario affinché ld accetti il ​​file per il collegamento con altri file per x86. Si potrebbe pensare che specificare il formato di output come "elf32-i386" implichi questo, ma non è così.

Ora che abbiamo un file oggetto dobbiamo solo includerlo quando eseguiamo il linker:

# gcc main.c data.o

Quando eseguiamo il risultato otteniamo l'output pregato:

# ./a.out
Hello world

Ovviamente, non ho ancora raccontato l'intera storia, né ti ho mostrato main.c. Quando objcopy esegue la conversione precedente, aggiunge alcuni simboli "linker" al file oggetto convertito:

_binary_data_txt_start
_binary_data_txt_end

Dopo il collegamento, questi simboli specificano l'inizio e la fine del file incorporato. I nomi dei simboli vengono formati anteponendo binary e aggiungendo _start o _end al nome del file. Se il nome del file contiene caratteri che non sarebbero validi in un nome di simbolo, vengono convertiti in trattini bassi (es. Data.txt diventa data_txt). Se si ottengono nomi irrisolti durante il collegamento utilizzando questi simboli, eseguire un hexdump -C sul file oggetto e cercare alla fine del dump i nomi scelti da objcopy.

Il codice per utilizzare effettivamente il file incorporato dovrebbe ora essere ragionevolmente ovvio:

#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 e sottile da notare è che i simboli aggiunti al file oggetto non sono "variabili". Non contengono dati, anzi, il loro indirizzo è il loro valore. Li dichiaro come tipo char perché è conveniente per questo esempio: i dati incorporati sono dati carattere. Tuttavia, puoi dichiararli come qualsiasi cosa, come int se i dati sono un array di numeri interi, o come struct foo_bar_t se i dati erano un qualsiasi array di foo bar. Se i dati incorporati non sono uniformi, allora char è probabilmente il più conveniente: prendi il suo indirizzo e lancia il puntatore al tipo corretto mentre attraversi i dati.


36

Se vuoi il controllo sul nome esatto del simbolo e sulla posizione delle risorse, puoi usare (o script) l'assemblatore GNU (non realmente parte di gcc) per importare interi file binari. Prova questo:

Assemblaggio (x86 / braccio):

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

Qualunque cosa tu usi, probabilmente è meglio creare uno script per generare tutte le risorse e avere nomi di simboli carini / uniformi per tutto.

A seconda dei dati e delle specifiche del sistema, potrebbe essere necessario utilizzare valori di allineamento diversi (preferibilmente con .balignper la portabilità) o tipi interi di dimensioni diverse per thing_sizeo un tipo di elemento diverso per l' thing[]array.


grazie per la condivisione! sembra decisamente interessante, ma questa volta non è quello che sto cercando =) saluti
Atmocreations

1
Esattamente quello che stavo cercando. Forse puoi verificare che vada bene anche per file con dimensioni non concepibili per 4. Sembra che thing_size includa i byte di riempimento extra.
Pavel P

E se volessi che una cosa sia un simbolo locale? Probabilmente posso cat l'output del compilatore insieme al mio assembly, ma c'è un modo migliore?
user877329

Per la cronaca: la mia modifica affronta il problema dei byte di riempimento extra annotati da @Pavel.
ndim

4

Leggendo tutti i post qui e in Internet sono giunto alla conclusione che non esiste uno strumento per le risorse, che è:

1) Facile da usare nel codice.

2) Automatizzato (per essere facile incluso in cmake / make).

3) Cross-platform.

Ho deciso di scrivere lo strumento da solo. Il codice è disponibile qui. https://github.com/orex/cpp_rsc

Usarlo con cmake è molto semplice.

Dovresti aggiungere al tuo file CMakeLists.txt tale codice.

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

Il vero esempio, utilizzando l'approccio può essere scaricato qui, https://bitbucket.org/orex/periodic_table


Penso che la tua risposta abbia bisogno di una spiegazione migliore per diventare utile per più persone.
kyb
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.