跳到主内容
@wquguru
精选75Hacker News Best(web_list)技巧与观点

用 SQLite 替代 ELF 作为可执行文件格式的工程实践

可执行文件本质是 SQLite 数据库的工程实践

原文
发到 X

I have been probably obsessed with two things in the last few years: Nix as a tool to explore innovative ideas that require the capability to rebuild the world and replacing ELF with SQLite as an executable format. You might have noticed that these two ideas are well suited to each other.

在过去的几年里,我可能痴迷于两件事:一是将Nix作为探索需要重建世界能力的创新想法的工具,二是用SQLite替代ELF作为可执行文件格式。你可能已经注意到,这两个想法非常契合。

I explored the idea during my PhD thesis but found feedback from others unmotivating. Radical ideas are hard to sell, as you are working against the inertia of the established solution.

我在博士论文中探索了这个想法,但发现他人的反馈并不鼓舞人心。激进的想法很难推销,因为你是在与现有解决方案的惯性作斗争。

One of the end results of that exploration was sqlelf, a tool that lets you explore an ELF file declaratively using SQL.11I wrote a paper, arXiv:2405.03883, that I failed to get published and a follow-up post on querying with it. SELECT name FROM elf_symbols instead of fiddling with readelf and grep. It was remarkably simple by leveraging virtual tables over the ELF: however I found it to be a refreshing improvement to explore the ELF file format. I knew however that there is still something much bigger to be done.

那次探索的最终成果之一是sqlelf,一个让你用SQL声明式地探索ELF文件的工具。我写了一篇论文(arXiv:2405.03883),但未能发表,随后又写了一篇关于使用它进行查询的后续文章。用SELECT name FROM elf_symbols代替摆弄readelf和grep。通过利用ELF上的虚拟表,这变得异常简单:然而,我发现这是探索ELF文件格式的一种令人耳目一新的改进。但我知道,还有更大的事情要做。

I never let the idea go and with the recent improvements with LLMs, I find it compelling to revisit these ideas to explore further. Specifically, can we replace ELF with SQLite as an executable format? 🤔

我从未放弃这个想法,随着LLM的最新进展,我觉得重新审视这些想法以进一步探索很有吸引力。具体来说,我们能否用SQLite替代ELF作为可执行文件格式?🤔

Not “a database that describes an executable”, but the actual file you chmod +x and run.

不是“描述可执行文件的数据库”,而是你chmod +x并运行的实际文件。

代码 · 6
$ file hello
hello: SQLite 3.x database, application id 0x53454c46, user version 1
$ ./hello
Hello, world!
$ sqlite3 hello 'SELECT soname FROM ldd'
libc.so.6
代码 · 6
$ file hello
hello: SQLite 3.x database, application id 0x53454c46, user version 1
$ ./hello
Hello, world!
$ sqlite3 hello 'SELECT soname FROM ldd'
libc.so.6

I developed a pretty fleshed out prototype. It is called SELF, the Structured Executable & Linkable Format, because I am unoriginal. It is on GitHub if you are interested. I’m surprised about all the interesting things that fall out of this idea.

我开发了一个相当完善的原型。它被称为SELF,即结构化可执行与可链接格式(Structured Executable & Linkable Format),因为我不擅长起名。如果你感兴趣,它已在GitHub上。我对从这个想法中涌现出的所有有趣事物感到惊讶。

§ELF is a database that refuses to admit it

§ELF是一个拒绝承认自己是数据库的数据库

Working through my PhD, I realized something that bugged me. ELF is already a database. It just implements many database primitives by hand, along with a surprising number of data structures for performance, like a bloom filter for symbol lookup.

在攻读博士期间,我意识到了一件困扰我的事情。ELF已经是一个数据库。它只是手工实现了许多数据库原语,以及大量用于性能的数据结构,比如用于符号查找的布隆过滤器。

ELF mechanismThe database primitive it reinvents
.strtab / .dynstrstring interning
.hash / .gnu.hashan index (CREATE INDEX)
section header tablesqlite_schema, a table of tables
st_name → offset into .strtaba foreign key, done by hand
sh_offset / sh_sizethe record layout of a b-tree page
.gnu.version_ra column
objcopy --strip-debugDELETE + VACUUM
ldconfig cache, debuginfodout-of-band indexes over the above
ELF机制它重新发明的数据库原语
.strtab / .dynstr字符串驻留
.hash / .gnu.hash索引(CREATE INDEX)
节头表sqlite_schema,一个表的表
st_name → .strtab中的偏移量外键,手工实现
sh_offset / sh_sizeb树页面的记录布局
.gnu.version_r一个列
objcopy --strip-debugDELETE + VACUUM
ldconfig缓存,debuginfod上述内容的带外索引

If you ever have to analyze or parse ELF, the kernel, ld.so, binutils, LIEF, goblin, readelf, you are re-implementing the same parser over and over again. Every producer re-implements the same serializer.

如果你曾经需要分析或解析ELF,无论是内核、ld.so、binutils、LIEF、goblin还是readelf,你都在一遍又一遍地重新实现相同的解析器。每个生产者都重新实现相同的序列化器。

The format itself is incredibly terse, designed for a world where disk space and network bandwidth was at an extreme premium. Modifying the format is hard, you often have to zero out sections and add new ones since it is packed so tightly. There is also no self-describing schema. ELF itself is a very generic format that supports sections of data that by convention are interpreted in specific ways but the format does not enforce it.

该格式本身极其简洁,专为磁盘空间和网络带宽极度稀缺的时代设计。修改这种格式很困难,由于它打包得非常紧密,你常常需要清零部分区域并添加新区域。此外,它也没有自描述的模式。ELF本身是一个非常通用的格式,支持按约定以特定方式解释的数据段,但格式本身并不强制执行这些约定。

SQLite is the counter-example. They are a self-describing format that is extremely stable. It is designed to be extended to support new features without breaking existing consumers and supporting a wide range of queries performantly.

SQLite是反例。它是一种自描述格式,极其稳定。它被设计为可扩展以支持新功能,同时不破坏现有消费者,并支持广泛的查询性能。

If we were to replace ELF with SQLite, what would fall out and can all of the necessary information be represented in a SQLite database? The answer is yes, and it is surprisingly simple.

如果我们用SQLite替换ELF,会有什么变化,所有必要的信息能否在SQLite数据库中表示?答案是肯定的,而且出奇地简单。

§What falls away

§什么会消失

A SELF file needs two tables to run: self_meta is the ELF header as key/value pairs and segments is the load image, one row per program header with the bytes in a BLOB:

一个SELF文件需要两个表来运行:self_meta作为键值对存储ELF头,segments存储加载映像,每个程序头对应一行,字节存储在BLOB中:

代码 · 15
CREATE TABLE segments (
  -- original phdr index
  id      INTEGER PRIMARY KEY,
  -- 'load' | 'tls' | 'stack' | 'relro'
  type    TEXT NOT NULL,
  -- original file offset
  offset  INTEGER NOT NULL,
  vaddr   INTEGER NOT NULL,
  filesz  INTEGER NOT NULL,
  memsz   INTEGER NOT NULL,
  r INTEGER, w INTEGER, x INTEGER,
  align   INTEGER NOT NULL DEFAULT 4096,
  -- the segment bytes; NULL for pure BSS
  content BLOB
);
代码 · 15
CREATE TABLE segments (
  -- original phdr index
  id      INTEGER PRIMARY KEY,
  -- 'load' | 'tls' | 'stack' | 'relro'
  type    TEXT NOT NULL,
  -- original file offset
  offset  INTEGER NOT NULL,
  vaddr   INTEGER NOT NULL,
  filesz  INTEGER NOT NULL,
  memsz   INTEGER NOT NULL,
  r INTEGER, w INTEGER, x INTEGER,
  align   INTEGER NOT NULL DEFAULT 4096,
  -- the segment bytes; NULL for pure BSS
  content BLOB
);

A single table for the symbol table replaces many of the ELF sections and the .gnu.hash index. It is a single table with a single index:

一个符号表单一表取代了ELF的许多节和.gnu.hash索引。它是一个带有单一索引的表:

代码 · 15
CREATE TABLE symbols (
  id      INTEGER PRIMARY KEY,
  name    TEXT NOT NULL,
  -- 'GLIBC_2.2.5'
  version TEXT,
  value   INTEGER,
  size    INTEGER,
  -- 'func' | 'object' | 'tls' | ...
  type    TEXT,
  -- 'global' | 'weak' | 'local'
  bind    TEXT,
  defined  INTEGER NOT NULL,
  exported INTEGER NOT NULL
);
CREATE INDEX idx_symbols_name ON symbols(name, version);
代码 · 15
CREATE TABLE symbols (
  id      INTEGER PRIMARY KEY,
  name    TEXT NOT NULL,
  -- 'GLIBC_2.2.5'
  version TEXT,
  value   INTEGER,
  size    INTEGER,
  -- 'func' | 'object' | 'tls' | ...
  type    TEXT,
  -- 'global' | 'weak' | 'local'
  bind    TEXT,
  defined  INTEGER NOT NULL,
  exported INTEGER NOT NULL
);
CREATE INDEX idx_symbols_name ON symbols(name, version);

Our capability to include an index is equivalent to .gnu.hash and .hash in ELF, but it is a proper b-tree index maintained by SQLite instead of a hand-rolled bloom filter.22.gnu.hash is a bloom filter plus bucket chains, laid out so ld.so can reject a miss without touching the chain during symbol discovery.

我们包含索引的能力相当于ELF中的.gnu.hash和.hash,但它是SQLite维护的真正的b-tree索引,而不是手工制作的布隆过滤器。22.gnu.hash是布隆过滤器加桶链,布局使得ld.so在符号发现过程中可以拒绝未命中而不触碰链。

Surprisingly a lot more falls out as well: .dynstr is gone, because name is TEXT and SQLite already interns strings, symbol versioning is a column, not the .gnu.version_r / .gnu.version_d contraption and there is no need for a strings table.

令人惊讶的是,更多内容也随之消失:.dynstr消失了,因为name是TEXT类型,SQLite已经内部化了字符串;符号版本化是一个列,而不是.gnu.version_r / .gnu.version_d那种复杂结构,也不需要字符串表。

Other tables exist as well for metadata which exist for tooling: sections, notes, dynamic_entries. Delete them and the program still runs, which means strip(1) is a transaction:

还有其他表用于工具链的元数据:sections、notes、dynamic_entries。删除它们程序仍能运行,这意味着strip(1)是一个事务:

代码 · 21
# ldd(1)
$ sqlite3 hello 'SELECT soname FROM ldd'
libc.so.6
# nm -D --undefined
$ sqlite3 hello 'SELECT name,version FROM imports LIMIT 3'
__libc_start_main|GLIBC_2.34
_ITM_deregisterTMCloneTable|
puts|GLIBC_2.2.5
# readelf -l
$ sqlite3 hello \
    "SELECT type,vaddr,memsz,r,w,x FROM segments WHERE type='load'"
load|0|1744|1|0|0
load|4096|361|1|0|1
load|8192|312|1|0|0
load|15768|640|1|1|0
# strip(1)
$ sqlite3 hello 'DELETE FROM sections; DELETE FROM notes; VACUUM;'
# 57344 -> 49152 bytes
# still runs,  the optional tables were optional
$ ./hello
Hello, world!
代码 · 21
# ldd(1)
$ sqlite3 hello 'SELECT soname FROM ldd'
libc.so.6
# nm -D --undefined
$ sqlite3 hello 'SELECT name,version FROM imports LIMIT 3'
__libc_start_main|GLIBC_2.34
_ITM_deregisterTMCloneTable|
puts|GLIBC_2.2.5
# readelf -l
$ sqlite3 hello \
    "SELECT type,vaddr,memsz,r,w,x FROM segments WHERE type='load'"
load|0|1744|1|0|0
load|4096|361|1|0|1
load|8192|312|1|0|0
load|15768|640|1|1|0
# strip(1)
$ sqlite3 hello 'DELETE FROM sections; DELETE FROM notes; VACUUM;'
# 57344 -> 49152 bytes
# still runs,  the optional tables were optional
$ ./hello
Hello, world!

All the tools that operate on ELF files for reading, reduce to queries over the database. Any tool that modifies an ELF file, like strip, can operate on the database within a transaction rather than performing fragile offset surgery: strip is a DELETE and VACUUM. patchelf is an UPDATE.

所有用于读取ELF文件的工具,都简化为对数据库的查询。任何修改ELF文件的工具,如strip,都可以在事务中对数据库进行操作,而不是进行脆弱的偏移量手术:strip是DELETE和VACUUM。patchelf是UPDATE。

Any information missing from the schema can be easily exposed via a view. For example, ldd is a query over the needed table, which is a join of the symbols table with the segments table to find the sonames of the libraries needed by the program.

模式中缺失的任何信息都可以通过视图轻松暴露。例如,ldd是对needed表的查询,该表是符号表与段表的连接,以找到程序所需库的soname。

代码 · 3
CREATE VIEW exports AS SELECT name, version, type, size FROM symbols WHERE exported = 1;
CREATE VIEW imports AS SELECT name, version FROM symbols WHERE defined = 0;
CREATE VIEW ldd     AS SELECT ord, soname FROM needed ORDER BY ord;
代码 · 3
CREATE VIEW exports AS SELECT name, version, type, size FROM symbols WHERE exported = 1;
CREATE VIEW imports AS SELECT name, version FROM symbols WHERE defined = 0;
CREATE VIEW ldd     AS SELECT ord, soname FROM needed ORDER BY ord;

§How does it work?

§它是如何工作的?

SQLite reserves a 4-byte application_id at byte offset 68 of its header, for exactly this purpose. We stamp it SELF, so an ordinary SQLite database never matches:

SQLite在其头部偏移量68字节处预留了一个4字节的application_id,正是为此目的。我们将其标记为SELF,因此普通的SQLite数据库永远不会匹配:

代码 · 2
$ xxd -s 64 -l 8 hello
00000040: 0000 0001 5345 4c46                      ....SELF
代码 · 2
$ xxd -s 64 -l 8 hello
00000040: 0000 0001 5345 4c46                      ....SELF

We can now leverage binfmt_misc, the subsystem that allows you to invoke any binary as if it were native. We need only to register the magic to trigger on and an interpreter that will invoke our new file format.

现在我们可以利用binfmt_misc,这个子系统允许你将任何二进制文件当作原生文件来调用。我们只需注册触发所需的魔数以及一个解释器,该解释器将调用我们的新文件格式。

On NixOS the registration is a few lines matching the SQLite magic at offset 0 and SELF at 68:

在NixOS上,注册只需几行,匹配偏移量0处的SQLite魔数和偏移量68处的SELF:

代码 · 9
boot.binfmt.registrations.self = {
  recognitionType = "magic";
  offset = 0;
  # bytes 0-15, 68-71
  magicOrExtension = "SQLite format 3\\x00" + ... + "SELF";
  # ignore the middle
  mask = "\\xff..\\x00..\\xff";
  interpreter = "${self-exec}/bin/self-exec";
};
代码 · 9
boot.binfmt.registrations.self = {
  recognitionType = "magic";
  offset = 0;
  # bytes 0-15, 68-71
  magicOrExtension = "SQLite format 3\\x00" + ... + "SELF";
  # ignore the middle
  mask = "\\xff..\\x00..\\xff";
  interpreter = "${self-exec}/bin/self-exec";
};

For now, I have a small tool elf2self that converts an ELF file into a SELF file. It is a simple postFixup hook you can opt into per package on NixOS. The tool reads the ELF, extracts the program headers and symbol table, and writes them into the SQLite database. We could look at extending gcc or ld to emit SELF directly, but for now this is a simple way to explore the idea.

目前,我有一个小工具elf2self,可以将ELF文件转换为SELF文件。这是一个简单的postFixup钩子,你可以在NixOS上按包选择启用。该工具读取ELF,提取程序头和符号表,并将它们写入SQLite数据库。我们可以考虑扩展gcc或ld以直接生成SELF,但就目前而言,这是探索这一想法的一种简单方式。

更进一步:量化金融体系

看懂新闻只是起点——沿量化金融路径,把它变成能交付的工程能力

进入量化体系 →

关联讨论

同一事件的更多信源

相似阅读

另一事件,读法相近