TECH 으로 돌아가기
TECH HACKER NEWS 오늘 9분 읽기 29 READS

실행 파일을 SQLite로 바꾼다면? ELF 대체 실험 'SELF'

실행 파일을 SQLite로 바꾼다면? ELF 대체 실험 'SELF'
SOURCE IMAGE · HACKER NEWS

리눅스에서 프로그램을 실행할 때 우리가 무심코 다루는 바이너리는 대부분 ELF(Executable and Linkable Format)라는 형식을 따른다. 커널, 동적 링커인 ld.so, binutils, LIEF, goblin 같은 도구들은 모두 이 ELF를 각자 다시 파싱하고 직렬화한다. 한 개발자가 이 관행에 근본적인 질문을 던졌다. ELF를 아예 SQLite 데이터베이스로 대체하면 어떻게 될까. 그가 만든 프로토타입이 바로 SELF(Structured Executable & Linkable Format)이고, 실제로 chmod +x를 붙여 실행되는 SQLite 파일을 목표로 한다.

ELF는 이미 손으로 만든 데이터베이스다

저자의 출발점은 ELF 자체가 사실상 데이터베이스라는 통찰이다. ELF는 심볼 조회를 위한 블룸 필터(.gnu.hash)까지 갖춘, 여러 데이터베이스 원시 기능을 직접 손으로 구현한 형식이다. 문제는 이 형식이 디스크와 대역폭이 극도로 귀하던 시절을 위해 지나치게 압축적으로 설계됐다는 점이다. 섹션이 촘촘하게 채워져 있어 수정하려면 기존 섹션을 0으로 지우고 새로 추가해야 하는 경우가 많고, 스스로를 설명하는 스키마도 없다. 데이터 섹션의 의미는 형식이 강제하는 것이 아니라 관습으로 해석될 뿐이다.

SQLite는 정확히 그 반대편에 있다. 자기 기술적(self-describing)이며 형식이 대단히 안정적이고, 기존 소비자를 깨뜨리지 않으면서 새 기능을 확장하도록 설계됐다. 저자는 이미 박사 과정에서 sqlelf라는 도구로 ELF를 SQL로 선언적으로 조회하는 실험을 했었다. readelf와 grep을 조합하는 대신 SELECT name FROM elf_symbols로 심볼을 뽑아내는 방식이다. SELF는 그 아이디어를 한 단계 더 밀어붙여, 데이터베이스가 실행 파일을 '설명'하는 것이 아니라 데이터베이스가 '곧 실행 파일'이 되게 한다.

실행에 필요한 것은 단 두 개의 테이블

놀라운 점은 구조가 단순하다는 것이다. SELF 파일이 실행되기 위해 필요한 핵심은 두 테이블뿐이다. ELF 헤더를 키/값 쌍으로 담은 self_meta, 그리고 프로그램 헤더 한 줄당 로드 이미지를 BLOB으로 담은 segments다. 심볼 테이블은 하나의 테이블과 하나의 인덱스로 여러 ELF 섹션과 .gnu.hash 색인을 대체한다. 손으로 만든 블룸 필터 대신 SQLite가 유지하는 정식 B-트리 인덱스가 그 역할을 맡는다.

이 전환에서 여러 군더더기가 자연스럽게 사라진다. 이름이 TEXT 컬럼이 되고 SQLite가 문자열을 이미 인터닝하므로 .dynstr 문자열 테이블이 필요 없어진다. 심볼 버전 관리도 별도의 .gnu.version 장치가 아니라 그저 하나의 컬럼이 된다. sections, notes, dynamic_entries 같은 테이블은 도구용 메타데이터일 뿐이라 삭제해도 프로그램은 돌아간다. 다시 말해 strip은 이제 offset을 조작하는 위험한 수술이 아니라 DELETE와 VACUUM으로 이뤄지는 하나의 트랜잭션이고, patchelf는 UPDATE가 된다. ELF를 읽는 도구는 질의로, 수정하는 도구는 트랜잭션으로 환원된다.

동적 링킹과 클로저에서 진가가 드러난다

정적 실행은 쉬웠지만 흥미로운 부분은 동적 링킹이다. 저자는 두 갈래로 접근했다. 하나는 기존 ld.so를 그대로 두되 glibc의 rtld-audit 인터페이스로 라이브러리 조회만 SQL 질의로 가로채는 방식이다. 이 경우 지연 PLT, IFUNC, TLS, 심볼 버전 관리 같은 glibc의 기능이 모두 그대로 동작하면서 라이브러리 저장은 행(row), 조회는 질의가 된다. 다른 하나는 ld.so를 통째로 대체해 조회와 바인딩 전체를 SQL로 처리하는 self-ld로, 개념 증명 수준이지만 실제로 동작한다. 다만 인터프리터인 self-exec 자체는 ELF로 남아야 한다. 인터프리터까지 같은 등록 규칙에 걸리면 -ELOOP로 무한 재귀에 빠지기 때문이다.

가장 눈길을 끄는 대목은 클로저다. 프로그램이 필요로 하는 라이브러리를 ldd로 보면 soname만 나올 뿐 그것을 만족시키는 구체적 파일은 모호하게 남는다. Nix는 RUNPATH로 모든 의존 관계를 특정 스토어 경로로 명시해 이 모호함을 없앤다. SELF는 각 의존 간선의 해석된 경로를 데이터베이스에 저장함으로써 라이브러리 해석을 추측이 아니라 외래 키로 만들고, ldd는 JOIN이 된다. ls 실행 파일과 다섯 개 라이브러리를 한 파일에 담은 4.8MiB짜리 클로저 안에는 간선당 정확히 하나의 제공자만 존재하니 soname 모호성이 원천적으로 사라진다.

여러 클로저를 하나의 데이터베이스에 합치면 규모의 효과가 두드러진다. 저자가 시스템 PATH의 ELF 바이너리 723개를 한 파일로 묶자, 400개의 서로 다른 공유 라이브러리를 포함해 1,123개 객체와 34만여 개 심볼이 하나의 SQLite 파일에 담겼다. 그런데 이 파일은 611.9MiB로, 원래 ELF 파일들의 644.4MiB보다 오히려 작았다. 라이브러리와 심볼의 중복 제거가 스키마에서 자연스럽게 이뤄지기 때문이다. 각 프로그램이 자기 클로저를 따로 갖는 AppImage 방식이었다면 같은 723개 프로그램이 5.53GiB로 불어났을 것이다. LD_PRELOAD 역시 환경 변수가 아니라 테이블의 한 행이 되어, 전체 userland에 추적용 malloc을 끼워 넣었다가 ROLLBACK 한 번으로 원자적으로 되돌리는 일이 가능해진다.

실무자가 짚어둘 한계

물론 이것은 아직 탐구 단계의 아이디어다. 비용은 크기와 지연 두 축에서 나타난다. 단일 SELF 파일은 SQLite의 B-트리 오버헤드 때문에 대략 ELF의 두 배 크기지만, 대부분은 디버깅·도구용 선택 테이블이라 스트립하면 회복된다. 스트립한 coreutils는 1,794,048바이트로 ELF의 1,768,632바이트와 1% 이내로 붙는다. 지연은 SQLite를 열고 인터프리터를 시작하는 데 약 5ms의 고정 비용이 들고, 이미지 크기에 비례하는 복사가 더해진다. 이 복사가 특히 문제인데, B-트리 페이지는 메모리에 매핑되지 않아 같은 SELF 바이너리를 실행하는 두 프로세스가 일반 ELF처럼 텍스트 페이지를 공유하지 못한다. 바이트를 B-트리에서 복사해 꺼내기 때문이다.

그럼에도 이 실험이 던지는 메시지는 분명하다. 형식이 스스로를 기술하고 안정적이며 질의·트랜잭션을 지원할 때, strip·patchelf·ldd·LD_PRELOAD 같은 오래된 관용구들이 별도 구현 없이 데이터베이스 연산으로 떨어져 나온다는 점이다. 저자가 이런 급진적 실험을 감행할 수 있었던 배경에는 커널까지 세상을 통째로 다시 빌드할 수 있게 해주는 Nix가 있다. 전체 코드는 fzakaria/selfdb에 공개돼 있고, nix run .#self-vm으로 hello가 SQLite 데이터베이스인 NixOS VM을 띄워볼 수 있다. 당장 프로덕션에 쓸 물건은 아니지만, 익숙한 형식을 데이터베이스 관점에서 다시 바라보게 만드는 사고 실험으로서 충분히 들여다볼 가치가 있다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-...
SHARE
NEXT · CHOOSE

변화를 읽었다면,
내가 만들 수익 구조를 고릅니다.

정보를 더 모으는 데서 멈추지 않고, 광고·외주·판매·중개·구독 중 내 상황에 맞는 출발점을 정해보세요.

21가지 수익 구조 살펴보기
처리 중...