리눅스(Linux) C++에서 지연 로딩(delay loading) 사용하기 (feat. Implib.so)
1. 지연 로딩 (delay loading) 이란?
보통 프로그램이 실행될 때 동적 라이브러리 (shared library) 들이 메모리로 로딩이 됩니다. 하지만 무조건 라이브러리를 로딩하게 되면 로딩 시간이 길어질 수도 있고, 호환되지 않는 라이브러리들이 동시에 로딩이 되면서 충돌이 발생하기도 합니다. 이런 경우에 delay loading 기법을 사용하면 프로그램이 실행될 때 라이브러리가 로딩되는 것이 아니라, 해당 라이브러리를 사용하는 코드가 실행될 때 로딩이 됩니다. 라이브러리가 늦게(delay) 로딩(loading) 된다고 해서 delay loading 이라고 부릅니다. 지연 로딩을 활용하면 앞서 언급한 이슈들을 해결할 수 있습니다.
2. Windows 에서 지연 로딩
Windows 에서는 Visual Studio 에 기본 기능으로 지연 로딩이 포함되어 있습니다. 아래의 화면에서 dll 들의 목록을 입력하면, 프로그램이 실행될 때 해당 dll 들이 로딩되지 않고, 해당 dll 을 사용하는 코드가 실행될 때 로딩이 됩니다.
3. Linux 에서 지연 로딩
Windows 와 달리 Linux 에서는 간단하게 지연 로딩이 지원되지 않습니다. 하지만 dlopen, dlsym 이라는 시스템 함수를 사용하면 비슷한 기능을 구현할 수 있습니다. 아래 예제를 보면 함수 포인터를 사용해야 해서 가독성이 안좋아지며, Windows 에서는 지원되지 않는 코드이므로 멀티플래폼 코드를 작성한다고 할 때 유지 보수가 어려워지는 단점이 있습니다. 그리고 아래 예제에서는 일반 C 스타일의 함수에 대해서 처리를 하는 경우이고, C++ Class 를 사용해야 한다면 코드가 더욱 더 복잡해 집니다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
int main(int argc, char** argv)
{
void *handle;
void (*func_print_name)(const char*);
handle = dlopen("./libdog.so", RTLD_LAZY);
if (!handle) {
/* fail to load the library */
fprintf(stderr, "Error: %s\n", dlerror());
return EXIT_FAILURE;
}
*(void**)(&func_print_name) = dlsym(handle, "print_name");
if (!func_print_name) {
/* no such symbol */
fprintf(stderr, "Error: %s\n", dlerror());
dlclose(handle);
return EXIT_FAILURE;
}
func_print_name("happy");
dlclose(handle);
return EXIT_SUCCESS;
}
4. Implib.so 소개
리눅스에서 delay loading 을 간편하게 구현할 방법을 고민하다가 멋진 오픈소스 프로젝트를 발견했습니다.
https://github.com/yugr/Implib.so
GitHub - yugr/Implib.so: POSIX equivalent of Windows DLL import libraries
POSIX equivalent of Windows DLL import libraries. Contribute to yugr/Implib.so development by creating an account on GitHub.
github.com
Windows DLL import library 의 동작을 모방에서 만들었다고 합니다. 지연 로딩을 수행할 라이브러리의 심볼들을 미리 생성해서 빌드를 수행하고, 관련 코드가 실행이 될 때 실제 라이브러리를 로딩하는 방식입니다.
Implib.so 에서 수행하는 방식을 간단하게 표현한 코드입니다. 앞서 나온 예제에서는 dlsym() 의 결과를 함수 포인터로 받기 때문에 코드가 복잡해지는 것을 피할 수 없었는데, 여기서는 어셈블리를 활용해서 jump 인스트럭션으로 해당 함수를 호출하고 있습니다. 라이브러리 함수를 호출하는 측면에서는 Implib.so를 적용하기 전/후의 코드에 차이가 없으므로 코드의 유지보수 측면에서 큰 장점이 됩니다.
$ cat lib.h
// Dynamic library header
#ifndef LIB_H
#define LIB_H
extern void foo(int);
extern void bar(int);
extern void baz(int);
#endif
$ cat lib.c
// Dynamic library implementation
#include <stdio.h>
void foo(int x) {
printf("Called library foo: %d\n", x);
}
void bar(int x) {
printf("Called library baz: %d\n", x);
}
void baz(int x) {
printf("Called library baz: %d\n", x);
}
$ cat main.c
// Main application
#include <dlfcn.h>
#include <stdio.h>
#include <lib.h>
// Should be autogenerated
void *fptrs[100];
void init_trampoline_table() {
void *h = dlopen("./lib.so", RTLD_LAZY);
fptrs[0] = dlsym(h, "foo");
fptrs[1] = dlsym(h, "bar");
fptrs[2] = dlsym(h, "baz");
}
int main() {
init_trampoline_table();
printf("Calling wrappers\n");
foo(123);
bar(456);
baz(789);
printf("Returned from wrappers\n");
return 0;
}
$ cat trampolines.S
// Trampoline code.
// Should be autogenerated. Each wrapper gets its own index in table.
// TODO: abort if table wasn't initialized.
.text
.globl foo
foo:
jmp *fptrs(%rip)
.globl bar
bar:
jmp *fptrs+8(%rip)
.globl baz
baz:
jmp *fptrs+16(%rip)
$ gcc -fPIC -shared -O2 lib.c -o lib.so
$ gcc -I. -O2 main.c trampolines.S -ldl
$ ./a.out
Calling wrappers
Called library foo: 123
Called library baz: 456
Called library baz: 789
Returned from wrappers
5. Implib.so 사용방법
가. 코드 생성하기
libxyz.so 가 지연 로딩의 대상이 되는 라이브러리라고 가정하고, 아래 스크립트를 실행하면 libxyz.so.tramp.S 및 libxyz.so.init.c 파일들을 생성해 줍니다.
implib-gen.py libxyz.so
나. 컴파일 하기
gcc (혹은 clang) 에 아래와 비슷한 명령줄 옵션을 사용해서 컴파일 합니다. 그러면 libxyz.so 를 지연 로딩하는 프로그램이 생성이 됩니다.
gcc myfile1.c myfile2.c ... libxyz.so.tramp.S libxyz.so.init.c ... -ldl
6. 결론
자세한 매뉴얼은 Implib.so github 에서 찾아 볼 수 있습니다. 간단한 방법으로 리눅스에서 지연 로딩을 구현할 수 있는 프로젝트이므로 추천드립니다.