examining the macOS gcc binary
macOS의 gcc 바이너리 들여다보기
macOS의 /usr/bin/gcc는 별칭이 아니라 실행 파일이며, 선택된 개발자 도구 경로에서 clang을 찾아 실행하는 래퍼입니다. 글은 바이너리 분석으로 이 호출 과정을 추적하고, GCC와 혼동을 부르는 이름과 도구 경로 선택에 관한 사용자 경험을 짚습니다.
- 주제
에디터 노트
바이너리 분석은 gcc라는 이름 뒤에서 xcrun과 clang이 차례로 실행되는 구조를 구체적으로 보여줍니다. 반면 SIP 때문에 이 방식을 택했다거나 경로 변경 자유를 원했다는 설명은 글쓴이의 추측입니다. 댓글에서 제시한 여러 Xcode 설치와 DEVELOPER_DIR에 따른 도구 선택은 PATH 대신 xcrun을 두는 실용적 이유를 보완합니다. 다만 실제 GCC와 호환되지 않는 이름은 수업에서 컴파일러별 옵션이나 OpenMP 지원을 혼동하게 만든다는 경험담과 맞닿습니다. 경로 선택의 편의와 gcc라는 이름이 주는 기대가 충돌하는 지점이 남습니다.
AI 요약
macOS에서 gcc를 실행하면 실제 GCC가 아니라 clang이 동작합니다. 글쓴이는 이를 단순한 셸 별칭으로 여기는 경우가 많지만, /usr/bin/gcc는 별도의 실행 파일이라고 설명합니다.
실행 파일의 동작
첫 번째 바이너리는 실행 파일 이름을 확인합니다. gcc와 llvm-gcc로 실행하면 clang을, c++나 llvm-g++로 실행하면 clang++를 선택한 뒤 libxcselect.dylib의 xcselect_invoke_xcrun()을 호출합니다. 글쓴이는 이 함수가 xcrun을 실행하는 것과 같은 역할을 하며, Xcode가 설치한 개발자 도구에서도 같은 구조를 확인했다고 설명합니다.
xcrun --find gcc 명령으로 실제 호출 대상의 위치를 확인하면 예시 환경에서 /Library/Developer/CommandLineTools/usr/bin/gcc가 나옵니다. 이 디렉터리의 gcc도 독립적인 GCC 컴파일러가 아닙니다. 실행 파일은 호출 이름이 ++로 끝나는지 확인해 clang 또는 clang++를 고르고, /usr/bin/xcrun에 인자와 함께 execv()를 호출합니다. 글에 따르면 c++와 llvm-gcc, llvm-g++ 이름은 첫 번째 바이너리 안에서 별도로 처리하며, cc와 clang++는 clang의 별칭입니다.
경로를 선택하는 방식
도구 실행을 xcrun에 맡기면 xcode-select가 선택한 개발자 도구 디렉터리의 컴파일러를 사용합니다. 글쓴이는 /usr/bin이 SIP(System Integrity Protection)로 보호되며 개발자 도구를 재시작 없이 업데이트할 수 있도록 이런 구조를 택했을 수 있다고 추측합니다. 도구 디렉터리를 PATH에 직접 추가하지 않는 이유도 경로를 바꾸기 위한 선택일 수 있다고 덧붙이지만, 두 설명 모두 추측으로 제시합니다.
Lobsters 반응
- @ema-pe — 제가 이해한 게 맞다면 바이너리는 실제로 clang인데 호환성을 위해 gcc로 위장하는 건가요?
- @athas — 맞습니다. 호환되지도 않아서 좋지 않습니다. 시스템 프로그래밍 입문 수업을 가르치는데 매년 macOS를 쓰는 학생들이 gcc 전용 코드나 명령 때문에 이상한 오류를 겪습니다. Homebrew 등으로 실제 GCC를 설치하면 PATH 설정도 배워야 합니다. 서로 다른 libc에 링크하는 문제도 있었던 것으로 기억합니다. macOS의 gcc 바이너리는 사실상 거짓말쟁이입니다. 사람뿐 아니라 프로그램도 거짓말하면 안 된다고 생각합니다.
- @athas — 문제는 학생들에게 취약한 코드를 준다는 게 아닙니다. sanitizer 설정 안내가 GCC와 clang에서 다르고, macOS가 제공하는 gcc 행세를 하는 clang은 OpenMP를 지원하지 않습니다. 학생들은 새 컴파일러를 설치해야 하는데 Homebrew 컴파일러가 기본 gcc 래퍼보다 먼저 선택되도록 하는 과정에서 어려움을 겪곤 합니다. 학생들이 모두 Unix에 익숙한 것은 아니고, 저는 macOS를 쓰지 않아 도와주는 데도 한계가 있습니다.
- @jklowden — Apple 노트북에서 두 차례 아키텍처가 바뀌는 동안 GCC를 써 왔습니다. Ubuntu Multipass와 가능하면 XQuartz를 설치하면 Linux 환경을 쓸 수 있습니다. ssh -Y도 예상대로 동작하고 IP 주소가 고정되어 있으며 apt와 원하는 버전의 GCC를 쓸 수 있습니다.
- @rjzak — macOS용 gcc는 Homebrew에서 받을 수 있습니다.
- @athas — 네, 제 댓글에서도 언급했습니다.
- @citizen428 — 사용자를 올바른 도구로 안내하는 shim이 더 나은 방법이지, clang이 gcc인 척하면 안 됩니다.
- @gignico — Apple은 예전에 실제 GCC를 제공했습니다. 당시에는 오픈소스 소프트웨어 빌드 시스템이 gcc를 호출하도록 고정된 경우가 많았습니다.
- @nonagoninf — clang이 준비되기 전 짧은 기간에는 GCC 프런트엔드와 LLVM 백엔드를 함께 쓴 llvm-gcc도 있었습니다. 기억이 맞다면 OS X 10.7에서 기본 컴파일러였습니다.
- @misty — 맞습니다. Xcode 4.1의 기본 컴파일러가 llvm-gcc였고, Xcode 4.0에서는 GCC, 4.2에서는 clang이 기본이었습니다. 당시 Mac용 오픈소스 패키지를 다루는 일은 흥미로웠습니다.
- @madsmtm — 이런 트램펄린 바이너리는 꽤 흔합니다. Nix와 Rustup도 비슷한 방식을 씁니다. 개발자 도구 경로를 PATH에 넣지 않는 이유는 환경 변수로 쉽게 경로를 덮어쓸 수 있기 때문일 수 있습니다. DEVELOPER_DIR=/my/path/to/Xcode.app/Contents/Developer를 지정하면 해당 Xcode의 clang과 SDK를 사용합니다. 그래도 PATH를 쓰는 편이 이해하기 쉽습니다.
- @realkc — rustup 실행 파일은 호출된 실행 파일 이름을 확인합니다. rustup 자체 기능을 실행하거나 래퍼로 동작하며, +stable이나 +nightly 같은 인자를 해석해 실제 rustc, cargo 등을 실행합니다. 실제 바이너리는 .rustup/toolchains/<툴체인 이름>/bin에 있습니다. 이런 방식은 오래된 다중 호출 바이너리 패턴과 비슷합니다.
- @Shorden — Nix 패키지 저장소에는 Bash makeWrapper로 트램펄린을 만드는 패턴이 널리 쓰입니다. 인자를 바꾸거나 환경 변수를 설정하고, 실제 프로그램 실행 전에 스크립트를 돌릴 수 있습니다. 부모 프로세스의 PATH와 무관하게 프로그램이 실행 중 호출하는 유틸리티의 경로를 추가하는 용도도 있습니다.
- @misty — 설치 환경에 따라 개발자 도구 경로가 다릅니다. Xcode가 /Applications 밖에 있을 수도 있고, Command Line Tools만 설치했을 수도 있으며, 여러 Xcode 버전을 함께 설치할 수도 있습니다. xcode-select로 버전을 바꾸면 셸의 PATH를 수정하지 않아도 도구가 바로 바뀌어야 합니다. xcode-select -print-path로 현재 선택된 경로를 확인할 수 있습니다.
- @stapelberg — macOS가 gcc와 다른 개발자 도구를 감싸는 방식에서 불편한 점은 Xcode 라이선스 동의를 요구한다는 것입니다. 처음 한 번만이 아니라 가끔 다시 동의해야 합니다. 어떤 업데이트나 라이선스 변경 뒤에 발생하는지는 모르겠습니다.
- @Shorden — /usr/bin에 가짜 shim을 두는 방식은 성가십니다. 실제로 유용한 시스템 유틸리티와 섞여 있고, Xcode나 Command Line Tools가 없으면 실행할 때 설치하라는 팝업이 뜹니다. 저는 시스템 경로보다 앞에 직접 만든 shim 디렉터리를 넣고, 해당 명령은 즉시 종료 코드 127을 반환하게 합니다.