一个可执行文件,用file命令一看,结果是"SQLite 3.x database";用sqlite3打开它,能直接查出它的符号表和依赖库;chmod +x之后照样能跑,敲一下就打印出"Hello, world!"。这不是恶搞,是开发者Farid Zakaria最近公开的一个原型项目,叫SELF(Structured Executable & Linkable Format)。
这套类比确实漂亮——ELF格式里塞满了字符串表、哈希索引、节头表,本质上就是一个不肯承认自己是数据库的数据库。把它真的做成SQLite文件,概念上顺理成章。但从公开的基准测试看,这套原型目前的启动延迟比原生ELF高出数倍,一些复杂的动态链接特性还没做出来,甚至它自己的加载器都得靠ELF格式才能启动。SELF更像是给二进制分析和打包场景准备的新工具,离替换ELF还有距离。
从"查ELF"到"吃掉ELF"
Zakaria不是心血来潮。他博士期间做过一个叫sqlelf的工具,用SQLite的虚拟表机制给ELF文件建了一层SQL查询接口,select name from elf_symbols就能拿到符号表,不用再拼readelf和grep。这个思路后来写成论文投出去被拒,但他没放弃——2025年的博士论文里专门用一章讲"可执行文件的关系数据模型"。SELF是把这个念头往前推了一步:不是给ELF包一层数据库外壳,而是让数据库本身就是可执行文件。
实现方式不复杂。文件用SQLite自带的application_id字段做标记(值为0x53454C46,正是SELF四个字母的ASCII码),配合Linux的binfmt_misc机制,内核看到这个魔数就转交给专门写的解释器。这个解释器功能上很像ld.so,只是它从数据库表里读段信息和符号表,而不是从ELF的节区里读。
两张表,拆掉一整个格式
一个能跑的SELF文件最少只需要两张表:self_meta存原来ELF头里的键值对,segments一行对应一个程序头,加载用的字节直接存成BLOB。符号表进一步把.dynstr、.hash、.gnu.version_r这些各管一段的机制收进一张symbols表加一个索引——strip变成一次DELETE加VACUUM,patchelf变成一次UPDATE,ldd变成一次JOIN查询。
体积上的代价比想象中小。剥离过的coreutils,SELF版本是1,794,048字节,原生ELF是1,768,632字节,差距不到1.4%——数据库结构的开销大部分来自可选的调试表,删掉基本抹平。
启动慢下来了,而且慢得不均匀
真正的问题出在速度。按公开的基准数据,一个15KB级别的hello程序,原生ELF启动只要0.422毫秒,同样内容的SELF文件走原生加载模式要2.168毫秒,慢了约5倍。程序越大差距越夸张:git从3.0毫秒变成20.8毫秒,gdb从86.1毫秒变成156.4毫秒。
问题不只是SQLite解析本身要花时间。ELF的做法是把可执行段直接mmap进内存,多个进程跑同一个程序时能共享同一份只读代码页。SELF的段数据存在SQLite的B-tree页里,不是按内存页对齐排布,加载器只能把字节从数据库里拷出来,拷贝量跟文件大小成正比,进程之间也没法再共享代码页。这不是调参能解决的小毛病,是选了数据库格式之后必然要付的代价。
ELF放弃自描述换来mmap直接能跑,SELF换回自描述,也换掉了这一层免费的共享内存。
- 风险.SELF目前的动态链接实现(M3b阶段)还不支持TLS、IFUNC这些glibc复杂特性,只能处理受限的静态闭包;负责在内核态直接加载SELF文件的M4阶段,作者还没开始动手。
自举悖论:替代ELF的格式,离不开ELF
比性能更根本的问题是,SELF目前跑不脱ELF。负责解析数据库、映射段、跳转到入口点的self-exec解释器,自己必须编译成一个ELF文件——如果它也用SELF格式,binfmt_misc的识别规则会递归匹配到它自己身上,直接死循环报错。换句话说,这套"取代ELF"的方案,运行时刻还得靠一个ELF程序把它领进门。
安全上也有一层没解开的矛盾。可执行文件的传统假设是只读、内容固定,改动要靠专门工具做偏移量手术;SQLite数据库的卖点恰恰是可以被事务化地写入和修改。SELF把这两件事捏到一起,意味着任何能写这个文件的进程,理论上都能改写程序本身的段和符号表——这在SQLite官方论坛的讨论里已经被提出来当作风险点,SELF的原型现阶段还没有给出对应的加固方案。
社区的反应也印证了这种分寸感。Hacker News和Reddit上,大家认可的场景集中在二进制分析、依赖审计、把一个程序和它全部依赖打包进一个文件这类工具属性上,而不是拿它去跑生产环境的进程。这和更早的先例吻合:HHVM多年前就用SQLite存过.hhbbc编译产物,但没人拿它去替代可执行文件本身跑进程。
Zakaria在Nix生态里做过不少"把黑盒变成可查询结构"的实验,SELF延续的是这条脉络——ldd等价于一次JOIN、strip等价于一次事务,确实能让二进制分析和可复现构建的工具链变得干净很多。把这套优雅搬到进程加载路径上,要面对的是操作系统几十年攒下来的mmap、共享页、glibc符号解析这些工程细节,不会因为换了个存储格式就自动解决。
对写工具链、做二进制分析的人来说,SELF这套SQL接口现在就值得玩一玩;对指望它进生产环境跑二进制的人,更现实的做法是继续用ELF,同时留意SELF的M4内核加载器什么时候真正启动。
