使用GCC的C/C++:将资源文件静态添加到可执行文件或库中

111

有没有人知道如何使用GCC将任何资源文件静态编译到可执行文件或共享库中?

例如,我想添加永远不会更改的图像文件(如果它们发生变化,我也必须替换文件),并且不希望它们留在文件系统中。

如果这是可能的(我认为这是可能的,因为Windows的Visual C++也可以做到这一点),那么我该如何加载存储在自己二进制文件中的文件?可执行文件会解析自身,找到文件并提取数据吗?

也许有GCC的选项我还没看到过。但搜索引擎并没有真正给出正确的结果。

我需要这个功能适用于共享库和普通的ELF-executables。


3
可能是与https://dev59.com/D3I-5IYBdhLWcg3wJE0K相同的问题,该问题询问是否有Linux等效于Windows资源文件。 - blueberryfields
问题中blueberryfields指向的objcopy链接也是一个很好的通用解决方案。 - Flexo
@blueberryfields:抱歉重复了。你是正确的。通常情况下,我会投票关闭为重复。但因为他们都发表了如此好的答案,我只会接受一个。 - Atmocreations
我能补充一下,John Ripley的方法可能是最好的,因为有一个非常重要的原因 - 对齐。如果你使用标准的objcopy或者"ld -r -b binary -o foo.o foo.txt"命令,然后用objdump -x查看结果对象,你会发现块的对齐方式被设置为0。如果你想要二进制数据的对齐方式正确,那么这显然不是一个好事情。 - carveone
1
可能是使用GCC将资源嵌入.exe的重复问题。 - jww
7个回答

91

更新 我已经更喜欢John Ripley的汇编.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参数。

好主意!在我的情况下,它并不是非常有用。但这是我真的要加入到我的代码片段集合中的东西。感谢分享! - Atmocreations
3
使用ld命令会更容易,因为输出格式已经被暗示了。参见链接:https://dev59.com/v2855IYBdhLWcg3ww3Sa#4158997。 - Jan Hudec

63

使用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
39
有时候,您可能需要在没有文件系统甚至没有操作系统的情况下运行可执行文件。或者您的算法需要一些预先计算好的表格进行查找。我相信还有很多情况下,在程序中存储数据非常有意义。 - ndim
22
"convert" 的使用与 xxd -i infile.bin outfile.h 完全相同。 - greyfade
6
这种方法的一个缺点是,如果您的图像特别大,一些编译器可能无法处理如此巨大的静态数组; 解决这个问题的方法是,正如ndim所建议的那样,使用objcopy直接将二进制数据转换为对象文件; 然而,这很少成为问题。 - Adam Rosenfield
5
记住,在头文件中这样定义意味着每个包含它的文件都会得到自己的副本。更好的做法是在头文件中声明为extern,然后在cpp文件中定义它。示例在此 - Nicholas Smith
显示剩余8条评论

55

你可以使用ld链接器在可执行文件中嵌入二进制文件。 例如,如果你有文件foo.bar,那么你可以通过将以下命令添加到ld来将其嵌入到可执行文件中。

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

如果你是通过 gcc 调用 ld,那么你需要添加 -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的类型更改为指针,则会得到由文件的前几个字节组成的指针,而不是指向其开头的指针。我是这么认为的。 - Simon
1
+1:您的答案让我能够将Java类加载器和Jar嵌入到exe中,以构建自定义的Java启动器。 - Aubin
2
@xtofl - 如果你要将它变成指针,那就让它成为一个“const指针”。编译器允许你改变非const指针的值,但如果它是一个数组,编译器就不允许你改变其值。所以使用数组语法可能会少打一些字。 - Jesse Chisholm
@JesseChisholm:只是想知道这是否涉及指针与数组的问题:*data_end=1不就等同于data_end[0]=1吗? - xtofl
显示剩余4条评论

42

您可以将所有资源放入一个ZIP文件中,并将其附加到可执行文件的末尾

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

这行代码可行,因为a) 大多数可执行文件格式不在意图像后面是否有额外数据,以及b) 压缩包会将文件签名储存于压缩文件的末尾。这意味着你的可执行文件在此之后是一个普通的压缩包(除了zip可以处理的前端可执行文件),可以使用libzip打开并读取。


7
如果我想将foo0和resources.zip合并成foo,那么需要在cat命令的命令行上同时输入两个输入文件(因为我不想追加到foo中已有的内容)。 - Nordic Mainframe
1
啊,是的,我的错误。我第一次阅读时没有正确地注意到名称中的0。 - Flexo
1
+1 很棒,特别是与miniz搭配使用时。 - mvp
这会生成一个无效的二进制文件(至少在Mac和Linux上),无法被像install_name_tool这样的工具处理。除此之外,该二进制文件仍然可以作为可执行文件运行。 - Andy Li

41

如果你想要控制资源的确切符号名称和位置,可以使用(或脚本化)GNU汇编器(实际上不是gcc的一部分)来导入整个二进制文件。尝试这样做:

汇编(x86 / arm):

thing.s

    .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:

main.c

#include <stdio.h>

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

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

你可以简单地使用 gcc main.c thing.s 来编译它。

无论你使用什么,最好制作一个脚本来生成所有资源,并为每个东西都有漂亮/统一的符号名称。

根据你的数据和系统特定情况,你可能需要使用不同的对齐值(最好使用 .balign 实现可移植性),或者对于 thing_size 使用不同大小的整数类型,或者在 thing[] 数组中使用不同的元素类型。


感谢分享!看起来确实很有趣,但这次不是我正在寻找的东西 =)问候 - Atmocreations
2
正是我所需要的。也许您可以验证一下对于大小不可被4整除的文件是否也适用。看起来thing_size将包括额外的填充字节。 - Pavel P
如果我想让某个东西成为本地符号怎么办?我可能可以将编译器输出与我的汇编代码拼接在一起,但有更好的方法吗? - user877329
记录一下:我对@Pavel提到的额外填充字节问题进行了编辑处理。 - ndim

38

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

我最近有需要将一个文件嵌入到可执行文件中。由于我是在命令行下使用gcc等工具而不是使用神奇的RAD工具,让所有事情都变得自动化,所以我一开始不知道如何做到这一点。在网上搜索了一下,发现了一种hack的方法,可以将文件简单地附加到可执行文件的末尾,然后根据一些我不想知道的信息来解密它的位置。看起来应该有更好的方法...

确实有,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我们的输入文件是“binary”格式,输出文件应该是“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

链接之后,这些符号指定了嵌入文件的起始和结束位置。符号名称由在文件名前加上binary并在其后添加 _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 bar数组。如果嵌入数据不统一,那么char可能是最方便的:在遍历数据时取其地址,并将指针转换为正确的类型。


5
阅读这里和互联网上所有的文章后,我得出结论:目前没有一个满足以下条件的资源工具:
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

1
我认为你的答案需要更好的解释,才能让更多人受益。 - kyb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接