C / C ++ z GCC: Statycznie dodawaj pliki zasobów do pliku wykonywalnego / biblioteki

94

Czy ktoś ma pomysł, jak statycznie skompilować dowolny plik zasobów bezpośrednio do pliku wykonywalnego lub pliku biblioteki współdzielonej za pomocą GCC?

Na przykład chciałbym dodać pliki graficzne, które nigdy się nie zmieniają (a gdyby tak się stało, i tak musiałbym zastąpić ten plik) i nie chciałbym, aby leżały w systemie plików.

Jeśli jest to możliwe (i myślę, że dzieje się tak dlatego, że Visual C ++ dla Windows też to potrafi), jak mogę załadować pliki, które są przechowywane we własnym pliku binarnym? Czy plik wykonywalny analizuje sam siebie, znajduje plik i wyodrębnia z niego dane?

Może jest opcja dla GCC, której jeszcze nie widziałem. Korzystanie z wyszukiwarek nie wypluwało właściwych rzeczy.

Potrzebowałbym tego do pracy dla bibliotek współdzielonych i normalnych plików wykonywalnych ELF.

Każda pomoc jest mile widziana

Atokreacje
źródło
3
Możliwy duplikat stackoverflow.com/questions/1997172/ ...
blueberryfields
Link do objcopy w pytaniu, na które wskazała firma blueberryfields, jest dobrym, ogólnym rozwiązaniem tego problemu
fleksografia
@blueberryfields: przepraszam za powielanie. Masz rację. Zwykle głosowałbym za blisko jako duplikat. Ale ponieważ wszyscy opublikowali tak dobre odpowiedzi, po prostu zaakceptuję jedną.
Atmocreations
Czy mogę dodać, że metoda Johna Ripleya jest prawdopodobnie najlepsza tutaj z jednego ważnego powodu - wyrównania. Jeśli wykonasz standardowe objcopy lub "ld -r -b binary -o foo.o foo.txt", a następnie spojrzysz na wynikowy obiekt poleceniem objdump -x, wygląda na to, że wyrównanie bloku jest ustawione na 0. Jeśli chcesz wyrównanie jest poprawne dla danych binarnych innych niż znak, nie wyobrażam sobie, że to dobra rzecz.
rzeźbił

Odpowiedzi:

51

Z imagemagick :

convert file.png data.h

Daje coś takiego:

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

....

Aby fmemopenzachować zgodność z innym kodem, możesz użyć albo do uzyskania „zwykłego” FILE *obiektu, albo alternatywnie std::stringstreamdo utworzenia pliku iostream. std::stringstreamnie jest jednak do tego świetny i oczywiście możesz po prostu użyć wskaźnika w dowolnym miejscu, w którym możesz użyć iteratora.

Jeśli używasz tego z automake, nie zapomnij odpowiednio ustawić BUILT_SOURCES .

Zaletą robienia tego w ten sposób jest:

  1. Otrzymujesz tekst, więc może być w kontroli wersji i rozsądnie łatać
  2. Jest przenośny i dobrze zdefiniowany na każdej platformie
Flexo
źródło
2
Bleahg! To rozwiązanie też pomyślałem. Nie rozumiem, dlaczego ktokolwiek chciałby to zrobić. Przechowywanie fragmentów danych w dobrze zdefiniowanej przestrzeni nazw jest tym, do czego służą systemy plików.
Omnifarious
36
Czasami masz plik wykonywalny, który działa tam, gdzie nie ma systemu plików lub nawet systemu operacyjnego. Lub twój algorytm potrzebuje jakiejś wstępnie obliczonej tabeli do wyszukiwania. I jestem pewien, że jest znacznie więcej przypadków, gdy przechowywanie danych w programie ma dużo sensu.
ndim
16
To użycie konwersji jest dokładnie takie samo jakxxd -i infile.bin outfile.h
greyfade
5
Wadą tego podejścia jest to, że niektóre kompilatory nie radzą sobie z tak ogromnymi tablicami statycznymi, jeśli obrazy są szczególnie duże; sposobem obejścia tego problemu jest, jak sugeruje ndim , użycie objcopydo konwersji danych binarnych bezpośrednio do pliku obiektowego; jednak rzadko jest to problem.
Adam Rosenfield,
3
Pamiętaj, że zdefiniowanie go w takim nagłówku oznacza, że ​​każdy plik, który go zawiera, otrzyma własną kopię. Lepiej jest zadeklarować go w nagłówku jako extern, a następnie zdefiniować w cpp. Przykład tutaj
Nicholas Smith
90

Uaktualnienie Zdecydowałem się preferować sterowanie oferowane przez .incbinrozwiązanie oparte na montażu Johna Ripleya, a teraz używam innego wariantu.

Użyłem objcopy (GNU binutils) do połączenia danych binarnych z pliku foo-data.bin z sekcją danych pliku wykonywalnego:

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

W ten sposób otrzymasz foo-data.oplik obiektu, który możesz połączyć z plikiem wykonywalnym. Interfejs C wygląda mniej więcej tak

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

więc możesz robić takie rzeczy

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

lub

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

Jeśli twoja architektura docelowa ma specjalne ograniczenia co do miejsca przechowywania stałych i zmiennych danych lub chcesz przechowywać te dane w .textsegmencie, aby pasowały do ​​tego samego typu pamięci co kod programu, możesz objcopytrochę więcej pobawić się parametrami.

ndim
źródło
dobry pomysł! W moim przypadku nie jest to zbyt przydatne. Ale to jest coś, co naprawdę umieszczę w mojej kolekcji fragmentów. Dzięki za udostępnienie tego!
Atmocreations
2
Jest nieco łatwiejszy w użyciu, ldponieważ sugeruje się tam format wyjściowy, patrz stackoverflow.com/a/4158997/201725 .
Jan Hudec
52

Możesz osadzać pliki binarne w pliku wykonywalnym za pomocą ldkonsolidatora. Na przykład, jeśli masz plik foo.bar, możesz go osadzić w pliku wykonywalnym, dodając następujące polecenia dold

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

Jeśli wywołujesz ldprzez gcc, będziesz musiał dodać-Wl

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

Tutaj --format=binarymówi konsolidatorowi, że następujący plik jest binarny i --format=defaultprzełącza się z powrotem na domyślny format wejściowy (jest to przydatne, jeśli określisz inne pliki wejściowe po foo.bar).

Następnie możesz uzyskać dostęp do zawartości pliku z kodu:

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

Znajduje się tam również symbol nazwany "_binary_foo_bar_size". Myślę, że to typowe, uintptr_tale nie sprawdziłem tego.

Szymon
źródło
Bardzo ciekawy komentarz. Dzięki za udostępnienie tego!
Atmocreations
1
Niezłe! Tylko jedno pytanie: dlaczego jest data_endtablicą, a nie wskaźnikiem? (A może to idiomatyczne C?)
xtofl
2
@xtofl, jeśli data_endbędzie wskaźnikiem, kompilator pomyśli, że po zawartości pliku znajduje się wskaźnik. Podobnie, jeśli zmienisz typ datana wskaźnik, otrzymasz wskaźnik składający się z pierwszych bajtów pliku zamiast wskaźnika do jego początku. Chyba tak.
Simon,
1
+1: Twoja odpowiedź pozwala mi umieścić program ładujący klasy java i Jar w exe, aby zbudować niestandardowy program uruchamiający java
Aubin
2
@xtofl - jeśli zamierzasz uczynić go wskaźnikiem, zrób to const pointer. Kompilator pozwala zmienić wartość wskaźników innych niż const, nie pozwala zmienić wartości, jeśli jest to tablica. Tak więc użycie składni tablicowej wymaga mniej pisania.
Jesse Chisholm
41

Możesz umieścić wszystkie swoje zasoby w pliku ZIP i dołączyć go na końcu pliku wykonywalnego :

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

To działa, ponieważ a) Większość wykonywalnych formatów obrazu nie przejmuje się tym, czy za obrazem znajdują się dodatkowe dane oraz b) zip przechowuje podpis pliku na końcu pliku zip . Oznacza to, że twój plik wykonywalny jest później zwykłym plikiem zip (z wyjątkiem pliku wykonywalnego z góry, który zip może obsłużyć), który można otworzyć i odczytać za pomocą libzip.

Nordic Mainframe
źródło
7
Jeśli chcę dołączyć foo0 i resources.zip do foo, potrzebuję> jeśli podam oba dane wejściowe w wierszu poleceń cat. (ponieważ nie chcę dodawać do tego, co już jest w foo)
Nordic Mainframe
1
ach tak, mój błąd. Nie zauważyłem poprawnie 0 w nazwie podczas pierwszego czytania
Flexo
To bardzo sprytne. +1.
Linuxios
1
+1 Cudownie, zwłaszcza w połączeniu z minizem
mvp
Spowoduje to utworzenie nieprawidłowego pliku binarnego (przynajmniej na Macu i Linuksie), którego nie mogą przetworzyć narzędzia takie jak install_name_tool. Poza tym plik binarny nadal działa jako plik wykonywalny.
Andy Li
37

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

Niedawno miałem potrzebę osadzenia pliku w pliku wykonywalnym. Ponieważ pracuję w wierszu poleceń z gcc, et al, a nie z fantazyjnym narzędziem RAD, które sprawia, że ​​wszystko dzieje się w magiczny sposób, nie od razu było dla mnie oczywiste, jak to zrobić. Trochę przeszukiwania sieci znalazło hack, który zasadniczo zakotował go na końcu pliku wykonywalnego, a następnie rozszyfrował, gdzie był oparty na zbiorze informacji, o których nie chciałem wiedzieć. Wydawało się, że powinien być lepszy sposób ...

I jest, to objcopy na ratunek. objcopy konwertuje pliki obiektowe lub pliki wykonywalne z jednego formatu na inny. Jeden z formatów, które rozumie, to „binarny”, czyli zasadniczo każdy plik, który nie jest w żadnym z innych formatów, które rozumie. Więc prawdopodobnie wyobraziłeś sobie pomysł: przekonwertuj plik, który chcemy osadzić w pliku obiektowym, a następnie można go po prostu połączyć z resztą naszego kodu.

Powiedzmy, że mamy nazwę pliku data.txt, którą chcemy osadzić w naszym pliku wykonywalnym:

# cat data.txt
Hello world

Aby przekonwertować to na plik obiektowy, który możemy połączyć z naszym programem, używamy po prostu objcopy do utworzenia pliku „.o”:

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

To mówi objcopy, że nasz plik wejściowy jest w formacie "binarnym", a nasz plik wyjściowy powinien być w formacie "elf32-i386" (pliki obiektowe na x86). Opcja --binary-architecture mówi objcopy, że plik wyjściowy ma "działać" na x86. Jest to potrzebne, aby ld zaakceptował plik do łączenia z innymi plikami dla x86. Można by pomyśleć, że określenie formatu wyjściowego jako „elf32-i386” oznaczałoby to, ale tak nie jest.

Teraz, gdy mamy plik obiektowy, musimy go dołączyć tylko wtedy, gdy uruchamiamy konsolidator:

# gcc main.c data.o

Kiedy uruchomimy wynik, otrzymamy modlony o wyjście:

# ./a.out
Hello world

Oczywiście nie opowiedziałem jeszcze całej historii, ani nie pokazałem ci main.c. Kiedy objcopy wykonuje powyższą konwersję, dodaje kilka symboli „konsolidatora” do przekonwertowanego pliku obiektowego:

_binary_data_txt_start
_binary_data_txt_end

Po połączeniu symbole te określają początek i koniec osadzonego pliku. Nazwy symboli są tworzone przez poprzedzające je binarne i dodanie _start lub _end do nazwy pliku. Jeśli nazwa pliku zawiera jakiekolwiek znaki, które byłyby nieprawidłowe w nazwie symbolu, są one konwertowane na podkreślenia (np. Data.txt staje się data_txt). Jeśli otrzymujesz nierozwiązane nazwy podczas łączenia przy użyciu tych symboli, wykonaj zrzut heksowy -C na pliku obiektowym i spójrz na koniec zrzutu, aby znaleźć nazwy wybrane przez objcopy.

Kod do faktycznego korzystania z osadzonego pliku powinien być teraz dość oczywisty:

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

Ważną i subtelną rzeczą, na którą należy zwrócić uwagę, jest to, że symbole dodane do pliku obiektowego nie są „zmiennymi”. Nie zawierają żadnych danych, raczej ich adres jest ich wartością. Deklaruję je jako typ char, ponieważ jest to wygodne w tym przykładzie: osadzone dane to dane znakowe. Jednak możesz zadeklarować je jako dowolne, jako int, jeśli dane są tablicą liczb całkowitych, lub jako struct foo_bar_t, jeśli dane byłyby dowolną tablicą słupków foo. Jeśli osadzone dane nie są jednolite, prawdopodobnie najwygodniejszy jest znak char: weź jego adres i rzuć wskaźnik na odpowiedni typ podczas przeglądania danych.

Hazok
źródło
36

Jeśli chcesz mieć kontrolę nad dokładną nazwą symbolu i rozmieszczeniem zasobów, możesz użyć (lub skryptu) asemblera GNU (nie będącego częścią gcc) do importowania całych plików binarnych. Spróbuj tego:

Montaż (x86 / ramię):

    .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

DO:

#include <stdio.h>

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

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

Czegokolwiek używasz, prawdopodobnie najlepiej jest stworzyć skrypt generujący wszystkie zasoby i mieć ładne / jednolite nazwy symboli dla wszystkiego.

W zależności od danych i specyfiki systemu może być konieczne użycie różnych wartości wyrównania (najlepiej z .baligndla przenośności) lub typów całkowitych o różnym rozmiarze thing_sizelub innego typu elementu thing[]tablicy.

John Ripley
źródło
dzięki za udostępnienie! zdecydowanie wygląda ciekawie, ale tym razem nie tego szukam =) pozdrawiam
Atmocreations
1
Dokładnie to, czego szukałem. Może możesz sprawdzić, czy jest to również w porządku dla plików o rozmiarach niemożliwych do podzielenia przez 4. Wygląda na to, że thing_size będzie zawierał dodatkowe bajty wypełnienia.
Pavel P,
A jeśli chcę, żeby coś było lokalnym symbolem? Prawdopodobnie mogę pobrać dane wyjściowe kompilatora razem z moim własnym zestawem, ale czy jest lepszy sposób?
user877329
Dla przypomnienia: moja edycja odnosi się do problemu dodatkowych bajtów wypełnienia, które odnotował @Pavel.
ndim
4

Czytając cały post tutaj iw Internecie doszedłem do wniosku, że nie ma narzędzia do zasobów, jakim jest:

1) Łatwy w użyciu w kodzie.

2) Zautomatyzowany (aby można go było łatwo uwzględnić w cmake / make).

3) Wiele platform.

Zdecydowałem się napisać to narzędzie samodzielnie. Kod jest dostępny tutaj. https://github.com/orex/cpp_rsc

Używanie go z cmake jest bardzo łatwe.

Należy dodać taki kod do pliku 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})

Prawdziwy przykład z zastosowaniem tego podejścia można pobrać tutaj, https://bitbucket.org/orex/periodic_table

user2794512
źródło
1
Myślę, że twoja odpowiedź wymaga lepszego wyjaśnienia, aby stała się użyteczna dla większej liczby osób.
kyb