Csmith와 YARPGen으로 Claude’s C Compiler(CCC)를 퍼징한 뒤, 발견된 오컴파일 버그 11개를 Codex로 수정하고 회귀 테스트를 추가한 경험을 정리한다.
John Regehr, March 2 2026.
처음에는 CCC — “Claude’s C Compiler” — 에 그다지 관심이 없었다. 하지만 Csmith와 YARPGen으로 퍼징했을 때 무슨 일이 벌어지는지 살짝 보고 나서는, 이 컴파일러가 실제로 얼마나 틀리는지 궁금해졌다. GitHub 이슈에 나온 결과—101개의 Csmith 프로그램 중 14개 오컴파일, 101개의 YARPGen 프로그램 중 5개 오컴파일—는 꽤 나빠 보였지만, 내가 이 컴파일러에 대해 들었던 이야기와는 일관되어 보였다. 즉, 컴파일러 수업 프로젝트로 만들 수 있는 것보다는 더 정교한데, 그렇다고 GCC나 Clang/LLVM 같은 프로덕션급 산출물을 평가하는 스케일에서는 아예 점수조차 매기기 어려운, 묘한 영역을 차지하고 있다는 것이다.
배경 설명을 조금 하자면, Csmith와 YARPGen은 우리 연구 그룹이 만든 무작위 컴파일러 테스트 도구다. 이 두 도구는 수백 개가 넘는 컴파일러 결함을 찾아내는 데 기여했는데, 그중 가장 흥미로운 결함은 오컴파일(miscompilation) 이다. 오컴파일이란 컴파일러가 어떤 입력 프로그램에 대해, 관련 프로그래밍 언어 표준이 허용하는 동작들의 집합에서 벗어나는 동작을 하는 출력을 조용히 만들어내는 경우를 말한다. YARPGen에는 (사실상) 내장 인터프리터가 있어서, 자신이 생성한 프로그램이 출력해야 할 값을 예측할 수 있다. Csmith에는 그런 기능이 없다. Csmith로 오컴파일을 탐지하려면 차분 테스트(differential testing) 를 사용해서, 두 컴파일러가 만든 실행 파일의 동작을 비교하거나(예: 서로 다른 컴파일러), 동일 컴파일러의 두 모드(예: 최적화 컴파일과 비최적화 컴파일)를 비교한다. 증명할 수는 없지만, 나는 이런 도구들(및 유사한 도구들)이 개발자들이 매일 사용하는 프로덕션 컴파일러를 더 견고하고 탄탄하게 만드는 데 도움이 되었기를 바란다.
나는 YARPGen 버전 1과 CCC를 테스트 루프에 연결했다. 예상대로 CCC는 많은 입력을 오컴파일했다. 오컴파일을 하나 찾을 때마다 C-Vise로 리덕션했다(C-Vise는 대부분 C-Reduce이지만, 핵심 부분이 Perl이 아니라 Python으로 다시 작성되어 있다). 큰 테스트 케이스가 트리거하는 오컴파일 버그를 다루는 것은 사실상 불가능하다—테스트 케이스 리덕션은 오컴파일 트리거를 (보통) 몇 줄로 줄여 준다. 다음은 CCC가 오컴파일한 프로그램의 예시다:
int printf(const char *, ...);
unsigned long long seed;
unsigned a = 3357492005;
int b;
void hash(long long *seed, int v) { *seed ^= v; }
int main() {
b = a / (long)3;
hash(&seed, b);
printf("%llu\n", seed);
}
다음으로—나는 Rust 프로그래머가 조금도 아니기 때문에—Codex에게 각 버그를 고치고, 물론 회귀 테스트도 추가해 달라고 요청했다. Codex(정확히는 “gpt-5.3-codex high”)를 고른 이유는 특별히 애정이 있어서가 아니라, 내 고용주가 무슨 이유에서인지 현재 비용을 지불하고 있는 것이 그것뿐이기 때문이다. 성공한 것처럼 보이면 다시 YARPGen을 더 돌렸다. 버그 11개를 수정한 뒤, YARPGen을 하룻밤 돌린 결과(개별 테스트 약 200,000개) CCC가 오컴파일을 일으키지 못했다. 그래서 Csmith로 넘어갔고, Csmith로 하룻밤 퍼징을 돌린 결과(역시 약 200,000개 테스트) 내가 고친 버전의 CCC에서도 오컴파일을 유발하지 못했다.
다음은 11개의 버그를 수정한 커밋들이다. 버그 요약은 Codex가 작성했다.
https://github.com/regehr/claudes-c-compiler/commit/4d9913e7f53be66e6de30869e1a324020ce81777
“이전에는 상수 표현식에서 -x가 항상 signed 스타일의 wrapping_neg(negate_const)를 사용했기 때문에, unsigned 결과가 잘못된 숫자 값/부호로 폴딩될 수 있었다. 예: -8u는 2^N 모듈러여야 하며(u32의 경우 0xFFFF_FFF8), 하지만 기존 폴딩은 signed -8처럼 동작할 수 있었다.”https://github.com/regehr/claudes-c-compiler/commit/32fe7f5e5fe08bb0b7bf3ee7e6bb90234356d29e
“x86 피프홀 compare+branch fusion 패스는 cmp; setcc; …; test %rax,%rax; je/jne 를 하나의 jcc로 바꾸려 했다. 이때 setcc와 test 사이에 ‘건너뛸 수 있는’ 명령에 대해 너무 관대했다. 특히 최종 test 전에 %rax가 무관한 스택 슬롯에서 다시 로드되더라도 여전히 fusion을 수행할 수 있었고, 그 결과 분기가 잘못된 값에 의존하게 됐다. 또한 setcc를 %al이 아닌 레지스터로 쓰는 경우와, 지나치게 넓은 movslq/movzb 패턴도 받아들였다.”
https://github.com/regehr/claudes-c-compiler/commit/abeb8fbdce8c6f2c99557cf148efc9483b9c902a
https://github.com/regehr/claudes-c-compiler/commit/00fbea89eb855a359eea6c2c976b0c2f2fbecd1e
“src/passes/narrow.rs(Phase 4)의 shift-narrowing 최적화가 I64 시프트 연산을 I32로 너무 공격적으로 축소했다. Shl은 항상 안전한 것으로 취급됐고, AShr/LShr는 LHS의 부호/0 확장만 확인했다. 축소된 폭에서 시프트 양이 유효한지 요구하지 않았다.”https://github.com/regehr/claudes-c-compiler/commit/90905856a09bba6ab4df4aade850342078db7850
“이 커밋은 정수 연산에 대한 IR lowering 중 usual arithmetic conversions 버그를 고친다. 버그: lower_expr_with_type()가 cast의 소스 타입으로 get_expr_type(expr)를 사용했다. LP64에서 정수의 경우 get_expr_type이 표현식의 진짜 C 의미 타입(I32/U32)이 아니라 더 넓은 저장 타입(종종 I64)을 반영할 수 있다. 그 결과 산술 이전의 암묵적 캐스트가 잘못되었고, 특히 signed/unsigned 혼합에서 문제가 됐다(여기서는: 나눗셈 전에 int가 unsigned long으로 승격됨).”
https://github.com/regehr/claudes-c-compiler/commit/c01bac0f988471855c5422cafe5d3d57e5ed2e58
https://github.com/regehr/claudes-c-compiler/commit/5b0447eabf19163c90484415d7a292df1781af66
https://github.com/regehr/claudes-c-compiler/commit/b1c97854ffa7b9d3d5f53f93f0a089ca0b56f0f6
https://github.com/regehr/claudes-c-compiler/commit/acc1b4a5f9618d7e7d9c7e917afe7b622caf346a
https://github.com/regehr/claudes-c-compiler/commit/ceff82eba63c2b9290370e48fac850a7a709d8f9
https://github.com/regehr/claudes-c-compiler/commit/9fe29b62241e3e08a82bbe61d752fc0660a6526c
이 중 일부는 다른 변경도 섞여 있고, 어느 것도 유용한 커밋 메시지가 없다—나는 이 작은 프로젝트를 누구와 공유할 생각으로 시작한 게 아니었다.
그럼 여기서 무엇을 배웠을까? 우선 Codex의 버그 수정은 꽤 인상적이었다. 나는 리덕션된 테스트 케이스와 좋은 레퍼런스 컴파일러(GCC와 LLVM) 외에는 아무 가이드도 주지 않았다. Codex가 CCC를 어설픈 방식으로 패치해서, 정합성보다는 혼돈 쪽으로 흘러갈 거라고 반쯤 예상했지만, 전혀 그렇지 않았다. 다만 Codex가 제대로 줄어들지 않은 입력(정의되지 않은 동작을 포함함)을 고치려 하다가, 한 번은 크게 잘못된 방향으로 간 적이 있는데, 그건 알아차리기 쉬웠고 작업 결과를 폐기하면 됐다. 그 수정들이 “동작하는 것 같다” 말고 다른 의미에서 좋은 수정인지? 모르겠다! 나는 바이브 코딩된 C 컴파일러를 이해하려고 애쓰고 싶지 않다.
또 하나 배운 점은, Csmith와 YARPGen이 생성하는 C의 부분집합에 관해서라면, CCC는 올바른 척을 그럴듯하게 할 수 있을 정도의 “합리적인 편집 거리” 안에 있었다는 것이다(위 11개 커밋). 이게 당연한 결론이었을까? 전혀 아니다. 바이브 코딩된 컴파일러가 초기 테스트 환경에 돌이킬 수 없을 정도로 특화되어, 더 일반적인 경우의 C 코드를 컴파일하는 데 아키텍처적으로 불가능했을 수도 있다.
마지막으로, YARPGen으로 내가 찾은 버그들의 성격에 대해 이야기해 보자. 대부분은 C 표준을 충분히 면밀하고 신중하게 읽지 않고 C 컴파일러를 구현할 때 할 법한 실수들이다. 진지한 컴파일러에서는 절대 발견되지 않을 표면적인 버그들이다. GCC에서는 이런 종류의 버그를 우리가 찾은 적이 없다고 생각한다. Clang에서는 아마 한두 개 정도 찾았던 것 같은데, 그건 우리가 Clang의 역사 초기에 매우 이른 시점부터 테스트를 시작했기 때문이다. LLVM 커뮤니티가 Clang을 막 키워 올리던 시기였고, 그와 동시에 우리가 Csmith를 개발했다. 이런 표면적 버그들과 달리, 프로덕션급 컴파일러에서 무작위 테스터가 발견하는 버그의 압도적 다수는 최적화기(optimizer)에 있다. 공격적인 최적화기에서는 결함이 생길 수 있는 의미적 표면적(semantic surface area)이 광대하다.
Claude’s C Compiler에 대한 평결은 무엇일까? 어떤 수준에서는 인상적이다. 세상에는 이 정도 능력의 컴파일러를 6개월 안에 만들 수 없는 프로그래머가 아주 많을 거라고 생각한다. 예를 들어, George Necula는 C 프런트엔드를 작성하는 것에 대해 이렇게 말했다:
When I (George) started to write CIL I thought it was going to take two weeks. Exactly a year has passed since then and I am still fixing bugs in it. This gross underestimate was due to the fact that I thought parsing and making sense of C is simple. You probably think the same. What I did not expect was how many dark corners this language has, especially if you want to parse real-world programs such as those written for GCC or if you are more ambitious and you want to parse the Linux or Windows NT sources (both of these were written without any respect for the standard and with the expectation that compilers will be changed to accommodate the program).
하지만 다른 한편으로, CCC는 최적화를 하지 않고, C 코드를 해석하는 데 있어 비교적 기본적인 버그 11개를 포함하고 있었으며, Csmith/YARPGen의 범위를 벗어나는 더 많은 버그가 들어 있을 것이 분명하다. 프로덕션 컴파일러를 다루는 사람들의 관점에서 CCC는 유용한 프로토타입조차 아니다. (좀 더 뉘앙스 있는 관점을 원한다면, 몇 주 전에 나온 Chris의 글이 좋다.) 게다가 Codex의 트레이닝 코퍼스에는 많은 C 컴파일러가 들어 있다. 그중 Rust로 쓰인 것이 있는지는 모르겠지만, 현대 LLM은 프로그래밍 언어 간 개념 번역을 정말 잘하는 것처럼 보인다.
누군가 내가 포크한 CCC를 계속 퍼징해 보고 싶다면, 여기에 있다. yarpgen 브랜치를 꼭 받아라.