file hello 返回「SQLite 数据库」,程序照样能跑
在终端里对一个可执行文件运行 file hello,正常结果是「ELF 64-bit LSB executable」。Farid Zakaria 的实验给出了另一个答案:SQLite 3.x database。然后他接着运行 ./hello,程序正常输出了 Hello, World!。
这个演示没有魔术。Zakaria 是 Nix 社区的活跃开发者,博士期间就做过 sqlelf——用 SQL 查询 ELF 文件内部结构。SELF(Structured Executable & Linkable Format)是他这次更进一步的尝试:把 SQLite 文件直接做成一种合法的可执行格式,而 Linux 内核的 binfmt_misc 机制负责把它送给对应的解释器运行。SQLite 在这里承担的是存储层本身,而非查询层。
ELF 三十年来一直在手工实现数据库
这件事能成立,前提是 ELF 格式本身从设计之初就在做数据库该做的事。Zakaria 在原文中列出了一张对照表:
.strtab / .dynstr 是字符串驻留(string interning);.hash / .gnu.hash 是 CREATE INDEX;section header table 对应 sqlite_schema;st_name 字段是 .strtab 里的字节偏移量,本质上是一条外键;objcopy --strip-debug 做的事等价于 DELETE 加上 VACUUM。ELF 没有发明这些结构,它只是在没有通用数据库引擎的年代,用手工方式把它们重新造了一遍。
这种重复实现带来的代价是:每个需要解析 ELF 的工具——链接器、调试器、打包工具——都要自己写一套二进制解析逻辑,且彼此不兼容。数据在文件里,但没有标准查询接口。
SELF 的核心表结构:把 ELF 概念翻译成关系型
SELF 的数据库里有四张核心表:segments 存程序头,其中 content 字段是 BLOB,直接存段的原始字节;symbols 存符号表,name 是 TEXT,idx_symbols_name 索引让符号查找变成一次 B 树查询;objects 和 needs 两张表负责依赖关系——resolved_path 作为外键消除 soname 歧义,依赖闭包从「递归解析 DT_NEEDED」变成一条 SQL 递归查询。
图:Zakaria 在演讲中用这张漫画引出 SELF——从 Nix 讲到「SQLite 能不能做对象格式」。来源:Farid Zakaria
运行机制依赖 Linux 的 binfmt_misc:SELF 文件在 SQLite magic bytes 之上附加了特定的 application_id,内核识别到这个标识后调用 self-exec 解释器,由它读取 segments 表、把 BLOB 映射进内存、跳转执行。
图:SELF 的执行路径,elf2self 是转换工具,self-exec 是运行时解释器。来源:Farid Zakaria
两个数字说清楚代价在哪里
性能权衡在两个地方体现得最清楚。第一,启动延迟:self-exec 打开 SQLite 连接、查询表、拷贝 BLOB 到内存,固定开销约 5 ms。BLOB 拷贝还有一个连带效果:SELF 无法像 ELF 那样让多个进程共享 text 段的物理页,每个进程都要单独映射一份内容。对长期运行的服务,这 5 ms 可以忽略;对高频 fork 的场景,这是一个真实的成本。
第二,文件大小:单个 hello 的 SELF 文件约为 ELF 的两倍,但剥离调试信息后的 coreutils,SELF 与 ELF 大小差距收窄到 1% 以内。Zakaria 用一个更极端的测试说明了规模效应:把整个 NixOS userland 的 723 个可执行文件、400 个共享库打包成一个 SQLite 数据库,得到 611.9 MiB,而原始 ELF 文件加总是 644.4 MiB——数据库比散装文件更小,因为跨文件的字符串去重在关系型结构里是自然发生的。
图:SELF 与 ELF 的启动延迟及文件大小对比。来源:Farid Zakaria
依赖闭包变成 SQL 查询意味着什么
SELF 在 NixOS 上最有说服力的演示是 ls 的依赖图。ls 依赖 libc.so.6、libgmp.so.10 等库,这些库又有各自的依赖。传统方式要递归解析 DT_NEEDED、处理 soname 冲突、处理 rpath/runpath。在 SELF 里,整个闭包是一条带递归公共表表达式(recursive CTE)的 SQL 查询,346,386 个符号的全集可以用 SELECT 检索、用 WHERE 过滤,用 GROUP BY 统计重复。
图:以 ls 为根的依赖闭包,边对应 needs 表中的外键关系。来源:Farid Zakaria
nix run .#self-vm 可以启动一个所有二进制都是 SQLite 数据库的虚拟机。这说明整个 userland 的状态可以用一个通用查询引擎检视、修改、迁移,而每个工具不再需要各自实现 ELF 解析。
SELF 是一把量尺,不是替代品
SELF 短期内不会取代 ELF。内核原生支持、共享页映射、生态工具链的惯性,这些都是真实的壁垒。Zakaria 自己也没有声称这是替代方案。
但 SELF 做到了一件更有价值的事:把 ELF 格式三十年来的结构性冗余量化成了可对比的表格。当「剥离调试信息」可以用 DELETE FROM symbols WHERE ... 表达、当「查找所有依赖」可以用 recursive CTE 完成,就能看清楚 ELF 当前的工具链在哪些地方付出了不必要的复杂度。SELF 是一把量尺:衡量 ELF 为它的设计年代付出的代价,以及任何想改进可执行格式的方案需要在哪些地方超过它。
参考链接:
- Farid Zakaria 博客:SELF: A Relational Executable Format
- fzakaria/selfdb GitHub 仓库
- Farid Zakaria 早期项目 sqlelf