真正可查询的可执行文件

查看原文 HN 讨论

文章摘要

这是 Farid Zakaria 对他此前那篇《你的可执行文件是一个 SQLite 数据库》的续作。前作提出了一种叫 SELF 的格式:程序本身就是一个 SQLite 数据库,通过 Linux 的 binfmt_misc 机制注册一个自定义解释器,由解释器把 segments 表里的行映射进内存并跳转到入口点执行。这样一来,一整类二进制工具链(读段表、查符号、看重定位)就全部塌缩成了 SQL 查询。

这篇续作追问的是一个在评论区被反复提出的问题:既然可执行文件是数据库,而数据库是可写的,那运行中的程序能不能把自己的状态也写回去?作者的回答是能,而且他做了一个概念验证——self-httpd,一个单文件的 Web 服务器。这个文件同时是程序、是网站内容、是路由表、是访客日志。用 file 命令看它,报告的是「SQLite 3.x database」;直接执行它,它就开始在 8080 端口提供服务;用 sqlite3 打开它,就能查出刚才有多少人访问了首页、按了多少次按钮。所有应用状态都以事务方式更新在同一个文件里,作者由此设想:可以把整个发行版和所有应用的状态收进单个文件,/var/tmp/home 这些目录都不再必要。

技术实现上有几个关键点。程序如何拿到自身的句柄?目前还不能用 /proc/self/exe——因为 binfmt_misc 匹配时内核根本没有 execve 你的文件,它执行的是解释器,只是把路径交了过去(作者顺带提到,VFS 维护者最近刚在内核里落地了透明 binfmt_misc 支持,将来这条路会通)。解释器把 argv 往后挪一位传给程序,于是程序的 argv[0] 就是自己的路径;解释器还会在跳转前释放自己的 SQLite 连接,程序拿 argv[0] 直接 sqlite3_open 就能读写自己。

由此衍生出的能力很有意思。构建过程平淡得反常:先用 cc 编译成普通 ELF,再用 elf2self 转换成数据库,然后用 DDL 建应用表、用 INSERT 加上 readfile() 把网站塞进去。修改线上站点变成一条 UPDATE,而且是事务性的——ROLLBACK 就能撤销,不用重启、不用重载、不用部署。因为格式是 SQLite,整个生态的工具都能白嫖:sqldiff --summary 可以精确告诉你「这次部署到底改了什么」,能审计出 routes 表改了一行而 segments、symbols、relocations 都没动;FTS5 只要一条 CREATE VIRTUAL TABLE,Web 服务器就能给自己的页面建全文索引,索引也存在自己体内,而它仍然是个能跑的 Web 服务器。部署退化成 scp 单个文件;而升级迁移就是两条 INSERT ... SELECT,因为程序和数据本来就是同一个文件——ATTACH 旧文件、把 visits 和 presses 搬进新版本、换文件、重启,访客日志就保住了。作者还促狭地指出:segments 表也不过是普通的表,所以你也可以反过来迁移程序本身。

作者坦承灵感来自 Justine Tunney 的 redbean(用自解压 ZIP 做的单文件 Web 服务器),并做了对比:redbean 需要额外引入归档格式 ZIP,而 SELF 里数据库本身就是容器;redbean 用 Lua 钩子操作响应,SELF 的等价物是往 handlers 表里插一行 SQL。如果 redbean 是「真正可移植的可执行文件」(APE),这个就是「真正可查询的可执行文件」——一个到处都能跑,另一个你可以对它做 SELECT。代码开源在 fzakaria/selfdb,作者大方承认「有点半成品,而且确实是 AI 辅助写的,但我无所谓」,结尾引用 Randy Pausch 的话:永远不要低估「玩得开心」的重要性。

HN 评论精华

这条帖子拿到 338 分,讨论氛围罕见地愉快——大量评论是「这玩意儿邪门但我爱死了」,另一半则在认真挖掘它的安全隐患和历史先例。