새 메모리 안전 C/C++ 컴파일러 Fil-C(filcc, fil++)의 호환성, 설치/빌드 요령, 포함 라이브러리와 애플리케이션, 성능, 그리고 Debian 통합(멀티아치, dpkg-buildpackage/sbuild) 방법 및 주의사항을 정리한 실전 메모
새 메모리 안전 C/C++ 컴파일러 Fil-C(https://fil-c.org/) (filcc, fil++)의 높은 호환성에 깊은 인상을 받았다. 내가 시도한 많은 라이브러리와 애플리케이션이 Fil-C에서 변경 없이 동작했고, 예외적으로 필요한 수정들도 어렵지 않았다.
이 페이지에는 Fil-C 사용과 관련한 잡다한 메모를 모아두기 시작했다. 나의 사적인 목적은 내가 관리하는 여러 머신을 Fil-C로 컴파일된 코드로 전환해 보호하는 것이지만, 이 글이 당신에게도 유용하기를 바란다.
아래의 타이밍은 특별히 언급하지 않는 한 phoenix라는 미니 PC에서의 측정이다. 이 미니 PC는 6코어(12스레드) AMD Ryzen 5 7640HS(Zen 4) CPU, RAM 12GB, 스왑 36GB를 갖고 있고, OS는 Debian 13이다. (나는 보통 LTS 소프트웨어를 사용하며, 오늘의 Debian 11 같은 4–5년 된 소프트웨어에서 오늘의 Debian 12 같은 2–3년 된 소프트웨어로 주기적으로 업그레이드한다. 하지만 Fil-C에 포함된 일부 패키지는 더 최신 유틸리티의 존재를 기대한다.)
관련:
Fil-C를 실행하는 또 다른 방법은 Mikael Brockman의 Filnix(https://github.com/mbrock/filnix)다. 예를 들어, Debian 12에서 권한 없는 사용자가 디스크 여유 10GB 정도로 Fil-C를 다운로드, 컴파일, 설치하고, Fil-C로 컴파일된 Nethack을 실행하는 과정은 다음과 같다:
unshare --user --pid echo YES # just to test
git clone https://github.com/nix-community/nix-user-chroot
cd nix-user-chroot
cargo build --release
mkdir -m 0755 ~/nix
~/nix-user-chroot/target/release/nix-user-chroot ~/nix \
bash -c 'curl -L https://nixos.org/nix/install | sh'
env TERM=vt102 \
~/nix-user-chroot/target/release/nix-user-chroot ~/nix \
~/nix/store/*-nix-2*/bin/nix \
--extra-experimental-features 'nix-command flakes' \
run 'github:mbrock/filnix#nethack'
루트로 가장 먼저 할 것을 현재 권장:
mkdir -p /var/empty
apt install \
autoconf-dickey build-essential bison clang cmake flex gawk \
gettext ninja-build patchelf quilt ruby texinfo time
권한 없는 filc 사용자를 만들었다. 그 외의 모든 작업은 해당 사용자로 했다.
Fil-C 소스 패키지를 다운로드했다:
git clone https://github.com/pizlonator/fil-c.git
cd fil-c
이건 단지 컴파일러만이 아니라 glibc, 그리고 더 높은 레벨의 라이브러리와 애플리케이션도 포함한다. Fil-C의 바이너리 패키지도 있지만, 현재로서는 주로 소스 패키지로 작업해 왔다.
Fil-C와 glibc를 컴파일했다:
time ./build_all_fast_glibc.sh
musl을 glibc 대신 사용하는 옵션도 있지만, musl은 Fil-C에 동봉된 일부 패키지와 호환되지 않는다: attr은 basename이 필요하고, elfutils는 argp_parse가 필요하며, sed의 테스트 스위트는 glibc 변형의 calloc을 필요로 하고, vim 빌드는 iconv가 CP932에서 UTF-8로의 변환을 지원해야 한다.
처음에 phoenix 서버는 스왑이 12GB뿐이었다. Fil-C 컴파일이 메모리를 소진하면서 ./build_all_fast_glibc.sh를 몇 번 재시작해야 했다. 스왑을 36GB로 늘리니 재시작 없이 모두 동작했고, 모니터링 결과 한 시점에 거의 19GB의 스왑(플러스 RAM 12GB)이 사용되었다. 더 큰 서버(코어 128개, RAM 512GB)는 Fil-C 8분 + musl 6분이 걸렸고, 재시작은 필요 없었다.
Fil-C에는 더 많은 라이브러리와 애플리케이션을 빌드하는 ./build_all_slow.sh가 있다(때때로 Fil-C 작성자의 패치 포함). 나는 다음과 같은 차이를 갖는 대체 스크립트 https://cr.yp.to/2025/build-parallel-20251023.py 를 작성했다:
phoenix에서 PATH="$HOME/bin:$HOME/fil-c/build/bin:$HOME/fil-c/pizfix/bin:$PATH" ./build-parallel.py를 실행하자 101분의 실시간(사용자 시간 467분, 시스템 시간 55분)에 61개 타깃을 처리했고, 그중 60개를 성공적으로 컴파일했다.
아래를 하기 전에 export PATH="$HOME/bin:$HOME/fil-c/build/bin:$HOME/fil-c/pizfix/bin:$PATH"를 해 두었다.
boost 1.89.0: 대부분 잘 동작하는 듯하다. 패키지의 대부분은 헤더 전용이고, 몇 가지 간단한 테스트는 문제없었다.
컴파일되는 부분도 조금 들여다봤다. ./bootstrap.sh --with-toolset=clang --prefix=$HOME 실행 중 Fil-C가 지원하지 않는 vfork에 부딪쳤는데, tools/build/src/engine/execunix.cpp에서 no-fork 테스트를 위해 defined(APPLE) || defined(FILC)를 사용하도록 수정하니 통과했다.
./b2 install --prefix=$HOME toolset=clang address-model=64 architecture=x86_64 binary-format=elf를 실행했더니 x86_64 대신 x86이라고 써야 해서 오류 메시지가 나왔고, 그 뒤에 Fil-C가 b2 프로그램의 안전성 문제를 잡았다고 말했다: filc safety error: argument size mismatch (actual = 8, expected = 16). 디버깅을 활성화하지 않았기 때문에 Fil-C는 b2의 어디에서 발생했는지 알려주지 않았다.
cdb-20251021: 동작하는 듯하다. 회귀 테스트 중 하나, 인위적인 메모리 부족 테스트가 현재 Fil-C에서는 다른 오류 메시지를 낸다: filc panic: src/libpas/pas_compact_heap_reservation.c:65: pas_aligned_allocation_result pas_compact_heap_reservation_try_allocate(size_t, size_t): assertion page_result.result failed.
libcpucycles-20250925: 동작하는 듯하다. cpucycles/options의 처음 세 줄을 주석 처리했다.
libgc: 이를 작은 gcshim 패키지(https://cr.yp.to/2025/gcshim-20251022.tar.gz)로 교체했는데, 단순히 malloc 등을 호출한다. 지금까지는 충분한 대체물로 보인다. (Fil-C에는 가비지 컬렉터가 포함되어 있다.)
libntruprime-20241021: 약간의 조정 후 동작하는 듯하나, 아직 전체 노트를 모으지는 않았다. chmod +t crypto_hashblocks/sha512/avx2로 어셈블리를 비활성화하면 컴파일되며; Fil-C가 valgrind를 지원하지 않으므로 --no-valgrind로 구성했다; cpuid가 동작하도록 몇 가지를 더 손봤다.
lpeg-1.1.0: 컴파일되고, 아마 동작(네오빔 의존성인 lua에 의존):
cd
PREFIX=$(dirname $(dirname $(which lua)))
wget https://www.inf.puc-rio.br/~roberto/lpeg/lpeg-1.1.0.tar.gz
tar -xf lpeg-1.1.0.tar.gz
cd lpeg-1.1.0
make CC=`which filcc` DLLFLAGS='-shared -fPIC' test
cp lpeg.so $PREFIX/lib
cd
PREFIX=$(dirname $(dirname $(which lua)))
wget https://github.com/luvit/luv/releases/download/1.51.0-1/luv-1.51.0-1.tar.gz
tar -xf luv-1.51.0-1.tar.gz
cd luv-1.51.0-1
mkdir build
cd build
LUA_DIR=$HOME/fil-c/projects/lua-5.4.7
# lua install should probably do this:
cp $LUA_DIR/lua.h $PREFIX/include/
cp $LUA_DIR/lauxlib.h $PREFIX/include/
cp $LUA_DIR/luaconf.h $PREFIX/include/
cp $LUA_DIR/lualib.h $PREFIX/include/
# and then:
cmake -DCMAKE_C_COMPILER=`which filcc` -DCMAKE_INSTALL_PREFIX=$PREFIX -DWITH_LUA_ENGINE=Lua -DLUA_DIR=$HOME/fil-c/projects/lua-5.4.7/ ..
make test
make install
wget https://github.com/muttmua/mutt/archive/refs/tags/mutt-2-2-15-rel.tar.gz
tar -xf mutt-2-2-15-rel.tar.gz
cd mutt-mutt-2-2-15-rel
CC=`which clang` ./prepare --prefix=$HOME/fil-c/pizfix --with-homespool
make -j12 install
적어도 이메일 읽기에는 동작하는 듯하다.
wget https://github.com/jonas/tig/releases/download/tig-2.6.0/tig-2.6.0.tar.gz
tar -xf tig-2.6.0.tar.gz
cd tig-2.6.0
CC=`which filcc` ./configure --prefix=$(dirname $(dirname $(which git)))
make -j12
make test
make -j12 install
적어도 Fil-C 저장소 보기에는 동작하는 듯하다.
아래 설명대로 Debian 13 머신에서 Fil-C를 컴파일러로 사용해 대체 Debian 패키지 일부를 빌드하고 설치했다. Debian 소스 패키지에 이미 내장된 기본 컴파일-설치-테스트 지식을 활용하면, 이는 많은 패키지로 빠르게 확장될 수 있기를 바란다. 다만 일부 패키지는 Fil-C에서 동작하도록 추가 패치가 필요해 더 많은 작업이 들 수도 있다.
구조. Debian은 이미 여러 아키텍처(ABI; Debian "ports")의 패키지를 동시에 설치하는 방법을 이해하고 있다. 예를 들어 dpkg --add-architecture i386; apt update; apt install bash:i386를 실행하면 32비트 버전의 bash가 설치되어 보통의 64비트 버전이 대체된다. apt install bash:amd64로 64비트 버전으로 되돌릴 수 있다. 한편 32비트 라이브러리와 64비트 라이브러리는 기본적으로 /lib/i386-linux-gnu 또는 /usr/lib/i386-linux-gnu vs. /lib/x86_64-linux-gnu 또는 /usr/lib/x86_64-linux-gnu 같은 별도 위치에 설치된다. (Debian 11 이상과 Ubuntu 22.04 이상에서는 /lib가 /usr/lib로 심볼릭 링크되어 있다.)
나는 Debian에 Fil-C를 꽂아 넣는 모델로 이를 따르고 있다: 목표는 apt install bash:amd64fil0가 Fil-C로 컴파일된(amd64fil0) bash 버전을 설치해 기존의(amd64) bash를 대체하도록 하는 것이고, amd64와 amd64fil0 라이브러리는 별도 위치에 설치된다.
포함 파일 문제. Debian은 여러 ABI로 컴파일된 라이브러리 패키지들이 모두 같은 include 파일을 제공하기를 기대한다(https://wiki.debian.org/Multiarch/LibraryPathOverview). 예를 들어 /usr/include/ncurses.h는 libncurses-dev:i386, libncurses-dev:amd64 등에서 제공된다. Debian은 libncurses-dev:i386과 libncurses-dev:amd64 등이 모두 같은 버전이어야 하도록 강제하므로 이는 안전하다. 가끔 ABI에 따라 include 파일이 달라지는 패키지는 /usr/include/x86_64-linux-gnu 등을 사용할 수 있다.
Fil-C는 /usr/include 대신 Fil-C 전용 디렉터리를 사용한다(심지어 Fil-C가 glibc로 컴파일되었더라도 보통 /usr/include와 버전이 다를 것이다). 이 차이가 아래의 지저분함의 주된 원인이다. 나는 Debian에서 Fil-C 드라이버가 /usr/include를 사용하도록 조정할 계획이다. [이 작업은 filian-install-compiler 스크립트에서 완료했다.]
또 다른 계획은 Fil-C의 glibc 컴파일이 최종 시스템 prefix를 사용하도록 손보는 것이다. [이 역시 filian-install-compiler 스크립트에 반영되어 있다.] 아래에서 설명하는 접근법은 대신 /home/filian/fil-c가 프로그램의 컴파일과 실행을 위해 유지되어야 한다.
Debian 패키지 빌드. Debian 패키지 빌드는 어떻게 동작하나? 먼저, 루트로 더 설치할 패키지들:
apt install dpkg-dev devscripts docbook2x \
dh-exec dh-python python3-setuptools fakeroot \
sbuild mmdebstrap uidmap piuparts
Debian에는 패키지를 빌드하는 여러 옵션(https://wiki.debian.org/PackagingTools#Package_build_tools)이 있다. 격리가 가장 좋고 Debian이 배포용 새 패키지를 계속 빌드할 때 사용하는 옵션은 sbuild지만, 빠른 개발을 위해서는 더 저수준의 dpkg-buildpackage를 직접 사용하는 것에 초점을 두겠다.
mkdir -p ~/shared/sbuild
time mmdebstrap --include=ca-certificates --skip=output/dev --variant=buildd unstable ~/shared/sbuild/unstable-amd64.tar.zst https://deb.debian.org/debian
mkdir -p ~/.config/sbuild
cat << "EOF" > ~/.config/sbuild/config.pl
$chroot_mode = 'unshare';
$external_commands = { "build-failed-commands" => [ [ '%SBUILD_SHELL' ] ] };
$build_arch_all = 1;
$build_source = 1;
$source_only_changes = 1;
$run_lintian = 1;
$lintian_opts = ['--display-info', '--verbose', '--fail-on', 'error,warning', '--info'];
$run_autopkgtest = 1;
$run_piuparts = 1;
$piuparts_opts = ['--no-eatmydata', '--distribution=%r', '--fake-essential-packages=systemd-sysv'];
EOF
mkdir -p ~/shared/packages
cd ~/shared/packages
apt source tinycdb
cd tinycdb-*/
time sbuild
mkdir -p ~/shared/packages
cd ~/shared/packages
apt source tinycdb
cd tinycdb-*/
time dpkg-buildpackage -us -uc -b
echo amd64fil0 x86_64+fil0 amd64fil0 64 little >> /usr/share/dpkg/cputable
또한 apt가 이 아키텍처로 컴파일된 패키지를 설치하도록 허용한다(주의: 나중에 apt update가 서버에서 해당 아키텍처를 찾으려 하고, 찾지 못하면 좀 끙끙댄다. 하지만 망가지는 것은 없다):
dpkg --add-architecture amd64fil0
또한 autoconf가 amd64fil0을 받아들이도록 가르친다(세 줄 중 세 번째 줄이 Debian 빌드에는 결정적이다):
sed -i '/| x86_64 / a| x86_64+fil0 \\' /usr/share/autoconf/build-aux/config.sub
sed -i '/| x86_64 / a| x86_64+fil0 \\' /usr/share/libtool/build-aux/config.sub
sed -i '/| x86_64 / a| x86_64+fil0 \\' /usr/share/misc/config.sub
[filian-install-compiler를 사용했다면 불필요:] filian 사용자로서 Fil-C와 표준 라이브러리를 컴파일:
cd
git clone https://github.com/pizlonator/fil-c.git
cd fil-c
time ./build_all_fast_glibc.sh
[filian-install-compiler를 사용했다면 불필요:] 루트로서 Fil-C와 표준 라이브러리를 시스템 위치에 복사:
mkdir -p /usr/libexec/fil/amd64/compiler
time cp -r /home/filian/fil-c/pizfix /usr/libexec/fil/amd64/
rm -rf /usr/lib/x86_64+fil0-linux-gnu
mv /usr/libexec/fil/amd64/pizfix/lib /usr/lib/x86_64+fil0-linux-gnu
ln -s /usr/lib/x86_64+fil0-linux-gnu /usr/libexec/fil/amd64/pizfix/lib
rm -rf /usr/include/x86_64+fil0-linux-gnu
mv /usr/libexec/fil/amd64/pizfix/include /usr/include/x86_64+fil0-linux-gnu
ln -s /usr/include/x86_64+fil0-linux-gnu /usr/libexec/fil/amd64/pizfix/include
time cp -r /home/filian/fil-c/build/bin /usr/libexec/fil/amd64/compiler/
time cp -r /home/filian/fil-c/build/include /usr/libexec/fil/amd64/compiler/
time cp -r /home/filian/fil-c/build/lib /usr/libexec/fil/amd64/compiler/
( echo '#!/bin/sh'
echo 'exec /usr/libexec/fil/amd64/compiler/bin/filcc "$@"' ) > /usr/bin/x86_64+fil0-linux-gnu-gcc
chmod 755 /usr/bin/x86_64+fil0-linux-gnu-gcc
( echo '#!/bin/sh'
echo 'exec /usr/libexec/fil/amd64/compiler/bin/fil++ "$@"' ) > /usr/bin/x86_64+fil0-linux-gnu-g++
chmod 755 /usr/bin/x86_64+fil0-linux-gnu-g++
ln -s /usr/libexec/fil/amd64/compiler/bin/llvm-objdump /usr/bin/x86_64+fil0-linux-gnu-objdump
ln -s x86_64+fil0-linux-gnu-gcc /usr/bin/filcc
ln -s x86_64+fil0-linux-gnu-g++ /usr/bin/fil++
이제 사용자 filian(또는 다른 사용자)로서, Debian 소스 패키지를 조정하는 작은 헬퍼 스크립트를 만들어보자:
mkdir -p $HOME/bin
( echo '#!/bin/sh'
echo 'sed -i '\''s/^ \([^"']*\)$/ pizlonated_\1/'\'' debian/*.symbols'
echo 'find . -name '\''*.map'\'' | while read fn'
echo 'do'
echo ' awk '\''{'
echo ' if ($1 == "local:") global = 0'
echo ' if ($1 == "}") global = 0'
echo ' if (global && NF > 0 && !index($0,"c++")) $1 = "pizlonated_"$1'
echo ' if ($1 == "global:") global = 1'
echo ' print'
echo ' }'\'' < $fn > $fn.tmp'
echo ' mv $fn.tmp $fn'
echo 'done'
echo 'find debian -name '\''*.install'\'' | while read fn'
echo 'do'
echo ' awk '\''{'
echo ' if (NF == 2 && $2 == "usr/include") $2 = $2"/${DEB_HOST_MULTIARCH}"'
echo ' if (NF == 1 && $1 == "usr/include") { $2 = $1"/${DEB_HOST_MULTIARCH}"; $1 = $1"/*" }'
echo ' print'
echo ' }'\'' < $fn > $fn.tmp'
echo ' mv $fn.tmp $fn'
echo 'done'
) > $HOME/bin/fillet
chmod 755 $HOME/bin/fillet
이제 작은 패키지를 한번 빌드해보자:
mkdir -p ~/shared/packages
cd ~/shared/packages
apt source tinycdb
cd tinycdb-*/
$HOME/bin/fillet
time env DPKG_GENSYMBOLS_CHECK_LEVEL=0 \
DEB_BUILD_OPTIONS='crossbuildcanrunhostbinaries nostrip' \
dpkg-buildpackage -d -us -uc -b -a amd64fil0
일반 빌드와의 차이에 대한 설명:
내 환경에서는 위 작업이 잘 동작했고, ../*.deb 패키지 세 개가 생성되었다. 루트로 설치도 잘 되었다:
apt install /home/filian/shared/packages/*.deb
# sanity check 몇 가지:
apt list | grep tinycdb
# "tinycdb/stable 0.81-2 amd64"(사용 가능한 패키지) 출력
# 그리고 "tinycdb/now 0.81-2 amd64fil0 [installed,local]" 출력
dpkg -L tinycdb:amd64fil0
# /usr/bin/cdb 등 여러 파일을 나열
nm /usr/bin/cdb
# "pizlonated"(Fil-C) 심볼 포함 다양한 심볼 표시
ldd /usr/bin/cdb
# /usr/libexec/fil의 라이브러리에 대한 의존성 표시
/usr/bin/cdb -h
# 도움말 출력: "cdb: Constant DataBase" 등
새로 설치한 라이브러리로 일부러 잘못된 테스트 프로그램을 컴파일하는 것도 동작하며, Fil-C의 런타임 보호를 트리거한다:
cd /root
( echo '#include <cdb.h>'
echo 'int main() { cdb_init(0,0); return 0; }' ) > usecdb.c
filcc -o usecdb usecdb.c -lcdb
./usecdb < /bin/bash
# ... "filc panic: thwarted a futile attempt to violate memory safety."
FAKEPACKAGE=libc-dev
mkdir -p ~/shared/packages/$FAKEPACKAGE/debian
cd ~/shared/packages/$FAKEPACKAGE
( echo $FAKEPACKAGE' (0.0) unstable; urgency=medium'
echo ''
echo ' * Initial Release.'
echo ''
echo ' -- djb <djb@cr.yp.to> Sun, 26 Oct 2025 16:05:17 +0000'
) > debian/changelog
( echo 'Source: '$FAKEPACKAGE
echo 'Build-Depends: debhelper-compat (= 13)'
echo 'Maintainer: djb '
echo ''
echo 'Package: '$FAKEPACKAGE
echo 'Architecture: any'
echo 'Multi-Arch: same'
echo 'Description: fake '$FAKEPACKAGE
) > debian/control
( echo '#!/usr/bin/make -f'
echo ''
echo 'build-arch build-indep build \'
echo 'install-arch install-indep install \'
echo 'binary-arch binary-indep binary \'
echo ':'
echo 'Xdh $@' | tr X '\011'
echo ''
echo 'clean:'
echo 'Xdh_clean' | tr X '\011'
) > debian/rules
time env DPKG_GENSYMBOLS_CHECK_LEVEL=0 \
DEB_BUILD_OPTIONS='crossbuildcanrunhostbinaries nostrip' \
dpkg-buildpackage -d -us -uc -b -a amd64fil0
mkdir -p ~/shared/packages
cd ~/shared/packages
apt source ncurses
cd ncurses-*/
$HOME/bin/fillet
time env DPKG_GENSYMBOLS_CHECK_LEVEL=0 \
DEB_BUILD_OPTIONS='crossbuildcanrunhostbinaries nostrip' \
dpkg-buildpackage -d -us -uc -b -a amd64fil0
rm ../ncurses-*deb # apt won't let us touch the binaries
루트로 위 라이브러리들을 설치:
apt install /home/filian/shared/packages/lib*.deb
libmd. 잘 동작하는 듯하다. 처음에는 설치되지 않았다. 컴파일된 버전(amd64fil0용)은 1.1.0-2였는데, 설치되어 있던 버전(amd64용)은 1.1.0-2+b1이었기 때문이다. Debian은 아키텍처 간 동일한 버전 번호를 요구한다(위의 include 파일 호환성 참조). 그래서 apt는 1.1.0-2+b1이 1.1.0-2를 깨뜨린다고 말했다. 나는 amd64와 amd64fil0 모두에 대해 1.1.0-2를 컴파일하고 설치해 해결했다. 이는 다운그레이드다. "+b"는 "binNMU", 즉 "binary-only non-maintainer upload"를 의미하며, 공식 소스에 추가된 패치다. 그 패치가 무엇인지는 모른다.
readline. 설치 후 ln -s /usr/include/readline /usr/include/x86_64+fil0-linux-gnu/readline가 필요하다. debian/rules(아마 *.install 이전 시절에 작성된 듯)에서 조정할 수도 있지만, 어쨌든 이는 내가 제거할 계획인 지저분함의 하나의 예다.
lua5.4. 동작하는 듯하다. readline에 의존한다.