C语言如何在编译期导入二进制数据

本文最后更新于 2026年10月8日

在做某个 OS 实验的时候遇到了这个需求,实验指南建议使用 xxd 将二进制文件转换成 C/C++ 可以接受的常量数组

1
2
3
4
5
6
7
8
// Generated by: xxd -i /bin/ls
unsigned char _bin_ls[] = {
0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00,
...
0x00, 0x00, 0x00, 0x00
};
unsigned int _bin_ls_len = 142144;

但是这样会导致代码文件体积膨胀。

更优雅的办法是通过incbin直接将二进制数据放进ELF文件的只读区,NEMU和iPXE都使用了类似的方法。

1
2
3
4
5
6
7
#define EMBED_STR(name, path)                \
extern const char name[]; \
asm(".section .rodata, \"a\", @progbits\n" \
#name ":\n" \
".incbin \"" path "\"\n" \
".byte 0\n" \
".previous\n");

甚至可以用这个方法实现 quine

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#define EMBED_STR(name, path) \
extern const char name[]; \
asm(".section .rodata, \"a\", @progbits\n" \
#name ":\n" \
".incbin \"" path "\"\n" \
".byte 0\n" \
".previous\n");

EMBED_STR(SourceFile, __FILE__);

int main() {
printf("%s", SourceFile);
return 0;
}

需要注意的是这个方法在有些平台/编译器并不适用,比如MSVC x64 版不支持内联汇编。

这篇文章给出了更多的方式,比如 objcopy, Windows rc file

github上的incbin库考虑了多个平台多个编译器。

C23中引入了#embed宏,可以在编译时将指定内容设置为字符数组:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main() {
const unsigned char data[] = {
#embed "data.bin"
};
printf("%zu\n", sizeof(data)); // 就是文件的字节数
return 0;
}

还支持limit、prefix、suffix、if_empty等参数,例如:

1
2
3
const unsigned char str[] = {
#embed "data.txt" suffix(, 0) // 末尾补个NUL,直接当字符串用
};

编译器支持方面,Clang 19和GCC 15起可用;C++26也采纳了#embed。

这个特性已经有了不少真实用户。GDAL用它内嵌各驱动的XML/CSV资源文件(有正式的RFC 102),PROJ用它内嵌proj.db数据库,pyodide用它内嵌初始化数据,ClassiCube用它内嵌Wii U/PSVita/3DS平台的shader二进制。

需要注意的是,#embed本质上是把文件展开成逗号分隔的字节列表(等价于xxd -i的预处理版),文件较大时编译时间和内存开销可观,大文件场景incbin这种直接塞进.rodata的方案仍然有优势。

另外值得一提的是,C++这边原本还有一个更强大的库版本提案std::embed(P1040):一个consteval函数,可以在constexpr求值时读文件,支持递归处理(嵌进去的文件还能再引用别的文件)。它没能进入C++26,主要卡在依赖追踪上——#embed是预处理指令,构建系统的依赖扫描器能直接看到;而std::embed的文件路径是编译期字符串,读取发生在constexpr求值阶段,扫描器无从得知,为此提案引入了#depend指令做补救,但递归glob、模块交互等问题一直没吵完。不过提案还在继续努力,2026年已投票转向C++29推进。