이번 포스팅에서는 라이브러리에 관해 얘기해보려한다. 리눅스 환경에서 C언어를 사용해서 정적 라이브러리와 동적 라이브러리를 생성하는 방법을 비교해보고 핵심적인 차이점을 알아보자. 라이브러리란 무엇인지, 정적/동적의 개념은 무엇인지는 이미 많은 정보가 있으니 여기서는 다루지 않는다. 기본적인 생성 및 사용법에 이어서 바이너리 레벨에서 어떤 차이점이 있고 왜 이런 차이점이 생기는지를 중심으로 이야기할 예정이다.

테스트 및 분석환경은 GNU/Linux 기반이다. 다른 플랫폼에서는 용어나 명령어등이 상이하므로 다른 글을 참고하도록 하고, 기본적인 오브젝트 파일 생성과 컴파일 파이프라인에 대한 내용은 이 포스팅을 참고한다.

아래는 분석에 사용할 간단한 예제코드다.

my_math.h

#ifndef MY_MATH_H
#define MY_MATH_H

#include <stdbool.h>

// Arithmetic operations
int add(int a, int b);
int sub(int a, int b);

// Compare
bool equal(int a, int b);
bool greater(int a, int b);
bool less(int a, int b);

#endif

my_arithmetic.c

#include "my_math.h"

int add(int a, int b) {
        return a + b;
}

int sub(int a, int b) {
        return a - b;
}

my_compare.c

#include "my_math.h"

bool equal(int a, int b) {
        return (a == b);
}

bool greater(int a, int b) {
        return (a > b);
}

bool less(int a, int b) {
        return (a < b);
}

main.c

#include "my_math.h"

int main(void) {
        int a = add(1, 3);   // 4
        int b = sub(3, 1);   // 2
        return a + b;        // 6
}

정적(Static) 라이브러리 생성

$ gcc -c my_arithmetic.c my_compare.c   # -o 옵션이 없을 경우 {이름}.o 로 자동할당된다.
$ ar crs libmymath.a my_arithmetic.o my_compare.o  

단 두줄이면 정적라이브러리를 만들 수 있다. ar 명령의 두번째 인자(libmymath.a)가 바로 우리가 생성한 정적 라이브러리다.

ar 명령은 아카이브(archive)파일을 생성/수정/해제하기 위한 GNU 유틸리티다. 여기서 말하는 아카이브는 인자로 주어진 파일들을 단순히 하나로 모아준다는 의미정도일 뿐, 특별한 변화를 주진 않는다. 특히, 오브젝트 파일들을 압축하지 않는다!

사용한 옵션 crs는 각각:

  • c : 아카이브 파일이 없을 경우 새로 생성한다.
  • r : 오브젝트 파일을 아카이브에 추가한다. 단, 같은 이름의 멤버가 이미 있으면 교체한다.
  • s : 인덱스(심볼 테이블)를 생성한다.

동적(Dynamic)/공유(Shared) 라이브러리 생성

$ gcc -fPIC -c my_arithmetic.c my_compare.c
$ gcc -shared my_arithmetic.o my_compare.o -o libmymath.so

이것도 역시 2줄이면 된다. GCC를 사용해 생성된 동적/공유 라이브러리libmymath.so다.

하지만 여기서는 2단계로 나누어 볼 필요가 있다. 첫번째는 라이브러리를 생성하기 위한 재료로 사용되는 오브젝트 파일의 생성 단계와 두번째는 오브젝트 파일들로 라이브러리를 생성하는 단계로 보자.


첫번째 단계에서는 일반적인 재배치 가능한 오브젝트 파일을 생성하는 옵션 외에 -fPIC 가 추가되었다. PICPosition-Independent Code 라는 의미로, 직역하면 위치에 의존하지 않는 코드라고 할 수 있다.

이 옵션에 대한 바이너리 레벨에서의 분석과 쓸모는 다른 포스팅에서 다룰 것이므로 간단하게만 설명한다. PIC의 각각의 단어 단위를 나누어 이해하는 것이 편리했다.

  • Position : 위치는 정확히는 메모리 상의 위치. 즉, 메모리 주소를 의미한다.
  • Independent : 앞선 Position, 메모리 주소에 종속성을 가지지 않는다.
  • Code : 여기서 말하는 코드는 오브젝트 파일의 코드(텍스트) 섹션을 의미한다. 이 섹션에는 CPU가 실행하는 기계어 명령 코드가 있다.

그래서, 위 의미들을 종합해서 풀이하면, “이 옵션으로 생성된 (재배치 가능한) 오브젝트 파일의 기계어 실행 코드들은 특정 고정된 메모리 주소에서 실행되지 않는다.” 라는 의미로 볼 수 있다.

-fPIC 말고 -fpic 를 사용해도 된다. 차이점은 gcc 매뉴얼 페이지를 확인하자.

두번째 단계인 라이브러리 생성 단계에서는 -shared 를 사용했다. 역시나 큰 의미는 없고 재배치 가능한 오브젝트 파일들을 합성하여 동적/공유 라이브러리로 만들겠다는 옵션이다.

정적 라이브러리 vs 동적/공유 라이브러리 생성 방법 비교

각 라이브러리의 생성 방법과 차이점을 아래 표로 정리했다.

유형 확장자 오브젝트 파일 생성 옵션 라이브러리 생성 주체 라이브러리 생성 옵션
정적 .a -c ar crs
동적/공유 .so -c -fPIC gcc -shared

두 라이브러리 모두 lib이란 접두사를 관례적으로 붙인다.


라이브러리를 생성했으니 이 라이브러리들을 실행파일에서 어떻게 사용하는지 살펴보자.

정적 라이브러리 링킹

$ gcc main.o -L. -lmymath -o math.out

라이브러리를 사용하지 않는 링킹에서는 재배치 가능한 오브젝트 파일(Relocatable Object File)들을 단순히 나열했었다. 하지만 라이브러리 링킹시에는 2개의 명령행 인자가 필요하다.

-L 옵션은 링크할 라이브러리가 위치한 디렉토리를 지정하고 -l 옵션은 라이브러리 이름을 지정한다.

따라서, -L. -lmymath 은 링커가 현재 디렉토리(.)에서 libmymath.a라는 이름의 라이브러리를 찾으려고 한다는걸 의미한다.

링커는 위 옵션으로 라이브러리를 찾아서 main.o파일과 링킹하고 최종 실행파일인 math.out을 생성한다.

동적/공유 라이브러리 링킹

$ gcc main.o -L. -lmymath -o math.out

기본적으로는 정적 라이브러리 링킹방법과 정확히 동일하다. -L. -lmymath 옵션은 현재 디렉토리에서 libmymath.so 파일을 찾는다.

정적라이브러리 이던 동적/공유 라이브러리 이던 상관없이, 일단 링커 입장에서는 이 라이브러리가 어디에 있고 이름이 무엇인지가 첫번째로 중요하다.

동일한 경로에 정적 라이브러리와 동적/공유 라이브러리가 동시에 존재할 때

앞서 살펴본 것 처럼 두 라이브러리 모두 링킹 옵션이 동일하다. 그렇다면 동일한 위치에 동일한 이름의 서로 다른 라이브러리가 있는 경우는 어떻게 될까?

예를 들어, /opt 디렉토리 아래에 libabc.a 파일과 libabc.so 파일이 있다면?

$ gcc main.o -L/opt -labc -o abc.out   # -labc 는 libabc.a? libabc.so?

이 경우, 링커는 /opt 디렉토리 아래에서 libabc.so 파일을, 즉, 동적/공유 라이브러리를 먼저 찾는다.

만약, 정적라이브러리를 링킹하고 싶을 경우는 -static 옵션을 사용하면 된다.

$ gcc main.o -static -L/opt -labc -o abc.out   # libabc.a 링킹

접두사 lib 이 없는 라이브러리 링킹

$ gcc main.o -L./libs -l:mmm.a -o try.out

-l: 옵션 뒤에 라이브러리 파일 이름을 직접 작성할 수 있다. (컴파일러 버전에 따라 다를지도…)

다만, 접두사가 없는 라이브러리를 링킹하려고 시도할 경우 최신 컴파일러는 어떻게 수정해야 하는지 친절히 알려주므로 너무 걱정하진 말자.

정적 라이브러리를 링크한 실행파일의 실행

$ ./math.out || echo $?
6

math.out 의 종료코드(반환값)인 6$? 를 출력해서 확인할 수 있다.

정적 라이브러리를 링킹할 경우 최종 실행파일은 라이브러리를 사용하지 않는 경우와 동일하게 실행된다. 정말로, 단 한개의 차이점도 없다. 그냥 실행파일을 실행하면 끝이다.

동적/공유 라이브러리를 링크한 실행파일의 실행

$ LD_LIBRARY_PATH=. ./math.out || echo $?
6

여기서 중요한 것은 LD_LIBRARY_PATH= 을 사용한 환경변수 설정이다.

이 환경 변수는 동적 로더(Dynamic Loader)가 런타임에 로딩할 동적/공유 라이브러리(.so) 파일의 위치를 저장한다. 중요한 것은, 프로세스를 실행 할 때 메모리에 로드할 라이브러리의 위치를 동적 로더가 알고 있어야 한다는 것이다.

동적/공유 라이브러리를 사용하는 방법은 사실 몇개가 더 있다. 하지만, 지금은 기본 사용법과 개념만 알아도 충분하므로 이 포스팅에서는 생략한다.


지금까지 표면적으로 라이브러리를 어떻게 만들고, 사용하는지 알아봤다. 하지만 내가 정말로 다루고 싶은 내용은 그 내부다. 정적라이브러리를 링킹하면 실제로 실행 오브젝트 파일에 바이너리 코드가 그대로 포함되는지, 그렇다면 동적라이브러리의 심볼은 실행 오브젝트 파일에 어떻게 작성되는지, 동적로더는 어떻게 동작하는지 등등 궁금하고 직접 확인해보고 싶은 것들이 아직 많다.