使用GCC将资源嵌入可执行文件

92
我正在寻找一种简单的方法,在GCC编译的C/C++应用程序中轻松嵌入任何外部二进制数据。
一个很好的例子是处理着色器代码-我可以将其保存在类似于“const char* shader =“ source here”;”的源文件中,但这非常不实用。
我希望编译器为我完成:在编译(链接)时,读取文件“foo.bar”,并将其内容链接到我的程序,以便我可以从代码中访问内容作为二进制数据。
对于我想要将其作为单个.exe文件分发的小型应用程序可能会有用。
GCC是否支持此类功能?

可能是C/C++ with GCC: Statically add resource files to executable/library的重复问题。 - Ciro Santilli OurBigBook.com
6个回答

94

有几种可能性:

ld -r -b binary -o binary.o foo.bar  # then link in binary.o
  • 使用bin2c / bin2h工具将任何文件转换为字节数组 (在不使用资源部分或外部图像的情况下嵌入图像)。


  • 更新:以下是如何使用ld -r -b binary将数据绑定到可执行文件中的更完整示例:

    #include <stdio.h>
    
    // a file named foo.bar with some example text is 'imported' into 
    // an object file using the following command:
    //
    //      ld -r -b binary -o foo.bar.o foo.bar
    //
    // That creates an bject file named "foo.bar.o" with the following 
    // symbols:
    //
    //      _binary_foo_bar_start
    //      _binary_foo_bar_end
    //      _binary_foo_bar_size
    //
    // Note that the symbols are addresses (so for example, to get the 
    // size value, you have to get the address of the _binary_foo_bar_size
    // symbol).
    //
    // In my example, foo.bar is a simple text file, and this program will
    // dump the contents of that file which has been linked in by specifying
    // foo.bar.o as an object file input to the linker when the progrma is built
    
    extern char _binary_foo_bar_start[];
    extern char _binary_foo_bar_end[];
    
    int main(void)
    {
        printf( "address of start: %p\n", &_binary_foo_bar_start);
        printf( "address of end: %p\n", &_binary_foo_bar_end);
    
        for (char* p = _binary_foo_bar_start; p != _binary_foo_bar_end; ++p) {
            putchar( *p);
        }
    
        return 0;
    }
    
    更新2 - 获取资源大小:我无法正确读取_binary_foo_bar_size的值。在运行时,通过使用display (unsigned int)&_binary_foo_bar_size,gdb向我显示了文本资源的正确大小。但将其分配给变量始终会得到错误的值。我通过以下方式解决了这个问题:
    unsigned int iSize =  (unsigned int)(&_binary_foo_bar_end - &_binary_foo_bar_start)
    

    这是一个解决方法,虽然不太优雅,但效果还不错。


    3
    将这个 blob 视为文本。如果需要以这种方式终止文本,您可能需要做一些工作,以确保文本末尾有一个 '\0'。可能需要进行一些实验。 - Michael Burr
    9
    @VJo:文本本质上是二进制的,电脑上的所有内容都是二进制的。 - MSalters
    2
    @MSalters 关于“文本是二进制”的说法没错,但是,在文本中,不同系统可能会以不同的方式对待行尾结束符。显式地将其称为二进制可以避免出现这种问题。 - Jesse Chisholm
    3
    你所描述的是可写(“数据”)和可执行(“代码”)之间的区别。只读数据不需要方法。 - MSalters
    2
    你能告诉 ld 为数据生成哪个符号名称吗? - Calmarius
    显示剩余16条评论

    44

    除了已提到的建议外,在Linux下,您可以使用十六进制转储工具xxd,它具有生成C头文件的功能:

    xxd -i mybinary > myheader.h
    

    10
    我认为这个解决方案是最好的。它还支持跨平台和跨编译器。 - Behrouz.M
    5
    确实如此,但它有一个缺点——生成的头文件比原始二进制文件大得多。这对最终编译结果没有影响,但作为构建过程的一部分可能不太理想。 - Riot
    4
    可以通过使用预编译头文件来解决这个问题。 - Behrouz.M

    24

    使用.incbin GAS指令可以完成此任务。这是一个完全免费的许可库,围绕它进行了封装:

    https://github.com/graphitemaster/incbin

    简而言之,incbin方法如下。您有一个thing.s汇编文件,使用gcc -c thing.s进行编译。

          .section .rodata
        .global thing
        .type   thing, @object
        .align  4
    thing:
        .incbin "meh.bin"
    thing_end:
        .global thing_size
        .type   thing_size, @object
        .align  4
    thing_size:
        .int    thing_end - thing
    

    在你的 C 或 C++ 代码中,你可以通过以下方式引用它:

    extern const char thing[];
    extern const char* thing_end;
    extern int thing_size;
    

    那么,您将生成的 .o 文件与其他编译单元链接起来。 要归功于@John Ripley在这里提供的答案:C/C++ with GCC: Statically add resource files to executable/library

    但是上述方法不如incbin提供的方便。 使用incbin实现上述操作无需编写任何汇编代码,只需使用以下内容即可:

    #include "incbin.h"
    
    INCBIN(thing, "meh.bin");
    
    int main(int argc, char* argv[])
    {
        // Now use thing
        printf("thing=%p\n", gThingData);
        printf("thing len=%d\n", gThingSize);   
    }
    

    3
    我喜欢这种方法,因为它允许控制符号名称。 - Ciro Santilli OurBigBook.com
    这个解决方案的问题,在于使用C++编程时,构建表示嵌入式数据的结果std::span无法是constexpr类型,因为它依赖于外部符号。尽管如此,我还是广泛使用您的解决方案。 - Laurent LA RIZZA

    17
    对于 C23,现在有了预处理指令#embed,可以在不使用外部工具的情况下实现您所需要的功能。可以参考 C23 标准的 6.10.3.1 节(这里是最新的草案链接working draft)。这篇优秀博客文章介绍了 #embed 的历史背景以及该特性背后的委员会成员之一的观点。
    以下是标准草案中演示其用法的代码片段:
    #include <stddef.h>
    void have_you_any_wool(const unsigned char*, size_t);
    
    int main (int, char*[]) {
        static const unsigned char baa_baa[] = {
    #embed "black_sheep.ico"
        };
        
        have_you_any_wool(baa_baa, sizeof(baa_baa));
        return 0;
    }
    

    目前为止,C++中不存在等效的指令。

    1
    当然,have_you_any_woolblack_sheep.ico并不是C23标准中实际使用的名称,对吧? - Miles Rout
    2
    这段代码片段是直接从标准中摘录的。请参阅此PDF文档的第169页:https://open-std.org/JTC1/SC22/WG14/www/docs/n3088.pdf。我们当然可以允许委员会有一点幽默感,不是吗? - irowe
    幽默感?当然。但这不是幽默,只是一个网站标志是黑羊试图自我插入所有标准的人。 - Miles Rout

    1
    如果我想将静态数据嵌入可执行文件中,我会将其打包成.lib/.a文件或头文件作为无符号字符数组。这样做可以实现可移植性。我创建了一个命令行工具,实际上可以同时完成这两种操作,你可以在这里找到: here。你只需要列出文件,然后选择选项“-l64”以输出一个64位库文件和一个包含每个数据指针的头文件。当然,你也可以探索更多选项。
    >BinPack image.png -j -hx
    

    此命令将以十六进制形式输出image.png的数据到头文件中,每行根据-j选项对齐。

    const unsigned char BP_icon[] = { 
    0x89,0x50,0x4e,0x47,0x0d,0x0a,0x1a,0x0a,0x00,0x00,0x00,0x0d,0x49,0x48,0x44,0x52,
    0x00,0x00,0x01,0xed,0x00,0x00,0x01,0xed,0x08,0x06,0x00,0x00,0x00,0x34,0xb4,0x26,
    0xfb,0x00,0x00,0x02,0xf1,0x7a,0x54,0x58,0x74,0x52,0x61,0x77,0x20,0x70,0x72,0x6f,
    0x66,0x69,0x6c,0x65,0x20,0x74,0x79,0x70,0x65,0x20,0x65,0x78,0x69,0x66,0x00,0x00,
    0x78,0xda,0xed,0x96,0x5d,0x92,0xe3,0x2a,0x0c,0x85,0xdf,0x59,0xc5,0x2c,0x01,0x49,
    0x08,0x89,0xe5,0x60,0x7e,0xaa,0xee,0x0e,0xee,0xf2,0xef,0x01,0x3b,0x9e,0x4e,0xba,
    0xbb,0x6a,0xa6,0x66,0x5e,0x6e,0x55,0x4c,0x8c,0x88,0x0c,0x07,0xd0,0x27,0x93,0x84,
    0xf1,0xef,0x3f,0x33,0xfc,0xc0,0x45,0xc5,0x52,0x48,0x6a,0x9e,0x4b,0xce,0x11,0x57,
    0x2a,0xa9,0x70,0x45,0xc3,0xe3,0x79,0xd5,0x5d,0x53,0x4c,0xbb,0xde,0xd7,0xe8,0x57,
    0x8b,0x9e,0xfd,0xe1,0x7e,0xc0,0xb0,0x02,0x2b,0xe7,0x03,0xcf,0xa7,0xa5,0x87,0xff,
    0x1a,0xf0,0xb0,0x54,0xd1,0xd2,0x0f,0x42,0xde,0xae,0x07,0xc7,0xf3,0x83,0x92,0x4e,
    0xcb,0xfe,0x22,0xc4,0xa7,0x91,0xb5,0xa2,0xd5,0xee,0x97,0x50,0xb9,0x84,0x84,0xcf,
    0x07,0x74,0x09,0xd4,0x73,0x5b,0x31,0x17,0xb7,0x8f,0x5b,0x38,0xc6,0x69,0xaf}
    

    -4
    你可以在头文件中实现这个功能:
    #ifndef SHADER_SRC_HPP
    #define SHADER_SRC_HPP
    const char* shader= "
    
    //source
    
    ";
    #endif
    

    只需要包含它。

    另一种方法是读取着色器文件。


    4
    我认为Kos希望能够维护着色器源代码而无需担心转义特殊字符(以及其他可能出现的问题)。 - Michael Burr
    2
    @VJo:不,从未使用过着色器。我的方法是将存储在外部文件中的任意数据嵌入程序中。我可以认可这对于特定的着色器来说可能是更好的解决方案。 - Michael Burr
    1
    定义(而不是声明)全局变量的文件不应该是头文件,而应该是源模块。而且你的类型非常低效。最好改为 const char shader[] = "source"; - R.. GitHub STOP HELPING ICE
    9
    另外,我认为C++不允许你以其他方式拥有多行字符串字面量,除了在每一行单独开启和关闭“”引号或在每一行结尾加上反斜杠。更别提开发过程中将着色的着色器作为单独的文件进行使用的其他好处了(至少包括语法着色)。 - Kos
    1
    自从C++11以来,您可以使用“原始字符串字面值”,它看起来像R"*( ... 多行文本 ... )*"。您可以使用其他分隔符代替星号。 - Zeno Rogue
    显示剩余2条评论

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