C / C ++ с GCC: статическое добавление файлов ресурсов в исполняемый файл / библиотеку

94

Кто-нибудь знает, как статически скомпилировать любой файл ресурсов прямо в исполняемый файл или файл общей библиотеки с помощью GCC?

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

Если это возможно (и я думаю, это потому, что Visual C ++ для Windows тоже может это сделать), как я могу загрузить файлы, которые хранятся в собственном двоичном файле? Разбирает ли исполняемый файл сам, находит ли файл и извлекает из него данные?

Может быть, есть вариант для GCC, которого я еще не видел. Использование поисковых систем на самом деле не выдавало того, что нужно.

Мне нужно, чтобы это работало для общих библиотек и обычных исполняемых файлов ELF.

Любая помощь приветствуется

Atmocreations
источник
3
Возможный дубликат stackoverflow.com/questions/1997172/…
blueberryfields
Ссылка objcopy в вопросе, на который указывает blueberryfields, также является хорошим универсальным решением этой проблемы
Flexo
@blueberryfields: извините за дублирование. Ты прав. Обычно я бы проголосовал за закрытие как за дубликат. Но поскольку все они прислали такие хорошие ответы, я просто приму один.
Atmocreations 01
Могу я добавить, что метод Джона Рипли, вероятно, лучший здесь по одной важной причине - выравнивание. Если вы выполните стандартный objcopy или «ld -r -b binary -o foo.o foo.txt», а затем посмотрите на полученный объект с помощью objdump -x, похоже, что выравнивание для блока установлено на 0. Если вы хотите выравнивание должно быть правильным для двоичных данных, кроме char, я не могу представить, что это хорошо.
carveone

Ответы:

51

С помощью 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
Блиг! Я тоже подумал об этом. Я не понимаю, зачем кому-то это нужно. Файловые системы предназначены для хранения фрагментов данных в четко определенном пространстве имен.
Omnifarious
36
Иногда у вас есть исполняемый файл, который запускается там, где нет файловой системы или даже операционной системы. Или вашему алгоритму нужна предварительно рассчитанная таблица для поиска. И я уверен, что гораздо больше случаев, когда хранение данных в программе имеет большой смысл.
ndim 01
16
Это использование convert точно такое же, как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немного поиграть с параметрами.

ndim
источник
хорошая идея! В моем случае это не очень полезно. Но это то, что я действительно собираюсь добавить в свою коллекцию сниппетов. Спасибо, что поделились этим!
Atmocreations
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но не проверял.

Саймон
источник
Очень интересный комментарий. Спасибо, что поделились этим!
Atmocreations
1
Хороший! Только один вопрос: почему data_endмассив, а не указатель? (Или это идиоматический C?)
xtofl
2
@xtofl, если data_endбудет указатель, то компилятор подумает, что после содержимого файла хранится указатель. Аналогично, если вы измените тип dataна указатель, то вместо указателя на его начало вы получите указатель, состоящий из первых байтов файла. Я так думаю.
Саймон
1
+1: Ваш ответ позволяет мне встроить загрузчик классов Java и Jar в исполняемый файл для создания настраиваемой программы запуска Java
Обин
2
@xtofl - Если вы собираетесь сделать его указателем, сделайте его const pointer. Компилятор позволяет вам изменять значение неконстантных указателей, он не позволяет вам изменять значение, если это массив. Таким образом, использование синтаксиса массива, возможно, требует меньше усилий.
Джесси Чизхолм
41

Вы можете поместить все свои ресурсы в 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)
Nordic Mainframe
1
ах да, моя ошибка. Я не заметил 0 в названии должным образом при первом чтении
Flexo
Это очень умно. +1.
Linuxios
1
+1 Замечательно, особенно в паре с miniz
mvp
Это приведет к созданию недопустимого двоичного файла (по крайней мере, на Mac и Linux), который не может быть обработан такими инструментами, как install_name_tool. Кроме того, двоичный файл по-прежнему работает как исполняемый файл.
Энди Ли
37

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

Недавно мне понадобилось встроить файл в исполняемый файл. Поскольку я работаю в командной строке с помощью gcc и др., А не с причудливым инструментом RAD, который заставляет все это происходить волшебным образом, мне не сразу было очевидно, как это сделать. Немного поискав в сети, был обнаружен хак, который по существу помещал его в конец исполняемого файла, а затем расшифровывал, где он был основан на кучке информации, о которой я не хотел знать. Казалось, что должен быть способ получше ...

И вот, objcopy спешит на помощь. 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). Если вы получаете неразрешенные имена при связывании с использованием этих символов, выполните шестнадцатеричный дамп -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: возьмите его адрес и приведите указатель к нужному типу при перемещении по данным.

Hazok
источник
36

Если вы хотите контролировать точное имя символа и размещение ресурсов, вы можете использовать (или создать скрипт) ассемблер GNU (на самом деле не являющийся частью gcc) для импорта целых двоичных файлов. Попробуй это:

Сборка (x86 / arm):

    .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[]массива.

Джон Рипли
источник
Спасибо, что поделился! определенно выглядит интересно, но на этот раз это не то, что я ищу =) С уважением
Atmocreations
1
Именно то, что я искал. Возможно, вы сможете убедиться, что это также нормально для файлов с размерами, которые не могут быть отображены на 4. Похоже, что thing_size будет включать в себя дополнительные байты заполнения.
Pavel P
Что, если я хочу, чтобы вещь была местным символом? Я, вероятно, смогу объединить вывод компилятора вместе с моей собственной сборкой, но есть ли способ лучше?
user877329
Для записи: Мое редактирование касается проблемы дополнительных байтов заполнения, отмеченных @Pavel.
ndim 02
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

user2794512
источник
1
Я думаю, что ваш ответ нуждается в более подробном объяснении, чтобы стать полезным для большего числа людей.
kyb