Lasiyan
Tech

Linux Clang에서 GCC가 필요한 이유

#Linux#Clang#GCC#크로스 컴파일#JetPack

Linux Clang에서 GCC가 필요한 이유

Clang으로 빌드하는데 왜 GCC, libstdc++, libgcc, crtbegin.o, --gcc-toolchain 같은 이름이 계속 등장할까?

이 글은 Ubuntu 호스트에서 NVIDIA JetPack 5.1.2 기반 aarch64 타겟을 크로스컴파일하면서 정리한 내용이다. 특히 “Clang을 쓰는데 왜 GCC가 필요한가?”라는 질문을 중심으로 설명한다.

먼저 분명히 해야 할 점이 있다. Clang을 사용한다고 해서 GCC 컴파일러가 실행되는 것은 아니다. Clang은 여전히 LLVM 백엔드를 사용해 코드를 생성한다.

다만 Linux 환경에서는 GCC가 제공하는 런타임 부품과 표준 라이브러리 생태계를 함께 사용하는 경우가 많다. 이 때문에 Clang을 사용하더라도 GCC 관련 파일과 옵션이 계속 등장한다.


1. 한 줄 결론

Clang으로 컴파일하더라도, Linux 환경에서는 GCC/libstdc++ 생태계의 부품을 함께 사용하는 경우가 많다.

--gcc-toolchain은 GCC 백엔드를 선택하는 옵션이 아니라, Clang에게 GCC 설치 트리의 위치를 알려주는 옵션이다.

즉, Clang을 사용한다고 해서 GCC와 완전히 분리되는 것은 아니다. Linux 배포판, 특히 Ubuntu나 JetPack 같은 환경은 기본적으로 GCC와 libstdc++ 생태계를 중심으로 구성되어 있다.

따라서 Clang도 이 환경에서 자연스럽게 GCC가 제공하는 런타임 부품들을 찾아서 사용한다.


2. 등장인물 3가지

C++ 프로그램을 만드는 과정을 요리에 비유하면 이해하기 쉽다.

역할비유예시
컴파일러요리사clang++-18, g++
C++ 표준 라이브러리식재료 창고libstdc++, libc++
CRT / 런타임 지원 파일요리를 시작하고 끝내기 위한 기본 도구crtbegin.o, crtend.o, libgcc

2-1. 컴파일러는 소스코드를 오브젝트 코드로 바꾼다

컴파일러는 C/C++ 소스코드를 읽고 타겟 CPU가 실행할 수 있는 오브젝트 코드나 실행파일을 만드는 도구이다.

GCC   = GCC 프론트엔드 + GCC 백엔드
Clang = Clang 프론트엔드 + LLVM 백엔드

GCC와 Clang은 같은 C/C++ 코드를 같은 타겟용 실행파일로 만들 수 있다. 다만 생성되는 기계어 코드, 최적화 결과, 경고 메시지, 기본 옵션, 디버그 정보 등은 서로 다를 수 있다.

따라서 “결과물이 항상 동일하다”고 말하기보다는 “같은 타겟용 프로그램을 만들 수 있다”고 이해하는 편이 정확하다.

2-2. C++ 표준 라이브러리는 실제 구현체이다

C++에서 std::string, std::vector, std::map 같은 기능은 컴파일러 자체에 전부 들어 있는 것이 아니다. 이 기능들의 실제 구현은 C++ 표준 라이브러리가 제공한다.

libstdc++ = GCC 프로젝트가 제공하는 C++ 표준 라이브러리
libc++    = LLVM 프로젝트가 제공하는 C++ 표준 라이브러리

중요한 점은 컴파일러와 C++ 표준 라이브러리가 서로 독립적인 축이라는 점이다. Clang을 사용한다고 해서 반드시 libc++를 써야 하는 것은 아니다.

Linux에서는 Clang을 사용하더라도 기본적으로 libstdc++를 사용하는 경우가 많다.

2-3. CRT와 libgcc는 저수준 런타임 부품이다

C/C++ 프로그램은 main() 함수에서 바로 시작하는 것처럼 보이지만, 실제로는 그 전에 런타임 초기화 코드가 먼저 실행된다. 이때 필요한 파일들이 CRT 파일이다.

crtbegin.o
crtend.o
crti.o
crtn.o
libgcc
libgcc_s

이 파일들은 보통 GCC 패키지 또는 GCC toolchain 안에 들어 있다. Clang으로 컴파일하더라도 Linux 환경에서는 이 부품들을 그대로 사용하는 경우가 많다.


3. 흔한 오해 1: Clang을 쓰면 GCC는 필요 없다

Clang을 사용하면 GCC 컴파일러 프로세스가 실행되지 않는 것은 맞다. 예를 들어 Clang으로 C++ 파일을 컴파일할 때 GCC의 cc1plus가 실행되는 것은 아니다.

Clang은 LLVM 백엔드를 사용해 코드를 생성한다. 하지만 이것이 GCC 생태계의 모든 부품이 필요 없다는 뜻은 아니다.

Linux에서는 Clang이 GCC 설치 트리 안의 CRT 파일, libgcc, libstdc++ 헤더와 라이브러리 경로를 참고하는 경우가 많다.

Clang을 사용한다는 것은 GCC 컴파일러를 실행하지 않는다는 뜻이다.
그러나 GCC가 제공하는 런타임 부품까지 전혀 사용하지 않는다는 뜻은 아니다.


4. 흔한 오해 2: LLVM이면 libc++, GCC면 libstdc++이다

이것도 자주 발생하는 오해이다. 컴파일러와 C++ 표준 라이브러리는 서로 다른 선택지이다.

컴파일러C++ 표준 라이브러리가능 여부
GCClibstdc++Linux 기본 조합
GCClibc++가능은 하지만 일반적이지 않음
Clanglibstdc++Linux에서 매우 흔한 조합
Clanglibc++가능한 조합

Linux에서 Clang을 사용하면 기본적으로 libstdc++를 링크하는 경우가 많다. libc++를 사용하려면 일반적으로 다음과 같이 명시해야 한다.

clang++ main.cpp -stdlib=libc++

반대로 아무 옵션 없이 Clang을 사용하면 배포판 기본 설정에 따라 libstdc++를 사용하는 경우가 많다.

clang++ hello.cpp -o hello
ldd hello

# 예시 출력
# libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6

따라서 “Clang = libc++”라고 이해하면 안 된다. Clang은 컴파일러이고, libc++와 libstdc++는 C++ 표준 라이브러리이다.


5. 흔한 오해 3: —gcc-toolchain은 GCC 백엔드를 선택하는 옵션이다

--gcc-toolchain이라는 이름 때문에 이 옵션이 GCC 백엔드를 선택하는 것처럼 보일 수 있다. 그러나 실제 의미는 다르다.

--gcc-toolchain=/some/path

이 옵션은 Clang에게 GCC 설치 트리의 기준 위치를 알려준다. Clang은 그 아래에서 타겟 triple과 GCC 버전에 맞는 include, lib, CRT, libgcc, libstdc++ 관련 경로를 찾는다.

즉, 다음 설명은 틀렸다.

--gcc-toolchain = GCC 백엔드 선택
--gcc-toolchain = GCC 컴파일러 실행
--gcc-toolchain = 무조건 libstdc++ 선택

더 정확한 설명은 다음과 같다.

--gcc-toolchain = Clang이 참고할 GCC 설치 트리 위치 지정

다만 --gcc-toolchain은 표준 라이브러리 종류를 선택하는 옵션은 아니다. libstdc++를 쓸지 libc++를 쓸지는 기본적으로 -stdlib= 옵션의 역할이다.

하지만 Linux에서 libstdc++를 사용하는 경우, --gcc-toolchain으로 지정한 GCC 설치 트리가 libstdc++ 헤더와 라이브러리 탐색에 영향을 줄 수 있다.


6. —sysroot와 —gcc-toolchain은 역할이 다르다

크로스컴파일에서 자주 함께 등장하는 옵션이 --sysroot--gcc-toolchain이다. 둘은 비슷해 보이지만 역할이 다르다.

옵션의미
--sysroot타겟 root filesystem의 기준 경로를 지정한다.
--gcc-toolchainGCC 설치 트리의 기준 경로를 지정한다.

예를 들어 JetPack rootfs가 다음 위치에 있다고 가정한다.

/mnt/jetpack5.1.2

이 경로 안에는 타겟 보드에서 사용하는 헤더와 라이브러리가 들어 있다. 따라서 Clang에게 다음 정보를 알려줘야 한다.

--target=aarch64-linux-gnu
--sysroot=/mnt/jetpack5.1.2

그러나 이것만으로 항상 충분하지는 않다. Clang이 GCC toolchain 레이아웃, multilib 구조, CRT 파일, libgcc 위치를 자동으로 정확히 찾지 못할 수 있기 때문이다.

이때 --gcc-toolchain을 함께 사용한다.

--gcc-toolchain=/mnt/jetpack5.1.2/usr

즉, --sysroot는 타겟 rootfs의 기준점을 알려주는 옵션이고, --gcc-toolchain은 GCC 설치 트리의 기준점을 알려주는 옵션이다. 둘은 서로 대체 관계가 아니라 보완 관계에 가깝다.


7. 네이티브 빌드에서는 왜 별도 설정이 거의 필요 없는가

Ubuntu x86_64 호스트에서 네이티브 빌드를 할 때는 보통 다음 명령만으로도 빌드가 된다.

clang++ hello.cpp -o hello

이 경우 Clang은 호스트 시스템의 표준 경로를 탐색한다. 예를 들어 x86_64 Ubuntu에서는 다음과 같은 경로를 자동으로 찾을 수 있다.

/usr/lib/gcc/x86_64-linux-gnu/
/usr/include/c++/...

호스트에 이미 GCC, libstdc++, CRT 파일이 설치되어 있기 때문이다. 따라서 네이티브 빌드에서는 Clang이 필요한 부품을 비교적 쉽게 찾는다.


8. 크로스컴파일에서는 왜 문제가 생기는가

크로스컴파일에서는 상황이 달라진다. 예를 들어 x86_64 Ubuntu 호스트에서 aarch64 Jetson 타겟용 프로그램을 빌드한다고 가정한다.

clang++ --target=aarch64-linux-gnu hello.cpp

이 명령만 사용하면 Clang은 aarch64용 코드를 만들려고 한다. 하지만 aarch64용 헤더, 라이브러리, CRT 파일, libgcc, libstdc++ 위치를 제대로 찾지 못할 수 있다.

이때 다음과 같은 에러가 발생할 수 있다.

clang: error: cannot find crtbegin.o
clang: error: unable to find library -lgcc
clang: error: unable to find library -lstdc++

이런 경우 Clang에게 다음 정보를 명시적으로 알려줘야 한다.

--target=aarch64-linux-gnu
--sysroot=/mnt/jetpack5.1.2
--gcc-toolchain=/mnt/jetpack5.1.2/usr

핵심은 Clang이 “aarch64 코드를 만들어라”라는 지시뿐 아니라, “aarch64용 헤더와 라이브러리는 여기 있고, GCC toolchain 부품은 여기 있다”라는 정보까지 필요로 한다는 점이다.


9. JetPack/Ubuntu 환경에서 libc++로 바꾸기 어려운 이유

이론적으로 Clang은 libc++를 사용할 수 있다. 하지만 JetPack이나 Ubuntu rootfs 기반 임베디드 환경에서는 libstdc++를 유지하는 것이 일반적으로 안전하다.

9-1. 배포판 패키지가 libstdc++ 생태계를 전제로 한다

Ubuntu와 JetPack의 C++ 패키지들은 대체로 배포판 기본 C++ ABI를 전제로 빌드된다. 그 기본값은 일반적으로 libstdc++이다.

예를 들어 apt로 설치한 C++ 라이브러리가 내부적으로 libstdc++에 의존하고 있을 수 있다. 이때 내 애플리케이션만 libc++로 빌드하면 C++ ABI 경계에서 문제가 생길 수 있다.

단, 모든 라이브러리가 같은 방식으로 문제가 되는 것은 아니다. GStreamer나 FFmpeg처럼 C API 중심으로 사용하는 라이브러리는 C++ 표준 라이브러리 ABI 문제가 직접 드러나지 않을 수도 있다.

문제가 되는 핵심 상황은 라이브러리 경계를 넘어 C++ 타입을 주고받는 경우이다.

9-2. libstdc++와 libc++의 C++ 타입 ABI는 호환된다고 가정하면 안 된다

예를 들어 std::string, std::vector, 예외 객체, RTTI, iostream, STL 컨테이너 같은 C++ 요소를 라이브러리 경계에서 주고받는 경우를 생각할 수 있다.

libstdc++와 libc++는 서로 다른 C++ 표준 라이브러리 구현체이다. 동일한 C++ 표준 인터페이스를 제공하더라도 내부 구현과 ABI는 다를 수 있다.

따라서 한쪽은 libstdc++로 빌드하고 다른 한쪽은 libc++로 빌드한 뒤 C++ 객체를 직접 주고받으면 안전하지 않다.

주의:
std::string의 실제 내부 구조는 구현체, 버전, 아키텍처, ABI 설정에 따라 달라진다. 핵심은 libstdc++와 libc++의 내부 메모리 레이아웃과 ABI가 같다고 가정하면 안 된다는 점이다.

이런 상황에서는 링크 오류, 런타임 크래시, 메모리 손상, undefined behavior가 발생할 수 있다. 운 좋게 당장 문제가 드러나지 않더라도 안전한 상태라고 볼 수 없다.

9-3. NVIDIA 바이너리 드라이버와 폐쇄형 라이브러리는 재빌드할 수 없다

Jetson 환경에서는 NVIDIA가 제공하는 바이너리 드라이버와 폐쇄형 라이브러리를 함께 사용한다. 이런 라이브러리는 사용자가 libc++ 기준으로 다시 빌드할 수 없다.

따라서 전체 rootfs와 모든 의존성을 libc++ 기준으로 통제할 수 있는 환경이 아니라면, JetPack 기반 임베디드 Linux에서는 libstdc++를 유지하는 것이 현실적인 선택이다.

JetPack/Ubuntu rootfs 기반 임베디드 Linux에서는 libstdc++를 사용하는 것이 일반적으로 가장 안전하다.
libc++ 사용이 이론적으로 불가능한 것은 아니지만, 전체 의존성을 통제하지 못하는 환경에서는 실무적으로 위험하다.


10. 실전 CMake toolchain 파일 예시

다음은 JetPack 5.1.2 rootfs가 /mnt/jetpack5.1.2에 마운트되어 있다고 가정한 CMake toolchain 파일 예시이다.

cmake_minimum_required(VERSION 3.19)

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)

# 타겟 보드의 rootfs 마운트 경로
set(CMAKE_SYSROOT /mnt/jetpack5.1.2)

# Clang을 컴파일러로 사용한다.
set(CMAKE_C_COMPILER   clang-18)
set(CMAKE_CXX_COMPILER clang++-18)

# Clang에게 aarch64 Linux 타겟용 코드를 생성하라고 알려준다.
set(CMAKE_C_COMPILER_TARGET   aarch64-linux-gnu)
set(CMAKE_CXX_COMPILER_TARGET aarch64-linux-gnu)

# Clang에게 GCC toolchain 설치 트리 위치를 알려준다.
# 이 경로 아래에서 CRT, libgcc, libstdc++ 관련 파일을 찾는다.
string(APPEND CMAKE_C_FLAGS_INIT
    " --gcc-toolchain=/mnt/jetpack5.1.2/usr -Wno-deprecated-declarations")

string(APPEND CMAKE_CXX_FLAGS_INIT
    " --gcc-toolchain=/mnt/jetpack5.1.2/usr -Wno-deprecated-declarations")

# lld 링커를 사용한다.
# 환경에 따라 -fuse-ld=lld-18 대신 -fuse-ld=lld가 더 안전할 수 있다.
string(APPEND CMAKE_EXE_LINKER_FLAGS_INIT " -fuse-ld=lld")
string(APPEND CMAKE_SHARED_LINKER_FLAGS_INIT " -fuse-ld=lld")

# CMake의 라이브러리, 헤더, 패키지 탐색을 타겟 sysroot 기준으로 제한한다.
set(CMAKE_FIND_ROOT_PATH /mnt/jetpack5.1.2)

# 실행 프로그램은 보통 호스트의 것을 찾아야 한다.
# 예: protoc, python, code generator 등
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)

# 라이브러리, 헤더, 패키지는 타겟 sysroot 안에서 찾는다.
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

이 파일의 핵심은 Clang에게 다음 정보를 모두 알려주는 것이다.

설정의미
CMAKE_C_COMPILERC 컴파일러로 Clang을 사용한다.
CMAKE_CXX_COMPILERC++ 컴파일러로 Clang++을 사용한다.
CMAKE_*_COMPILER_TARGETaarch64 Linux용 코드를 생성한다.
CMAKE_SYSROOT타겟 rootfs의 기준 경로를 지정한다.
--gcc-toolchainGCC 설치 트리 기준 경로를 지정한다.
-fuse-ld=lld링커로 lld를 사용한다.
CMAKE_FIND_ROOT_PATH_MODE_*CMake가 호스트 라이브러리와 타겟 라이브러리를 섞어 잡지 않도록 제어한다.

11. 전체 구조 다시 정리

Linux 크로스컴파일을 할 때는 다음 요소들이 함께 맞아야 한다.

컴파일러:
  Clang

코드 생성 백엔드:
  LLVM

타겟:
  aarch64-linux-gnu

타겟 rootfs:
  /mnt/jetpack5.1.2

C++ 표준 라이브러리:
  libstdc++

CRT / libgcc:
  GCC toolchain이 제공

링커:
  lld 또는 GNU ld

Clang은 컴파일러이다.
LLVM은 Clang이 사용하는 코드 생성 백엔드이다.
libstdc++는 C++ 표준 라이브러리이다.
GCC toolchain은 Clang이 참고할 수 있는 CRT, libgcc, libstdc++ 관련 부품을 제공한다.
--gcc-toolchain은 GCC 백엔드를 고르는 옵션이 아니라, 그 부품들이 들어 있는 GCC 설치 트리의 위치를 알려주는 옵션이다.


12. 핵심 요약

  • Clang을 사용해도 Linux에서는 libstdc++를 사용하는 경우가 많다.
  • Clang을 사용한다고 해서 GCC 컴파일러가 실행되는 것은 아니다.
  • 하지만 Clang은 GCC toolchain 안의 CRT, libgcc, libstdc++ 관련 파일을 참고할 수 있다.
  • --gcc-toolchain은 GCC 백엔드를 선택하는 옵션이 아니다.
  • --gcc-toolchain은 Clang에게 GCC 설치 트리의 위치를 알려주는 옵션이다.
  • --sysroot는 타겟 rootfs의 기준 경로를 지정한다.
  • JetPack/Ubuntu rootfs 기반 임베디드 Linux에서는 libstdc++를 유지하는 것이 일반적으로 안전하다.
  • libc++ 사용은 가능하지만, 전체 의존성을 libc++ 기준으로 통제하지 못하면 C++ ABI 문제가 발생할 수 있다.

마무리

Clang 크로스컴파일을 이해할 때 가장 중요한 점은 컴파일러, 표준 라이브러리, 런타임 부품, sysroot를 서로 구분하는 것이다.

Clang은 GCC를 대체하는 컴파일러이지만, Linux 환경에서는 GCC 생태계가 제공하는 여러 부품을 함께 사용하는 경우가 많다. 이 때문에 Clang을 쓰면서도 libstdc++, libgcc, crtbegin.o, --gcc-toolchain 같은 이름이 계속 등장한다.

이 구조를 이해하면 CMake toolchain 파일을 볼 때도 훨씬 덜 헷갈린다.

Clang이 코드를 만든다.
타겟 rootfs에서 헤더와 라이브러리를 찾는다.
GCC toolchain에서 CRT와 런타임 부품을 찾는다.
그리고 이 모든 경로를 명시적으로 알려주는 것이 크로스컴파일 toolchain 설정이다.