如何在程序中包含数据对象文件(图像等)并访问符号?

How to include data object files (images, etc.) in program and access the symbols?

我已经使用 objcopy 将几个资源文件转换为 .obj 文件,并且 link 我将它们与我的程序源代码一起使用。 我可以使用以下代码很好地访问程序中目标文件中的符号,但只能使用 GCC/G++ (Cygwin):

extern uint8_t data[]   asm("_binary_Resources_0_png_start");
extern uint8_t size[]   asm("_binary_Resources_0_png_size");
extern uint8_t end[]    asm("_binary_Resources_0_png_end");

代码在 Visual Studio 中不起作用,可能是因为 VS 有它自己的 __asm 命令。 我想通过 linking 将我的程序资源(图像、着色器等)包含在我的最终可执行文件的 .data 部分中。

但是我如何访问 VC++ 中目标文件中定义的符号? 我在没有汇编命令的情况下尝试了 extern uint8_t _binary_Resources_0_png_start[]extern "C" uint8_t _binary_Resources_0_png_start[],但我得到了未解析的符号 link 错误。

objcopy 的技巧并不意味着嵌入资源的全功能方式,而且根本不可移植,如您所见。

Microsoft 有自己的资源机制,因此如果您专门针对 windows,您可以使用 windows 资源文件和 RCDATA resource.

如果您想要完全可移植的东西,您唯一的选择是将文件格式化为 C 源代码,例如

const uint8_t my_binary[] = { 0x00, 0x01, ... }

为此编写您自己的转换工具很简单。

这可能是一种完全不同的方法,但它提供了一个相当简单但可移植的解决方案:

我们使用一个小工具加载二进制文件并将其输出为C(或C++源)。实际上,我在 XPM 和 GIMP 中看到过类似的东西,但它可以用于任何二进制数据。

在 VS 中将此类工具包含在构建链中并不困难,在 makecmake 中更简单。

这样的工具可能如下所示:

#include <fstream>
#include <iostream>
#include <string>

using namespace std;

int main(int argc, char **argv)
{
  if (argc < 2) {
    cerr << "Usage: " << argv[0] << " FILE [FILE...]" << endl;
    return -1;
  }
  for (size_t i = 1; i < argc; ++i) {
    fstream fIn(argv[i], ios::in | ios::binary);
    if (!fIn.good()) {
      cerr << "ERROR: Cannot open '" << argv[i] << "'!" << endl;
      continue;
    }
    // make name
    string name = argv[i];
    name = name.substr(0, name.find('.'));
    /// @todo more sophisticated name mangling?
    // print preface
    cout << "struct { const char *data; size_t size; } " << name << " = {" << endl
      << "  \"";
    // print data
    const char hex[] = "0123456789abcdef";
    unsigned char byte;
    enum { BytesPerLine = 16 };
    size_t n = 0;
    for (unsigned char byte; fIn.get((char&)byte); ++n) {
      if (n && !(n % BytesPerLine)) cout << "\"\n  \"";
      cout << "\x" << hex[byte / 16] << hex[byte % 16];
    }
    // print size
    cout << "\",\n"
      "  " << n << "\n"
      "};" << endl;
  }
  return 0;
}

编译测试:

$ g++ -std=c++11 -o binToC binToC.cc

$ ./binToC
Usage: ./binToC FILE [FILE...]

更多测试 fluffy_cat.png :

$ ./binToC fluffy_cat.png > fluffy_cat.inc

$ cat >fluffy_cat_test.cc <<'EOF'
> #include <fstream>
> 
> using namespace std;
> 
> #include "fluffy_cat.inc"
> 
> int main()
> {
>   ofstream fOut("fluffy_cat_test.png", ios::out | ios::binary);
>   fOut.write(fluffy_cat.data, fluffy_cat.size);
>   fOut.close();
>   return 0;
> }
> EOF

$ g++ -std=c++11 -o fluffy_cat_test fluffy_cat_test.cc

$ ./fluffy_cat_test

$ diff fluffy_cat.png fluffy_cat_test.png

$

diff 所示 – C 源代码完全复制了原件。

顺便说一句。我在 .

的回答中使用了相同的技术(以类似的形式)

你的问题最初没有说明这是针对 64 位 Cygwin G++/MSVC++ 还是 32 位的。在名称装饰方面存在细微差别。


x86(32 位 Windows PE)解决方案与 OBJCOPY

我假设您有一个名为 Resources_0.png 的资源文件。您可以生成 32 位 Windows PE 目标文件:

objcopy --prefix-symbol=_ --input-target binary --output-target \
    pe-i386 --binary-architecture i386 Resources_0.png Resources_0.obj

--prefix-symbol=_ 为每个标签附加了一个额外的下划线 (_)。使用额外的 _ 修饰名称是 Win32/PE 外部对象的标准。生成的文件会产生一个带有这些标签的对象:

__binary_Resources_0_png_start
__binary_Resources_0_png_end
__binary_Resources_0_png_size

针对 32 位可执行文件的 MSVC++ 和 Cygwin G++ 可以将这些标签引用为:

extern "C" uint8_t _binary_Resources_0_png_start[];
extern "C" uint8_t _binary_Resources_0_png_end[];
extern "C" uint8_t _binary_Resources_0_png_size[];

x86-64 (64-bit Windows PE) OBJCOPY解决方案

您可以生成 64 位 Windows PE 目标文件:

objcopy --input-target binary --output-target pe-x86-64 --binary-architecture i386 \
    Resources_0.png Resources_0.obj

这与 32 位类似,但我们不再在每个标签前添加额外的下划线 (_)。这是因为在 64 位 PE 代码中,名称没有用额外的下划线修饰。

生成的文件会生成一个带有这些标签的对象:

_binary_Resources_0_png_start
_binary_Resources_0_png_end
_binary_Resources_0_png_size

MSVC++ 和 Cygwin G++ 针对 64 位 Windows PE 可执行文件可以引用这些标签,与上面的 32 位 Windows PE 版本完全相同:

extern "C" uint8_t _binary_Resources_0_png_start[];
extern "C" uint8_t _binary_Resources_0_png_end[];
extern "C" uint8_t _binary_Resources_0_png_size[];

特别说明:使用 MSVC++ 作为 64 位代码编译时,使用 size 标签时可能会出现此链接错误:

absolute symbol '_binary_Resources_0_png_size' used as target of REL32 relocation in section 4

对于 64 位代码,您可以通过使用 startend 标签之间的差异计算 C++ 代码中的大小来避免这种情况,如下所示:

size_t binary_Resources_0_png_size = _binary_Resources_0_png_end - \
                                     _binary_Resources_0_png_start;

其他观察结果

即使使用 G++/GCC,这也是错误的形式:

extern uint8_t data[]   asm("_binary_Resources_0_png_start");
extern uint8_t size[]   asm("_binary_Resources_0_png_size");
extern uint8_t end[]    asm("_binary_Resources_0_png_end");

几乎不需要这样做,而且不太便携。请参阅上面的解决方案,这些解决方案不对 G++ 代码的变量使用 asm 指令。


问题同时标记为 C 和 C++,并且问题包含带有 extern "C" 的代码。上面的答案假设您正在使用 G++/MSVC++ 编译 .cpp 文件。如果使用 GCC/MSVC 编译 .c 文件,则将 extern "C" 更改为 extern


如果要生成 Windows 带有 OBJCOPY 的 PE 对象,其中数据放置在只读 .rdata 部分而不是 .data 部分,您可以将此选项添加到上面的 OBJCOPY 命令:

--rename-section .data=.rdata,CONTENTS,ALLOC,LOAD,READONLY,DATA

我在这个 中讨论了这个选项。不同之处在于,在 Windows PE 中,只读部分通常称为 .rdata,而对于 ELF 对象,它是 .rodata

在解决和测试不同的事情之后,我回到了我原来的方法(linking)并且它像魔术一样工作,这里是详细信息:

为了在最终可执行文件的 .data 部分中包含数据,您需要首先将该数据文件(可以是任意二进制文件(任何东西!))转换为 linkable文件格式,也称为目标文件。

工具 objcopy 包含在 GNU Binutils 中并且可以在 windows 中通过 CygwinMinGW 访问,获取一个文件并生成一个对象文件。 objcopy 在生成目标文件之前需要知道两件事,即输出文件格式和输出体系结构。 为了确定这两件事,我使用工具 objdump:

检查了一个有效的 linkable 对象文件
objdump -f main.o

这为我提供了以下信息:

main.o:     file format pe-x86-64
architecture: i386:x86-64, flags 0x00000039:
HAS_RELOC, HAS_DEBUG, HAS_SYMS, HAS_LOCALS
start address 0x0000000000000000

有了这些知识,我现在可以创建目标文件了:

objcopy -I binary -O pe-x86-64 -B i386 data_file.data data_file_data.o

为了处理大量文件,批处理文件可以派上用场。

然后我简单地 link 生成的目标文件连同我的程序源和取消引用 objcopy 生成的指针,通过符号,其名称可以很容易地查询:

objdump -t data_file_data.o

这导致:

data_file_data.o:     file format pe-x86-64

SYMBOL TABLE:
[  0](sec  1)(fl 0x00)(ty  0)(scl  2) (nx 0) 0x0000000000000000 _binary_data_file_data_start
[  1](sec  1)(fl 0x00)(ty  0)(scl  2) (nx 0) 0x0000000000000006 _binary_data_file_data_end
[  2](sec -1)(fl 0x00)(ty  0)(scl  2) (nx 0) 0x0000000000000006 _binary_data_file_data_size

实际上,以下代码适用于 GCC/G++

extern uint8_t data[]   asm("_binary_data_file_data_start");
extern uint8_t end[]    asm("_binary_data_file_data_end");

以及以下 MSVC++

extern "C" uint8_t _binary_data_file_data_start[]; // Same name as symbol
extern "C" uint8_t _binary_data_file_data_end[];   // Same name as symbol

每个文件的大小计算如下:

_binary_data_file_data_end - _binary_data_file_data_start

例如,您可以将数据写回到文件中:

FILE* file;

file = fopen("data_file_reproduced.data", "wb");
fwrite(_binary_data_file_data_start,                               //Pointer to data
       1,                                                          //Write block size
       _binary_data_file_data_end - _binary_data_file_data_start,  //Data size
       file);

fclose(file);