Farid Zakaria 有篇博客《Your executable is a SQLite database》,讲他想把 ELF 换成 SQLite 作为可执行格式。他博士期间就在琢磨这事,写过一篇没发出去的论文(arXiv:2405.03883),最近又把它捡起来做成了能跑的原型,叫 SELF。

我一开始以为这是个行为艺术,读完发现里面有几条很实际的技术判断,值得记下来。

效果:它真的能跑

最直观的验证是这两行:

$ file hello
hello: SQLite 3.x database, application id 0x53454c46, user version 1
$ ./hello
Hello, world!

file 把它认成 SQLite 数据库,但它是可执行的——直接 ./hello 就能运行。application id 0x53454c46 拆成 ASCII 正好是 "SELF"(SQLite Executable and Linkable Format)。

既然是个数据库,就能用 sqlite3 打开看结构。里面是一张表加一个索引:

CREATE TABLE symbols (
  id      INTEGER PRIMARY KEY,
  name    TEXT NOT NULL,   -- 'GLIBC_2.2.5'
  version TEXT,
  value   INTEGER,
  size    INTEGER,         -- 'func' | 'object' | ...
  ...
);

一张 symbols 表装下符号表,一个索引加速按名字查询。这就是整个"可执行格式"的元数据部分。

为什么这个想法值得认真对待

ELF 是个四十多年前的设计,它的核心假设是"磁盘上的布局要尽量贴近加载时的内存布局"。所以 ELF 里有 program header、section header、各种对齐填充、节之间的间隙。这套设计在磁头和页式内存的时代是合理的:加载器尽量少做转换,直接把数据映射进地址空间。

但今天的情况变了。磁盘和内存的差距被 page cache 和 SSD 抹平,加载器要做的转换有成熟工具,而"格式的可读性和可查询性"变成了新的瓶颈。你想想调试一个 ELF 有多麻烦:要看符号,得 readelf 或 nm;要看依赖,得 ldd;要看节信息,得 objdump。每个工具都重新实现一遍解析器,彼此还经常不一致。

如果把格式换成 SQLite,这些全变成 SQL 查询:

-- 这个二进制导出了哪些函数
SELECT name FROM symbols WHERE type = 'func' AND value != 0;

-- 它依赖哪些符号、来自哪个库版本
SELECT name, version FROM symbols WHERE value = 0 ORDER BY version;

不用为了查一个符号去记 nm 的参数,不用为了看依赖去翻 ldd 的输出格式。元数据是结构化的、可查询的、有索引的。这个好处是实打实的。

代价:也就大了 1%

这类"换格式"的想法通常死在体积和性能上。他给了一个数字:一个 stripped 的 coreutils SELF 是 1,794,048 字节,对应的 ELF 是 1,768,632 字节,差距在 1% 以内。

这个结果其实合理。ELF 本身为了对齐和节间填充,也浪费了不少空间。SQLite 的 B-tree 结构虽然有自己的页开销,但它把符号表组织得更紧凑,两边基本打平。用户不会因为多了 1% 的磁盘占用而拒绝一个更好用的格式。

真正需要担心的是加载性能。SQLite 打开一个数据库文件、解析 B-tree、找到需要的页——这套操作比"直接把 ELF 映射进内存"要慢。作者的博士论文里应该处理过这个问题(我没找到公开部分),从原型能跑来看,至少不是不可逾越的障碍。一个可行的方向是保留 SQLite 作为"存储格式",同时为加载准备一个平铺的页布局,让 mmap 之后能近似顺序访问。

为什么这类激进想法值得做

作者在文里坦白了一句:博士期间这个想法得到的反馈"不激励人",因为激进的想法很难卖,你在跟整个生态的惯性对抗。

这句我很有共鸣。格式替换这类工作,最大的阻力从来不是技术,是"所有人都已经围绕 ELF 建了工具链"。你的新格式再好,gdb 不认、perf 不认、各大发行版的打包系统不认,就等于没有生态。

但反过来看,Nix 的经验说明了另一条路:当一个想法足够自洽,并且解决了真实的痛点,生态可以慢慢长出来。作者把 Nix 和 SELF 放在一起讲不是偶然——Nix 证明了一个"用能力来重建世界"的思路能活下来,SELF 想用同样的耐心去替换 ELF。

我的做法

这类"能不能换个格式"的问题,我现在的判断框架是三条:

  • 先量体积和吞吐的实际差距。差 1% 和差 10 倍是完全不同的两个问题。他没有停留在"理论上会更紧凑",而是实测了 coreutils 的字节数。
  • 想清楚加载路径。任何格式替换的成败都在"从磁盘到运行"这一段。如果加载比原来慢一个数量级,再好的可读性也没人用。
  • 接受生态是慢变量。别指望发布就能被采用,把它当成一个长期的技术储备。你从中学到的格式设计经验,往往比格式本身更有价值。

把可执行文件当数据库,短期不会取代 ELF。但它逼着人重新问一遍:"我们为什么需要这些节、这些对齐、这些专门的解析工具?"——能问出好问题的项目,即使失败了也有价值。