리눅스에서 프로그램이 실행될 때, 대부분의 경우 외부 라이브러리의 코드를 필요로 한다. 이 라이브러리들이 프로그램에 어떻게 연결되고, 시스템이 어디서 라이브러리를 찾는지 이해하는 것은 리눅스 시스템 관리와 개발에서 핵심적인 지식이다.
프로그램이 외부 함수를 호출할 때, 그 함수의 실제 코드가 어디에 있는지 연결해주는 과정을 링킹(Linking)이라 한다. 링킹은 두 가지 방식으로 수행된다.
정적 링킹 (Static Linking)
컴파일 시점에 라이브러리 코드를 실행 파일에 직접 복사한다.
# 정적 라이브러리 생성gcc -c mylib.c -o mylib.oar rcs libmylib.a mylib.o
# 정적 링킹으로 컴파일gcc main.c -L. -lmylib -static -o main- 장점: 실행 시 외부 의존성 없음, 배포가 단순함
- 단점: 실행 파일 크기 증가, 라이브러리 업데이트 시 재컴파일 필요, 메모리 낭비 (동일 코드가 여러 프로세스에 중복 적재)
동적 링킹 (Dynamic Linking)
실행 시점에 라이브러리를 메모리에 로드하고 연결한다.
# 공유 라이브러리 생성gcc -c -fPIC mylib.c -o mylib.ogcc -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)로 컴파일된다.
$ 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)를 사용한다.
# PIC 옵션으로 컴파일gcc -c -fPIC source.c -o source.o-fPIC 플래그는 컴파일러에게 위치 독립적 코드를 생성하도록 지시한다. 이 코드는 GOT를 통해 전역 변수에 접근하고, PLT를 통해 외부 함수를 호출한다.
라이브러리 명명 규칙
공유 라이브러리는 세 가지 이름을 가진다.
실제 파일명 (Real Name): libfoo.so.1.2.3soname: libfoo.so.1링커 이름 (Linker Name): libfoo.so- Real Name: 실제 파일명으로 전체 버전 정보 포함 (major.minor.patch)
- soname: 라이브러리의 ABI 호환성을 나타내는 이름. major 버전만 포함
- Linker Name: 컴파일 시
-l옵션으로 사용하는 이름
$ ls -la /lib/x86_64-linux-gnu/libzlrwxrwxrwx 1 root root 13 libz.so - >libz.so.1.2.11lrwxrwxrwx 1 root root 13 libz.so.1 - >libz.so.1.2.11-rw-r--r-- 1 root root 116960 libz.so.1.2.11soname의 중요성
soname은 ABI(Application Binary Interface) 호환성의 핵심이다.
# 라이브러리의 soname 확인$ readelf -d /lib/x86_64-linux-gnu/libc.so.6 | grep SONAME 0x000000000000000e (SONAME) Library soname: [libc.so.6]프로그램이 컴파일될 때, 링커는 라이브러리의 soname을 실행 파일에 기록한다. 실행 시 동적 링커는 이 soname을 사용해 라이브러리를 찾는다.
# 실행 파일이 요구하는 라이브러리 확인$ 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)라고 불리는 이 프로그램은 실행 파일을 메모리에 로드하고 필요한 공유 라이브러리를 연결한다.
$ ls -la /lib64/ld-linux-x86-64.so.2lrwxrwxrwx 1 root root 32 /lib64/ld-linux-x86-64.so.2 - >/lib/x86_64-linux-gnu/ld-2.31.so동작 과정
- 프로그램 시작: 커널이 ELF 헤더의
.interp섹션을 읽어 동적 링커 경로 확인 - 동적 링커 로드: 커널이 동적 링커를 메모리에 로드
- 의존성 분석:
.dynamic섹션의DT_NEEDED태그로 필요한 라이브러리 파악 - 라이브러리 검색: 정해진 순서대로 라이브러리 파일 탐색
- 라이브러리 로드: 찾은 라이브러리를 메모리에 매핑
- 심볼 해석: GOT와 PLT를 사용해 함수 주소 연결
- 초기화 실행: 라이브러리의 초기화 함수 실행
- 제어권 전달: 프로그램의
_start로 제어권 전달
# 프로그램의 인터프리터 확인$ 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): 실제 함수/변수 주소를 저장하는 테이블
# 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 RPATH0x000000000000000f (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 RUNPATH0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN/../lib] -
/etc/ld.so.cache:
ldconfig가 생성한 캐시 파일이다./etc/ld.so.conf에 지정된 경로의 라이브러리 정보를 빠르게 조회할 수 있도록 바이너리 형태로 저장한다.Terminal window # 캐시 내용 확인$ ldconfig -p | head -201800 libs found in cache `/etc/ld.so.cache'libz.so.1 (libc6,x86-64) => /lib/x86_64-linux-gnu/libz.so.1libyaml-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는 동적 링커가 공유 라이브러리를 찾을 추가 경로를 지정한다.
기본 사용법
# 단일 경로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주의사항과 모범 사례
보안 고려사항
# 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를 전역으로 설정하는 것은 권장되지 않는다.
# 나쁜 예: .bashrc에 전역 설정# export LD_LIBRARY_PATH=/custom/lib:$LD_LIBRARY_PATH
# 좋은 예: 래퍼 스크립트 사용#!/bin/bash# /opt/myapp/bin/myapp-wrapperexport LD_LIBRARY_PATH=/opt/myapp/libexec /opt/myapp/bin/myapp "$@"더 좋은 방법은 RPATH/RUNPATH를 사용하거나 ldconfig로 시스템에 등록하는 것이다.
RPATH와 RUNPATH
실행 파일에 라이브러리 검색 경로를 직접 내장할 수 있다.
RPATH vs RUNPATH
| 특성 | RPATH | RUNPATH |
|---|---|---|
| 검색 우선순위 | LD_LIBRARY_PATH보다 먼저 | LD_LIBRARY_PATH보다 나중 |
| 상속 | 의존 라이브러리에도 적용 | 직접 의존성에만 적용 |
| 현대적 사용 | Deprecated | 권장 |
설정 방법
# 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특수 토큰
$ORIGIN # 실행 파일이 위치한 디렉토리$LIB # 시스템에 따라 lib 또는 lib64$PLATFORM # 플랫폼 이름 (예: x86_64)# 실제 예시: 실행 파일 기준 상대 경로gcc main.c -L. -lmylib -Wl,-rpath,'$ORIGIN/../lib' -o bin/main
# 디렉토리 구조myapp/├── bin/│ └── main # RUNPATH: $ORIGIN/../lib└── lib/└── libmylib.soRPATH/RUNPATH 확인 및 수정
# 확인$ readelf -d ./main | grep -E 'RPATH|RUNPATH'
# patchelf로 수정$ patchelf --set-rpath '/new/path:$ORIGIN/../lib' ./main
# chrpath로 수정 (제한적)$ chrpath -r '/new/path' ./mainldconfig와 시스템 라이브러리 관리
ldconfig는 시스템의 공유 라이브러리 캐시를 관리하는 도구이다.
/etc/ld.so.conf
라이브러리 검색 경로를 정의하는 설정 파일이다.
$ cat /etc/ld.so.confinclude /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/libldconfig 사용법
# 캐시 업데이트 (새 라이브러리 설치 후)sudo ldconfig
# verbose 모드로 업데이트sudo ldconfig -v
# 캐시 내용 출력ldconfig -p
# 특정 라이브러리 검색ldconfig -p | grep libssl
# 캐시 업데이트 없이 테스트sudo ldconfig -N -v새 라이브러리 시스템에 등록
# 방법 1: 설정 파일 추가echo "/opt/myapp/lib" | sudo tee /etc/ld.so.conf.d/myapp.confsudo ldconfig
# 방법 2: 표준 경로에 심볼릭 링크sudo ln -s /opt/myapp/lib/libfoo.so.1 /usr/lib/sudo ldconfig관련 명령어
ldd (의존성 확인)
$ 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”)- 괄호 안 주소: 메모리 로드 주소
# 재귀적으로 모든 의존성 확인ldd -r /bin/ls
# 누락된 함수 확인ldd -d /bin/ls주의: ldd는 실행 파일을 실제로 로드하므로, 신뢰할 수 없는 바이너리에는 사용하지 않는다.
readelf (ELF 정보 확인)
# 동적 섹션 정보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.6objdump - 바이너리 분석
# 동적 재배치 정보objdump -R /bin/ls
# 디스어셈블리objdump -d /bin/ls
# 동적 심볼objdump -T /lib/x86_64-linux-gnu/libc.so.6 | headnm - 심볼 테이블 확인
# 정적 심볼 테이블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 - 동적 링커 디버깅
동적 링커의 동작을 상세히 추적할 수 있다.
# 사용 가능한 옵션 확인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 - 실행 중인 프로세스의 라이브러리 확인
$ pldd $(pgrep firefox) | head14523: /usr/lib/firefox/firefoxlinux-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”
가장 흔한 오류로, 동적 링커가 라이브러리를 찾지 못할 때 발생한다.
$ ./myprogram./myprogram: error while loading shared libraries: libfoo.so.1:cannot open shared object file: No such file or directory해결 방법
# 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.confsudo ldconfig
# 2-c. RUNPATH 수정patchelf --set-rpath '/path/to/lib' ./myprogram
# 3. 라이브러리 설치 (패키지 매니저)apt-cache search libfoosudo apt install libfoo-dev”symbol lookup error: undefined symbol”
라이브러리가 로드됐지만 필요한 심볼(함수/변수)을 찾지 못할 때 발생한다.
$ ./myprogram./myprogram: symbol lookup error: ./myprogram: undefined symbol: foo_function해결 방법
# 심볼이 어느 라이브러리에 있는지 확인nm -D /lib/x86_64-linux-gnu/lib.so 2>/dev/null | grep foo_function
# 라이브러리 버전 확인ldd ./myprogram | grep libfoo버전 불일치가 원인인 경우가 많다. 올바른 버전의 라이브러리를 설치하거나 프로그램을 재컴파일해야 한다.
버전 충돌 (version mismatch)
$ ./myprogram./myprogram: /lib/x86_64-linux-gnu/libfoo.so.1: version `FOO_2.0' not found프로그램이 요구하는 버전과 시스템의 라이브러리 버전이 맞지 않는 경우이다.
# 프로그램이 요구하는 버전 확인readelf -V ./myprogram
# 라이브러리가 제공하는 버전 확인readelf -V /lib/x86_64-linux-gnu/libfoo.so.1순환 의존성
A가 B를 의존하고, B가 A를 의존하는 경우. 현대 동적 링커는 대부분 처리하지만, 초기화 순서 문제가 발생할 수 있다.
# 의존성 그래프 확인ldd -r ./myprogram라이브러리 로드 순서 문제
LD_PRELOAD로 특정 라이브러리를 먼저 로드할 수 있다.
# 특정 라이브러리를 먼저 로드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;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)
동적 링커의 동작을 가로채고 수정할 수 있다.
# 감사 라이브러리 사용LD_AUDIT=/path/to/audit.so ./myprogram참고