Pure Shell Script로 작성된 C89 컴파일러 c89cc.sh: 진정한 광기인가 예술인가
주말에 해커뉴스를 훑어보다가 제 눈을 의심하게 만드는 프로젝트를 하나 발견했습니다. 보통 새로운 컴파일러가 등장했다고 하면 Rust, Go, 혹은 OCaml 같은 언어로 작성된 우아한 파서와 LLVM 백엔드를 기대하기 마련입니다. 하지만 이번엔 완전히 달랐습니다.
오늘 다룰 프로젝트는 c89cc.sh 입니다. 이름에서 알 수 있듯 C89 코드를 x86-64 ELF64 실행 파일로 컴파일하는 도구인데, 놀랍게도 순수 POSIX Shell Script 로만 작성되어 있습니다. 의존성 제로, 외부 도구 사용 제로입니다.
처음 이 코드를 열어보고 저는 경악을 금치 못했습니다. 15년 넘게 엔지니어로 일하면서 수많은 레거시 쉘 스크립트와 기상천외한 해킹을 봐왔지만, 쉘 스크립트로 C 컴파일러를 바닥부터 구현한 것은 정말 흔치 않은 일이기 때문입니다. 이 코드가 어떻게 동작하는지, 그리고 왜 이것이 엔지니어링 관점에서 흥미로운지 깊게 파헤쳐 보겠습니다.
외부 세계와의 단절: PATH=
이 스크립트의 도입부를 보면 작성자의 결연한 의지를 엿볼 수 있습니다. 쉘 스크립트를 작성할 때 우리는 무의식적으로 sed, awk, grep 같은 Coreutils에 의존합니다. 하지만 이 스크립트는 시작하자마자 외부 명령어의 가능성을 원천 차단해 버립니다.
# Clear PATH: no external commands needed
PATH=
이 한 줄이 의미하는 바는 명확합니다. 오직 쉘의 Built-in 기능(문자열 치환, 산술 연산, 함수 등)만으로 렉싱(Lexing), 파싱(Parsing), AST(Abstract Syntax Tree) 생성, 그리고 바이너리 코드 제너레이션까지 모두 수행하겠다는 뜻입니다.
Shell Script로 AST를 구현하는 방법
가장 흥미로웠던 부분은 자료구조가 빈약한 쉘 스크립트 환경에서 어떻게 복잡한 트리 구조인 AST를 메모리에 올리고 탐색하는가 였습니다. 작성자는 eval 과 동적 변수 할당을 극한으로 활용하여 이 문제를 해결했습니다.
alias ast_push='local X$V="$STATE"
PARN="${NODES##*" "}"
eval "PARNT=\"\${X$PARN%% *}\/""
NODE=$V; NODES="$NODES $NODE"'
쉘에는 Struct나 Pointer 개념이 없습니다. 따라서 작성자는 X1, X2, V1, V2 와 같은 동적 이름의 로컬 변수를 생성하여 각 노드의 상태와 값을 저장합니다. 그리고 NODES 라는 문자열 변수에 스택처럼 노드 ID를 공백으로 구분하여 쌓아 올립니다.
이러한 패턴은 과거 2010년대 초반, 제한된 임베디드 환경에서 Bash만으로 복잡한 상태 관리를 해야 했을 때 종종 쓰이던 흑마법입니다. 유지보수성은 최악으로 치닫지만, 의존성 없이 순수 쉘만으로 트리 구조를 모사할 수 있다는 점에서는 매우 우아한 꼼수입니다.
State Machine과 Autogeneration의 흔적
코드를 계속 내려가다 보면 수백 개의 파서 상태(State) 정의를 마주하게 됩니다.
alias ast_Ca="ast_new;STATE=Ca;ast_push"
alias ast_Cb="ast_new;STATE=Cb;ast_push"
# ... (중략) ...
alias ast_C188="ast_new;STATE=C188;ast_push"
_EXP_Ca='"\"" or '\''\'\'\'' or '\''I'\'' or '\''C'\'' ... or identifier or [0-9a-fA-FxXuUlL.] or text'
이 거대한 alias 목록과 정규식에 가까운 문자열 매칭 패턴들을 보면, 이 스크립트 전체가 사람이 한 땀 한 땀 타이핑한 것이 아님을 알 수 있습니다. 해커뉴스 댓글에서도 누군가 지적했듯, 이것은 상위 레벨의 문법 정의(Grammar definition) 파일로부터 생성된(Autogenerated) 상태 머신 코드가 분명합니다.
Hacker News의 반응과 나의 생각
이 프로젝트가 해커뉴스에 올라왔을 때 커뮤니티의 반응은 극명하게 갈렸습니다.
한편에서는 실용주의적 관점의 비판이 쏟아졌습니다.
- “테스트 코드가 없는 C 컴파일러는 가치가 없다.”
- “왜 POSIX 표준 도구들을 쓰지 않았나?
awk나sed를 쓰면 저 거대한 switch 문들을 훨씬 간단하게 만들 수 있었을 텐데.” - “실행하기 두렵다. 쉘코드나 백도어가 숨어있어도 알아낼 방도가 없다.”
특히 마지막 보안 관련 지적은 시니어 엔지니어로서 100% 동의합니다. 코드가 심하게 난독화되어 있고 기계가 생성한 상태 머신이 포함되어 있어서, 켄 톰슨의 유명한 논문인 Trusting Trust 에 나오는 백도어 공격을 시연하기에 이보다 완벽한 타겟은 없습니다.
하지만 반대편에서는 이 프로젝트의 진정한 가치를 알아보는 찬사도 이어졌습니다.
- “2026년의 뇌로는 취미 프로그래밍(Recreational programming)을 결코 이해할 수 없다.”
- “많은 부분이 자동 생성된 것은 맞지만, 그렇다고 해서 이 프로젝트의 미친듯한 경이로움이 줄어들지는 않는다.”
솔직히 저는 처음엔 회의적이었습니다. ‘굳이 왜 이런 짓을?’ 이라는 생각이 먼저 들었죠. 하지만 코드를 깊게 파고들면서, 작성자가 쉘 스크립트의 한계를 극한까지 밀어붙이기 위해 치열하게 고민한 흔적들을 발견할 수 있었습니다. printf 와 echo 의 동작 차이를 탐지하는 헬퍼 함수나, 문자열 반복을 최적화하기 위해 Exponentiation-by-squaring (분할 정복을 이용한 거듭제곱) 알고리즘을 쉘 스크립트로 구현한 _repeat 함수는 그 자체로 훌륭한 엔지니어링 퍼즐의 해답입니다.
결론
이 c89cc.sh 를 프로덕션에 사용할 사람이 있을까요? 당연히 없습니다. 속도는 끔찍하게 느릴 것이고, 디버깅은 불가능에 가까우며, 보안은 보장할 수 없습니다.
하지만 엔지니어링의 세계에서 모든 프로젝트가 반드시 실용적이어야만 가치 있는 것은 아닙니다. 때로는 기술 그 자체의 한계를 시험하고, 불가능해 보이는 제약 조건 속에서 동작하는 결과물을 만들어내는 행위 자체가 예술이 되기도 합니다. 이 프로젝트는 현대의 비대해진 툴체인에 지친 엔지니어들에게 바치는, 순수하고 원초적인 형태의 해커 정신 그 자체입니다.
쉘 스크립트의 깊은 동작 원리나 파서 구현에 관심이 있다면, 주말에 커피 한 잔과 함께 이 코드를 천천히 뜯어보시는 것을 강력히 추천합니다.
References
- Original Article: c89cc.sh – standalone C89/ELF64 compiler in pure portable shell
- Hacker News Thread: https://news.ycombinator.com/item?id=47598413