Executable Is a SQLite Database
The Executable as a SQLite Database
By Farid Zakaria (Rewritten)
Over the last few years, my intellectual curiosity has been consumed by two primary pursuits: utilizing Nix to experiment with world-rebuilding concepts and the ambitious goal of replacing the ELF format substituting SQLite as the standard for executable files.
It is evident that these two trajectories are complementary.
I initially pursued this during my doctoral studies, but the feedback I received was largely uninspiring. Selling a radical departure from the norm is difficult because you are fighting the massive inertia of established industry standards.
One tangible outcome of that research was sqlelf, a utility designed to let users explore ELF files through declarative SQL queries. I documented this in a paper (arXiv:2405.03883) and a subsequent blog post. Instead of the tedious process of readelf | grep, one could simply run:
SELECT name FROM elf_symbols
While this was a refreshing way to interact with ELF files via virtual tables, I knew a more fundamental shift was possible. With the advent of modern LLMs, I felt compelled to revisit the core question: Can we replace ELF with SQLite as the actual executable format?
To be clear: I am not talking about a database that describes a binary, but a file that you actually
chmod +xand execute.
The Vision in Practice
Imagine a world where the file command returns this:
$ 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
To test this, I built a prototype called SELF (Structured Executable Linkable Format). The results revealed several fascinating insights.
🧩 ELF: The Database in Denial
During my PhD, I realized that ELF is essentially a database that refuses to admit its identity. It manually implements various database primitives and complex data structures (like bloom filters for symbol lookups) to maintain performance.
Comparison of Mechanisms
| ELF Mechanism | Database Primitive Equivalent |
|---|---|
.strtab / .dynstr | String Interning |
.hash / .gnu.hash | CREATE INDEX |
| Section Header Table | sqlite_schema (Table of tables) |
st_name .strtab offset | Foreign Key (implemented manually) |
sh_offset / sh_size | B-Tree page record layout |
.gnu.version_r | A database column |
objcopy --strip-debug | DELETE + VACUUM |
ldconfig / debuginfod | Out-of-band indexes |
Because ELF is so terse—designed for an era of extreme scarcity in disk space and bandwidth—it is incredibly rigid. Modifying it often requires "surgery," such as zeroing out sections to make room for new ones. Every tool that interacts with it (ld.so, binutils, LIEF, readelf) ends up re-implementing the same parser.
The Transition Logic
🛠️ The Architecture of SELF
To make a SELF file runnable, we only need a few core components.
Essential Requirements
-
self_metatable: Stores the ELF header as key/value pairs. -
segmentstable: Stores the load image. -
symbolstable: Replaces multiple ELF sections and the.gnu.hashindex.
1. The Segments Table
Each row represents a program header, with the actual binary data stored as a BLOB.
CREATE TABLE segments (
id INTEGER PRIMARY KEY, -- Original phdr index
type TEXT NOT NULL, -- 'load', 'tls', 'stack', etc.
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,
BLOB -- The actual segment bytes
);
2. The Symbols Table
Instead of a complex web of sections, we use a single table with a proper B-Tree index.
CREATE TABLE symbols (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
version TEXT, -- e.g., 'GLIBC_2.2.5'
value INTEGER,
size INTEGER,
type TEXT, -- 'func', 'object', etc.
bind TEXT, -- 'global', 'weak', 'local'
defined INTEGER NOT NULL,
exported INTEGER NOT NULL
);
CREATE INDEX idx_symbols_name ON symbols (name, version);
What is eliminated?
By using SQLite, we can discard several legacy ELF structures:
.dynstr: Gone, because SQLite handlesTEXTand string interning natively..gnu.hash: Replaced by a standard B-Tree index (whereas ELF uses a bloom filter + bucket chains to avoid touching the chain on a miss)..gnu.version_r: Simplified into a standard column.
🚀 The Result: Tooling as Queries
In the SELF ecosystem, every traditional binary tool becomes a simple SQL operation.
Querying Metadata:
# Equivalent to ldd(1)
$ sqlite3 hello 'SELECT soname FROM ldd'
libc.so.6
# Equivalent to 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
# Equivalent to 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
Modifying the Binary:
The strip(1) utility, which is usually a fragile process of removing sections, becomes a simple atomic transaction:
-- strip(1) is now just:
DELETE FROM sections;
DELETE FROM notes;
VACUUM;
The result is a smaller file that still executes perfectly:
$ ./hello Hello, world!
By treating the executable as a database, we replace "offset surgery" with structured transactions. The application ID marks the beginning of a new way to think about linkable formats.