다음 이전 차례

3. 공유 라이브러리

공유 라이브러리란 프로그램이 시작될 때 이 프로그램에 의해서 적재되는 라 이브러리를 말한다. 어떤 공유 라이브러리가 바르게 적재었다면 이후에 실행되 는 모든 프로그램들은 자동적으로 이 새로운 공유 라이브러리를 사용할 것이 다. 실재에 있어서, 리눅스에서 사용되는 접근방식은 아래와 같은 기능들을 허 용하므로 더욱 탄력적이며 또한, 세련되게 기능한다.

* 라이브러리를 갱신하면서 이 라이브러리의 구버전 지원용 버전이 없이도 여전히 구 버전의 라이브러리의 사용을 원하는 프로그램을 지원할수 있다.

* 특정 프로그램의 수행시 라이브러리 또는 심지어 라이브러리의 특정함수의 오버라이딩(대치)이 가능하다.

* 이 모든 기능을 기존의 라이브러리를 이용하여 프로그램 실행중에 가능하 다.

관 례

공유 라이브러리가 이러한 기능들을 지원하기 위해서는 사용자가 반드시 따 라야할 이용상의 몇가지 관례와 가이드라인이 있다. 당신은 한 라이브러리를 일컫는 이름들의 차이 특히 "soname"과 "real name"을 구별할수 있어야하 며, (또한 이들이 어떻게 상호작용 하는가도 알아야 한다) 또한, 라이브러리가 파일 시스템상의 어디에 위치해야 하는가도 알아야 한다.

공유 라이브러리 이름들

모든 공유 라이브러리는 "soname"이라 불리는 특별한 이름을 가지고 있다. "soname"은 접두어 "lib", 라이브러리 이름, ".so"라는 문구, 그 다음 마침표와 인터페이스 변경시 증가되는 버전 넘버로 구성된다. ( 예외로 가장 저수준의 C라이브러리들은 "lib"로 시작되지 않는다) 완전한 공식적 "soname"에는 라이 브러리가 속한 디렉토리명이 앞부분에 들어간다. 운용중인 시스템에서는 완전 한 공식적 "soname" 이란 공유라이브러리의 실제 이름("real name")에 대한 심볼릭 링크(실제 코드를 담고있는 파일이 아닌)가 될 것이다.

모든 공유 라이브러리는 실제 라이브러리 코드가 저장되어 있는 파일의 파일 명인 "real name"을 또한 가진다. 실제 파일명은 "soname"에 마침표와 마이너 버전 넘버, 마침표 그리고 릴리즈 넘버를 더한 것이다. 릴리즈 넘버는 선택사 항이다. 마이너 넘버와 릴리즈 넘버로 정확히 어떤 버전의 라이브러리가 설치 되어 있는지 알게 함으로써 시스템 구성을 조절할수 있다. 이 번호들은 라이브 러리를 기술하기위한 문서화시에 사용된 번호와 맞지 않을수도 있음에 유의하 라. 이렇게 함으로써 라이브러리의 문서화가 더 용이해진다.

부가적으로 컴파일러가 라이브러리를 요구할 때 사용하는 이름이 하나 더 존 재한다.(나는 이를 "linker name"이라 일컬을 것이다.) 여기에는 어떠한 버전넘 버도 포함되지 않는 "soname"이 해당된다. 공유 라이브러리를 관리하는 핵심은 이러한 이름들을 잘 분별하는 것이다. 프 로그램에서 필요한 공유 라이브러리를 내부적으로 표현할때는 그 라이브러리 의 "soname"을 써야한다. 반대로 만약에 당신이 공유 라이브러리를 만들었다 면 자세한 버전 정보를 포함한 실제 파일명을 주어야 한다. 당신이 새로운 버 전의 라이브러리를 설치할때는 몇 개의 정해진 디렉토리들중의 한곳에 설치한 뒤에 ldconfig라는 프로그램을 실행하여야 한다. ldconfig는 디렉토리에 존재하 는 파일들을 점검한 뒤 실제 파일명의 심볼릭 링크로써 soname을 생성하며 동시에 캐쉬 파일인 /etc/ld.so.cache를 셋팅한다.

ldconfig는 linker name을 셋팅하지 않는다. 이것은 보통 라이브러리의 설치 시에 셋팅을 하는데, 간단하게 가장 최근의 soname이나 real name에 심볼릭 링크를 만들면 된다. 대부분의 경우 라이브러리를 갱신하고 자동적으로 링크시 에 사용하기를 원할것이므로 soname에 심볼릭 링크하기를 권장한다. H. J. Lu 에게 ldconfig가 왜 자동으로 linker name을 셋팅하지 않느냐고 물었더니 그 의 설명은 가장 최근의 라이브러리로 링크시키고 싶을때가 있을것이며 또한 이전 버전의 라이브러리를 개발하거나할 때 그것을 링크하려 할 때가 있으므 로 ldconfig는 여러분이 어떤 라이브러리와 링크하고 싶은지 가정하지 않는 것이며 그래서 설치자는 어떤 라이브러리가 링크될것인지를 명시해야 하는것 이라 했다.

이상의 얘기를 종합한 예를 들어보면 ldconfig가 /usr/lib/libreadline.so.3.0과 같은 real name의 심볼릭 링크로 셋팅한 /usr/lib/libreadline.so.3 형태의 완 전한 공식적 soname이 있을것이며 /usr/lib/libreadline.so.3에 심볼릭 링크된 link name으로써 /usr/lib/libreadline.so가 또한 있을 것이다.

파일 시스템상의 위치

공유 라이브러리는 파일 시스템 상의 특정위치에 존재해야한다. 오픈 소스 소 프트웨어들은 GNU의 표준을 따르는 경향이 있으며 더 자세한 내용은 info문 서의 [13]info:standards#Directory_Variables 부분을 살펴보길 바란다. GNU 표 준은 소스코드의 배포시에 모든 라이브러리를 /usr/local/lib에 설치하길 권 장한다(/usr/local/bin에는 명령들이 모임). GNU표준은 또한 이러한 디폴트들 을 무시하거나 설치루틴을 호출하는 절차 등에 관한 관례들을 정의하고 있다. 파일시스템 계층적 구조표준(FHS:File system Hierarchy Standard) 또한 소 스배포시에 어떤 위치에 무엇을 두어야 하는지에 대해 논하고 있다.( www.pathname.com/fhs 를 참조하기 바람). FHS에서는 대부분의 라이브러리를 /usr/lib에 설치할 것을 권장한다, 그러나 시스템 시작시 필요한 라이브러리는 /lib에 또한, 비시스템 라이브러리는 /usr/local/lib에 설치되어야 한다.

위의 두 표준이 서로 모순되는 듯 보이지만 실제로 그렇지는 않다. GNU의 표준은 소스코드의 개발자를 위한 디폴트값을 제안하는 것이고, FHS는 소스 의 배포자(시스템의 디폴트 소스코드를 선택적으로 그 시스템의 패키지 관리 도구를 통해서 대치시키는)를 위한 디폴트값을 제안하는 것이다. 실제로 이것 은 상당히 효과적인 것이다, 즉, 당신이 다운로드한 가장 최신의(버그의 가능 성이 있는) 소스코드는 자동적으로 로컬 디렉토리인 /usr/local에 설치될것이며 그 코드가 이제 완벽해지면 패키지 매니저를 이용하여 시스템에 설치된 디 폴트(라이브러리)를 대치하고 배포시의 표준 디렉토리로 설치할수 있는 것이 다. 라이브러리를 통해서만 호출가능한 프로그램들을 라이브러리가 호출하는 경우에는 반드시 그 프로그램들을 usr/local/libexec(배포본에서는 /usr/libexec 임)에 두어야함을 명심하라. 한가지 혼동은 레드햇의 배포본들은 라이브러리를 찾는 과정에서 /usr/local 디렉토리를 제외 시킨다는 것인데, 아래에서 논의 될 /etc/so.conf부분을 참고하길 바란다. 그외에 또다른 하나의 표준 라이브 러리 디렉토리가 있는데 X-윈도우 시스템이 사용하는 /usr/X11R6/lib이 그 것이다. /lib/security 디렉토리는 PAM모듈에서 사용됨에 유의하라. 이 디렉 토리는 통상 동적 라이브러리로써 적재된다.(역시 뒷부분에서 논의될것임)

라이브러리는 어떻게 사용되는가

모든 리눅스 시스템이 포함되는 GNU의 glibc-기반의 시스템은 ELF 이진 실 행 프로그램의 시작시에 자동적으로 프로그램 로더를 적재하고 실행시킨다. 리눅스 시스템에서 이 로더는 /lib/ld-linux.so.X(X는 버전 넘버임)이라 불리워 지며 프로그램에서 사용되는 모든 공유 라이브러리를 차례대로 찾아서 적재시 킨다.

탐색되어질 디렉토리의 목차는 /etc/ld.so.conf에 들어 있다. 레드햇에 기반한 배포본에는 일반적으로 /usr/local/lib가 /etc/ld.so.conf파일에 들어 있지않은데 저자가 생각하기로 이것은 일종의 버그가 아닌가 생각한다. 레드햇의 경우 /usr/local/lib을 /etc/ld.so.conf에 첨가하는 것은 많은 프로그램들을 수행시키 기위해 흔히 있는 수정작업이다.

만약 라이브러리내의 특정한 함수만을 대치시키고 나머지는 그대로 유지하려 할때는 대치시킬 라이브러리(.o파일들)들을 /etc/ld.so.preload 파일에 명시하면 되는데 이러한 프리로드 라이브러리들은 표준적인 함수보다 우선권을 가지게 된다. 이러한 프리로딩 파일들은 보통 응급 패치본에서 사용되는데 정상적 배포본에는 잘 포함되지 않는다.

매 프로그램의 시작시마다 이 모든 디렉토리들을 탐색하는 것은 전반적으로 매우 비효율적이기 때문에 실제적으로는 캐쉬가 사용된다. ldconfig 프로 그램은 통상 /etc/ld.so.conf를 읽어서 모든 필요한 동적 링크 디렉토리들로부 터 적당한 심볼릭 링크를 생성한다.(그래서 이름 사용의 규칙을 잘 따라야 한다) 그 다음으로 /etc/ld.so.cache 에 이 내용들을 기록하며 이 다음 부터 는 프로그램들이 이 파일을 이용하는 것이다. 이렇게 해서 라이브러리의 액세 스 속도는 상당히 향상되는 것이다. 위의 내용이 함축하는 바는 DLL이 추가 또는 제거될 때, 또는 DLL의 디렉토리가 변경될때는 반드시 ldconfig가 실행되어야 한다는 것이며, 그래서 패키지 매니저가 라이브러리를 설치할 때, 이 과정은 통상으로 수행되는 절차인 것이다. 그래서 프로그램 시 작시 동적 로더는 실제로는 /etc/ld.so.cache를 사용하여 필요한 라이브러리를 적재하는 것이다.

환경 변수

여러 종류의 환경변수들이 위의 이러한 프로그램 시작시의 적재과정들을 조 절할수 있으며 또한 이 과정들을 대치하도록 하는 환경변수들도 있다. 예를 들 면 이 수행과정중, 어떤 라이브러리를 잠정적으로 교체하여 수행할수도 있다. 리눅스에서 환경변수 LD_LIBRARY_PATH는 콜론으로 분리되어 있는 표준 적인 라이브러리셋에 앞서서 가장 먼저 탐색되어질 라이브러리 디렉토리집합 이다. 이렇게 하면 새로운 라이브러리를 디버깅하거나 비표준적인 라이브러리 를 어떤 특수한 용도로 사용할 때에 유용할것이다. 환경변수 LD_PRELOAD 는 /etc/ld.so. preload처럼 표준적인 함수들에 오버라이드될 함수들이 있는 오 브젝트 파일의 리스트를 나타낸다. 위의 이러한 기능들은 로더 /lib/ld-linux.so 가 구현한다.

적재과정을 제어하는 다른 종류의 환경변수들이 있다; 이름이 LD_ 또는 RTLD_로 시작한다. 이러한 변수들의 대부분은 적재과정의 저수준 디버깅이나 특수기능의 구현을 위해서 사용되어 진다. 이들 대부분이 잘 문서화되지 않았 기 때문에 자세히 알려면 소스코드를 읽어나가는 것이 최선의 방도일 것이다. 이렇게 동적링크 라이브러리에 대한 제어권을 사용자에게 허락하는 것은 특 별한 조치가 취해지지 않는다면 setuid/setgid 등의 프로그램에 있어서는 대단 히 위험한 것이다. 그래서 GNU로더의 경우 만약에 프로그램이 setuid 또는 setgid일 경우에는 이러한 환경변수들(또는 이와 유사한 다른 변수들)은 무시 되거나 아니면 할수있는 기능이 대단히 제한된다. 로더는 프로그램이 setuid 또는 setgid인지 점검하기위해 프로그램의 credential를 체크한다; uid와 euid(effective user ID)가 다르거나 gid와 egid(effective group ID)가 다른경 우에 로더는 이 프로그램을 setuid/setgid(또는 상속받은것)이라 가정하고 링 크과정의 제어기능을 대단히 제한하게 된다. GNU의 glibc라이브러리의 소스코 드를 읽어보면 이러한 내용을 잘 볼수 있을것이며 특히 elf/rtld.c, sysdeps/ generic/dl-sysdep.c를 보길 바란다. 이것이 뜻하는 바는 만약에 당신이 uid, gid를 euid, egid와 같게하고 프로그램을 호출하면 이러한 환경변수들은 완전 한 영향력을 미칠수 있다는 뜻이다. 다른 유닉스 유사 시스템들에서는 이러한 상황을 다르게 다루기도 하지만 같은 이유로 인해서 setuid/setgid 프로그램 은 환경변수에 의해서 잘못된 영향을 받아서는 않되는 것이다.

공유 라이브러리의 생성

공유 라이브러리의 생성은 쉽다. 먼저 gcc의 fPIC플래그("position inde- pendent code" 공유라이브러리로 사용할 때 필수)를 사용하여 오브젝트 파일 을 생성한다. 그 다음엔 다음과 같은 명령형태로 공유 라이브러리를 생성한다.

gcc -shared  -WI, -soname,   your_soname\
 -o   library_name file_list library_list
두 개의 오브젝프 파일(a.o, b.o)을 포함하는 하나의 공유 라이브러리를 생성 하는 예를 들면 아래와 같다. 컴파일시에 공유 라이브러리에 필요한 것은 아니 지만 권고되는 사항인 디버깅 정보(-g옵션)와 경고(- wall옵션)의 생성에 유의 하라, 그리고 -c옵션으로 오브젝트 파일이 생성되며 분명히 -fPIC 옵션이 사 용되고 있다.
gcc -fPIC -g -c -Wall a.c
gcc -fPIC -g -c -Wall b.c
gcc -shared -Wl, -soname, libmystuff.so.1 \ 
 -o libmystuff.so.1.0.1 a.o b.o -lc
이때 주의사항 몇가지를 적어보며 아래와 같다.

* 컴파일 결과 생성될 라이브러리를 명기하는 것을 잊지 말것이며 -fomit, -frame, -pointer 등의 컴파일러 옵션은 진짜 필요할 경우외엔 쓰지 말 것. 결과적으로 생성된 라이브러리는 동작하지만 이렇게 하면 디버거는 무용지물 이 된다.

* -fpic가 아닌 -fPIC를 사용하여 코드롤 생성시켜야 한다.(-fpic를 쓸 경우 분 기점에서 큰 위치변화가 필요할경우 완전한 위치 독립형 코드를 얻을수 없다.)

공유 라이브러리의 설치 및 사용

이제 만약에 당신이 하나의 공유 라이브러리를 만들었다면 그 다음으로 설치 를 원할 것이다. 가장 간단한 방법은 만들어진 공유 라이브러리를 표준 디렉토 리들중의 하나(예를 들면 /usr/lib)에 복사하고 ldconfig(8)를 실행시키는 것이 다.

만약에 (/usr/lib 디렉토리의 변경권을 가지지 못하거나 해서) 위처럼 할수 없 는 경우가 있다면 환경변수를 사용하여 조정할수 있다. 먼저 어딘가에 공유 라 이브러리를 생성해야한다. 그 다음에 필요한 것은 real name에 관한 심볼릭 링크로써 soname을 생성(또는 버전명을 전혀 명시하지 않는 사용자을 위한 버전명이 포함되지 않은 soname을 심볼릭 링크로 생성)하는 것인데 이를 위 한 가장 간단한 방법은 다음과 같이 ldconfig를 실행하는 것이다.

ldconfig -n directory_with_shared_library

그 다음으로 표준 라이브러리 보다 앞서서 탐색될 라이브러리 디렉토리 목록 인 LD_LIBRARY_PATH 변수를 설정한다. 만약에 bash라면 다음과 같이 하 여 my_program을 포함되게 할수 있다.

LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH my_program

만약 특정한 함수만을 오버라이드 하려면 오버라이딩 오브젝트 파일을 생성 한 뒤에 LD_PRELOAD를 설정해서 할수 있다. 즉, 이 파일에 있는 함수들이 기존 함수를 오버라이드한다.

일반적으로 라이브러리를 갱신하는데 있어 특별한 고려사항은 없다. 만약 API가 변화한다면 라이브러리의 제작자는 soname을 변경할 것이다. 같은 soname을 가지면서 내용은 갱신된 신버전의 라이브러리를 무시하고 구버전의 라이브러리를 사용하려면, 구버전의 라이브러리를 어딘가에 복사하고 그것의 이름을 변경(이를테면 옛날이름.orig등)한 뒤 이 이름 변경된 라이브러리를 실 제로 호출하여 사용하도록 재설정하는 작은 wrap스크립트를 작성하여 강제적 으로 구버전을 사용하게 할수있다. 관례상 여러버전의 라이브러리들이 같은 디 렉토리에 있을수 있지만 이 구버전의 라이브러리를 따로 특정한 디렉토리에 위치시킬수도 있다. wrap 스크ㄹ트는 아마 아래와 같을 것이다.

#!/bin/sh
  export LD_LIBRARY_PATH=/usr/local/my_lib:$LD_LIBRARY_PATH
  exec /usr/bin/my_program.orig $*
한 프로그램이 사용하는 공유 라이브러리의 목록은 ldd(1)를 써서 알 수 있 다. 그래서, 예를 들면 ls명령이 사용하는 공유 라이브러리의 목록은 다음의 예 와 같이 알수있다

ldd /bin/ls

보통 프로그램에서 의존하고 있는 라이브러리의 soname의 목록과 포함된 디 렉토리가 보일것이다. 실질적으로 다음의 두 모듈의존성은 항상 보일것인데 그 것은 아래와 같다.

* /lib/ld-linux.so.N(N은 1이상 적어도 2). 이 라이브러리는 다른 모든 라이브 러리를 적재한다.

* libc.so.N(N은 6또는 그이상). 이것은 C 라이브러리이다. 다른 프로그래밍 언어들에서도(적어도 그 언어의 라이브러리를 구현하기위해서) C 라이브러리 를 쓰는 경향이 있다. 그래서 모든 프로그램에서 적어도 이 라이브러리는 포 함될 것이다.

경고: ldd를 신뢰할수 없는 프로그램에 사용하여서는 않된다. 이는 ldd의 매뉴 얼에도 명백히 언급된 얘기이다. ldd는 직접 그 프로그램을 호출함으로써 수행 되기 때문에 예기치 못한 코드의 실행을 가져올수 있기 때문이다.

비호환 라이브러리

새로운 버전의 라이브러리가 구버전의 라이브러리와 이진코드 차원에서 호환 되지 않는다면 soname은 변경될 필요가 있다. 이진코드에서 비호환되는 경 우에는 다음의 네가지 이유가 가장 기본적이다.

  1. 함수의 행동이 변경되어 원래의 함수스펙(규격)을 더 이상 만족하지 못할 때.
  2. 외래 데이터 아이템이 변했을때(예외:옵션 아이템을 라이브러리의 구조 마 지막 부분에 첨가하는 것은 이러한 아이템구조가 라이브러리의 내부에 할당되 어 있는한 괜찮음)
  3. 외래함수가 제거되었을 때.
  4. 외래함수의 인터페이스가 변경되었을 때.
만약 이러한 원인들을 당신이 피해간다면 당신의 라이브러리는 이진모드에 서 호환가능할 것이다. 다른 식으로 얘기하자면 당신 응용프로그램의 이진 인터페이스(ABI) 호환성은 이러한 변경을 하지않는다면 지켜질 것이다. 예 를들면 당신은 새로운 함수를 추가하되 기존함수들을 삭제하지않길 원할수 있 을 것이다. 당신은 구버전의 프로그램이 라이브러리 구조의 끝부분에 아이템 을 추가하거나, 또는 (응용 프로그램이 아닌)라이브러리에서만 이러한 구조를 할당하거나, 또는 어떤 특정한 아이템을 옵션화하는(또는 라이브러리에서 그 아이템을 채워넣는)등등의 이러한 변경에 영향을 받지않음이 확실할때만 그 라이브러리 구조에 이러한 아이템추가를 할수있는 것이다. 사용자들이 배열을 사용하고 있을시에는 그러한 구조확장을 아마 할수 없을 것임에 유의하라.


다음 이전 차례