
2026/08/24 13:48
実行ファイルは SQLite データベースです
RSS: https://news.ycombinator.com/rss
要約▶
日本語翻訳:
本文は、従来の ELF エキcute を SQLite データベースで置き換え、バイナリをデータベースファイルとして格納する革新的な文件格式「SELF」を提案しています。プログラムヘッダーとシンボルテーブルを SQL スキーマにマッピングすることで、SELF は SQLite の堅牢性を自己記述的な構造体の利点として活用します。オーバーヘッドにより初期サイズは大きくなりますが、オプションのテーブルを削除する戦略的最適化によって、ストリップされた
coreutils の足場は、元の ELF サイズの 1% 以内にまで削減されます。特に重要なのは、PATH にあるすべての異なるライブラリを含むシステム全体のエキecute とライブラリを単一のデータベースにパックすることで、それらを個別のファイルとして保持したままの総サイズよりも小さい合計サイズを実現できる点です。
実装では、依存関係が外部キーを介して解決される「クロージャー」をサポートし、あいまいなライブラリパスを排除します。動的リンキングは、glibc の
rtld-audit を使用して SQL 照会を行う方法か、ld.so をカスタム C プログラム(self-ld)で置き換える方法のいずれかによって実現されます。LD_PRELOAD といった高度な慣習は、preload テーブルへの挿入などの原子的な SQL トランザクションに簡素化されます。このフォーマットは無損失のラウンドトリップをサポートしており、コアユーティリティを書き直すことなく、既存のツールがバイナリを検査および修正することを可能にします。遅延分析は、データベースを開くための固定コストとして約 5 ms と、比例するコピー時間のコストを示していますが、このプロジェクトでは「hello」などのコマンドが完全にデータベース内で実行される NixOS 環境での実現可能性を実証しています。fzakaria/selfdb にホストされたこの統一フォーマットは、開発者が複雑なユーザーランドを管理できるようになり、複雑なカーネルインターフェースの代わりに単純な SQL クエリを使用することを目的としています。本文
SQLite で ELF バイナリを実行する:SELF (Structured Executable & Linkable Format) の探求
近年、私は主に 2 つの技術に夢中になってきました。 一つは世界を再構築する能力を持つ革新的なツールである「Nix」。 もう一つは、実行ファイル形式として ELF(Executable and Linkable Format) を代替する 「SQLite」 を採用するというものです。
これら 2 つの着想は実に相性が良すぎます。私は博士課程時代にこの考え方を模索しましたが、既存の解決策への慣れ親しんだ人々の**惰性(Inertia)**に直面し、発表されることは叶いませんでした。しかし、このアイデアを放棄することはありませんでした。LLM の進歩に伴い、再度このコンセプトを深掘りする中で、驚くべき現実的な成果が生まれました。
§ 概要:SELF とは何なのか?
私が開発したのは、「実行可能コードを記述するデータベース」ではなく、実際に
chmod +x して実行可能なファイルそのものを指します。これを**「SELF(Structured Executable & Linkable Format)」**と命名しました。
GitHub で確認できる興味深いプロトタイプでは、ELF ファイルの構造を SQLite のテーブルに変換し、それを直接プログラムとして実行します。
動作のイメージ
# ELF ヘッダー情報に "SELF" と書かれている $ file hello hello: SQLite 3.x database, application id 0x53454c46, user version 1 # 通常の実行 $ ./hello Hello, world! # SQL クエリでライブラリ依存を確認 $ sqlite3 hello 'SELECT soname FROM ldd' libc.so.6
§ ELF は「自覚していないデータベース」
ELF も実は既にデータベースですが、多くの機能は手動実装されています。
- シンボルルックアップ: Bloom Filter (ブルームフィルタ) 等。
- ツールの冗長性: カーネル、
、binutils など、同じパースロジックを各自が再実装しています。ld.so - 形式の硬さ: スキーマは自己記述的ではなく、セクション追加にはゼロ化が必要などの変更が困難です。
対照的に、SQLite は極めて安定した自己記述的な形式で、既存データを壊さずに新機能をサポートできます。ELF を SQLite に置き換えることで、以下のような劇的な変化が可能になります。
§ 何が残されるか (What falls away)
SELF ファイルを実行するには以下の 2 つのテーブルが必須です:
: ELF ヘッダーやロードイメージを保存(BLOB でプログラムヘッダを格納)。segments
: シンボルテーブル全体を単一の表に集約(symbols
の代わりとして正規の B-Tree インデックスを使用)。.gnu.hash
メタデータの削減と簡素化
の省略:.dynstr
が TEXT 型のため、SQLite は文字列を自動的にインターン(共有)するからです。name- バージョニングの単純化:
のような複雑な機構は単なるカラムになり、文字列テーブルが不要になります。.gnu.version_r
デバッグ情報の削除 (strip
)
stripプログラムが動作し続ける限り、不要なメタデータを削除できます。これは
VACUUM と DELETE を含むトランザクションとして処理可能です。
# ライブラリ依存の検索 (ldd) $ sqlite3 hello 'SELECT soname FROM ldd' libc.so.6 # シンボル一覧 (nm) $ sqlite3 hello 'SELECT name,version FROM imports LIMIT 3' __libc_start_main|GLIBC_2.34 puts|GLIBC_2.2.5 # セクション情報の削除 (strip) $ sqlite3 hello "DELETE FROM sections; DELETE FROM notes; VACUUM;" # サイズ削減: 57344 -> 49152 bytes # 動作確認 $ ./hello Hello, world!
スキーマの拡張とビュー (View)
欠落している情報は、ビュー(VIEW)を通じて容易にアクセスできます。
ldd や nm といった従来のツールは、単なるクエリや結合(JOIN)として実装されます。
-- エクスポートされたシンボル 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; -- ライブラリ依存一覧 (ldd) CREATE VIEW ldd AS SELECT ord, soname FROM needed ORDER BY ord;
§ それはどのように機能するか?
ファイル形式の識別
SQLite ヘッダー(オフセット 68 バイト)には、通常の SQLite と区別するための
application_id が予約されています。これを SELF に印付けます。
$ xxd -s 64 -l 8 hello 00000040: 0000 0001 5345 4c46 ....SELF
カーネルレベルでの実行 (binfmt_misc
)
binfmt_miscLinux カーネルの
binfmt_misc メカニズムを利用することで、この特殊なファイル形式を認識させます。
- NixOS では数行の設定で、SQLite のマジック番号(オフセット 0)と SELF(オフセット 68)を登録できます。
boot.binfmt.registrations.self = { recognitionType = "magic"; offset = 0; magicOrExtension = "SQLite format 3\\x00" + ... + "SELF"; mask = "\\xff..\\x00..\\x00"; interpreter = "${self-exec}/bin/self-exec"; };
エグジュート時の仕組み (elf2self
)
elf2selfELF ファイルを SELF に変換するツール
elf2self を使用します。これは NixOS の postFixup フックとしても動作可能です。
- 変換: ELF → SQLite データベース (
)elf2self - 識別: カーネルがマジック番号 "SELF" を検知
- インタープリタ: 予約されたバイトオフセット(68)を参照し、
をリンクしたlibsqlite3
が起動self-exec
graph LR; elf("elf<br/>hello") -- conv --> self("self<br/>hello<br/>(SQLite db)"); magic -- "SELF@68" --> binfmt_misc["binfmt_misc"]; binfmt_misc --> interp["interpreter (self-exec)"]; interp --> run["running<br/>process"]; subgraph krn ["kernel"] direction TB; execve["execve()"] end elf -- "magic SELF@68" -.-> magic; note["Note:<br/>self-exec は ELF ファイルである必要があります。<br>再帰するインタープリタは -ELOOP になります。"] -.-> self-exec["self-exec"];
インタープリタ (self-exec
)
self-execself-exec は、データベースからプログラムヘッダとシンボルテーブルを取得し、メモリにマッピングして再配置(Relocation)を行い、エントリーポイントへジャンプする C プログラムです。その実装は ld.so と驚くほど似ています。
§ ダイナミックリンキング
静的リンクは簡単ですが面白くありません。ダイナミックリンキングこそが、データベースの真価を発揮する場所です。 2 つのアプローチを試みました:
- rtld-audit:
のインタフェースを使い、ルックアップ処理を SQL クエリに置き換える(素早い反復)。glibc - 完全置換: ルックアップ全体を SQL で実装し、
を完全に置き換える (ld.so
)。self-ld
rtld-audit を使ったルックアップ
glibc の
rtld-audit インターフェースを介して、dlopen やファイル検索をすべてインターセプトします。RUNPATH などを辿る代わりに、SQL クエリで「どのライブラリがこのシンボルを満たすか?」を検出します。
# ライブラリを削除 $ rm libgreet.so.1 $ ./app # エラーが発生 # SQL を通じたルックアップ (rtld-audit) $ SELF_SYSTEM_DB=system.db LD_AUDIT=libself-audit.so ./app Hello, world, from a SQLite library!
完全 SQL 実装のダイナミックリンカー (self-ld
)
self-ldオブジェクトのマッピング、エクスポート情報の公開、GOT パッチ適用など、すべての処理を SQL で行います。
SELECT s.value + o.load_bias -- シンボルのアドレス計算 FROM relocations r JOIN symbols s ON r.symbol = s.id JOIN objects o ON s.object = o.id WHERE r.id = ? ORDER BY o.load_order LIMIT 1;
§ コストとベンチマーク
新しい形式を定着させる上で、サイズとレイテンシが重要な要素です。
サイズ
SELF ファイルは B-Tree オーバーヘッドの影響で、ELF の約 2 倍のサイズになります。ただし、不要なテーブル(デバッグ情報)をストリップすれば、1% 以内の差異まで削減可能です。
分析: 過剰なオーバーヘッドの多くは除去可能ですが、コアな構造(オブジェクト、セグメント、シンボル)は維持されます。
レイテンシ
- SQLite を開き、インタープリタを起動するには約 5 ms かかります。
- データベースの B-Tree ページがメモリにマッピングされていないため、イメージサイズに比例したコピーコストが発生します。
(274 KiB) とcurl
(4.6 MiB) では、ライブラリ数の違いよりもコピーコストの影響を強く受けます。git
§ システムはクロージャ(閉じた環境)である
SELF は単一ファイルではなく、プログラムとその**推移的依存関係を含むクロージャ(Closure)**として機能します。
- 従来の
は曖昧なライブラリ名 (ldd
) しか出力しません。soname - SELF は
テーブルに解決済みパス (needs
) を格納し、その曖昧さを解消します。resolved_path
# クロージャ化 (self closure) $ self closure "$(readlink -f $(command -v ls))" coreutils.db ls + closure -> coreutils.db # 依存関係の明示的なパス表示 $ sqlite3 coreutils.db "SELECT soname, resolved_path FROM needs WHERE is_root = 1" libc.so.6 /nix/store/xxxxx/lib/libc-2.39.so libgmp.so.10 /nix/store/yyyyy/lib/libgmp-12.so
これにより、単一のデータベースに実行ファイルと依存ライブラリをパックし、
soname の曖昧さを完全に排除できます。
§ これはどこまで可能か?一つのファイル、一つのユーザーランド
さらに進んで、システム全体(Userland)のすべての ELF バイナリに対してクロージャ化を行いました。
- 対象: 723 つの実行可能ファイル、400 の異なる共有ライブラリ。
- 結果: 1,123 個のオブジェクト、346,386 個のシンボル、3,808 本の依存エッジが、1 つの SQLite ファイル (611.9 MiB) に収まりました。
これにより、以下のような革新的な機能が実現可能です:
- ライブラリ共有: 同じ
は複数のクロージャ間で共有されます(ストアパス同一)。libc.so.6 - LD_PRELOAD のトランザクション化: 環境変数ではなくテーブル内の一行として表現され、オン/オフをトランザクション (
...BEGIN
) で管理できます。COMMIT
# Preload の動的追加 (1 行の INSERT) sqlite3 system.db "INSERT INTO preload VALUES (0, 'libmul.so.1.self');" ./app.self # 動作変化 # プリロードの即時無効化 (DELETE) sqlite3 system.db 'DELETE FROM preload;'
§ 現在の立ち位置
- 形式: 完成。ELF と SELF の間でのラウンドトリップは無損失で行えます。
- ツールング: クエリ、変更、クロージャパッキングが可能。
- 互換性: glibc プログラムを修正せずとも SQL 通じたルックアップで動作し、ネイティブ SQL ロADER も検証には十分です。
fzakaria/selfdb でコードを確認できます。
nix run .#self-vm コマンドは、hello が SQLite データベースである NixOS VM を起動します。
Nix には、世界を再構築する自由があります。既存の決定や制約に縛られず、新たなアイデアを探求し、そこから何が生じるかを見ることも可能です。
参考文献:
- sqlelf 論文
- [.gnu.hash 実装詳細]
- [B-Tree マッピングと mmap のパフォーマンス分析]
- [NixOS での RUNPATH 最適化]