Lone Lisp를 만든 Matheus Moreira 인터뷰
요약
Lone Lisp 개발자 Matheus Moreira의 인터뷰로, libc 없이 Linux 시스템 호출 위에서 직접 실행되는 Lisp 언어 구현 과정을 다룹니다. 언어의 내부 구현, 프로그래밍 철학, 그리고 다양한 언어를 탐구하며 쌓아온 개발 경험을 소개합니다.
핵심 포인트
- Lone Lisp는 libc를 배제하고 Linux 시스템 호출을 직접 사용하여 구현됨
- FEXPR, 꼬리 호출 최적화, 재개 가능한 오류 처리 등 고급 언어 기능 지원
- C의 투명성과 Ruby의 표현력을 동시에 추구하는 개발 철학
- 라이브러리 사용을 넘어 내부 구현과 하드웨어 계층을 이해하려는 탐구적 태도
Lone Lisp는 libc 없이 Linux 시스템 호출 위에서 직접 실행되는 Lisp로, Matheus Moreira가 freestanding C만으로 온전한 언어와 사용자 공간 프로그램을 만들 수 있는지 확인하려 시작함
안정적인 Linux 사용자 공간 ABI를 직접 이용해 errno, 로케일, 암묵적 파일 버퍼링 같은 C 라이브러리의 전역 상태와 레거시 API를 제거하고 런타임·메모리 할당기·테스트 도구를 직접 구현함
FEXPR와 적절한 꼬리 호출 최적화 (Tail Call Optimization)를 비롯해 생성기, 구분된 연속체, 재개 가능한 오류 처리를 지원하며 벡터·테이블·연속체도 같은 함수 호출 방식으로 다룸
시작은 빠르지만 임시 벤치마크에서 Python보다 10~100배 느린 리스트 수준 인터프리터이며, C 인터프리터를 부트스트랩 기준으로 남기고 장기적으로 Lone 자체에 JIT 컴플러를 구현할 계획임
Moreira는 Claude를 코드 검토와 프로젝트 관리에 활용하되 Lone 코드는 직접 작성·검토하며, 정적 사이트 생성기·셸·유틸리티를 만들어 전통적인 Linux 사용자 공간을 재구축 하려 함
게임에서 C++, Ruby, Lisp로
프로그래밍에 관심을 갖게 된 계기는 Mega Man Battle Network 였으며, 관련 포럼에서 교류하려 영어를 익히고 첫 언어인 C++도 배움
브라질의 기술 고등학교에서 13~14세 무렵 정보학 과정을 시작함
Dev-C++ 와 cplusplus.com 튜토리얼을 이용함
6개월 만에 학교 교육과정을 모두 익힌 뒤 다른 학생을 돕는 조교로 채용됨
게임 제작도 시도했지만 필요한 물리 지식이 부족해 성공하지 못함
C++ 이후 Java, Ruby, Python을 거쳐 Lisp와 Scheme을 접했으며, Scheme의 언어적 우아함에 끌림
Sun의 Java 튜토리얼을 거의 전부 읽으며 객체지향 프로그래밍 (Object-Oriented Programming)을 배움
Java Swing과 Nimbus 디자인을 좋아해 수학 교수를 위한 원·호 그리기 앱도 제작함
Ruby에서는 Git 형태의 명령 기반 인자 파서 Acclaim을 포함한 gem을 작성함
여러 언어를 탐색한 동력은 학교 프로젝트에서 출발했지만, 이후에는 호기심과 자신에게 맞는 올바른 언어를 찾으려는 욕구가 중심이 됨
Ruby의 표현력과 C의 투명성
Ruby는 메서드 이름을 추측해도 맞는 경우가 많고 단수·복수형과 동의어를 지원해 자신의 사고방식에 잘 맞았음
items.each.with_index.map
처럼 셸 파이프라인 (Shell Pipeline)에 가까운 조합으로 적은 코드에서 많은 작업을 수행할 수 있음
지금도 Ruby를 항상 설치해 두며 최근에는 Rails 프로젝트도 시작함
C는 함수가 ELF 심볼과 어셈블리 코드 (Assembly Code)로 직접 대응해 컴파일 결과를 이해하기 쉬웠음
현대 C++은 과거와 사실상 다른 언어가 됐다고 보고 더는 자신이 C++을 안다고 말하지 않음
Rust로 옮기려 했지만 정착하지 못했고, C로 재작성하는 이유를 속도와 ABI에서 찾는 Zig와 그 창시자에게도 관심을 가짐
십 대 때 CRuby VM 소스를 읽으며 Stack Overflow의 “왜 Ruby가 이렇게 동작하는가”라는 질문에 구현으로 답을 찾음
이후 CRuby, CPython, JVM, Guile, V8 코드를 탐색했고, 특히 Linux 커널 소스를 전문 C 프로그래밍의 기준으로 평가함
다른 사람의 코드를 읽는 방식
라이브러리를 사용하는 데 그치지 않고 내부 구현, 드라이버와 하드웨어의 통신, 숨겨진 계층을 이해하려 꾸준히 소스를 읽음
실제 애플리케이션을 만들다가도 기반 기술 탐구로 빠지는 일이 많았고, 이미 해결된 문제를 더 낫게 다시 만들려는 성향도 생김
이런 성향이 일반적인 소프트웨어 개발 경력과 맞지 않는다고 판단해 프로그래밍을 직업으로 선택하지 않고 취미로 유지함
처음부터 드라이버 코드를 이해한 것은 아니며, 좌절하면서도 계속 읽어 일부 동작을 파악할 수 있게 됨
RGB 키보드 역공학 (Reverse Engineering)
Clevo 베어본 노트북의 RGB 키보드 제어용 Windows 앱이 창을 띄우는 데 1분 넘게 걸릴 정도로 느려 직접 대체함
Wireshark로 앱이 키보드에 보내는 USB 패킷을 캡처하고 구조를 분석해 Linux 사용자 공간 (User Space) C 드라이버를 작성함
GitHub에 올린 뒤 잊고 있었지만 실제 사용자들이 생겼고, 한 사용자는 GUI까지 제작함
이어 ACPI 전원 관리 등 노트북의 다른 기능도 역공학했으나 모든 기능을 알아내지는 못함
libc를 우회한 liblinux
Linux 시스템 호출 (System Call)은 일반 함수 호출과 비슷할 만큼 코드가 단순하며, libc를 거치지 않고 Linux를 직접 대상으로 삼을 수 있음
Linux는 사용자 공간 ABI 안정성으로 알려졌지만 사용자 공간 구성 요소는 그렇지 않다고 봄
매뉴얼에서 일부 시스템 호출을 glibc가 지원하지 않는다는 문구를 접함
getrandom이 glibc에 들어가기까지 약 2년이 걸렸다는 LWN 사례도 영향을 줌
시스템 호출만 담은 liblinux.a를 만들어 -llinux로 연결하자는 발상에서 liblinux를 개발함
freestanding C용 Linux 시스템 호출 래퍼 (Wrapper) 모음이었음
C 런타임 (CRT)은 glibc, musl, uclibc 같은 표준 C 라이브러리를 뜻함
Freestanding C에서 사라지는 문제
Linux는 오류를 일반 반환값으로 돌려주지만 C 라이브러리는 이를 전역 변수인 errno에 저장함
libc를 제거하면 여러 전역 상태와 오래된 API를 함께 피할 수 있음
errno, rand/srand, 로케일 (Locale) 상태
재진입 가능하지 않은 (Non-reentrant) strtok
예기치 않은 문제를 일으킬 수 있는 암묵적 파일 버퍼링 (Implicit File Buffering)
gets, atexit, signal, malloc
NaN 및 errno와 결합하면서 복잡해지는 math.h
반면 정수 승격 (Integer Promotion) 규칙과 오버플로 (Overflow) 검사 부재는 C 자체의 문제라 그대로 남음
오버플로에는 컴파일러 내장 기능 (Compiler Built-ins)을 사용함
자신이 새로운 결함을 만들 가능성도 경계함
표준 라이브러리 없이 바이트 복사부터 정수→문자열 변환까지 직접 작성해야 했지만 예상만큼 불가능한 작업은 아니었음
glibc 저장소는 해석하기 어려웠으나 musl libc 소스는 깨끗하고 탐색하기 쉬워 큰 도움이 됨
C 라이브러리와 동적 메모리 할당 없이 정적 메모리만 사용하는 자동 테스트 스위트도 Lone에서 구현함
nolibc
발견과 Lone Lisp의 시작
Greg Kroah-Hartman에게 Linux에 공식 C 라이브러리가 없는 이유를 물었고, 과거 klibc 프로젝트가 있었다는 답을 받음
이후 Linux 커널 개발자들이 내부 도구용으로 nolibc.h를 만들었다는 사실을 발견함
자신의 liblinux보다 완성도와 사용성이 낫다고 판단해 프로젝트를 보관 처리함
현재 nolibc.h는 커널 트리 안에서 임시 libc에 가까운 디렉터리로 커졌지만 외부 안정성 보장은 확실하지 않음
다음 프로젝트로 Lone Lisp를 시작함
Linux에서 직접 실행되는 freestanding Lisp를 만들 수 있다면 어떤 프로그램도 같은 방식으로 작성할 수 있다고 봄
복잡한 현대 Linux 사용자 공간을 걷어내고 안정적인 시스템 호출 바이너리 인터페이스 위에서 다시 시작하려는 실험임
Cosmopolitan에도 부동소수점 파싱 구현 등을 위한 패치를 보냄
여러 플랫폼의 실행 파일 형식으로 동시에 해석되는 APE 형식과 실행 파일 내 임베디드 아카이브 기능을 높이 평가함
오류 처리와 구분된 연속체
초기 Lone의 모든 오류 경로는 인터프리터에서 exit(255)로 종료됐으나, Common Lisp 계열의 재개 가능한 조건 시스템 (resumable condition system)을 구현함
오류 처리의 기반은 구분된 연속체 (delimited continuations)임
연속체를 스택 프레임 복사와 다른 위치에서의 재생으로 이해함
예외도 값을 캡처하지 않는 연속체의 한 형태(a form of continuation that does not capture values)로 봄
오류 종류를 'not-found 같은 심볼에서 lambda로 매핑함
인자 하나인 (lambda (error) ...)는 일반적인 예외 처리기처럼 동작함
인자 둘인 (lambda (error continuation) ...)는 연속체를 호출해 오류가 난 코드를 값과 함께 재개할 수 있음
Common Lisp의 use-value처럼 신호 발생 코드가 재시작 선택지를 열거하는 restart API는 의도적으로 채택하지 않음
signal, throw, transfer가 단순히 값을 반환한 것처럼 동작하도록 평가기(evaluator) 내부의 기본 메커니즘으로 구성함
FEXPR와 호출 가능한 언어 요소
전통적인 Lisp 평가기가 if 같은 특수 형식 (special forms)을 직접 처리하는 방식도 피함
Lone은 평가기 플러그인에 해당하는 FEXPR를 지원해 특수 형식을 평가기 밖으로 분리함
이 설계로 순수 Lisp 함수의 꼬리 호출 최적화 (tail call optimization)는 먼저 가능했지만 primitive에는 한동안 버그가 있었음
primitive가 꼬리 위치에서 반환할 수 있도록 별도 메커니즘을 추가해 해결함
벡터와 테이블도 호출 가능함
([1 2 3] 0)은 1을 반환함
([1 2 3] 0 4)는 벡터를 [4 2 3]으로 변경함
연속체와 생성기도 같은 호출 규약을 따름
control은 연속체를 지원하는 begin과 비슷함
transfer는 가장 가까운 control 블록을 빠져나오며 중단 지점을 나타내는 호출 가능한 연속체를 반환함
해당 연속체를 값과 함께 호출하면 중단된 계산을 다른 반환값으로 재개할 수 있음
시스템 호출과 실행 구조
Lone 시작 전 부모 프로세스가 execve를 호출하고, Lone은 내부 메모리 관리를 위해 약 12회의 mmap을 수행함
표준 입력에서 스크립트를 읽어 실행한 뒤 종료하며, 실행 중 mmap, munmap, mremap이 추가로 발생할 수 있음
모듈 시스템은 openat과 close를 사용함
Linux의 300개 이상 시스템 호출 중 자체적으로 쓰는 것은 일부지만, Lisp 수준에 system-call primitive를 제공해 프로그램에서 나머지도 호출할 수 있음
ioctl
처럼 커널 C 구조체가 필요한 호출을 위해 구조체 생성·조작 기능을 추가해야 함
터미널 크기 조회도 이런 지원이 필요한 사례임
Lone으로 만들려는 사용자 공간
언어 수준에서는 오류 처리, 적절한 꼬리 호출 최적화 (Tail Call Optimization), 생성기 (Generators), 구분된 연속체 (Delimited Continuations)를 구현했으며 장기적으로 Erlang/BEAM의 아이디어도 도입하려 함
여러 인터프리터가 Lisp 코드를 병렬 실행할 수 있도록 구조를 준비함
Lone으로 만들 첫 프로그램은 현재 사용 중인 PugJS 포크 기반 정적 사이트 생성기 의 대체품임
이후 자체 셸과 유틸리티를 작성해 전통적인 Linux 사용자 공간을 자신의 방식으로 다시 만들 계획임
env
를 Lone Lisp 테이블로 내보내는 구현도 만들었으며, 당시에는 순회 (Iteration) 기능이 없었지만 현재는 생성기를 지원함
인터프리터가 특수 ELF 세그먼트의 Lisp 코드를 읽어 실행하는 기능도 있음
인터프리터 바이너리를 복사하고 Lisp 코드를 삽입하면 독립 실행 프로그램을 만들 수 있음
Claude를 통한 검토와 프로젝트 관리
전체 작업량에는 제한이 없지만 언어 자체의 기능 집합은 완성에 가까워지고 있다고 봄
Claude를 일종의 관리자처럼 사용하며 코드 검토와 프로젝트 관리 에서 가장 큰 도움을 받음
전체 코드베이스를 대상으로 문제, 미개발 영역, 향후 방향을 엄격하게 찾도록 요청함
거부한 접근법과 나중에 처리할 기능을 문서와 프로젝트 메모리로 관리함
쓰레기 수집기 (Garbage Collector)와 메모리 할당기 (Memory Allocator)는 Claude가 더 나은 알고리즘을 알려줘 크게 개선함
Lone에 들어가는 모든 줄을 직접 검토하며 코드 작성 자체도 교육적이라고 보기 때문에 AI가 Lone 코드를 대신 작성하도록 신뢰하지는 않음
반면 JavaScript 정적 사이트 생성기에서는 Claude가 기능 전체를 작성하게 한 적이 있음
예정된 기능에는 GCC/Clang 컴파일러 API와 스택 보호 함수 지원이 있음
Ruby와 Scheme에서 가져온 문법
Ruby의 표현력과 Scheme의 간결함을 결합하려 했으며, Lisp의 파서 (Parser)가 간단해 직접 작성한 뒤 중첩 리스트를 바로 다룰 수 있었음
Lisp를 제대로 이해한 계기는 함수가 평가된 값이 아니라 리스트 자체를 받는 FEXPR 를 구현했을 때였음
배열은 [1 2 3]
, 사전 (Dictionary)은 {k v}
문법을 사용함
Clojure와 newLisp뿐 아니라 Ruby, Python, JavaScript 사용자에게도 익숙해 불필요한 진입 장벽을 줄임
C 코딩 스타일은 Linux 커널 스타일을 수정해 사용하되 정렬과 장식을 더 선호함
Ruby에서는 method('arg')
보다 method 'arg'
처럼 구두점을 줄여 영어 문장에 가깝게 쓰는 방식을 선호함
Lisp에서도 구문 중첩을 줄임
Lone의 let
은 (let (a b c d ...) body...)
형태임
전통적인 (let ((a b) (c d) ...) body...)
보다 이름과 값이 순서대로 나오는 형태가 함수 적용 (Function Application) 방식에 더 자연스럽다고 판단함
성능과 부트스트랩 (Bootstrap) 계획
Lone이 앞서는 벤치마크는 시작 속도 로, Ruby가 Rubygems를 읽는 시간보다 빠르게 프로그램을 시작하고 끝낼 수 있음
임시 측정에서는 Python보다 약 10~100배 느림
Lone은 SICP 5.4장의 형태와 비슷한 리스트 수준 인터프리터임
Python과 Ruby는 바이트코드 (Bytecode) 인터프리터라 성능 범주가 다름
리스트를 바이트코드로 바꾸면 Lisp의 본질을 잃는다고 느껴 바이트코드 컴파일을 피하고 있음
장기적으로 C 인터프리터는 최종 부트스트랩 기준으로 남기고, Lone 내부에서 JIT 컴파일러 를 작성할 계획임
실행 가능한 페이지를 mmap
으로 확보한 뒤 명령어를 생성하는 방식임
Bash와의 재귀 Fibonacci 임시 비교에서는 Lone이 훨씬 빨랐지만, Bash가 반복마다 프로세스를 생성했을 가능성 등 측정 왜곡을 인정함
스크립트에서는 시작 시간이 중요하므로 Lone의 빠른 부트스트랩 (bootstrap)이 유용하다고 봄
해시 테이블 (hash table)을 빌드 단계에서 미리 계산해 C 초기화 데이터로 만들면 실행 시 초기화 비용을 더 줄일 수 있음
메모리와 최소 환경
mmap은 커널에 메모리 페이지를 요청하며, 일반적인 페이지 크기는 4,096바이트 임
읽기·쓰기·실행 권한을 지정할 수 있고 JIT (Just-In-Time) 컴파일러는 실행 가능한 페이지를 이용함
16바이트만 필요해도 4KiB를 요청하면 낭비가 크므로 일반 할당기 (allocator)는 128MB 같은 큰 영역을 받은 뒤 작은 조각으로 나눔
Lone의 최상위 환경에는 import와 export 두 심볼 (symbol)만 있음
리스트 (list), 리터럴 (literal), 함수 호출 (function call) 메커니즘 외에는 if, quote까지 모두 가져와야 함
사용자가 환경 (environment)의 모든 이름을 직접 통제하도록 한 설계임
현재 함수는 약 50개이며 if, let, begin, control/transfer, intercept/signal, flatten 등이 중심임
표준 라이브러리와 문서화
언어 문서는 아직 불완전하지만 언어 기능보다 표준 라이브러리 (standard library) 작업이 가까워지고 있음
Ruby의 표준 라이브러리와 Scheme의 명명 방식을 참고할 계획임
다음 작업은 순회 프로토콜 (iteration protocol)임
(for (each collection) (lambda (item) ...)) 형태를 고려함
기존 each 함수들은 모두 생성기 (generator)를 반환하도록 재설계할 예정임
이후 문자열 조작과 효율적인 연결을 위한 문자열 빌더 (string builder)를 구현하려 함
순회와 문자열 기능이 완성되면 Hiccup 형태의 웹 프레임워크 (web framework)도 작성할 수 있다고 봄
저수준 (low-level) 기능이 끝나고 반복적인 표준 라이브러리 작업으로 넘어가면 집중력이 떨어질 가능성을 걱정하며 Claude의 관리 도움을 활용할 계획임
전체 스택을 직접 소유한다는 목표
연속체처럼 신비해 보이던 기능도 스택 프레임 (stack frame) 복사로 이해하면 평범한 구현 문제가 되며, Lone은 다른 사람도 자신의 시스템을 만들고 탐색하도록 자극하려는 목적을 가짐
라이브러리 소비자에 머무르지 않고 라이브러리를 만드는 사람이 되려 하며, 일반 개발자에게 금지된 것처럼 취급되는 시스템 호출 (system call)도 직접 사용하려 함
자신만의 시스템을 가지려면 고장 났을 때 직접 고쳐야 하지만, 더 많은 사람이 그 책임과 보상을 선택하기를 바람
컴퓨팅에 축적된 오래된 코드를 두려워하지 말고 불필요한 계층 (layer)을 제거할 수 있다고 봄
Linux 사용자 공간 (user space)을 다시 만드는 주체가 자신이 아니라 Rust나 Zig 개발자일 수도 있음
Lone이 성공하지 못하더라도 이런 시도를 고무한다면 만족할 수 있음
자유 소프트웨어와 Android
자유 소프트웨어가 추구하는 사용자 권한 강화 (user empowerment)를 중시하며, 범용 컴퓨터가 기업이나 정부가 관리하는 가전제품처럼 축소되는 상황을 우려함
systemd는 무겁게 접근한 측면이 있어도 전통적인 Linux 사용자 공간을 다시 상상하고 재구축한 성공적인 사례로 평가함
모든 계층을 직접 만들기보다 싸움을 선택해야 한다고 봄
Lone을 베어메탈 (bare-metal) Lisp로 만들었다면 부팅 단계에도 도달하지 못했을 수 있음
512바이트 미만 부트 섹터 (boot sector) Lisp인 jart의 Sector Lisp를 사례로 듦
Lone은 Termux 안에서 시작 됐고 상당 부분을 휴대전화에서 작성함
Android가 statx
를 포함한 일부 시스템 호출 (system calls)을 차단하지만, Lone은 Android에서 실행되며 테스트 스위트 (test suite)가 100% 통과함
의사이자 비전문 프로그래머
Moreira는 2019년 의대를 졸업한 의사임
고등학교에서 신경망 (neural network) 프로그래밍을 읽다가 생물학에 관심을 갖게 됨
어떤 날은 10~14시간 진료하고 다른 날은 쉬는 일정으로 프로그래밍 시간을 확보함
전자 의무기록 (electronic medical records)을 사용하는 병원에서 템플릿과 타자 능력으로 업무를 크게 단축함
환자와 눈을 맞춘 채 기록할 수 있음
최근 시작한 Rails 프로젝트는 자신의 필요를 해결하기 위한 전자 환자 기록 시스템임
전문 개발자들이 개인적 문제를 직접 해결하지 않는 이유 중 하나로 컴퓨터 자체에 대한 관심 부족을 꼽음
결과만 얻으려는 태도보다 호기심이 있어야 구현 내부를 파고들 수 있으며, 사람들에게 그 호기심을 어떻게 불러일으킬지는 아직 알지 못함
댓글과 토론
AI 자동 생성 콘텐츠
본 콘텐츠는 RSS: GeekNews (한국어)의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기