Unix 시스템에서 라이브러리를 공부합니다.
어쩌다 윈도우 시스템쪽 프로그래밍을 하게 되었지만 원래는 유닉스나 리눅스쪽이 더 친했었죠. 그 이유는 명확하기 때문입니다.
M$ 의 윈도우 프로그램은 너무 숨기는것도 많고 명확하지 않은게 많으니...
아무튼 라이브러리에 대해 잘 설명해둔 문서를 긁어왔습니다.
좋은 글을 올려주신 윤상배님 감사합니다.
출처 : http://www.joinc.co.kr/modules/moniwiki/wiki.php/article/library_%B8%B8%B5%E9%B1%E2
- 차례
- 1절. 소개
- 2절. Library 이야기
- 1절. 소개
-
- 2.1절. 라이브러리란 무엇인가
- 2.2절. 라이브러리의 종류
-
- 2.2.1절. 왜 정적라이브러리의 사용을 지양하는가
- 2.1절. 라이브러리란 무엇인가
- 3절. 라이브러리 만들고 사용하기
-
- 3.1절. 라이브러리화 할 코드
- 3.2절. 정적라이브러리 제작
- 3.3절. 공유라이브러리 제작 / 사용
- 3.4절. 동적라이브러리의 사용
- 3.5절. 동적라이브러리를 사용하여 프로그램의 확장성과 유연성을 높이기
- 3.2절. 정적라이브러리 제작
-
- 3.5.1절. 동적라이브러리를 이용한 예제
- 3.1절. 라이브러리화 할 코드
- 4절. 결론
1절. 소개
이 문서는 library 의 사용방법에 대한 내용을 담고 있다. 왜 라이브러리가 필요한지, 라이브러리는 어떤 종류가 있으며, 어떻게 작성할수 있는지, 그리고 어떻게 사용하는지에 대해서 얘기하도록 할것이다. 그리고 중간중간에 이해를 돕기 위한 실제 코딩역시 들어갈 것이다.
라이브러리에 대한 이러저러한 세부적인 내용까지 다루진 않을것이다. 좀더 이론적인 내용을 필요로 한다면 Program Library HOWTO 를 참고하기 바란다. 이 문서에서는 라이브러리를 만들고 활용하는 면에 중점을 둘것이다. 그러므로 위의 문서는 이문서를 읽기전에 대충이라도 한번 읽어보도록 한다.
정적 라이브러리와 공유라이브러리는 일반적인 내용임으로 간단한 설명과 일반적인 예제를 드는 정도로 넘어갈 것이다. 그러나 동적라이브러리에 대해서는 몇가지 다루어야할 이슈들이 있음으로 다른 것들에 비해서 좀더 비중있게 다루게 될것이다.
2절. Library 이야기
2.1절. 라이브러리란 무엇인가
라이브러리란 특정한 코드(함수 혹은 클래스)를 포함하고 있는 컴파일된 파일이다. 이러한 라이브러리를 만드는 이유는 자주 사용되는 특정한 기능을 main 함수에서 분리시켜 놓음으로써, 프로그램을 유지, 디버깅을 쉽게하고 컴파일 시간을 좀더 빠르게 할수 있기 때문이다.
만약 라이브러리를 만들지 않고 모든 함수를 main 에 집어 넣는다면, 수정할때 마다 main 코드를 수정해야 하고 다시 컴파일 해야 할것이다. 당연히 수정하기도 어렵고 컴파일에도 많은 시간이 걸린다.
반면 라이브러리화 해두면 우리는 해당 라이브러리만 다시 컴파일 시켜서 main 함수와 링크 시켜주면 된다. 시간도 아낄뿐더러 수정하기도 매우 쉽다.
2.2절. 라이브러리의 종류
라이브러리에도 그 쓰임새에 따라서 여러가지 종류가 있다(크게 3가지). 가장 흔하게 쓰일수 있는 "정적라이브러리"와 "공유라이브러리", "동적라이브러리" 가 있다.
이들 라이브러리가 서로 구분되어지는 특징은 적재 시간이 될것이다.
- 정적라이브러리
-
정적라이브러리는 object file(.o로 끝나는) 의 단순한 모음이다. 정적라이브러린느 보통 .a 의 확장자를 가진다. 간단히 사용할수 있다. 컴파일시 적재되므로 유연성이 떨어진다. 최근에는 정적라이브러리는 지양되고 있는 추세이다. 컴파일시 적재되므로 아무래도 바이너리크기가 약간 커지는 문제가 있을것이다.
- 공유라이브러리
-
공유라이브러리는 프로그램이 시작될때 적재된다. 만약 하나의 프로그램이 실행되어서 공유라이브러리를 사용했다면, 그뒤에 공유라이브러리를 사용하는 모든 프로그램은 자동적으로 만들어져 있는 공유라이브러리를 사용하게 된다. 그럼으로써 우리는 좀더 유연한 프로그램을 만들수 잇게 된다.
정적라이브러리와 달리 라이브러리가 컴파일시 적재되지 않으므로 프로그램의 사이즈 자체는 작아지지만 이론상으로 봤을때, 라이브러리를 적재하는 시간이 필요할것이므로 정적라이브러리를 사용한 프로그램보다는 1-5% 정도 느려질수 있다. 하지만 보통은 이러한 느림을 느낄수는 없을것이다.
- 동적라이브러리
-
공유라이브러리가 프로그램이 시작될때 적재되는 반면 이것은 프로그램시작중 특정한때에 적재되는 라이브러리이다. 플러그인 모듈등을 구현할때 적합하다. 설정파일등에 읽어들인 라이브러리를 등록시키고 원하는 라이브러리를 실행시키게 하는등의 매우 유연하게 작동하는 프로그램을 만들고자 할때 유용하다.
2.2.1절. 왜 정적라이브러리의 사용을 지양하는가
예전에 libz 라는 라이브러리에 보안 문제가 생겨서 한창 시끄러웠던적이 있다. libz 라이브러리는 각종 서버프로그램에 매우 널리 사용되는 라이브러리였는데, 실제 문제가 되었던 이유는 많은 libz 를 사용하는 프로그램들이 "정적라이브러리" 형식으로 라이브러리를 사용했기 때문에, 버그픽스(bug fix)를 위해서는 문제가 되는 libz 를 사용하는 프로그램들을 다시 컴파일 시켜야 했기 때문이다. 한마디로 버그픽스 자체가 어려웠던게 큰 문제였었다. 도대체 이 프로그램들이 libz 를 사용하고 있는지 그렇지 않은지를 완전하게 알기도 힘들뿐더러, 언제 그많은 프로그램을 다시 컴파일 한단 말인가.
만약 libz 를 정적으로 사용하지 않고 "공유라이브러리" 형태로 사용한다면 bug fix 가 훨씬 쉬웠을것이다. 왜냐면 libz 공유라이브러리는 하나만 있을 것이므로 이것만 업그레이드 시켜주면 되기 때문이다.
아뭏든 이렇게 유연성이 지나치게 떨어진다는 측면이 정적라이브러리를 사용하지 않는 가장 큰 이유가 될것이다. 프로그램들의 덩치가 커지는 문제는 유연성 문제에 비하면 그리큰문제가 되지는 않을것이다.
3절. 라이브러리 만들고 사용하기
이번장에서는 실제로 라이브러리를 만들고 사용하는 방법에 대해서 각 라이브러리 종류별로 알아볼 것이다.
3.1절. 라이브러리화 할 코드
라이브러리의 이름은 libmysum 이 될것이며, 여기에는 2개의 함수가 들어갈 것이다. 하나는 덧셈을 할 함수로 "ysum" 또 하나는 뺄셈을 위한 함수로 "ydiff" 으로 할것이다. 이 라이브러리를 만들기 위해서 mysum.h 와 mysum.c 2개의 파일이 만들어질것이다.
mysum.h
int ysum(int a, int b); int ydiff(int a, int b); |
mysun.c
#include "mysum.h" int ysum(int a, int b) { return a + b; } int ydiff(int a, int b) { return a - b; } |
3.2절. 정적라이브러리 제작
정적라이브러리는 위에서 말했듯이 단순히 오브젝트(.o)들의 모임이다. 오브젝트를 만든다음에 ar 이라는 명령을 이용해서 라이브러리 아카이브를 만들면 된다.
[root@localhost test]# gcc -c mysum.c [root@localhost test]# ar rc libmysum.a mysum.o |
이제 라이브러리가 실제로 사용가능한지 테스트해보도록 하자.
예제 : print_sum.c
#include "mysum.h" #include <stdio.h> #include <string.h> int main() { char oper[5]; char left[11]; char right[11]; int result; memset(left, 0x00, 11); memset(right, 0x00, 11); // 표준입력(키보드)으로 부터 문자를 입력받는다. // 100+20, 100-20 과 같이 연산자와 피연산자 사이에 공백을 두지 않아야 한다. fscanf(stdin, "%[0-9]%[^0-9]%[0-9]", left, oper, right); if (oper[0] == '-') { printf("%s %s %s = %d\n", left, oper, right, ydiff(atoi(left), atoi(right))); } if (oper[0] == '+') { printf("%s %s %s = %d\n", left, oper, right, ysum(atoi(left), atoi(right))); } } |
위의 프로그램을 컴파일 하기 위해서는 라이브러리의 위치와 어떤 라이브러리를 사용할것인지를 알려줘야 한다. 라이브러리의 위치는 '-L' 옵션을 이용해서 알려줄수 있으며, '-l' 옵션을 이용해서 어떤 라이브러리를 사용할것인지를 알려줄수 있다. -l 뒤에 사용될 라이브러리 이름은 라이브러리의 이름에서 "lib"와 확장자 "a"를 제외한 나머지 이름이다. 즉 libmysum.a 를 사용할 것이라면 "-lmysum" 이 될것이다.
[root@localhost test]# gcc -o print_sum print_num.c -L./ -lmysum |
정적라이브러리 상태로 컴파일한 프로그램의 경우 컴파일시에 라이브러리가 포함되므로 라이브러리를 함께 배포할 필요는 없다.
3.3절. 공유라이브러리 제작 / 사용
print_sum.c 가 컴파일되기 위해서 사용할 라이브러리 형태가 정적라이브러리에서 공유라이브러리로 바뀌였다고 해서 print_sum.c 의 코드가 변경되는건 아니다. 컴파일 방법역시 동일하며 단지 라이브러리 제작방법에 있어서만 차이가 날뿐이다.
이제 위의 mysum.c 를 공유라이브러리 형태로 만들어보자. 공유라이브러리는 보통 .so 의 확장자를 가진다.
# gcc -fPIC -c mysum.c # gcc -shared -W1,-soname,libmysutff.so.1 -o libmysum.so.1.0.1 mysum.o # cp libmysum.so.1.0.1 /usr/local/lib # ln -s /usr/local/lib/libmysum.so.1.0.1 /usr/local/lib/libmysum.so |
컴파일 방법은 정적라이브러리를 이용한 코드의 컴파일 방법과 동일하다.
[root@coco test]# gcc -o print_sum print_sum.c -L/usr/local/lib -lmysum |
공유라이브러리는 실행시에 라이브러리를 적재함으로 프로그램을 배포할때는 공유라이브러리도 함께 배포되어야 한다. 그렇지 않을경우 다음과 같이 공유라이브러리를 찾을수 없다는 메시지를 출력하면서 프로그램 실행이 중단될 것이다.
[root@coco library]# ./print_sum ./print_sum: error while loading shared libraries: libmysub.so: cannot open shared object file: No such file or directory |
만약 libmysub.so 가 /usr/my/lib 에 복사되어 있고 환경변수를 통해서 라이브러리의 위치를 알려주고자 할때는 아래와 같이 하면된다.
[root@localhost test]# export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/my/lib |
[root@localhost test]# cat /usr/my/lib >> /etc/ld.so.conf [root@localhost test]# ldconfig |
3.4절. 동적라이브러리의 사용
동적라이브러리라고 해서 동적라이브러리를 만들기 위한 어떤 특별한 방법이 있는것은 아니다. 일반 공유라이브러리를 그대로 쓰며, 단지 실행시간에 동적라이브러리를 호출하기 위한 방법상의 차이만 존재할 뿐이다.
정적/공유 라이브러리가 라이브러리의 생성방법과 컴파일방법에 약간의 차이만 있고 코드는 동일하게 사용되었던것과는 달리 동적라이브러리는 코드자체에 차이가 있다. 그럴수밖에 없는게, 동적라이브러리는 프로그램이 샐행되는 중에 특정한 시점에서 부르고 싶을때 라이브러리를 적재해야 하므로, 라이브러리를 적재하고, 사용하고 해제(free) 하기 위한 코드를 생성해야 하기 때문이다.
linux 에서는 이러한 라이브러리를 호출하기 위한 아래와 같은 함수들을 제공한다. 아래의 함수들은 solaris 에서 동일하게 사용될수 있다.
#include <dlfcn.h> void *dlopen (const char *filename, int flag); const char *dlerror(void); void *dlsym(void *handle, char *symbol); int dlclose(void *handle); |
dlerror 는 dl 관련함수들이 제대로 작동을 수행하지 않았을경우 에러메시지를 되돌려준다. dleooro(), dlsym(), dlclose(), dlopen()중 마지막 호출된 함수의 에러메시지를 되돌려준다.
dlsym 은 dlopen 을 통해서 열린라이브러리를 사용할수 있도록 심볼값을 찾아준다. 심볼이라고 하면 좀 애매한데, 심볼값은 즉 열린라이브러리에서 여러분이 실제로 호출할 함수의이름이라고 생각하면 된다. handle 는 dlopen 에 의해서 반환된 값이다. symbol 은 열린라이브러리에서 여러분이 실제로 부르게될 함수의 이름이다. dlsym 의 리턴값은 dlopen 으로 열린 라이브러리의 호출함수를 가르키게 된다. 리턴값을 보면 void * 형으로 되어 있는데, void 형을 사용하지 말고 호출함수가 리턴하는 형을 직접명시하도록 하자. 이렇게 함으로써 나중에 프로그램을 유지보수가 좀더 수월해진다.
3.5절. 동적라이브러리를 사용하여 프로그램의 확장성과 유연성을 높이기
동적라이브러리는 실행시간에 필요한 라이브러리를 호출할수 있음으로 조금만(사실은 아주 많이겠지만 T.T) 신경쓴다면 매우 확장성높고 유연한 프로그램을 만들수 있다.
동적라이브러리의 가장 대표적인 예가 아마도 Plug-in 이 아닐까 싶다. 만약에 모질라 브라우저가 plug-in 을 지원하지 않는 다면 우리는 새로운 기능들 이 추가될때 마다 브라우저를 다시 코딩하고 컴파일하는 수고를 해야할것이다. 그러나 동적라이브러리를 사용하면 브라우저를 다시 코딩하고 컴파일 할필요 없이, 해당 기능을 지원하는 라이브러리 파일만 받아서 특정 디렉토리에 설치하기만 하면 될것이다. 물론 동적라이브러리를 사용하기만 한다고 해서 이러한 기능이 바로 구현되는 건 아니다. Plug-in 의 효율적인 구성을 위한 표준화된 API를 제공하고 여기에 맞게 Plug-in 용 라이브러리를 제작해야만 할것이다.
우리가 지금까지 얘로든 프로그램을 보면 현재 '+', '-' 연산을 지원하고 있는데, 만약 'x', '/' 연산을 지원하는 라이브러리가 만들어졌다면, 우리는 프로그램의 코딩을 다시해야만 할것이다. 이번에는 동적라이브러리를 이용해서 plug-in 방식의 확장이 가능하도록 프로그램을 다시 만들어 보도록 할것이다.
3.5.1절. 동적라이브러리를 이용한 예제
동적라이브러리를 이용해서 main 프로그램의 재코딩 없이 추가되는 새로운 기능을 추가시키기 위해서는 통일된 인터페이스를 지니는 특정한 형식을 가지도록 라이브러리가 작성되어야 하며, 설정파일을 통하여서 어떤 라이브러리가 불리어져야 하는지에 대한 정보를 읽어들일수 있어야 한다. 그래서 어떤 기능을 추가시키고자 한다면 특정 형식에 맞도록 라이브러리를 제작하고, 설정파일을 변경하는 정도로 만들어진 새로운 라이브러리의 기능을 이용할수 있어야 한다.
설정파일은 다음과 같은 형식으로 만들어진다. 설정파일의 이름은 plugin.cfg 라고 정했다.
+,ysum,libmysum.so -,ydiff,libmysum.so |
다음은 동적라이브러리로 만들어진 print_sum 의 새로운 버젼이다.
예제 : print_sum_dl.c
#include <stdlib.h> #include <stdio.h> #include <dlfcn.h> #include <string.h> struct input_data { char oper[2]; char func[10]; char lib[30]; }; int main(int argc, char **argv) { char oper[2]; char left[11]; char right[11]; char buf[50]; char null[1]; int data_num; struct input_data plug_num[10]; void *handle; int (*result)(int, int); int i; char *error; FILE *fp; // 설정파일을 읽어들이고 // 내용을 구조체에 저장한다. fp = fopen("plugin.cfg", "r"); data_num = 0; while(fgets(buf, 50, fp) != NULL) { buf[strlen(buf) -1] = '\0'; sscanf(buf, "%[^,]%[,]%[^,]%[,]%[^,]", plug_num[data_num].oper, null, plug_num[data_num].func, null, plug_num[data_num].lib); data_num ++; } fclose(fp); printf("> "); memset(left, 0x00, 11); memset(right, 0x00, 11); fscanf(stdin, "%[0-9]%[^0-9]%[0-9]", left, oper, right); // 연산자를 비교해서 // 적당한 라이브러리를 불러온다. for (i = 0; i < data_num ; i++) { int state; if ((state = strcmp(plug_num[i].oper, oper)) == 0) { printf("my operator is : %s\n", plug_num[i].oper); printf("my call function is : %s\n", plug_num[i].func); break; } } if (i == data_num) { printf("--> unknown operator\n"); exit(0); } handle = dlopen(plug_num[i].lib, RTLD_NOW); if (!handle) { printf("open error\n"); fputs(dlerror(), stderr); exit(1); } // 연산자에 적당한 함수를 불러온다. result = dlsym(handle, plug_num[i].func); if ((error = dlerror()) != NULL) { fputs(error, stderr); exit(1); } printf("%s %s %s = %d\n",left, oper, right, result(atoi(left), atoi(right)) ); dlclose(handle); } |
[root@localhost test]# gcc -o print_sum_dl print_sum_dl.c -ldl |
[root@localhost test]# ./print_sum_dl > 99+99 my operator is : + my call function is : ysum 99 + 99 = 198 [root@localhost test]# |
자 이렇게 해서 우리는 '+', '-' 연산이 가능한 프로그램을 하나 만들게 되었다. 그런데 A 라는 개발자가 '*','/' 연산도 있으면 좋겠다고 생각해서 아래와 같은 코드를 가지는 '*', '/' 연산을 위한 라이브러리를 제작하였다.
예제 : mymulti.h
int multi(int a, int b); int div(int a, int b); |
int multi(int a, int b) { return a * b; } int div(int a, int b) { return a / b; } |
A 라는 개발자는 이것을 다음과 같이 공유라이브러리 형태로 만들어서 간단한 라이브러리의 설명과 함께 email 로 전송했다.
[root@localhost test]# gcc -c -fPIC mymulti.c [root@localhost test]# gcc -shared -W1,-soname,libmymulti.so.1 -o libmymulti.so.1.0.1 mymulti.o |
라이브러리를 받았으므로 새로운 라이브러리가 제대로 작동을 하는지 확인을 해보도록 하자. 우선 libmymulti.so.1.0.1 을 /usr/my/lib 로 복사하도록 하자. 그다음 설정파일에 다음과 같은 내용을 가지도록 변경 시키도록 하자.
+,ysum,libmystuff.so -,ydiff,libmystuff.so *,ymulti,libmymulti.so.1.0.1 /,ydiv,libmymulti.so.1.0.1 |
[root@localhost test]# ./print_sum_dl > 10*10 my operator is : * my call function is : ymulti 10 * 10 = 100 [root@localhost test]# ./print_sum_dl > 10/10 my operator is : / my call function is : ydiv 10 / 10 = 1 |
위에서도 말했듯이 이러한 Plug-in 비슷한 기능을 구현하기 위해서는 통일된 함수 API가 제공될수 있어야 한다.
4절. 결론
여기에 있는 내용중 동적라이브러리에 대한 내용은 솔라리스와 리눅스에서만 동일하게 사용할수 있다. Hp-Ux 혹은 윈도우에서는 사용가능하지 않는 방법이다. 이에 대한 몇가지 해법이 존재하는데, 이 내용은 나중에 시간이 되면 다루도록 하겠다. 어쨋든 솔라리스와 리눅스 상에서 코딩되고 윈도우 혹은 다른 유닉스로 포팅될 프로그램이 아니라면 위의 방법을 사용하는데 있어서 문제가 없을것이다.