정적 라이브러리와 공유 라이브러리

라이브러리의 개념은 이전 글에서도 간략하게 언급하였지만, 여기에서는 조금 더 자세하게 다루고자 한다. 라이브러리를 사용하는 이유는 명확하다. 재사용 되는 부분에 대한 모듈화.

정적 라이브러리

정적 라이브러리는 어떤 하나의 실행 파일에 포함된 라이브러리를 말한다. Linux 에서는 .a 확장자를 가지며 (archive), 목적 파일에서 ar 커맨드를 이용하여 손쉽게 생성 가능하다.

정적 라이브러리 생성

이전 글에서 사용하였던 rand.cpp 파일을 이용해서 정적 라이브러리를 생성해보자.

$ g++ -c rand.cpp -o rand.o
$ ar rcs librand.a rand.o

첫번째 인자로 ‘rcs’ 값을 넣어주고, 두번째 인자로 생성할 라이브러리의 이름을 ’lib[이름].a’ 형태로 넣어주고, 세번째 인자로 1개 이상의 목적 파일 경로를 적어주면 된다.

정적 라이브러리 사용

정적 라이브러리를 사용하는 것은 그리 어렵지 않다. 그냥 빌드 과정에서 목적 파일처럼 같이 나열해주면 된다.

# 이전 글에서 사용했던 main.cpp 를 그대로 이용
$ g++ main.cpp librand.a -o static_build_program

왜 정적 라이브러리를, 언제?

정적 라이브러리를 사용할 경우 배포가 쉬워진다는 강점이 있지만, 한편으로는 파일의 크기가 커진다는 단점도 있다. 라이브러리 자체가 파일에 포함되니 어쩔수 없지. 하지만 이 배포가 쉬워진다는 점이 굉장히 큰 장점이 되기도 한다. Go 언어를 사용하여 실행 파일을 빌드하면 정적 라이브러리를 사용하여 빌드가 이루어진다. 쉬운 배포, Go 만세!!

공유 라이브러리

공유 라이브러리는 여러 실행 파일들이 공유해서 사용할 수 있는 라이브러리를 말한다. Linux 에서는 .so 확장자를 가지며 (shared object), GCC 에서 -shared 옵션을 주어 생성할 수 있다.

정적 라이브러리가 좋은 면이 많지만, C++ 로 프로젝트를 하다보면 사용할 일이 많지는 않은것 같다. 왜냐하면 파일의 크기가 너무 크기 때문이다. 대표적으로 FFmpeg 의 경우, 정적 라이브러리의 크기는 100MB를 훌쩍 넘지만, 공유 라이브러리의 크기는 기껏 해야 20MB 정도 내외이다. 버전 별로 다르겠지만 내가 사용했던 버전의 경우 7배의 차이가 나더라. 그 정도면 굳이 정적 라이브러리를 쓰기는 힘들듯… ㅎㅎ…

하지만 공유 라이브러리는 제법 사용하기가 어렵다. 정적 라이브러리처럼 간단하게 사용할 수가 없고, 특히나 배포시 문제가 좀 있다. (그래서 쉬운 배포를 위해 컨테이너 기반의 배포가 유명세를 탄 것이라고 생각한다)

공유 라이브러리 생성

공유 라이브러리를 생성할 때에는 GCC를 이용하여 생성할 수 있다.

$ g++ -c rand.cpp -fPIC -o rand.o
$ g++ -shared rand.o -o librand.so

공유 라이브러리를 컴파일 할 때에는 반드시 -fPIC 플래그를 넣어주어야 한다 (Position Independent Code). 다른 목적 파일들과는 다르게 공유 라이브러리로 생성되는 목적 파일은 호출되는 방식이 다르므로, 어느 위치에서 해당 함수가 호출되더라도 가상 메모리에 맞게 맵핑되는 과정을 갖기 위해서는 해당 플래그가 반드시 필요하다.

공유 라이브러리 사용

공유 라이브러리를 사용하는 방법에는 두 가지 방법이 있다.

  • 동적 링크: 링크 과정에서 라이브러리를 링크하는 방법
  • 동적 로드: 프로그램 실행 시점에 라이브러리를 링크하는 방법

동적 링크는 프로그램을 컴파일하여 목적 코드들을 링크하는 과정에서 공유 라이브러리를 링크하는 방식이다. 프로그램의 심볼 내에 링크된 공유 라이브러리들이 기록되기 때문에, ldd 명령으로 어떠한 라이브러리가 필요한지 알아볼 수 있다.

한편 동적 로드는 프로그램 내에서 동적으로 라이브러리를 로드하는 방식이다. 배포가 쉬워지지만, ldd 명령에서 어떠한 라이브러리가 필요한지 알아볼 수 없을 뿐 아니라, 라이브러리 내에서 제공하는 함수를 이용하기 위해서 코드 레벨에서 처리를 해 주어야 하기 때문에 조금 더 처리가 복잡해지는 점이 있다. libdl 라이브러리를 이용한다.

라이브러리 경로

라이브러리 경로는 Linux가 찾는 공유 라이브러리의 경로를 의미한다. 기본적으로 운영체제 레벨에서는 다음과 같은 라이브러리 경로가 있다.

  • /lib, /usr/lib (/lib/usr/lib 의 심볼릭 링크임)
  • /lib64, /usr/lib64 (/lib64/usr/lib64 의 심볼릭 링크임)

그 외에도 CentOS 나 Ubuntu 의 경우 /etc/ld.so.conf.d 디렉토리 하위에 위치한 파일들의 경로를 참조하여 공유 라이브러리를 검색한다. 여기서 주의할 점은, 해당 파일 내에 반드시 경로를 ‘절대 경로’ 로 써주어야 한다는 것이다. 아니면 정상적으로 동작하지 않는다.

대개 파일명은 ‘프로젝트명.conf’ 로 놓는다. 예를 들어 ffmpeg 관련 라이브러리가 /home/kesuskim/ffmpeg_build/lib 에 위치할 경우, /etc/ld.so.conf.d/ffmpeg.conf 파일에 /home/kesuskim/ffmpeg_build/lib 경로를 추가하면 ffmpeg 관련 공유 라이브러리를 사용할 수 있다.

한편 간단하게 LD_LIBRARY_PATH 환경변수에 라이브러리 경로를 추가할 수도 있다. PATH 변수처럼 콜론(:) 을 구분자로 갖는다.


사실 C++ 의 전체적인 많은 개념 중에 C++의 문법 자체나 소프트웨어 설계 방법을 제외한 관점에서 보면 제일 중요한게 이 공유 라이브러리라고 생각하는데, 계속 쓰면서 생각해보니 너무나도 복잡한 것 같다. 특히나 라이브러리에서 다른 라이브러리를 참조하는 경우, 서로의 디펜던트를 해결하기 위해 제일 끝에 위치한 라이브러리부터 하나 하나 컴파일을 다시 해야하는 경우도 존재한다. 이것 저것 겪어보며 정리해야겠다는 필요를 느껴서 좀 적어 내려가다보니, 모든 경우를 대처할 수 있도록 잘 정제되어 요약된 지식을 만드는게 너무 어렵다는 것을 느꼈다. =_= 경력이 더 쌓이면 가능할런지.