환경: GCC 4.1.2 20080704 (Red Hat 4.1.2-46) on CentOS 5.4
프로그램이 실행 중에, 필요한 기능이 구현되어 있는 파일을 찾아서 그 기능을 사용할 수 있도록 하는, 일반적으로 말하는 plug-in 형식을 구현하기 위해서 동적 라이브러리(주로 .so 나 .dll 파일)를 이용한다. Windows의 DLL과 같은 개념인데, C++의 경우 MFC라면 확장 DLL을 이용하여 class 를 다룰 수 있지만 linux의 경우 일반적으로는 class 를 다루지 못하는 것으로 보인다. 대신, 돌아 가서, 라이브러리에서 class로 구현된 객체를 생성/파괴하는 함수를 만들고 C 로 extern 한 이후 사용하면 된다.
(이 글을 읽기 위해서는 C++의 상속/가상함수, 함수 포인터 정도의 개념이 배경지식으로 필요함)
만약 C++ 을 사용하지 않는다면, host 프로그램[각주:1]에서 함수 포인터를 이용하여 기능들을 호출할텐데, C++ 을 사용할 경우에는 class의 member function 을 virtual 로 해 놓은 후, 클래스를 상속받아서 overriding 시킨다. 그 후, host 프로그램에서는 부모 class의 pointer로 자식 클래스를 가리킨 후, virtual 로 된 함수를 호출하면 되겠다. 그림으로 보면 다음과 같다.
동적 library 를 사용하는 프로그램의 모식도.
host 프로그램은 compile 시 순수추상함수 (pure virtual function)를 호출한다. 동적으로 올라갈 library는 순수추상함수를 갖고 있는 클래스를 상속받은 후, host 프로그램에서 호출된 순수추상함수를 overriding 함으로써 기능을 제공한다. 그렇게 되면 host 프로그램에 의해 호출되는 '실제' 함수는 라이브러리에 있는 함수가 될 것이다.
이제 실제 구현을 살펴 보자. 이 글은 간단한 toy-example 로 살펴 본다. 보다 현실적 예는, 공개할 수 있을 때 별도의 글로 공개한다.
우선 동적 라이브러리를 만들어 보자. 실제 라이브러리 파일인 .so 파일을 만들기 전에, 위의 구조상 필요한 가장 상위의 부모 클래스, 즉 plug-in 으로 제공될 기능을 담당하는 함수를 pure virtual function 으로 갖고 있는 클래스를 정의한다.
// abc.h
#ifndef __DEFINITION_OF_ABC__
#define __DEFINITION_OF_ABC__
classABC{
public:
ABC(){};
virtual~ABC(){};
virtualvoid show_message()=0;
};
#endif
가장 최상위 부모 클래스의 이름은 ABC 이고, show_message 라는 함수를 member function 으로 갖는다. 이 함수는 이 클래스를 상속받은 클래스에서 구현해야만 하는 순수가상함수이다.
다음으로 ABC를 상속받고, 실제로 show_message 함수에 기능을 구현할 클래스 ABCC 를 다음과 같이 정의/구현한다.
// abcc.h
#ifndef __DEFINITION_OF_ABCC__
#define __DEFINITION_OF_ABCC__
#include"abc.h"
class ABCC :publicABC{
public:
ABCC();
virtual~ABCC();
virtualvoid show_message();
};
#endif
// abcc.cpp
#include"abcc.h"
#include<iostream>
ABCC::ABCC()
{
}
ABCC::~ABCC()
{
}
void ABCC::show_message()
{
std::cout <<"ABCC"<< std::endl;
}
extern"C"
{
ABC* create()
{
returnnew ABCC();
}
void destroy(ABC* p)
{
delete p;
}
}
중요한 것은 abcc.cpp 파일에서, C++의 클래스를 동적 라이브러리를 호출할 때[각주:2] 밖에서 보일 수 있도록 하기 위해 다음과 같은 함수를 만들어 주어야 한다는 것.
extern"C"
{
ABC* create()
{
returnnew ABCC();
}
void destroy(ABC* p)
{
delete p;
}
}
즉, class ABC를 가리키는 포인터를 반환하되, 실제로 반환하는 것은 ABC를 상속받은 ABCC 를 할당해서 그 포인터를 넘기는 것이다. 이것은 C++에서 그 유명한, 부모 클래스의 포인터는 자식 클래스를 가리킬 수 있다, 가 실제로 사용된 예. ㅋ 여하튼 그처럼 ABCC 를 생성하는 함수와 파괴하는 함수를 C 형식으로 부를 수 있도록 함수 2개를 제공하는데, 후에 host 프로그램에서 이 함수 이름으로 각 개체를 접근할 것이므로 이 함수 이름은 ABC 를 상속받은 모든 클래스에서 동일하게 제공되어야 한다.
ABCC와 비슷하게 ABC에서 상속받은 ABCD 라는 클래스를 다음과 같이 정의/구현한다.
// abcd.h
#ifndef __DEFINITION_OF_ABCD__
#define __DEFINITION_OF_ABCD__
#include"abc.h"
class ABCD :publicABC{
public:
ABCD();
virtual~ABCD();
virtualvoid show_message();
};
#endif
// abcd.cpp
#include"abcd.h"
#include<iostream>
ABCD::ABCD()
{
}
ABCD::~ABCD()
{
}
void ABCD::show_message()
{
std::cout <<"ABCD"<< std::endl;
}
extern"C"
{
ABC* create()
{
returnnew ABCD();
}
void destroy(ABC* p)
{
delete p;
}
}
지금까지의 작업을 잠깐 정리하면, ABC 라는 클래스가 있고, ABC::show_message 라는 함수가 있다. 이 때 ABC::show_message 라는 함수가 실제로 plug-in 으로 구현될 기능을 담당하는 함수이다. 이 함수는 ABC에서는 순수추상함수로 구현되어 있다. host 프로그램에서는 ABC의 포인터로 show_message 를 호출할 것이다. 이 때, ABC를 상속받고 show_message를 적당한 것으로 구현한 클래스의 포인터를 host 프로그램에서 받는다면, host 프로그램에서 실제로 호출되는 show_message 라는 함수가 제대로 정의된 함수인 것이다.
이제 ABCC와 ABCD 를 제공하기 위한 동적 라이브러리를 만들어 보자. gcc 에 의한 compile 은 다음과 같이 한다.
-fPIC, -shared, -Wl(소문자 L), -soname, -o 에 대한 설명은 gcc manual 을 참고한다. 위에서는 libabcc.so.1.0 과 libabcd.so.1.0 으로 동적 라이브러리 파일을 만들고 있다.
이제 이렇게 만든 동적 라이브러리 파일을 실제로 사용하는 host 프로그램을 살펴 보자. plug-in 의 가장 큰 장점이라면 host program 을 재컴파일하지 않고 라이브러리 파일만 특정 위치에 추가시키는 것으로 host program의 기능을 확장할 수 있다는 것이다. 지금 만드는 host 프로그램은 비록 라이브러리 파일 경로를 hard-coding 해 놓았으나, 실제로는 지정된 경로에 있는 라이브러리 파일을 모두 불러가도록 해 놓고 사용할 수 있을 것이다.
차근차근 살펴 보자. 우선 가장 최상위 클래스만 있으면 되므로[각주:3] abc.h 만 include 하면 된다 (줄번호 3). 동적 라이브러리를 열고, 함수를 찾기 위해 필요한 함수는 dlfcn.h 파일에 정의되어 있으므로 이 파일을 include 한다 (줄번호 5). 현재는 라이브러리 파일 경로를 직접 적어 준다 (줄번호 9~10). 이 때, 전체 경로를 지정해 주어야 하며, 그렇지 않을 경우 library 를 찾는 옵션에 따라 그 경로들만 찾게 된다. systemic한 위치에 위의 테스트 라이브러리를 놓고 싶지 않았기 때문에 위처럼 전체 경로를 입력해 주는 방식으로 test host program 을 작성했다.
동적 라이브러리 파일을 열기 위해 dlopen 함수를 이용한다 (줄번호 13). RTLD_NOW나 그 이외의 옵션은 참고 Sites를 참고한다. 열려진 라이브러리 파일에서 'create'라는 함수를 찾는 부분이 줄번호 20에 있다. abcc.cpp 나 abcd.cpp 파일에 C 스타일로 extern 했던 create, destroy 함수가 이 때 찾아지는 것이다. dlsym 함수는 반환값의 type이 void* 인데, 실제로는 함수 포인터를 반환하는 것이므로, create와 prototype이 같은 형태의 함수 포인터 형태로 casting 해준다. 줄번호 20 에 의하여, 동적 라이브러리에 구현되어 있는 create 라는 함수를 creator 라는 변수로 접근할 수 있게 되었다 (요 부분이 조금 혼잡하니 뒤에서 다시 설명).
이제 동적 라이브러리에서 받아 온 create 함수를 호출해 보자. 이 때, create 함수는 ABCC 또는 ABCD의 포인터를 반환하게끔 되어 있었으므로 우리는 이 값을 ABC의 포인터에 저장하자, 왜냐 하면, 부모 클래스의 포인터는 자식 클래스를 가리킬 수 있으므로. 이 부분이 줄번호 21 번이다. create를 creator 로 접근하고 있으므로 줄번호 21번의 = 오른쪽과 같이 호출하고, 반환값은 ABC의 포인터에 저장한다.
이제 실제로 필요했던 기능이 구현된 show_message 함수를 호출해 보자. 그 부분이 바로 줄번호 22. 물론 host 프로그램에서는 ABC::show_message() 로 호출되는 것처럼 되어 있으나, ㅋ, p 가 가리키는 것은 ABCC 혹은 ABCD 이고, 따라서 줄번호 22 에서 호출되는 show_message() 는 ABCC::show_message(), ABCD::show_message() 인 것이다. 이것이 바로 가상함수의 힘!
그 후, 동적으로 할당된 객체를 메모리에서 해제하기 위하여 동적 라이브러리 파일에 구현해 놓은 destroy 함수를 찾아서 (줄번호 23), 객체를 해제한다 (줄번호 24). 바로 이러한 경우 때문에 class의 파괴자는 virtual 로 해야 한다. 즉, destroy 함수는 parameter로 ABC의 포인터를 받아서 delete 를 하고 있지만, 이 때 실제로 호출되는 것은 ABCD와 ABCC도 포함이 되는데, 만약 ABC::~ABC() 가 virtual 이 아니었다면 이와 같은 일이 불가능한 것이다.
메모리가 깔끔하게 해제되었는지 확인해 보기 위하여 destroy 함수를 호출했을 때와 하지 않았을 때의 차이를 valgrind 로 확인해 보자. 우선 24 번째 줄을 주석처리한 후 valgrind를 호출해 보면, 다음과 같은 부분이 있다.
==7995== 8 bytes in 2 blocks are definitely lost in loss record 1 of 1
==7995== at 0x4005B65: operator new(unsigned)
(vg_replace_malloc.c:163)
==7995== by 0x4009BC8: ???
==7995== by 0x8048884: main (in /home/adnoctum/test/test)
[adnoctum@bioism test]$ valgrind --leak-check=full -v --show-reachable=yes ./test
==7995== Memcheck, a memory error detector.
==7995== Copyright (C) 2002-2006, and GNU GPL'd, by Julian Seward et al.
==7995== Using LibVEX rev 1658, a library for dynamic binary translation.
==7995== Copyright (C) 2004-2006, and GNU GPL'd, by OpenWorks LLP.
==7995== Using valgrind-3.2.1, a dynamic binary instrumentation framework.
==7995== Copyright (C) 2000-2006, and GNU GPL'd, by Julian Seward et al.
==7995==
--7995-- Command line
--7995-- ./test
--7995-- Startup, with flags:
--7995-- --leak-check=full
--7995-- -v
--7995-- --show-reachable=yes
--7995-- Contents of /proc/version:
--7995-- Linux version 2.6.18-128.el5PAE (mockbuild@builder16.centos.org) (gcc version 4.1.2 20080704 (Red Hat 4.1.2-44)) #1 SMP Wed Jan 21 11:19:46 EST 2009
--7995-- Arch and hwcaps: X86, x86-sse1-sse2
--7995-- Valgrind library directory: /usr/lib/valgrind
--7995-- Reading syms from /lib/ld-2.5.so (0xAFB000)
--7995-- Reading syms from /home/adnoctum/test/test (0x8048000)
--7995-- Reading syms from /usr/lib/valgrind/x86-linux/memcheck (0x38000000)
--7995-- object doesn't have a dynamic symbol table
--7995-- Reading suppressions file: /usr/lib/valgrind/default.supp
--7995-- REDIR: 0xB10790 (index) redirected to 0x38027D0F (vgPlain_x86_linux_REDIR_FOR_index)
--7995-- Reading syms from /usr/lib/valgrind/x86-linux/vgpreload_core.so (0x4001000)
--7995-- Reading syms from /usr/lib/valgrind/x86-linux/vgpreload_memcheck.so (0x4003000)
==7995== WARNING: new redirection conflicts with existing -- ignoring it
--7995-- new: 0x00B10790 (index ) R-> 0x04006080 index
--7995-- REDIR: 0xB10930 (strlen) redirected to 0x4006250 (strlen)
--7995-- Reading syms from /lib/libdl-2.5.so (0xC89000)
--7995-- Reading syms from /usr/lib/libstdc++.so.6.0.8 (0x80A000)
--7995-- object doesn't have a symbol table
--7995-- Reading syms from /lib/libm-2.5.so (0xC60000)
--7995-- Reading syms from /lib/libgcc_s-4.1.2-20080825.so.1 (0x36C5000)
--7995-- object doesn't have a symbol table
--7995-- Reading syms from /lib/libc-2.5.so (0xB19000)
--7995-- REDIR: 0xB89430 (rindex) redirected to 0x4005F60 (rindex)
--7995-- REDIR: 0xB8A2D0 (memset) redirected to 0x4006540 (memset)
--7995-- REDIR: 0xB89090 (strlen) redirected to 0x4006230 (strlen)
--7995-- REDIR: 0xB84D20 (malloc) redirected to 0x400533B (malloc)
--7995-- REDIR: 0xB849E0 (calloc) redirected to 0x4004668 (calloc)
--7995-- Reading syms from /home/adnoctum/test/libabcd.so.1.0 (0x4009000)
--7995-- REDIR: 0x8BEA90 (operator new(unsigned)) redirected to 0x4005AD9 (operator new(unsigned))
--7995-- REDIR: 0xB8A7C0 (memcpy) redirected to 0x4006C20 (memcpy)
--7995-- Discarding syms at 0x4009000-0x400C000 in /home/adnoctum/test/libabcd.so.1.0 due to munmap()
--7995-- REDIR: 0xB82980 (free) redirected to 0x4004F55 (free)
--7995-- Reading syms from /home/adnoctum/test/libabcc.so.1.0 (0x4009000)
--7995-- Discarding syms at 0x4009000-0x400C000 in /home/adnoctum/test/libabcc.so.1.0 due to munmap()
==7995==
==7995== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 26 from 1)
--7995--
--7995-- supp: 26 Fedora-Core-6-hack3-ld25
==7995== malloc/free: in use at exit: 8 bytes in 2 blocks.
==7995== malloc/free: 12 allocs, 10 frees, 1,642 bytes allocated.
==7995==
==7995== searching for pointers to 2 not-freed blocks.
==7995== checked 91,916 bytes.
==7995==
==7995== 8 bytes in 2 blocks are definitely lost in loss record 1 of 1
==7995== at 0x4005B65: operator new(unsigned) (vg_replace_malloc.c:163)
==7995== by 0x4009BC8: ???
==7995== by 0x8048884: main (in /home/adnoctum/test/test)
==7995==
==7995== LEAK SUMMARY:
==7995== definitely lost: 8 bytes in 2 blocks.
==7995== possibly lost: 0 bytes in 0 blocks.
==7995== still reachable: 0 bytes in 0 blocks.
==7995== suppressed: 0 bytes in 0 blocks.
--7995-- memcheck: sanity checks: 2 cheap, 1 expensive
--7995-- memcheck: auxmaps: 0 auxmap entries (0k, 0M) in use
--7995-- memcheck: auxmaps: 0 searches, 0 comparisons
--7995-- memcheck: SMs: n_issued = 14 (224k, 0M)
--7995-- memcheck: SMs: n_deissued = 0 (0k, 0M)
--7995-- memcheck: SMs: max_noaccess = 65535 (1048560k, 1023M)
--7995-- memcheck: SMs: max_undefined = 0 (0k, 0M)
--7995-- memcheck: SMs: max_defined = 36 (576k, 0M)
--7995-- memcheck: SMs: max_non_DSM = 14 (224k, 0M)
--7995-- memcheck: max sec V bit nodes: 0 (0k, 0M)
--7995-- memcheck: set_sec_vbits8 calls: 0 (new: 0, updates: 0)
--7995-- memcheck: max shadow mem size: 528k, 0M
--7995-- translate: fast SP updates identified: 3,320 ( 89.8%)
--7995-- translate: generic_known SP updates identified: 197 ( 5.3%)
--7995-- translate: generic_unknown SP updates identified: 177 ( 4.7%)
--7995-- tt/tc: 7,068 tt lookups requiring 7,247 probes
--7995-- tt/tc: 7,068 fast-cache updates, 5 flushes
--7995-- transtab: new 2,870 (64,313 -> 1,042,567; ratio 162:10) [0 scs]
--7995-- transtab: dumped 0 (0 -> ??)
--7995-- transtab: discarded 92 (1,609 -> ??)
--7995-- scheduler: 258,735 jumps (bb entries).
--7995-- scheduler: 2/4,352 major/minor sched events.
--7995-- sanity: 3 cheap, 1 expensive checks.
--7995-- exectx: 30,011 lists, 24 contexts (avg 0 per list)
--7995-- exectx: 48 searches, 24 full compares (500 per 1000)
--7995-- exectx: 1 cmp2, 116 cmp4, 0 cmpAll
위에서 보는 바와 같이 leak 이 생겼다. 이젠 24번째 줄을 주석처리 하지 않고 해보면,
==8009== malloc/free: in use at exit: 0 bytes in 0 blocks.
==8009== malloc/free: 12 allocs, 12 frees, 1,642 bytes allocated.
==8009==
==8009== All heap blocks were freed -- no leaks are possible.
[adnoctum@bioism test]$ valgrind --leak-check=full -v --show-reachable=yes ./test
==8009== Memcheck, a memory error detector.
==8009== Copyright (C) 2002-2006, and GNU GPL'd, by Julian Seward et al.
==8009== Using LibVEX rev 1658, a library for dynamic binary translation.
==8009== Copyright (C) 2004-2006, and GNU GPL'd, by OpenWorks LLP.
==8009== Using valgrind-3.2.1, a dynamic binary instrumentation framework.
==8009== Copyright (C) 2000-2006, and GNU GPL'd, by Julian Seward et al.
==8009==
--8009-- Command line
--8009-- ./test
--8009-- Startup, with flags:
--8009-- --leak-check=full
--8009-- -v
--8009-- --show-reachable=yes
--8009-- Contents of /proc/version:
--8009-- Linux version 2.6.18-128.el5PAE (mockbuild@builder16.centos.org) (gcc version 4.1.2 20080704 (Red Hat 4.1.2-44)) #1 SMP Wed Jan 21 11:19:46 EST 2009
--8009-- Arch and hwcaps: X86, x86-sse1-sse2
--8009-- Valgrind library directory: /usr/lib/valgrind
--8009-- Reading syms from /lib/ld-2.5.so (0xAFB000)
--8009-- Reading syms from /home/adnoctum/test/test (0x8048000)
--8009-- Reading syms from /usr/lib/valgrind/x86-linux/memcheck (0x38000000)
--8009-- object doesn't have a dynamic symbol table
--8009-- Reading suppressions file: /usr/lib/valgrind/default.supp
--8009-- REDIR: 0xB10790 (index) redirected to 0x38027D0F (vgPlain_x86_linux_REDIR_FOR_index)
--8009-- Reading syms from /usr/lib/valgrind/x86-linux/vgpreload_core.so (0x4001000)
--8009-- Reading syms from /usr/lib/valgrind/x86-linux/vgpreload_memcheck.so (0x4003000)
==8009== WARNING: new redirection conflicts with existing -- ignoring it
--8009-- new: 0x00B10790 (index ) R-> 0x04006080 index
--8009-- REDIR: 0xB10930 (strlen) redirected to 0x4006250 (strlen)
--8009-- Reading syms from /lib/libdl-2.5.so (0xC89000)
--8009-- Reading syms from /usr/lib/libstdc++.so.6.0.8 (0x80A000)
--8009-- object doesn't have a symbol table
--8009-- Reading syms from /lib/libm-2.5.so (0xC60000)
--8009-- Reading syms from /lib/libgcc_s-4.1.2-20080825.so.1 (0x36C5000)
--8009-- object doesn't have a symbol table
--8009-- Reading syms from /lib/libc-2.5.so (0xB19000)
--8009-- REDIR: 0xB89430 (rindex) redirected to 0x4005F60 (rindex)
--8009-- REDIR: 0xB8A2D0 (memset) redirected to 0x4006540 (memset)
--8009-- REDIR: 0xB89090 (strlen) redirected to 0x4006230 (strlen)
--8009-- REDIR: 0xB84D20 (malloc) redirected to 0x400533B (malloc)
--8009-- REDIR: 0xB849E0 (calloc) redirected to 0x4004668 (calloc)
--8009-- Reading syms from /home/adnoctum/test/libabcd.so.1.0 (0x4009000)
--8009-- REDIR: 0x8BEA90 (operator new(unsigned)) redirected to 0x4005AD9 (operator new(unsigned))
--8009-- REDIR: 0xB8A7C0 (memcpy) redirected to 0x4006C20 (memcpy)
ABCD
--8009-- REDIR: 0x8BD560 (operator delete(void*)) redirected to 0x4004C6C (operator delete(void*))
--8009-- Discarding syms at 0x4009000-0x400C000 in /home/adnoctum/test/libabcd.so.1.0 due to munmap()
--8009-- REDIR: 0xB82980 (free) redirected to 0x4004F55 (free)
--8009-- Reading syms from /home/adnoctum/test/libabcc.so.1.0 (0x4009000)
ABCC
--8009-- Discarding syms at 0x4009000-0x400C000 in /home/adnoctum/test/libabcc.so.1.0 due to munmap()
==8009==
==8009== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 26 from 1)
--8009--
--8009-- supp: 26 Fedora-Core-6-hack3-ld25
==8009== malloc/free: in use at exit: 0 bytes in 0 blocks.
==8009== malloc/free: 12 allocs, 12 frees, 1,642 bytes allocated.
==8009==
==8009== All heap blocks were freed -- no leaks are possible.
--8009-- memcheck: sanity checks: 2 cheap, 1 expensive
--8009-- memcheck: auxmaps: 0 auxmap entries (0k, 0M) in use
--8009-- memcheck: auxmaps: 0 searches, 0 comparisons
--8009-- memcheck: SMs: n_issued = 14 (224k, 0M)
--8009-- memcheck: SMs: n_deissued = 0 (0k, 0M)
--8009-- memcheck: SMs: max_noaccess = 65535 (1048560k, 1023M)
--8009-- memcheck: SMs: max_undefined = 0 (0k, 0M)
--8009-- memcheck: SMs: max_defined = 36 (576k, 0M)
--8009-- memcheck: SMs: max_non_DSM = 14 (224k, 0M)
--8009-- memcheck: max sec V bit nodes: 0 (0k, 0M)
--8009-- memcheck: set_sec_vbits8 calls: 0 (new: 0, updates: 0)
--8009-- memcheck: max shadow mem size: 528k, 0M
--8009-- translate: fast SP updates identified: 3,382 ( 89.8%)
--8009-- translate: generic_known SP updates identified: 198 ( 5.2%)
--8009-- translate: generic_unknown SP updates identified: 182 ( 4.8%)
--8009-- tt/tc: 7,177 tt lookups requiring 7,373 probes
--8009-- tt/tc: 7,177 fast-cache updates, 5 flushes
--8009-- transtab: new 2,913 (65,005 -> 1,054,359; ratio 162:10) [0 scs]
--8009-- transtab: dumped 0 (0 -> ??)
--8009-- transtab: discarded 114 (1,945 -> ??)
--8009-- scheduler: 258,859 jumps (bb entries).
--8009-- scheduler: 2/4,420 major/minor sched events.
--8009-- sanity: 3 cheap, 1 expensive checks.
--8009-- exectx: 30,011 lists, 25 contexts (avg 0 per list)
--8009-- exectx: 50 searches, 25 full compares (500 per 1000)
--8009-- exectx: 0 cmp2, 116 cmp4, 0 cmpAll
언제 봐도 기분 좋은 All heap blocks were freed -- no leaks are possible 가 있다. ㅋ.