Rust에서의 정적 링킹

손호준·2025년 5월 14일

정적 링크 vs 동적 링크

정적 링크(static linking)동적 링크(dynamic linking)는 프로그램이 외부 라이브러리(예: libc, libssl, libmecab 등)를 어떻게 포함하고 실행하느냐를 결정하는 방식.

1. 정적 링크 (Static Linking)

라이브러리의 코드가 실행 파일에 통째로 복사됨. 결과적으로 하나의 큰 독립 실행 파일이 생성됨

[main.rs] + [libssl.a] + [libc.a] → [big_binary]

2. 동적 링크 (Dynamic Linking)

실행 파일에는 라이브러리 이름과 위치 정보만 들어있고, 실행할 때(런타임에) .so 같은 공유 라이브러리 파일을 로딩함

[main.rs] → [libssl.so], [libc.so] (런타임에 필요)

비교

항목정적 링크동적 링크
포함 방식라이브러리 코드가 실행 파일에 포함됨실행 파일은 라이브러리를 참조만 함
의존성실행 시 외부 .so 파일 필요 없음.so 파일이 시스템에 있어야 함
파일 크기커짐 (라이브러리까지 포함하니까)작음 (라이브러리를 포함 안 하니까)
이식성높음낮음(해당 .so 파일이 없는 시스템에서는 실행 안 됨)
성능조금 빠름(라이브러리 직접 접근)조금 느림(런타임 로딩)
보안 업데이트 반영안됨(재빌드 필요)됨(라이브러리만 업데이트)

확인 방법

아래는 빌드된 바이너리가 어떤 라이브러리에 의존하는지 확인하는 명령어다.

ldd {대상 바이너리파일}

만약 정적 링크되었으면 출력은:

not a dynamic executable

동적 링크되어있다면 출력은:

linux-vdso.so.1 (0x00007ffd3d7ba000)
libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f3930473000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f3930453000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f393036c000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3930143000)
/lib64/ld-linux-x86-64.so.2 (0x00007f3930cb8000)

이런식으로 나온다.

Rust에서는?

Rust는 기본적으로 시스템 라이브러리(glibc 등)를 동적 링크하지만, 정적 링크 바이너리로도 빌드할 수 있다.

아래와 같이 환경 변수 설정을 해주거나 .cargo/config.toml에 해당 내용을 추가해준다.

환경변수 설정

RUSTFLAGS='-C target-feature=+crt-static'

.cargo/config.toml

[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "target-feature=+crt-static"]
  • target-feature=+crt-static: C runtime까지 정적 링크하겠다는 플래그

이제 musl을 타겟으로 지정해서 빌드하면 정적 링크 바이너리를 만들 수 있다.

musl 란?

musl은 C 언어 프로그램을 위한 가볍고 정적 링크가 가능한 C 표준 라이브러리. 일반적으로 리눅스에서 사용하는 C 라이브러리는 glibc인데, 이는 보통 동적 링크를 요구한다. musl을 쓰면 rust 바이너리를 빌드할 때 의존 .so파일 없이 정적 링크된 실행 파일을 만들 수 있다.

우선 musl-tools를 설치한다.

sudo apt update
sudo apt install musl-tools

이 패키지에는 musl-gcc, musl-g++, musl-ld 등이 포함되어 있다.

다음은, 아래 명령어로 x86_64-unknown-linux-musl 타겟용 Rust 표준 라이브러리를 툴체인에 추가한다. 이렇게 해야 Rust가 해당 타겟으로 크로스 컴파일할 수 있다.

rustup target add x86_64-unknown-linux-musl
  • x86_64 → 64비트 아키텍처
  • unknown-linux → 리눅스 OS
  • musl → musl libc를 사용하는 환경

musl을 타겟으로 지정하면 기본적으로 정적 링크를 시도하지만, 사용하는 라이브러리에 따라 일부 동적 링크가 남을 수도 있다. 특히 C++ 라이브러리나 외부 바이너리 blob을 사용하는 경우 주의가 필요하다.

musl을 타겟으로 지정해서 빌드하면 정적 링크 바이너리가 만들어진다.

cargo build --release --target x86_64-unknown-linux-musl

빌드에 성공했다면, 러스트 프로젝트의 target/{타겟_이름}/release/ 밑에 바이너리 파일이 생성된다. 이렇게 정적 링크된 바이너리는, OS 커널, CPU 아키텍처, 실행 권한 등만 맞으면 대부분 실행된다. 하지만 보안 설정에 의해 막히거나, 커널 버전이 너무 오래되어 일부 시스템 콜이 없을 가능성이 있기 때문에, 무조건 보장되는건 아니다.

일반적으로 타겟을 명시하지 않고 build 하면, 현재 시스템의 플랫폼을 타겟으로 자동 설정한다.
예를 들어, x86_64 리눅스(glibc 기반) 환경에서 작업하고 있다면 아래 두 명령어는 같은 의미가 된다:

cargo build
cargo build --target x86_64-unknown-linux-gnu

기본 타겟 확인법:

rustc -vV

host 항목을 확인하면된다.

glibc vs musl의 차이?

  • glibc는 대부분 리눅스 배포판에서 사용하는 표준 C 라이브러리
  • musl은 더 가볍고 정적 링크에 더 적합한 C 라이브러리
  • 그래서 musl로 빌드한 바이너리는 glibc 버전 호환 걱정 없이 실행 가능

⚠️ 주의: libstdc++와 같은 C++ 표준 라이브러리의 경우

libstdc++.a는 내부적으로 glibc의 내부 구현(__wmemcpy_chk 등)에 의존하는 경우가 많으며, 이는 musl이나 다른 libc에서 제대로 작동하지 않을 수 있다.
이들은 glibc의 내부 구현에 따라 달라지기 때문에 정적 링크가 매우 까다롭고 종종 깨진다.
따라서 glibc를 정적으로 링크하는 건 공식적으로 권장되지 않으며, 대부분의 리눅스 배포판에서도 정적으로 링크된 glibc는 지원하지 않는다.

결론

프로젝트가 순수 Rust 또는 Rust + C 라이브러리 기반이라면 musl을 통해 비교적 쉽게 정적 링크가 가능하다.
그러나 C++ 라이브러리(libstdc++ 등)가 포함된다면, 정적 링크는 매우 어렵고 비권장된다. 이 경우에는 Docker, Nix, AppImage 같은 패키징 툴을 사용하는 것이 훨씬 현실적인 접근이다.
(애초에 libstdc++, glibc는 동적 링크를 전제로 설계되어 있다.)

profile
Rustacean🦀

0개의 댓글