동적 라이브러리(shared library)와 Linker/Loader 이해하기
이제는 직접 C 언어를 사용하여 개발을 할 일이 많지 않지만 C 언어로 만들어진 프로그램과 라이브러리는 여전히 서비스 인프라에서 중요한 위치를 차지하고 있습니다.
linux 등 운영체제가 C 로 만들어져 있고 편리하게 개발을 할수 있는 생산성 좋은 script(ruby 나 python, PHP 등..) 언어의 엔진들은 대부분 C 로 제작되었고 openssl, database driver 등의 기능은 C 로 작성된 라이브러리를 호출하여 언어의 기능을 확장하고 있습니다.
운영체제에서 프로그램이 어떻게 동작하는지 이해하는 것은 견고한 서비스를 만들고 문제가 생겼을 때 대응을 하는데 도움이 되는 지식이지만 바쁜 시대에서 이런 지식을 체계적으로 학습하기는 쉽지가 않습니다.
그래서 프로그램의 동작 방식을 이해하기 위한 공유 라이브러리(shared library, 또는 동적 라이브러리라고도 합니다.)와 프로그램 로더에 대해서 다뤄볼까 합니다.
공유 라이브러리(shared library)
컴파일러는 무슨 일들을 할까.
#include <stdio.h>
int main(int argc, char** argv)
{
printf("Hello World\n");
return 0;
}
위와 같은 hello.c 소스가 있을 경우 gcc hello.c 명령어로 컴파일을 하면 컴파일러는 다음과 같은 절차를 거쳐서 실행 파일을 생성하게 됩니다. (괄호 안에 있는 것은 gcc 에서 해당 기능을 수행하는 명령어입니다.)
C Pre Processor(cpp) 가 #define, #include 구문등을 전처리하여 hello.i 생성
cpp hello.c > hello.i
CODEC compiler(cc1) 가 전처리한 hello.i 소스를 어셈블리로 컴파일하여 hello.s 생성
gcc -S hello.i
CODEassembler(as) 는 hello.s 를 어셈블하여 object (hello.o) 생성
as -o hello.o hello.s
CODElinker(collect2) 는 printf 등 외부 library 에 있는 symbol 을 링크해서 최종 프로그램 생성(a.out)
gcc는 위와 과정을 간편하게 해주는 wrapper 역할을 수행하며 -v 옵션을 주고 실행할 경우 이 모든 과정을 확인할 수 있습니다.
gcc -v hello.c
공유 라이브러리
printf 같이 프로그램마다 자주 사용하는 외부 함수를 실행 프로그램에 포함시킬 경우 프로그램의 덩치가 커지고 외부 라이브러리가 업그레이드 됐을 경우 이를 사용하는 프로그램을 다시 컴파일해야 하는 부담이 있습니다.
외부 함수를 포함해서 프로그램을 만드는 것을 정적 링크(static linking)라고 부릅니다.)
그래서 라이브러리를 공유 라이브러리(shared library)라는 형식으로 만들어 놓고 컴파일 시점에 사용할 라이브러리를 연결만 하는 방법을 사용합니다.
windows 에서는 공유 라이브러리대신 DLL(Dynamic Link Library) 라고 부릅니다.
어떤 프로그램이 공유 라이브러리를 사용하는지 알아보려면 리눅스에는 file 명령어를 사용하면 되며 아래는 공유 라이브러리를 사용했을 때 결과입니다.
$ file a.out
a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.18, not stripped
정적 링크된 프로그램은 아래와 같이 표시합니다.
$ file a.out
a.out: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.18, not stripped
dynamic loader
공유 라이브러리와 연결된 프로그램을 실행하면 내부적으로 dynamic loader라는 프로그램이 먼저 동작하여 대략 다음과 같은 작업을 실행합니다.
- dynamic link 된 공유 라이브러리를 찾아서 메모리에 로딩
- entry function (C 언어일 경우 main 함수)를 찾아서 호출
- 프로그램 실행
각 운영체제의 로더 이름은 아래와 같습니다.
운영체제 | 로더 |
---|---|
Linux | ld.so 또는 ld-linux.so |
Mac OS X | dyld |
Solaris | ld.so |
HP-UX | dld.so |
어디에서 shared library 를 찾을까?
LD_LIBRARY_PATH
loader 는 program 구동에 필요한 shared library 를 찾을 때 LD_LIBRARY_PATH(리눅스의 경우)와 같은 고유의 환경 변수를 참고하며 OS 마다 경로를 찾는 순서가 다릅니다.
운영체제 | 환경 변수 | 비고 |
---|---|---|
Windows | PATH | Windows에서 DLL을 찾는 데 사용되는 검색 경로 순서 |
Linux | LD_LIBRARY_PATH | LD_LIBRARY_PATH 가 설정되지 않아도 /lib64, /usr/lib64 폴더는 기본적으로 설정됩니다. |
Mac OS X | DYLD_LIBRARY_PATH | LD_LIBRARY_PATH 와 동일 역할 |
DYLD_FALLBACK_LIBRARY_PATH | 라이브러리를 못 찾을 경우 검색할 경로. $(HOME)/lib:/usr/local/lib:/lib:/usr/lib 로 설정되어 있음 |
사용할 일이 많지는 않겠지만 상용 유닉스의 경우 아래와 같은 환경 변수를 사용합니다.
- Solaris : LD_LIBRARY_PATH,
- AIX: LIBPATH
- HP-UX: SHLIB_PATH
RPATH
리눅스는 ELF 형식의 바이너리는 rpath 라고 부르는 "실행시 라이브러리를 찾을 경로 정보"를 컴파일 시점에 넣어줄 수 있으며 이 정보는 LD_LIBRARY_PATH 환경 변수 전에 참고됩니다.
rpath 정보는 아래와 같이 gcc 를 실행할 때 -Wl,-rpath 옵션을 추가해 주면 됩니다.
gcc -Wl,-rpath,/usr/local/lib hello.c
바이너리에 rpath가 있는지 여부는 readelf 명령어를 사용하여 파일의 헤더를 확인하면 됩니다.
$ readelf -d a.out | grep RPATH
0x000000000000000f (RPATH) Library rpath: [/usr/local/lib]
LD_PRELOAD
LD_PRELOAD 환경 변수는 라이브러리의 후킹(hooking)이 필요할 때 사용하는 환경 변수로 이게 설정되어 있으면 여기에 지정된 라이브러리내 함수를 먼저 호출해 줍니다.
대표적인 용도로는 debugging 때문에 구동시에 특정 동적 library 를 변경해야할 경우등에 사용합니다.
예로 메모리 leak을 찾는데 사용하는 memory debugger는 runtime 에 기존 프로그램에서 사용한 malloc/free 를 debugger 가 구현한 함수로 대체해서 동작하기 위해 LD_PRELOAD 환경 변수를 사용하여 디버깅을 수행합니다.
라이브러리 의존성 확인
환경 변수가 잘못 설정되어 있거나 프로그램 설치를 잘못했거나 등의 이유로 프로그램 실행시 "Can't load library libevent.so"와 비슷한 류의 에러를 만나면 경험이 없을 경우 머리털을 쥐어 뜯게 됩니다.
그래서 프로그램이 의존하는 shared library 는 무엇이고 로더가 어떻게 찾는지를 이해하는 것은 에러 처리를 위해 꼭 필요한 지식입니다.
운영체제 별로 의존하는 shared library를 찾는 방법은 다음과 같습니다.
Windows
Dependency Walker 가 32/64 Windows에서 모두 사용 가능하고 공개이며 유용하니 이 프로그램을 사용하여 라이브러리의 의존성을 확인하세요.
Linux
특정 프로그램이 참고하는 shared library를 확인하려면 ldd 명령을 사용하면 됩니다.
$ ldd /usr/bin/vim
linux-vdso.so.1 => (0x00007ffd9167b000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fd8375d5000)
libtinfo.so.5 => /lib/x86_64-linux-gnu/libtinfo.so.5 (0x00007fd8373ac000)
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007fd837189000)
libacl.so.1 => /lib/x86_64-linux-gnu/libacl.so.1 (0x00007fd836f81000)
libgpm.so.2 => /usr/lib/x86_64-linux-gnu/libgpm.so.2 (0x00007fd836d7a000)
좌측에 있는 항목(예: libm.so.6) 이 의존하는 라이브러리 이름이며 우측에 있는 항목이 로딩할 실제 라이브러리(예: /lib/x86_64-linux-gnu/libm.so.6) 입니다.
라이브러리가 없다면 우측 항목에는 대신 "not found" 라는 문구가 표시되며 프로그램을 실행하면 에러가 발생하며 이 경우 /lib64, /usr/lib64 에 해당 라이브러리가 있는지 확인한 후에
없을 경우 해당 라이브러리가 있는 경로를 LD_LIBRARY_PATH 환경 변수에 추가해 주면 됩니다.
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/myprog/lib
Mac OS X
OS X 는 otool (object file displaying tool) 라는 명령어를 -L 옵션을 주고 실행하면 다음과 같이 의존성 있는 동적 라이브러리 및 버전을 확인할 수 있습니다.
otool -L /usr/bin/afplay
/usr/bin/afplay:
/System/Library/Frameworks/AudioToolbox.framework/Versions/A/AudioToolbox (compatibility version 1.0.0, current version 1.0.0)
/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 550.42.0)
/System/Library/Frameworks/AudioUnit.framework/Versions/A/AudioUnit (compatibility version 1.0.0, current version 1.0.0)
/System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio (compatibility version 1.0.0, current version 1.0.0)
/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices (compatibility version 1.0.0, current version 44.0.0)
/System/Library/Frameworks/vecLib.framework/Versions/A/vecLib (compatibility version 1.0.0, current version 268.0.1)
/usr/lib/libstdc++.6.dylib (compatibility version 7.0.0, current version 7.9.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 125.2.10)
콘솔에서는 잘 되는데 cron 에서는 에러가 나는 이유
cron은 보안상의 이유로 실행시 쉘의 초기화 파일을 참고하지 않습니다.
사용자 계정으로는 잘 실행되는데 cron 에 등록하여 프로그램을 실행할 때 제대로 실행이 안 된다면 대부분의 원인은 바로 해당 프로그램을 실행하는데 필요한 동적 라이브러리를 못 찾아서입니다.
이럴 경우 처리 방안은 cron에서 실행하는 프로그램을 별도의 쉘 스크립트를 통해서 구동하고 그 안에서 PATH, LD_LIBRARY_PATH 변수를 설정하고 실행하는 것입니다.
#!/bin/bash
export PATH=$PATH:/opt/prog/lib
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/prog/bin
myprog --option
cron에 대한 자세한 내용은 "리눅스를 활용한 회사 인프라 구축의 모든 것"중 cron 사용법 항목을 참고하세요.
프로그램에서 dynamic link를 하려면?
프로그램내에서도 런타임에 사용할 라이브러리와 함수를 동적으로 로딩할 수 있습니다.
windows 에서는 LoadLibrary 를 사용하여 dll 을 로딩한 후에 GetProcAddress 로 실행하려는 함수의 주소를 얻은 후에 호출할 수 있으며 리눅스는 dlopen, dlsym 함수를 사용하면 됩니다.
각 운영체제마다 dynamic link를 처리하는 함수와 호출 방식이 상이하므로 이식성 좋은 프로그램을 작성하려면 GNU의 libtool에 포함되어 있는 libltdl 을 사용하면 됩니다.