동적 로드 라이브러리 작성 방법
Ashish Bansal
소프트웨어 엔지니어 (Sapient Corporation)
2001년 4월
동적으로 로드가 가능한 라이브러리를 작성하는 방법과 프로세스에 사용할 수 있는 툴을 설명한다. 컴파일 과정, 네이밍 규정과 공유 라이브러리의 작성, 컴파일, 설치 방법 등이 소개되어 있다.
사실, 공유 객체(shared object)는 객체 지향 기술과 관련이 없다. 이 글에서는 Linux 플랫폼상의 동적 링크 라이브러리에 대해 다룰 것이다. (Windows의 DLL과 비교해 볼 것). 여러분도 C 에서 printf ()와 같은 간단한 함수에 라이브러리를 사용하거나 C++ 일반 함수 라이브러리에서 sort()와 같은 복잡한 함수에도 라이브러리를 사용해 본적이 있을 것이다. 라이브러리를 이용하면 프로그래밍이 쉬워지며 개발자들이 작업에 집중하는데 도움이 된다.
소프트웨어를 구축할 때 라이브러리에 대한 의존도는 상당히 높다. 라이브러리를 이용 함으로서 소프트웨어는 다양한 일을 처리할 수 있다. 스크린 상에서 프린트가 가능하고 네트워크에 로그도 할 수 있다. 어떤 라이브러리는 시스템에서 제공을 받는다. 또, 어떤 것은 서드파티 벤더가 작성하기도 하고 사용자 스스로 작성하기도 한다. 컴파일이 실행되는 동안 라이브러리는 애플리케이션으로 링크 된다. 애플리케이션이 많은 라이브러리를 사용하고 모든 코드가 링크 된다면 애플리케이션의 사이즈도 커지게 된다. 공유 라이브러리는 애플리케이션 소스로 링크 되지 않았지만 애플리케이션에 의해서 요청된 순간에 동적으로 로드 된다.
컴파일 절차 (Compilation process)
공유 객체를 본격적으로 구축하기에 앞서 컴파일 절차와 공유 객체에 대해서 알아보자. hello world code의 예제를 이용하여 설명하겠다.
Listing 1: Hello.c; 공유 객체 정의
다음과 같은 명령 행을 사용하여 위 프로그램을 컴파일 해보자:
이로써 hello라는 이름의 실행파일이 만들어진다. 다음은 컴파일 절차이다:
구문 검사: 파일의 구문과 문법을 검사한다.
컴파일(compilation): 파일을 컴파일하여 코드에 맞는 목적 파일을 만든다. 이런 경우 printf ()와 같은 미정(unresolved)의 함수 이름은 앞서 만들어진 목적 파일에서 정해진다. (파일 포맷 참조)
3. 링크: 유닉스의 ld와 비슷한 링커(linker) 라는 개별 프로그램을 불러온다. 링커(linker)는 다양한 라이브러리에서 코드를 찾아가면서 함수와 변수를 해독한다. 예를 들어, printf() 에 맞는 코드는 libc.a (또는 libc.so) 파일에 있다. 표준 세트와는 다른 라이브러리가 필요하다면 라이브러리가 지정되어야 한다.
코드의 컴파일과 링크에 대한 위 명령은 아래와 같이 두 가지로 나뉘어질 수 있다:
이것이 컴파일 과정이다.(-c 옵션이 컴파일을 지정한다.) 두 번째 단계는 링크이다. 실행파일 hello를 만들기 위해 ld 프로그램이 사용된다.
파일 포맷
ELF는 Executable and Linking Format의 이니셜이다. 이 포맷은 Linux를 비롯해서 Unix 플랫폼의 객체 파일에 사용된다. 기본적으로 세가지 타입의 파일이 있다:
relocatable 파일은 실행 파일이나 공유객체 파일, 또는 다른 relocatable 파일을 만들기 위해 다른 객체 필드와 링크 될 수 있는 코드와 데이터를 가지고 있다.
실행파일 에는 실행할 준비가 되어있는 프로그램이 있다.
공유 객체 파일에는 다른 공유 객체나 relocatable 파일로 링크될 수 있는 코드와 데이터가 있다. (객체 파일 포맷은 아래 그림을 참조)
모든 파일에는 ELF 헤더가 있다. 이 ELF 헤더는 파일의 처음부분에 있고 나머지 파일들에게 로드맵 역할을 한다. 섹션은 최소의 단위를 나타내며 파일에서 실행될 수 있고 링크에 필요한 정보가 있다. 섹션 헤더 테이블에는 파일의 섹션에 대한 정보를 포함하고 있다. 프로그램 헤더 테이블이 존재한다면 프로세스 이미지를 만드는 방법이 나온다. 프로세스 이미지는 객체파일이 실행파일이 될 때 사용된다. exec 프로그램은 프로그램 헤더 테이블을 사용하여 프로세스를 복제(fork) 한다. ELF 헤더의 위치가 파일에 항상 존재하는지 주목해야 한다. 다른 부분들이 다른 장소에 나타날 수 있다.
ELF 헤더는 file 프로그램에 의해 사용되고 파일에 대한 정보를 출력한다. (본 글의 유틸리티 프로그램과 툴 참조). libelf 라이브러리 패키지는 ELF 헤더의 정보에 액세스할 수 있는 프로그래밍 인터페이스를 제공한다.
이러한 타입의 링크를 정적 링크 (static link)라고 한다. 라이브러리의 코드는 정적 컴파일을 사용해가면서 애플리케이션과 결합한다. 훌륭한 애플리케이션에는 수백개의 함수들이 라이브러리로서 사용된다. 표준 라이브러리, 제 3의 라이브러리, 내부 라이브러리 등이 있다. 정적 컴파일을 사용하면, 최종 실행파일 사이즈는 매우 커지게 되며 모두가 런타임 동안에 메모리로 로드 된다. 그래서 함수가 사용되든 안되든 상관없이 코드는 메모리에 있게 된다.
필요할 때마다 라이브러리가 메모리에 동적으로 로드 되고, 프로그램의 메모리 사용흔적(footprint)을 줄이고, 애플리케이션을 좀 더 작은 부분으로 나눌 수 있는 메커니즘이 있다면 꽤 유용할 것이다. 배포가 쉬워지고, 설치와 업그레이드 역시 가능하게 될 것이다. 그와 같은 메커니즘은 주로 동적 링크 라이브러리(Windows의 DLL, Linux의 Shared Object)에 존재한다. 이 라이브러리를 사용하는 애플리케이션을 동적 실행파일 이라고 한다.
네이밍 규정
공유 객체를 시작하기 전에 라이브러리의 네이밍 규정에 대해 살펴보자. 정적 라이브러리는 일반적으로 lib 라는 문자로 시작되며. .a 확장자를 가지고 있다. 공유 객체는 두 가지의 다른 이름(soname 과 real name) 이 있다. soname은 접두어 "lib", 라이브러리 이름, ".so" , 그리고 주요 버전 넘버로 구성된다. 경로 정보에 접두사를 붙이면 공식적인 soname이 된다. real name은 컴파일 된 라이브러리 코드가 포함되어 있는 실제 파일 이름이다. real name은 "soname"에 마침표(dot)를 붙이고 그 다음에 마이너 버전 넘버, 또 하나의 마침표, 그 다음에 릴리즈 넘버를 더한 것이다. (릴리즈 넘버는 옵션이다).
Program-Library How-To 에서 정의된 다른 이름인 linker name 이 있다. 이것은 버전 넘버가 없는 soname이라고 할 수 있다. 일반적으로 soname에 링크 된다. 그리고 soname은 real name 에 링크 된다.
예를 들어서 soname, /usr/lib/libhello.so.1 를 살펴보자. 이것은 공식적인 soname 이며 /usr/lib/libhello.so.1.5를 가리키는 링크일 수 있다. 상응하는(corresponding) 링커 이름으로 /usr/lib/libhello.so 가 있을 것이다. 관리해야 할 파일이 많아보인다. 하지만 관리를 도와줄 툴이 있다.(유틸리티 프로그램과 툴의 ldconfig 참조)
이제 본격적으로 공유 객체의 샘플을 작성해 보자. 라이브러리의 soname은 libprint.so.1 이고 실제 이름은 libprint.so.1.0이다. 이로서 라이브러리는 printsring (char*)이라는 한 개의 함수를 가지게 된다. 이 함수는 "String:" 뒤에 이 함수에 아규먼트로 전해진 문자를 출력한다.
공유 라이브러리 작성하기
유용한 라이브러리를 만들기 위해 두 가지의 파일은 기본적으로 작성되어야 한다. 하나는 header 파일 이다. 이 header 파일은 라이브러리에 의해 export 되고 클라이언트의 코드에 include 될 모든 함수를 정의한다. 다른 하나는 공유 라이브러리로서 컴파일 되고 위치가 지정되어야 하는 함수의 정의 파일 이다. 우리의 예제에서 header 파일은 다음과 같은 형태를 가진다.
Listing 2: Libprint.h Code; Header file
라이브러리의 코드는 아주 기본적인 것이다. 다음 listing를 보자.
Listing 3: libprint.c Code
_init(void)과 _fini(void)라는 두개의 특별한 함수가 있다. 이 함수들은 라이브러리가 로드 될 때마다 동적 로더에 의해 자동적으로 호출된다. 함수가 호출될 때마다 나오는 진단 메시지를 출력하도록 하기 위해 두 함수를 libprint.c 코드에 추가시켜보자. 이 두 함수는 디폴트로 제공이 되며 이것을 무시하고 자신의 함수를 작성하는 것도 가능하다.
Listing 4: Code for _init() and _fini()
이제 이것을 Listing 3에 있는 코드에 붙여보자. 매우 간단하다. 자신의 라이브러리를 작성하려면 libprint.c 와 libprint.h의 템플릿을 사용한다. 그리고 나서 적절한 함수를 작성한다. 이제 라이브러리를 컴파일 하자.
공유 라이브러리 컴파일
다음은 라이브러리를 컴파일하는 명령 순서 이다:
$ gcc -fPIC -c libprint.c
$ ld -shared -soname libprint.so.1 -o libprint.so.1.0 -lc libprint.o
gcc 명령행에서 -fPIC 옵션을 주목하자. 이것은 Position-independent Code(위치 독립 코드)를 만들어내는데 있어서 매우 필수적인 옵션이다. 이 명령은 "한 프로세스가 활동할 수 있는 공간 어디서든지 로드 될 수 있는 코드를 만들어내는 것" 이라는 의미가 된다. -fPIC 옵션은 공유 객체에 있어서도 매우 중요하다. 이 옵션을 사용하면 수행되어야 하는 relocation의 넘버는 최소가 된다. 실행파일에 의해 사용되는 공유 객체를 로딩할 때 이 옵션을 위해서 어느 정도의 스페이스가 할당되어야 한다. 텍스트와 데이터 섹션이 어떤 위치에 할당되어야 한다. location이 위치 독립 방식(position-independent way)에 의해서 구현되지 않는다면 많은 relocation이 프로그램에 공유 객체를 로딩하는 프로그램에 의해 수행되고 이는 성능에 불리한 영향을 미친다.
이제 ld로 전달 된 옵션을 살펴보자. -shared 옵션은 output 파일이 공유 라이브러리가 될 것이라는 것을 나타낸다. -soname name 옵션을 지정해서 soname으로 어떤 것이 될 것인지를 정한다. -o name은 공유 객체의 real name을 지정한다. soname과 real name은 라이브러리를 설치하는 동안 사용되기 때문에 이 두개를 지정하는 것은 중요하다.
공유 라이브러리의 설치와 사용
지금까지 라이브러리를 구축해 보았다. 라이브러리를 설치해서 라이브러리를 사용하는 작은 클라이언트 프로그램을 만들어보자. ldconfig 라고 불리는 특별한 프로그램은 공유 라이브러리를 설치하는데 사용된다. 일반적으로 공유 라이브러리는 /usr/lib 이나 lib 또는 /usr/local/lib 에서 설치 될 수 있다. 라이브러리가 만들어지면 그러한 디렉터리 중 하나에 복사되어야 한다. 이제 ldconfig 프로그램을 실행시켜보자.
ldconfig
$ldconfig -v -n .
...:
libprint.so.1 => ./libprint.so.1.0
libprint.so.1 에서 libprint.so.1.0 라는 이름의 심볼릭 링크를 만들었다. 다음의 설치 단계는 링커 이름을 위해 다른 링크를 만드는 것이다:
linker name에 링크하기
$ ln -sf libprint.so libprint.so.1
/usr/lib 이나 lib 또는 /usr/local/lib 디렉터리에서 내용을 복사하려면 super user 권한이 필요하다. 애플리케이션이 실행될 때, 이러한 디렉터리는 애플리케이션이 실행될 때 라이브러리를 찾기 위해 자동적으로 검색된다. super user 권한이 없으면 공유 라이브러리는 어떤 디렉터리에도 설치될 수 있다. 하지만 이러한 공유 라이브러리를 사용하는 실행파일이 실행되기 전에 다른 세팅이 이루어져야 한다. 환경 변수 (environment variable), LD_LIBRARY_PATH는 공유 라이브러리가 위치하고 있는 경로를 지정해야 한다. 공유 라이브러리가 실행파일 로서 같은 디렉터리에 있다면 다음과 같이 해보자:
LD_LIBRARY_PATH
$ export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
이제는 구현된 공유 라이브러리를 사용 할 클라이언트를 컴파일 하고 실행시켜 보자. 공유 라이브러리의 사용은 매우 간단하다. :)
Listing 5: Client.c; 라이브러리에서 printstring() 함수를 사용하는 샘플 클라이언트
이 프로그램은 명령 행을 사용하여 실행파일로 컴파일 될 수 있다:
$ gcc -o client client.c -L. -lprint
이것은 client 라는 실행파일 을 만들어내며 라이브러리가 코드와 같은 디렉터리에 있다는 것을 알 수 있다. 이것은 -L. -lprint 를 보고 알 수 있다. 실행을 하면 다음과 같은 output이 생성된다:
$ client
Inside _init()
In Main!
String: In Main!
Inside _fini()
$
지금까지 공유 객체를 만들고 설치하여 사용해 보았다. 이제, client를 실행시키려고 할 때 어떤 일이 일어나는지 살펴보자. 프로그램이 시작되면 시스템은 이 프로그램이 동적 라이브러리에 의존한다는 것을 인식한다. 그래서 시스템은 /lib/ld-linux.so.X 라는 로더를 호출하여 요청 받은 라이브러리를 로드한다. 실행파일이 어떤 라이브러리에 속하는지를 결정하는 데에 ldd를 사용할 수 있다.
ldd
$ ldd client
libprint.so.1 => ./libprint.so.1
libc.so.6 => /lib/libc.so.6
/lib/ld-linux.so.2=> /lib/ld-linux.so.2
이로써, 파일 클라이언트가 어떤 라이브러리에 의존하는지 알 수 있다. 이러한 라이브러리는 로더에 의해 로드된다. 그리고 상응하는 _init () 섹션이 호출될 것이다. 로더는 우선 환경 변수, LD_LIBRARY_PATH 에 나타난 경로에서 라이브러리를 찾는다. 그리고 나서 /etc/ld.so.conf 에 나타난 표준 경로로 간다. 라이브러리를 찾을 수 없을 때 에러가 발생한다. 정상적 환경에서라면 로더는 라이브러리를 로드하여 정상적으로 프로그램을 실행한다.
유틸리티 프로그램 및 툴
이제 매우 유용한 바이너리 유틸리티인, file, nm, objdump 를 살펴보자.
파일 프로그램은 파일 타입을 알아내는 데에 사용된다. 테스트가 된 파일은 텍스트 파일(ex. libprint.c) 이나 실행파일 (ex. client) 또는 데이터 (ex. /dev/hda5) 가 될 수 있다. 특정파일이 어떤 플랫폼을 위해 컴파일 되었는지, 그리고 무엇보다도 이것이 실행파일 인지 아닌지를 알아내는 데에 있어서 파일은 매우 유용하게 쓰인다. 이것은 다음과 같은 명령 행으로 호출된다:
File
$ file client
client: ELF 32-bit LSB executable, Intel 80386, version 1, dynamically linked
(uses shared libs), not stripped
nm 은 객체에 존재하는 모든 심볼을 심볼을 나열한다. (여기서 말하는 객체는 우리가 일반적으로 말하는 객체 파일이나 라이브러리를 의미한다). nm 뒤에 객체 이름을 주고 nm 이라는 프로그램을 실행시키면 nm이라는 프로그램은 이 객체에 의해 사용되거나 익스포트되는 모든 함수, 심볼, 객체 타입을 리스트한다. 심볼을 예로 들면 정의되지 않았거나, 외부 심볼이 될 수 있으며 global이 되거나 다른 식별자가 될 수 있다. libprint.so대해 nm을 실행시키면 다음과 같은 output이 생긴다:
nm
$ nm libprint.so
00001490 A _DYNAMIC
00001480 A _GLOBAL_OFFSET_TABLE
00001510 A __bss_start
00001510 A _edata
00001510 A _end
00000452 A _etext
00000400 T _fini
000003d8 T _init
000003d8 t gcc2_compiled
U printf@@GLIBC_2.0
00000428 T printstring
objdump 는 목적 파일에 대한 정보를 디스플레이 한다. 이러한 객체 파일에 대한 정보는 명령 행에서 지정될 수 있다. objdump로 전해진 옵션은 어떠한 정보를 디스플레이 해야 할 지를 제어한다. 목적 파일 내부를 자세하게 볼 수 있는 유용한 유틸리티 이다.
원문 : http://www-903.ibm.com/developerworks/k ··· obj.html
Ashish Bansal
소프트웨어 엔지니어 (Sapient Corporation)
2001년 4월
동적으로 로드가 가능한 라이브러리를 작성하는 방법과 프로세스에 사용할 수 있는 툴을 설명한다. 컴파일 과정, 네이밍 규정과 공유 라이브러리의 작성, 컴파일, 설치 방법 등이 소개되어 있다.
사실, 공유 객체(shared object)는 객체 지향 기술과 관련이 없다. 이 글에서는 Linux 플랫폼상의 동적 링크 라이브러리에 대해 다룰 것이다. (Windows의 DLL과 비교해 볼 것). 여러분도 C 에서 printf ()와 같은 간단한 함수에 라이브러리를 사용하거나 C++ 일반 함수 라이브러리에서 sort()와 같은 복잡한 함수에도 라이브러리를 사용해 본적이 있을 것이다. 라이브러리를 이용하면 프로그래밍이 쉬워지며 개발자들이 작업에 집중하는데 도움이 된다.
소프트웨어를 구축할 때 라이브러리에 대한 의존도는 상당히 높다. 라이브러리를 이용 함으로서 소프트웨어는 다양한 일을 처리할 수 있다. 스크린 상에서 프린트가 가능하고 네트워크에 로그도 할 수 있다. 어떤 라이브러리는 시스템에서 제공을 받는다. 또, 어떤 것은 서드파티 벤더가 작성하기도 하고 사용자 스스로 작성하기도 한다. 컴파일이 실행되는 동안 라이브러리는 애플리케이션으로 링크 된다. 애플리케이션이 많은 라이브러리를 사용하고 모든 코드가 링크 된다면 애플리케이션의 사이즈도 커지게 된다. 공유 라이브러리는 애플리케이션 소스로 링크 되지 않았지만 애플리케이션에 의해서 요청된 순간에 동적으로 로드 된다.
컴파일 절차 (Compilation process)
공유 객체를 본격적으로 구축하기에 앞서 컴파일 절차와 공유 객체에 대해서 알아보자. hello world code의 예제를 이용하여 설명하겠다.
Listing 1: Hello.c; 공유 객체 정의
다음과 같은 명령 행을 사용하여 위 프로그램을 컴파일 해보자:
이로써 hello라는 이름의 실행파일이 만들어진다. 다음은 컴파일 절차이다:
구문 검사: 파일의 구문과 문법을 검사한다.
컴파일(compilation): 파일을 컴파일하여 코드에 맞는 목적 파일을 만든다. 이런 경우 printf ()와 같은 미정(unresolved)의 함수 이름은 앞서 만들어진 목적 파일에서 정해진다. (파일 포맷 참조)
3. 링크: 유닉스의 ld와 비슷한 링커(linker) 라는 개별 프로그램을 불러온다. 링커(linker)는 다양한 라이브러리에서 코드를 찾아가면서 함수와 변수를 해독한다. 예를 들어, printf() 에 맞는 코드는 libc.a (또는 libc.so) 파일에 있다. 표준 세트와는 다른 라이브러리가 필요하다면 라이브러리가 지정되어야 한다.
코드의 컴파일과 링크에 대한 위 명령은 아래와 같이 두 가지로 나뉘어질 수 있다:
이것이 컴파일 과정이다.(-c 옵션이 컴파일을 지정한다.) 두 번째 단계는 링크이다. 실행파일 hello를 만들기 위해 ld 프로그램이 사용된다.
파일 포맷
ELF는 Executable and Linking Format의 이니셜이다. 이 포맷은 Linux를 비롯해서 Unix 플랫폼의 객체 파일에 사용된다. 기본적으로 세가지 타입의 파일이 있다:
relocatable 파일은 실행 파일이나 공유객체 파일, 또는 다른 relocatable 파일을 만들기 위해 다른 객체 필드와 링크 될 수 있는 코드와 데이터를 가지고 있다.
실행파일 에는 실행할 준비가 되어있는 프로그램이 있다.
공유 객체 파일에는 다른 공유 객체나 relocatable 파일로 링크될 수 있는 코드와 데이터가 있다. (객체 파일 포맷은 아래 그림을 참조)
모든 파일에는 ELF 헤더가 있다. 이 ELF 헤더는 파일의 처음부분에 있고 나머지 파일들에게 로드맵 역할을 한다. 섹션은 최소의 단위를 나타내며 파일에서 실행될 수 있고 링크에 필요한 정보가 있다. 섹션 헤더 테이블에는 파일의 섹션에 대한 정보를 포함하고 있다. 프로그램 헤더 테이블이 존재한다면 프로세스 이미지를 만드는 방법이 나온다. 프로세스 이미지는 객체파일이 실행파일이 될 때 사용된다. exec 프로그램은 프로그램 헤더 테이블을 사용하여 프로세스를 복제(fork) 한다. ELF 헤더의 위치가 파일에 항상 존재하는지 주목해야 한다. 다른 부분들이 다른 장소에 나타날 수 있다.
ELF 헤더는 file 프로그램에 의해 사용되고 파일에 대한 정보를 출력한다. (본 글의 유틸리티 프로그램과 툴 참조). libelf 라이브러리 패키지는 ELF 헤더의 정보에 액세스할 수 있는 프로그래밍 인터페이스를 제공한다.
이러한 타입의 링크를 정적 링크 (static link)라고 한다. 라이브러리의 코드는 정적 컴파일을 사용해가면서 애플리케이션과 결합한다. 훌륭한 애플리케이션에는 수백개의 함수들이 라이브러리로서 사용된다. 표준 라이브러리, 제 3의 라이브러리, 내부 라이브러리 등이 있다. 정적 컴파일을 사용하면, 최종 실행파일 사이즈는 매우 커지게 되며 모두가 런타임 동안에 메모리로 로드 된다. 그래서 함수가 사용되든 안되든 상관없이 코드는 메모리에 있게 된다.
필요할 때마다 라이브러리가 메모리에 동적으로 로드 되고, 프로그램의 메모리 사용흔적(footprint)을 줄이고, 애플리케이션을 좀 더 작은 부분으로 나눌 수 있는 메커니즘이 있다면 꽤 유용할 것이다. 배포가 쉬워지고, 설치와 업그레이드 역시 가능하게 될 것이다. 그와 같은 메커니즘은 주로 동적 링크 라이브러리(Windows의 DLL, Linux의 Shared Object)에 존재한다. 이 라이브러리를 사용하는 애플리케이션을 동적 실행파일 이라고 한다.
네이밍 규정
공유 객체를 시작하기 전에 라이브러리의 네이밍 규정에 대해 살펴보자. 정적 라이브러리는 일반적으로 lib 라는 문자로 시작되며. .a 확장자를 가지고 있다. 공유 객체는 두 가지의 다른 이름(soname 과 real name) 이 있다. soname은 접두어 "lib", 라이브러리 이름, ".so" , 그리고 주요 버전 넘버로 구성된다. 경로 정보에 접두사를 붙이면 공식적인 soname이 된다. real name은 컴파일 된 라이브러리 코드가 포함되어 있는 실제 파일 이름이다. real name은 "soname"에 마침표(dot)를 붙이고 그 다음에 마이너 버전 넘버, 또 하나의 마침표, 그 다음에 릴리즈 넘버를 더한 것이다. (릴리즈 넘버는 옵션이다).
Program-Library How-To 에서 정의된 다른 이름인 linker name 이 있다. 이것은 버전 넘버가 없는 soname이라고 할 수 있다. 일반적으로 soname에 링크 된다. 그리고 soname은 real name 에 링크 된다.
예를 들어서 soname, /usr/lib/libhello.so.1 를 살펴보자. 이것은 공식적인 soname 이며 /usr/lib/libhello.so.1.5를 가리키는 링크일 수 있다. 상응하는(corresponding) 링커 이름으로 /usr/lib/libhello.so 가 있을 것이다. 관리해야 할 파일이 많아보인다. 하지만 관리를 도와줄 툴이 있다.(유틸리티 프로그램과 툴의 ldconfig 참조)
이제 본격적으로 공유 객체의 샘플을 작성해 보자. 라이브러리의 soname은 libprint.so.1 이고 실제 이름은 libprint.so.1.0이다. 이로서 라이브러리는 printsring (char*)이라는 한 개의 함수를 가지게 된다. 이 함수는 "String:" 뒤에 이 함수에 아규먼트로 전해진 문자를 출력한다.
공유 라이브러리 작성하기
유용한 라이브러리를 만들기 위해 두 가지의 파일은 기본적으로 작성되어야 한다. 하나는 header 파일 이다. 이 header 파일은 라이브러리에 의해 export 되고 클라이언트의 코드에 include 될 모든 함수를 정의한다. 다른 하나는 공유 라이브러리로서 컴파일 되고 위치가 지정되어야 하는 함수의 정의 파일 이다. 우리의 예제에서 header 파일은 다음과 같은 형태를 가진다.
Listing 2: Libprint.h Code; Header file
라이브러리의 코드는 아주 기본적인 것이다. 다음 listing를 보자.
Listing 3: libprint.c Code
_init(void)과 _fini(void)라는 두개의 특별한 함수가 있다. 이 함수들은 라이브러리가 로드 될 때마다 동적 로더에 의해 자동적으로 호출된다. 함수가 호출될 때마다 나오는 진단 메시지를 출력하도록 하기 위해 두 함수를 libprint.c 코드에 추가시켜보자. 이 두 함수는 디폴트로 제공이 되며 이것을 무시하고 자신의 함수를 작성하는 것도 가능하다.
Listing 4: Code for _init() and _fini()
이제 이것을 Listing 3에 있는 코드에 붙여보자. 매우 간단하다. 자신의 라이브러리를 작성하려면 libprint.c 와 libprint.h의 템플릿을 사용한다. 그리고 나서 적절한 함수를 작성한다. 이제 라이브러리를 컴파일 하자.
공유 라이브러리 컴파일
다음은 라이브러리를 컴파일하는 명령 순서 이다:
$ gcc -fPIC -c libprint.c
$ ld -shared -soname libprint.so.1 -o libprint.so.1.0 -lc libprint.o
gcc 명령행에서 -fPIC 옵션을 주목하자. 이것은 Position-independent Code(위치 독립 코드)를 만들어내는데 있어서 매우 필수적인 옵션이다. 이 명령은 "한 프로세스가 활동할 수 있는 공간 어디서든지 로드 될 수 있는 코드를 만들어내는 것" 이라는 의미가 된다. -fPIC 옵션은 공유 객체에 있어서도 매우 중요하다. 이 옵션을 사용하면 수행되어야 하는 relocation의 넘버는 최소가 된다. 실행파일에 의해 사용되는 공유 객체를 로딩할 때 이 옵션을 위해서 어느 정도의 스페이스가 할당되어야 한다. 텍스트와 데이터 섹션이 어떤 위치에 할당되어야 한다. location이 위치 독립 방식(position-independent way)에 의해서 구현되지 않는다면 많은 relocation이 프로그램에 공유 객체를 로딩하는 프로그램에 의해 수행되고 이는 성능에 불리한 영향을 미친다.
이제 ld로 전달 된 옵션을 살펴보자. -shared 옵션은 output 파일이 공유 라이브러리가 될 것이라는 것을 나타낸다. -soname name 옵션을 지정해서 soname으로 어떤 것이 될 것인지를 정한다. -o name은 공유 객체의 real name을 지정한다. soname과 real name은 라이브러리를 설치하는 동안 사용되기 때문에 이 두개를 지정하는 것은 중요하다.
공유 라이브러리의 설치와 사용
지금까지 라이브러리를 구축해 보았다. 라이브러리를 설치해서 라이브러리를 사용하는 작은 클라이언트 프로그램을 만들어보자. ldconfig 라고 불리는 특별한 프로그램은 공유 라이브러리를 설치하는데 사용된다. 일반적으로 공유 라이브러리는 /usr/lib 이나 lib 또는 /usr/local/lib 에서 설치 될 수 있다. 라이브러리가 만들어지면 그러한 디렉터리 중 하나에 복사되어야 한다. 이제 ldconfig 프로그램을 실행시켜보자.
ldconfig
$ldconfig -v -n .
...:
libprint.so.1 => ./libprint.so.1.0
libprint.so.1 에서 libprint.so.1.0 라는 이름의 심볼릭 링크를 만들었다. 다음의 설치 단계는 링커 이름을 위해 다른 링크를 만드는 것이다:
linker name에 링크하기
$ ln -sf libprint.so libprint.so.1
/usr/lib 이나 lib 또는 /usr/local/lib 디렉터리에서 내용을 복사하려면 super user 권한이 필요하다. 애플리케이션이 실행될 때, 이러한 디렉터리는 애플리케이션이 실행될 때 라이브러리를 찾기 위해 자동적으로 검색된다. super user 권한이 없으면 공유 라이브러리는 어떤 디렉터리에도 설치될 수 있다. 하지만 이러한 공유 라이브러리를 사용하는 실행파일이 실행되기 전에 다른 세팅이 이루어져야 한다. 환경 변수 (environment variable), LD_LIBRARY_PATH는 공유 라이브러리가 위치하고 있는 경로를 지정해야 한다. 공유 라이브러리가 실행파일 로서 같은 디렉터리에 있다면 다음과 같이 해보자:
LD_LIBRARY_PATH
$ export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
이제는 구현된 공유 라이브러리를 사용 할 클라이언트를 컴파일 하고 실행시켜 보자. 공유 라이브러리의 사용은 매우 간단하다. :)
Listing 5: Client.c; 라이브러리에서 printstring() 함수를 사용하는 샘플 클라이언트
이 프로그램은 명령 행을 사용하여 실행파일로 컴파일 될 수 있다:
$ gcc -o client client.c -L. -lprint
이것은 client 라는 실행파일 을 만들어내며 라이브러리가 코드와 같은 디렉터리에 있다는 것을 알 수 있다. 이것은 -L. -lprint 를 보고 알 수 있다. 실행을 하면 다음과 같은 output이 생성된다:
$ client
Inside _init()
In Main!
String: In Main!
Inside _fini()
$
지금까지 공유 객체를 만들고 설치하여 사용해 보았다. 이제, client를 실행시키려고 할 때 어떤 일이 일어나는지 살펴보자. 프로그램이 시작되면 시스템은 이 프로그램이 동적 라이브러리에 의존한다는 것을 인식한다. 그래서 시스템은 /lib/ld-linux.so.X 라는 로더를 호출하여 요청 받은 라이브러리를 로드한다. 실행파일이 어떤 라이브러리에 속하는지를 결정하는 데에 ldd를 사용할 수 있다.
ldd
$ ldd client
libprint.so.1 => ./libprint.so.1
libc.so.6 => /lib/libc.so.6
/lib/ld-linux.so.2=> /lib/ld-linux.so.2
이로써, 파일 클라이언트가 어떤 라이브러리에 의존하는지 알 수 있다. 이러한 라이브러리는 로더에 의해 로드된다. 그리고 상응하는 _init () 섹션이 호출될 것이다. 로더는 우선 환경 변수, LD_LIBRARY_PATH 에 나타난 경로에서 라이브러리를 찾는다. 그리고 나서 /etc/ld.so.conf 에 나타난 표준 경로로 간다. 라이브러리를 찾을 수 없을 때 에러가 발생한다. 정상적 환경에서라면 로더는 라이브러리를 로드하여 정상적으로 프로그램을 실행한다.
유틸리티 프로그램 및 툴
이제 매우 유용한 바이너리 유틸리티인, file, nm, objdump 를 살펴보자.
파일 프로그램은 파일 타입을 알아내는 데에 사용된다. 테스트가 된 파일은 텍스트 파일(ex. libprint.c) 이나 실행파일 (ex. client) 또는 데이터 (ex. /dev/hda5) 가 될 수 있다. 특정파일이 어떤 플랫폼을 위해 컴파일 되었는지, 그리고 무엇보다도 이것이 실행파일 인지 아닌지를 알아내는 데에 있어서 파일은 매우 유용하게 쓰인다. 이것은 다음과 같은 명령 행으로 호출된다:
File
$ file client
client: ELF 32-bit LSB executable, Intel 80386, version 1, dynamically linked
(uses shared libs), not stripped
nm 은 객체에 존재하는 모든 심볼을 심볼을 나열한다. (여기서 말하는 객체는 우리가 일반적으로 말하는 객체 파일이나 라이브러리를 의미한다). nm 뒤에 객체 이름을 주고 nm 이라는 프로그램을 실행시키면 nm이라는 프로그램은 이 객체에 의해 사용되거나 익스포트되는 모든 함수, 심볼, 객체 타입을 리스트한다. 심볼을 예로 들면 정의되지 않았거나, 외부 심볼이 될 수 있으며 global이 되거나 다른 식별자가 될 수 있다. libprint.so대해 nm을 실행시키면 다음과 같은 output이 생긴다:
nm
$ nm libprint.so
00001490 A _DYNAMIC
00001480 A _GLOBAL_OFFSET_TABLE
00001510 A __bss_start
00001510 A _edata
00001510 A _end
00000452 A _etext
00000400 T _fini
000003d8 T _init
000003d8 t gcc2_compiled
U printf@@GLIBC_2.0
00000428 T printstring
objdump 는 목적 파일에 대한 정보를 디스플레이 한다. 이러한 객체 파일에 대한 정보는 명령 행에서 지정될 수 있다. objdump로 전해진 옵션은 어떠한 정보를 디스플레이 해야 할 지를 제어한다. 목적 파일 내부를 자세하게 볼 수 있는 유용한 유틸리티 이다.
원문 : http://www-903.ibm.com/developerworks/k ··· obj.html
"Develop" 카테고리의 다른 글
- 컴포넌트를 안전하게 호출하기 (0)2007/05/10
- GDB를 이용한 Linux 소프트웨어의 디버깅 (0)2007/05/04
- 객체 비지향(object disoriented)을 위한 공유 객체 (0)2007/05/04
- Linux 애플리케이션을 위한 DLL 작성하기 (0)2007/05/04
- 리눅스에 네트워크 라우터 구현하기 (0)2007/05/04

수안이의 컴퓨터 연구실



Leave your greetings.