C / C ++ з GCC: Статично додайте файли ресурсів до виконуваного файлу / бібліотеки


94

Хтось має ідею, як статично скомпілювати будь-який файл ресурсу прямо у виконуваний файл або файл спільної бібліотеки за допомогою GCC?

Наприклад, я хотів би додати файли зображень, які ніколи не змінюються (а якщо і будуть, мені все одно доведеться замінити файл) і не хотів би, щоб вони лежали у файловій системі.

Якщо це можливо (і я думаю, що це тому, що це може зробити і Visual C ++ для Windows), як я можу завантажити файли, які зберігаються у власному двійковому файлі? Чи виконується виконуваний файл сам, аналізує файл і витягує з нього дані?

Можливо, є варіант для GCC, якого я ще не бачив. Використання пошукових систем насправді не виплюнуло правильних речей.

Мені це знадобилося б для роботи для спільних бібліотек та звичайних ELF-виконуваних файлів.

Будь-яка допомога вдячна


3
Можливий дублікат stackoverflow.com/questions/1997172/…
blueberryfields

Посилання на objcopy у питанні, на яке вказали blueberryfields, є хорошим загальним рішенням і для цього
Flexo

@blueberryfields: вибачте за дублювання. Ти маєш рацію. Зазвичай я би проголосував за дублікат. Але оскільки всі вони розмістили такі приємні відповіді, я просто прийму одну.
Атака

Чи можу я додати, що метод Джона Ріплі, мабуть, найкращий тут з однієї величезної причини - вирівнювання. Якщо ви зробите стандартну objcopy або "ld -r -b binary -o foo.o foo.txt", а потім подивитесь на отриманий об'єкт з objdump -x, схоже, вирівнювання для блоку встановлено на 0. Якщо ви хочете вирівнювання, щоб бути правильним для двійкових даних, відмінних від char, я не можу уявити, що це добре.
carveone

Відповіді:


49

З imagemagick :

convert file.png data.h

Дає щось на зразок:

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

....

Для сумісності з іншим кодом ви можете скористатися або fmemopenдля отримання "звичайного" FILE *об'єкта, або std::stringstreamдля створення iostream. std::stringstreamце не чудово для цього, і ви, звичайно, можете просто використовувати вказівник у будь-якому місці, де ви можете використовувати ітератор.

Якщо ви використовуєте це з automake, не забудьте встановити BUILT_SOURCES належним чином.

Приємна річ робити це таким чином:

  1. Ви отримуєте текст, тому він може бути в контролі версій і розумно виправляти
  2. Він портативний і чітко визначений на кожній платформі

2
Боже! Це рішення я теж думав. Чому хтось коли-небудь захоче це зробити, мені не під силу. Зберігання фрагментів даних у чітко визначеному просторі імен - це те, для чого потрібні файлові системи.
Вселюдний

35
Іноді у вас є виконуваний файл, який працює там, де немає файлової системи або навіть немає операційної системи. Або вашому алгоритму потрібна якась попередньо розрахована таблиця для пошуку. І я впевнений, що випадків, коли зберігання даних у програмі має багато сенсу, набагато більше.
ndim

15
Це використання перетворення точно таке ж, якxxd -i infile.bin outfile.h
greyfade

5
Недоліком цього підходу є те, що деякі компілятори не можуть обробляти такі величезні статичні масиви, якщо ваші зображення особливо великі; спосіб обійти, як передбачає ndim , використовувати objcopyдля перетворення бінарних даних безпосередньо у файл об’єкта; однак це рідко викликає занепокоєння.
Адам Розенфілд

3
Майте на увазі, що визначення його в такому заголовку означає, що кожен файл, що включає його, отримає свою власну копію. Краще оголосити це в заголовку як extern, а потім визначити в cpp. Приклад тут
Ніколас Сміт

90

Оновлення Я вирішив віддавати перевагу контрольним рішенням, заснованим на збірці Джона Ріплі,.incbin і тепер використовую для цього варіант.

Я використовував objcopy (GNU binutils), щоб зв'язати двійкові дані з файлу foo-data.bin у розділ даних виконуваного файлу:

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

Це дає вам foo-data.oоб’єктний файл, який ви можете зв’язати зі своїм виконуваним файлом. Інтерфейс C виглядає приблизно так

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

щоб ви могли робити подібні речі

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

або

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

Якщо ваша цільова архітектура має спеціальні обмеження щодо того, де зберігаються постійні та змінні дані, або ви хочете зберегти ці дані в .textсегменті, щоб вони помістилися в той самий тип пам’яті, що і ваш програмний код, ви можете ще objcopyтрохи пограти з параметрами.


гарна ідея! У моєму випадку це не дуже корисно. Але це те, що я справді збираюся вкласти у свою колекцію фрагментів. Дякуємо, що поділилися цим!
Атомації

2
Це трохи простіше у використанні, ldоскільки там мається на увазі вихідний формат, див. Stackoverflow.com/a/4158997/201725 .
Ян Гудек,

52

Ви можете вставляти двійкові файли у виконуваний файл за допомогою ldкомпонувальника. Наприклад, якщо у вас є файл, foo.barви можете вбудувати його у виконуваний файл, додавши до нього наступні командиld

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

Якщо ви використовуєте ldчерез, gccтоді вам потрібно буде додати-Wl

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

Тут --format=binaryповідомляється лінкеру, що наступний файл є двійковим і --format=defaultперемикається назад на формат введення за замовчуванням (це корисно, якщо ви вкажете інші вхідні файли післяfoo.bar ).

Тоді ви можете отримати доступ до вмісту вашого файлу з коду:

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

Існує також названий символ "_binary_foo_bar_size". Я думаю, що це тип, uintptr_tале я його не перевіряв.


Дуже цікавий коментар. Дякуємо, що поділилися цим!
Атмокреації

1
Хороший! Лише одне питання: чому data_endмасив, а не вказівник? (Або це ідіоматична C?)
xtofl

2
@xtofl, якщо data_endбуде вказівником, то компілятор подумає, що після вмісту файлу зберігається вказівник. Подібно, якщо ви зміните тип dataна покажчик, тоді ви отримаєте покажчик, що складається з перших байтів файлу замість вказівника на його початок. Я думаю так.
Саймон

1
+1: Ваша відповідь дозволяє мені вбудувати навантажувач класу Java і Jar в exe для створення власної запускової програми Java
Aubin

2
@xtofl - Якщо ви збираєтеся зробити його вказівником, зробіть це const pointer. Компілятор дозволяє змінювати значення показників, що не є const, і не дозволяє змінювати значення, якщо це масив. Тому, можливо, менше набору тексту, щоб використовувати синтаксис масиву.
Jesse Chisholm

40

Ви можете помістити всі свої ресурси у ZIP-файл і додати це до кінця виконуваного файлу :

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

Це працює, оскільки а) більшості виконуваних форматів зображень байдуже, чи є за зображенням зайві дані; б) zip зберігає підпис файлу в кінці zip-файлу . Це означає, що ваш виконуваний файл - це звичайний zip-файл після цього (за винятком вашого попереднього виконуваного файлу, який zip може обробляти), який можна відкрити та прочитати за допомогою libzip.


7
Якщо я хочу приєднати foo0 і resources.zip до foo, тоді мені потрібно> якщо я вводжу обидва входи в командному рядку cat. (тому що я не хочу додавати до того, що вже у foo)
Північний головний кадр

1
а так, моя помилка. Я не помітив нуля там у назві під час свого першого прочитаного
Flexo

Це дуже розумно. +1.
Linuxios

1
+1 Чудово, особливо в парі з miniz
mvp

Це створить недійсний двійковий файл (принаймні на Mac та Linux), який не може бути оброблений такими інструментами, як install_name_tool. Крім того, двійковий файл все ще працює як виконуваний файл.
Andy Li

36

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

Нещодавно у мене була потреба вбудувати файл у виконуваний файл. Оскільки я працюю в командному рядку з gcc та ін., А не з вигадливим інструментом RAD, який робить все магічним, мені не одразу було очевидно, як це зробити. Трохи пошуків у мережі знайшов хакер, який по суті перевів його на кінець виконуваного файлу, а потім розшифрував, де це базується на купу інформації, про яку я не хотів знати. Здавалося, повинен бути кращий спосіб ...

І є, це обйкопія на допомогу. objcopy перетворює об'єктні файли або виконувані файли з одного формату в інший. Одним із форматів, який він розуміє, є "двійковий", що в основному є будь-яким файлом, який не входить в інший формат, який він розуміє. Отже, ви, мабуть, передбачили ідею: перетворіть файл, який ми хочемо вбудувати, у об’єктний файл, тоді його можна просто пов’язати з рештою нашого коду.

Скажімо, у нас є ім’я файлу data.txt, яке ми хочемо вбудувати у наш виконуваний файл:

# cat data.txt
Hello world

Щоб перетворити це в файл об'єкта, який ми можемо зв'язати з нашою програмою, ми просто використовуємо objcopy для створення файлу ".o":

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

Це повідомляє objcopy, що наш вхідний файл має "двійковий" формат, що наш вихідний файл повинен бути у форматі "elf32-i386" (об'єктні файли на x86). Параметр --binary-architecture повідомляє objcopy, що вихідний файл призначений для "запуску" на x86. Це потрібно для того, щоб ld прийняв файл для зв'язування з іншими файлами для x86. Можна подумати, що вказівка ​​вихідного формату як "elf32-i386" означатиме це, але це не так.

Тепер, коли у нас є файл об’єкта, нам потрібно включити його лише під час запуску компоновщика:

# gcc main.c data.o

Коли ми запускаємо результат, ми отримуємо молитву за вихід:

# ./a.out
Hello world

Звичайно, я ще не розповів усієї історії, і не показав вам main.c. Коли objcopy виконує вищезазначене перетворення, він додає деякі символи "зв’язки" до перетвореного об’єктного файлу:

_binary_data_txt_start
_binary_data_txt_end

Після зв’язування ці символи визначають початок і кінець вбудованого файлу. Назви символів утворюються шляхом додавання двійкових символів та додавання _start або _end до імені файлу. Якщо ім'я файлу містить будь-які символи, які були б неприпустимими в назві символу, вони перетворюються на підкреслення (наприклад, data.txt стає data_txt). Якщо при зв'язуванні за допомогою цих символів ви отримуєте невирішені імена, зробіть hexdump -C на об'єктному файлі та подивіться на кінець дампа, щоб вибрати імена, які вибрав objcopy.

Код для фактичного використання вбудованого файлу тепер повинен бути досить очевидним:

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

Важливо і тонко помітити, що символи, додані до об’єктного файлу, не є “змінними”. Вони не містять жодних даних, швидше, їх адреса є їх значенням. Я оголошую їх як тип char, оскільки це зручно для цього прикладу: вбудовані дані - це символьні дані. Однак ви можете оголосити їх як що завгодно, як int, якщо дані є масивом цілих чисел, або як struct foo_bar_t, якщо дані являють собою будь-який масив foo-барів. Якщо вбудовані дані не є однорідними, то char, мабуть, найзручніший: візьміть його адресу та перекиньте покажчик на потрібний тип під час обходу даних.


36

Якщо ви хочете контролювати точне ім'я символу та розміщення ресурсів, ви можете використовувати (або сценарій) асемблер GNU (який насправді не є частиною gcc) для імпорту цілих двійкових файлів. Спробуйте це:

Збірка (x86 / рука):

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

Що б ви не використовували, мабуть, найкраще створити скрипт для генерації всіх ресурсів і мати приємні / уніфіковані імена символів для всього.

Залежно від ваших даних та специфіки системи, вам може знадобитися використовувати різні значення вирівнювання (бажано .balignдля переносимості), або цілі типи іншого розміру для масиву thing_sizeабо іншого типу елемента thing[].


Дякую, що поділились! однозначно виглядає цікаво, але цього разу це не те, що я шукаю =) з повагою
Атмосфери

1
Саме те, що я шукав. Можливо, ви можете переконатись, що це нормально і для файлів із розмірами, які не деляться на 4. Схоже, thing_size включатиме додаткові байти заповнення.
Павло П

Що робити, якщо я хочу, щоб річ була місцевим символом? Можливо, я можу поставити вихідні дані компілятора разом із власною збіркою, але чи є кращий спосіб?
user877329

Для протоколу: Моя редакція розглядає проблему додаткових байтів заповнення, яку зазначив @Pavel.
ndim

4

Читаючи всі публікації тут та в Інтернеті, я зробив висновок, що не існує інструменту для ресурсів, а саме:

1) Простий у використанні в коді.

2) Автоматизований (щоб його легко було включити в cmake / make).

3) Кроссплатформенність.

Я вирішив написати інструмент самостійно. Код доступний тут. https://github.com/orex/cpp_rsc

Використовувати його з cmake дуже просто.

Ви повинні додати до свого файлу 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})

Реальний приклад використання підходу можна завантажити тут, https://bitbucket.org/orex/periodic_table


Я думаю, що ваша відповідь потребує кращого пояснення, щоб стати корисною для більшої кількості людей.
kyb
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.