Skip to content

라이브러리

리눅스에서 프로그램이 실행될 때, 대부분의 경우 외부 라이브러리의 코드를 필요로 한다. 이 라이브러리들이 프로그램에 어떻게 연결되고, 시스템이 어디서 라이브러리를 찾는지 이해하는 것은 리눅스 시스템 관리와 개발에서 핵심적인 지식이다.

프로그램이 외부 함수를 호출할 때, 그 함수의 실제 코드가 어디에 있는지 연결해주는 과정을 링킹(Linking)이라 한다. 링킹은 두 가지 방식으로 수행된다.

정적 링킹 (Static Linking)

컴파일 시점에 라이브러리 코드를 실행 파일에 직접 복사한다.

Terminal window
# 정적 라이브러리 생성
gcc -c mylib.c -o mylib.o
ar rcs libmylib.a mylib.o
# 정적 링킹으로 컴파일
gcc main.c -L. -lmylib -static -o main
  • 장점: 실행 시 외부 의존성 없음, 배포가 단순함
  • 단점: 실행 파일 크기 증가, 라이브러리 업데이트 시 재컴파일 필요, 메모리 낭비 (동일 코드가 여러 프로세스에 중복 적재)

동적 링킹 (Dynamic Linking)

실행 시점에 라이브러리를 메모리에 로드하고 연결한다.

Terminal window
# 공유 라이브러리 생성
gcc -c -fPIC mylib.c -o mylib.o
gcc -shared -o libmylib.so mylib.o
# 동적 링킹으로 컴파일
gcc main.c -L. -lmylib -o main
  • 장점: 실행 파일 크기 감소, 라이브러리 공유로 메모리 효율적, 라이브러리 업데이트 용이
  • 단점: 실행 시 라이브러리 필요, 버전 호환성 문제 발생 가능

공유 라이브러리 (.so 파일)

공유 라이브러리(Shared Object)는 .so 확장자를 가진 파일로, 여러 프로그램이 동시에 사용할 수 있는 코드 모음이다. Windows의 DLL(Dynamic Link Library)과 유사한 개념이다.

공유 라이브러리의 구조

공유 라이브러리는 ELF 형식을 따르며, Position Independent Code(PIC)로 컴파일된다.

Terminal window
$ file /lib/x86_64-linux-gnu/libc.so.6
/lib/x86_64-linux-gnu/libc.so.6: ELF 64-bit LSB shared object, x86-64, ...

PIC (Position Independent Code)란?

메모리의 어느 위치에 로드되더라도 수정 없이 실행될 수 있는 코드이다. 이를 위해 상대 주소 지정과 GOT(Global Offset Table), PLT(Procedure Linkage Table)를 사용한다.

Terminal window
# PIC 옵션으로 컴파일
gcc -c -fPIC source.c -o source.o

-fPIC 플래그는 컴파일러에게 위치 독립적 코드를 생성하도록 지시한다. 이 코드는 GOT를 통해 전역 변수에 접근하고, PLT를 통해 외부 함수를 호출한다.

라이브러리 명명 규칙

공유 라이브러리는 세 가지 이름을 가진다.

실제 파일명 (Real Name): libfoo.so.1.2.3
soname: libfoo.so.1
링커 이름 (Linker Name): libfoo.so
  • Real Name: 실제 파일명으로 전체 버전 정보 포함 (major.minor.patch)
  • soname: 라이브러리의 ABI 호환성을 나타내는 이름. major 버전만 포함
  • Linker Name: 컴파일 시 -l 옵션으로 사용하는 이름
Terminal window
$ ls -la /lib/x86_64-linux-gnu/libz
lrwxrwxrwx 1 root root 13 libz.so - >libz.so.1.2.11
lrwxrwxrwx 1 root root 13 libz.so.1 - >libz.so.1.2.11
-rw-r--r-- 1 root root 116960 libz.so.1.2.11

soname의 중요성

soname은 ABI(Application Binary Interface) 호환성의 핵심이다.

Terminal window
# 라이브러리의 soname 확인
$ readelf -d /lib/x86_64-linux-gnu/libc.so.6 | grep SONAME
0x000000000000000e (SONAME) Library soname: [libc.so.6]

프로그램이 컴파일될 때, 링커는 라이브러리의 soname을 실행 파일에 기록한다. 실행 시 동적 링커는 이 soname을 사용해 라이브러리를 찾는다.

Terminal window
# 실행 파일이 요구하는 라이브러리 확인
$ readelf -d /bin/ls | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libselinux.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]

major 버전(soname)이 같으면 하위 호환성이 보장되므로, libfoo.so.1.2.3에서 libfoo.so.1.3.0으로 업그레이드해도 기존 프로그램은 정상 작동한다.

동적 링커/로더 (ld-linux.so)

동적 링커(Dynamic Linker), 또는 런타임 링커(Runtime Linker), 동적 로더(Dynamic Loader)라고 불리는 이 프로그램은 실행 파일을 메모리에 로드하고 필요한 공유 라이브러리를 연결한다.

Terminal window
$ ls -la /lib64/ld-linux-x86-64.so.2
lrwxrwxrwx 1 root root 32 /lib64/ld-linux-x86-64.so.2 - >/lib/x86_64-linux-gnu/ld-2.31.so

동작 과정

  1. 프로그램 시작: 커널이 ELF 헤더의 .interp 섹션을 읽어 동적 링커 경로 확인
  2. 동적 링커 로드: 커널이 동적 링커를 메모리에 로드
  3. 의존성 분석: .dynamic 섹션의 DT_NEEDED 태그로 필요한 라이브러리 파악
  4. 라이브러리 검색: 정해진 순서대로 라이브러리 파일 탐색
  5. 라이브러리 로드: 찾은 라이브러리를 메모리에 매핑
  6. 심볼 해석: GOT와 PLT를 사용해 함수 주소 연결
  7. 초기화 실행: 라이브러리의 초기화 함수 실행
  8. 제어권 전달: 프로그램의 _start로 제어권 전달
Terminal window
# 프로그램의 인터프리터 확인
$ readelf -l /bin/ls | grep interpreter
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

Lazy Binding과 PLT/GOT

기본적으로 동적 링커는 lazy binding을 사용한다. 함수가 처음 호출될 때 실제 주소를 해석한다.

프로그램 → PLT entry → GOT entry → (첫 호출 시) 동적 링커 → 실제 함수 주소
(이후 호출 시) 직접 점프
  • PLT (Procedure Linkage Table): 외부 함수 호출을 위한 트램펄린 코드
  • GOT (Global Offset Table): 실제 함수/변수 주소를 저장하는 테이블
Terminal window
# GOT/PLT 섹션 확인
$ readelf -S /bin/ls | grep -E '\.got|\.plt'
[12] .plt PROGBITS 0000000000004000 00004000
[13] .plt.got PROGBITS 0000000000004020 00004020
[23] .got PROGBITS 000000000001ffe8 0000ffe8
[24] .got.plt PROGBITS 0000000000020000 00010000

라이브러리 검색 순서

동적 링커가 라이브러리를 찾는 순서는 다음과 같다.

  • DT_RPATH (Deprecated): ELF 바이너리의 .dynamic 섹션에 기록된 경로이다. DT_RUNPATH가 없을 때만 적용된다.

    Terminal window
    # RPATH 확인
    $ readelf -d ./myprogram | grep RPATH
    0x000000000000000f (RPATH) Library rpath: [/opt/mylibs]
  • LD_LIBRARY_PATH 환경 변수: 사용자가 지정한 라이브러리 검색 경로이다. 콜론(:)으로 여러 경로를 구분한다. DT_RUNPATH가 설정된 경우에도 LD_LIBRARY_PATH가 먼저 검색된다.

    Terminal window
    export LD_LIBRARY_PATH=/custom/lib:/another/lib:$LD_LIBRARY_PATH
  • DT_RUNPATH: RPATH의 현대적 대안이다. LD_LIBRARY_PATH 다음에 검색된다. $ORIGIN은 실행 파일이 위치한 디렉토리를 나타내는 특수 토큰이다.

    Terminal window
    # RUNPATH로 컴파일
    gcc main.c -L. -lmylib -Wl,-rpath,'$ORIGIN/../lib' -o main
    # RUNPATH 확인
    $ readelf -d ./myprogram | grep RUNPATH
    0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN/../lib]
  • /etc/ld.so.cache: ldconfig가 생성한 캐시 파일이다. /etc/ld.so.conf에 지정된 경로의 라이브러리 정보를 빠르게 조회할 수 있도록 바이너리 형태로 저장한다.

    Terminal window
    # 캐시 내용 확인
    $ ldconfig -p | head -20
    1800 libs found in cache `/etc/ld.so.cache'
    libz.so.1 (libc6,x86-64) => /lib/x86_64-linux-gnu/libz.so.1
    libyaml-0.so.2 (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libyaml-0.so.2
    ...
  • 기본 경로: 캐시에서 찾지 못하면 /lib, /usr/lib, /lib64, /usr/lib64 등 기본 시스템 경로를 검색한다.

요약하면 다음과 같다

1. DT_RPATH (DT_RUNPATH 없을 때만)
2. LD_LIBRARY_PATH
3. DT_RUNPATH
4. /etc/ld.so.cache (ldconfig 캐시)
5. 기본 경로 (/lib, /usr/lib, ...)

LD_LIBRARY_PATH 환경 변수

LD_LIBRARY_PATH는 동적 링커가 공유 라이브러리를 찾을 추가 경로를 지정한다.

기본 사용법

Terminal window
# 단일 경로
export LD_LIBRARY_PATH=/opt/myapp/lib
# 여러 경로 (콜론으로 구분)
export LD_LIBRARY_PATH=/opt/myapp/lib:/home/user/lib
# 기존 경로 유지하면서 추가
export LD_LIBRARY_PATH=/new/path:$LD_LIBRARY_PATH
# 일회성 실행
LD_LIBRARY_PATH=/opt/myapp/lib ./myprogram

주의사항과 모범 사례

보안 고려사항

Terminal window
# setuid/setgid 프로그램에서는 LD_LIBRARY_PATH가 무시됨
$ ls -l /usr/bin/sudo
-rwsr-xr-x 1 root root 166056 /usr/bin/sudo
# 's' 플래그는 setuid를 의미, 보안상 LD_LIBRARY_PATH 무시

프로덕션 환경에서의 사용

프로덕션 환경에서 LD_LIBRARY_PATH를 전역으로 설정하는 것은 권장되지 않는다.

Terminal window
# 나쁜 예: .bashrc에 전역 설정
# export LD_LIBRARY_PATH=/custom/lib:$LD_LIBRARY_PATH
# 좋은 예: 래퍼 스크립트 사용
#!/bin/bash
# /opt/myapp/bin/myapp-wrapper
export LD_LIBRARY_PATH=/opt/myapp/lib
exec /opt/myapp/bin/myapp "$@"

더 좋은 방법은 RPATH/RUNPATH를 사용하거나 ldconfig로 시스템에 등록하는 것이다.

RPATH와 RUNPATH

실행 파일에 라이브러리 검색 경로를 직접 내장할 수 있다.

RPATH vs RUNPATH

특성RPATHRUNPATH
검색 우선순위LD_LIBRARY_PATH보다 먼저LD_LIBRARY_PATH보다 나중
상속의존 라이브러리에도 적용직접 의존성에만 적용
현대적 사용Deprecated권장

설정 방법

Terminal window
# RPATH 설정 (구식, 권장하지 않음)
gcc main.c -L. -lmylib -Wl,-rpath,/opt/mylibs -o main
# RUNPATH 설정 (권장)
gcc main.c -L. -lmylib -Wl,-rpath,/opt/mylibs -Wl,--enable-new-dtags -o main
# 또는 환경 변수 사용 (GCC)
gcc main.c -L. -lmylib -Wl,-rpath,'$ORIGIN/../lib' -o main

특수 토큰

Terminal window
$ORIGIN # 실행 파일이 위치한 디렉토리
$LIB # 시스템에 따라 lib 또는 lib64
$PLATFORM # 플랫폼 이름 (예: x86_64)
Terminal window
# 실제 예시: 실행 파일 기준 상대 경로
gcc main.c -L. -lmylib -Wl,-rpath,'$ORIGIN/../lib' -o bin/main
# 디렉토리 구조
myapp/
├── bin/
└── main # RUNPATH: $ORIGIN/../lib
└── lib/
└── libmylib.so

RPATH/RUNPATH 확인 및 수정

Terminal window
# 확인
$ readelf -d ./main | grep -E 'RPATH|RUNPATH'
# patchelf로 수정
$ patchelf --set-rpath '/new/path:$ORIGIN/../lib' ./main
# chrpath로 수정 (제한적)
$ chrpath -r '/new/path' ./main

ldconfig와 시스템 라이브러리 관리

ldconfig는 시스템의 공유 라이브러리 캐시를 관리하는 도구이다.

/etc/ld.so.conf

라이브러리 검색 경로를 정의하는 설정 파일이다.

Terminal window
$ cat /etc/ld.so.conf
include /etc/ld.so.conf.d/.conf
$ ls /etc/ld.so.conf.d/
libc.conf x86_64-linux-gnu.conf ...
$ cat /etc/ld.so.conf.d/libc.conf
# libc default configuration
/usr/local/lib

ldconfig 사용법

Terminal window
# 캐시 업데이트 (새 라이브러리 설치 후)
sudo ldconfig
# verbose 모드로 업데이트
sudo ldconfig -v
# 캐시 내용 출력
ldconfig -p
# 특정 라이브러리 검색
ldconfig -p | grep libssl
# 캐시 업데이트 없이 테스트
sudo ldconfig -N -v

새 라이브러리 시스템에 등록

Terminal window
# 방법 1: 설정 파일 추가
echo "/opt/myapp/lib" | sudo tee /etc/ld.so.conf.d/myapp.conf
sudo ldconfig
# 방법 2: 표준 경로에 심볼릭 링크
sudo ln -s /opt/myapp/lib/libfoo.so.1 /usr/lib/
sudo ldconfig

관련 명령어

ldd (의존성 확인)

Terminal window
$ ldd /bin/ls
linux-vdso.so.1 (0x00007ffd1a5fe000)
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f2b7c800000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2b7c400000)
...
/lib64/ld-linux-x86-64.so.2 (0x00007f2b7ca00000)

출력 해석:

  • linux-vdso.so.1: 커널이 제공하는 가상 동적 공유 객체
  • => 왼쪽: 프로그램이 요청한 라이브러리
  • => 오른쪽: 실제 파일 경로 (또는 “not found”)
  • 괄호 안 주소: 메모리 로드 주소
Terminal window
# 재귀적으로 모든 의존성 확인
ldd -r /bin/ls
# 누락된 함수 확인
ldd -d /bin/ls

주의: ldd는 실행 파일을 실제로 로드하므로, 신뢰할 수 없는 바이너리에는 사용하지 않는다.

readelf (ELF 정보 확인)

Terminal window
# 동적 섹션 정보
readelf -d /bin/ls
# 필요한 라이브러리만 확인
readelf -d /bin/ls | grep NEEDED
# RPATH/RUNPATH 확인
readelf -d ./myprogram | grep -E 'RPATH|RUNPATH'
# 동적 심볼 테이블
readelf --dyn-syms /lib/x86_64-linux-gnu/libc.so.6

objdump - 바이너리 분석

Terminal window
# 동적 재배치 정보
objdump -R /bin/ls
# 디스어셈블리
objdump -d /bin/ls
# 동적 심볼
objdump -T /lib/x86_64-linux-gnu/libc.so.6 | head

nm - 심볼 테이블 확인

Terminal window
# 정적 심볼 테이블
nm /lib/x86_64-linux-gnu/libc.a
# 동적 심볼 테이블
nm -D /lib/x86_64-linux-gnu/libc.so.6
# 정의되지 않은 심볼 (외부 의존성)
nm -u ./myprogram

심볼 타입:

  • T: 텍스트(코드) 섹션의 전역 심볼
  • U: 미정의 심볼 (외부에서 가져와야 함)
  • D: 초기화된 데이터 섹션
  • B: BSS 섹션 (초기화되지 않은 데이터)

LD_DEBUG - 동적 링커 디버깅

동적 링커의 동작을 상세히 추적할 수 있다.

Terminal window
# 사용 가능한 옵션 확인
LD_DEBUG=help /bin/ls
# 라이브러리 검색 과정 추적
LD_DEBUG=libs /bin/ls
# 심볼 해석 과정 추적
LD_DEBUG=symbols /bin/ls
# 모든 정보 출력
LD_DEBUG=all /bin/ls
# 파일로 출력
LD_DEBUG=libs LD_DEBUG_OUTPUT=/tmp/debug ./myprogram

주요 카테고리:

  • libs: 라이브러리 검색
  • symbols: 심볼 해석
  • bindings: 심볼 바인딩
  • versions: 버전 의존성
  • reloc: 재배치 처리

pldd - 실행 중인 프로세스의 라이브러리 확인

Terminal window
$ pldd $(pgrep firefox) | head
14523: /usr/lib/firefox/firefox
linux-vdso.so.1
/lib/x86_64-linux-gnu/libpthread.so.0
/lib/x86_64-linux-gnu/libdl.so.2
...

일반적인 문제와 해결 방법

”cannot open shared object file: No such file or directory”

가장 흔한 오류로, 동적 링커가 라이브러리를 찾지 못할 때 발생한다.

Terminal window
$ ./myprogram
./myprogram: error while loading shared libraries: libfoo.so.1:
cannot open shared object file: No such file or directory

해결 방법

Terminal window
# 1. 라이브러리 위치 확인
find / -name "libfoo.so" 2>/dev/null
# 2-a. LD_LIBRARY_PATH로 임시 해결
export LD_LIBRARY_PATH=/path/to/lib:$LD_LIBRARY_PATH
# 2-b. ldconfig로 영구 해결
echo "/path/to/lib" | sudo tee /etc/ld.so.conf.d/foo.conf
sudo ldconfig
# 2-c. RUNPATH 수정
patchelf --set-rpath '/path/to/lib' ./myprogram
# 3. 라이브러리 설치 (패키지 매니저)
apt-cache search libfoo
sudo apt install libfoo-dev

”symbol lookup error: undefined symbol”

라이브러리가 로드됐지만 필요한 심볼(함수/변수)을 찾지 못할 때 발생한다.

Terminal window
$ ./myprogram
./myprogram: symbol lookup error: ./myprogram: undefined symbol: foo_function

해결 방법

Terminal window
# 심볼이 어느 라이브러리에 있는지 확인
nm -D /lib/x86_64-linux-gnu/lib.so 2>/dev/null | grep foo_function
# 라이브러리 버전 확인
ldd ./myprogram | grep libfoo

버전 불일치가 원인인 경우가 많다. 올바른 버전의 라이브러리를 설치하거나 프로그램을 재컴파일해야 한다.

버전 충돌 (version mismatch)

Terminal window
$ ./myprogram
./myprogram: /lib/x86_64-linux-gnu/libfoo.so.1: version `FOO_2.0' not found

프로그램이 요구하는 버전과 시스템의 라이브러리 버전이 맞지 않는 경우이다.

Terminal window
# 프로그램이 요구하는 버전 확인
readelf -V ./myprogram
# 라이브러리가 제공하는 버전 확인
readelf -V /lib/x86_64-linux-gnu/libfoo.so.1

순환 의존성

A가 B를 의존하고, B가 A를 의존하는 경우. 현대 동적 링커는 대부분 처리하지만, 초기화 순서 문제가 발생할 수 있다.

Terminal window
# 의존성 그래프 확인
ldd -r ./myprogram

라이브러리 로드 순서 문제

LD_PRELOAD로 특정 라이브러리를 먼저 로드할 수 있다.

Terminal window
# 특정 라이브러리를 먼저 로드
LD_PRELOAD=/path/to/liboverride.so ./myprogram
# malloc 디버깅 예시
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libtcmalloc.so.4 ./myprogram

고급 주제

dlopen을 통한 런타임 로딩

프로그램 실행 중에 라이브러리를 명시적으로 로드할 수 있다.

#include <dlfcn.h>
void handle = dlopen("libplugin.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "Error: %s\n", dlerror());
exit(1);
}
// 함수 포인터 가져오기
void (plugin_init)() = dlsym(handle, "plugin_init");
if (!plugin_init) {
fprintf(stderr, "Error: %s\n", dlerror());
exit(1);
}
plugin_init(); // 함수 호출
dlclose(handle);

dlopen 플래그:

  • RTLD_LAZY: lazy binding (함수 호출 시 해석)
  • RTLD_NOW: 즉시 모든 심볼 해석
  • RTLD_GLOBAL: 로드된 심볼을 다른 라이브러리에서 사용 가능
  • RTLD_LOCAL: 심볼을 해당 라이브러리 내에서만 사용

심볼 버저닝

라이브러리가 여러 버전의 함수를 동시에 제공할 수 있다.

// 버전 스크립트 (version.script)
FOO_1.0 {
global: foo_function;
local: ;
};
FOO_2.0 {
global: foo_function_v2;
} FOO_1.0;
Terminal window
gcc -shared -Wl,--version-script,version.script -o libfoo.so foo.c

네임스페이스 격리 (dlmopen)

라이브러리를 별도의 네임스페이스에 로드하여 심볼 충돌을 방지한다.

// 새로운 네임스페이스에 라이브러리 로드
void handle = dlmopen(LM_ID_NEWLM, "libplugin.so", RTLD_LAZY);

감사 인터페이스 (LD_AUDIT)

동적 링커의 동작을 가로채고 수정할 수 있다.

Terminal window
# 감사 라이브러리 사용
LD_AUDIT=/path/to/audit.so ./myprogram

참고