용어가 매번 혼동되는데 이번 기회에 조금 정리해 봤다.
'정적/공유/정적 라이브러리' 비교
간단하게 테이블로 정리하면 아래와 같다.
정적(static) 라이브러리 (Static linking) |
공유(shared) 라이브러리 (Dynamic Linking) |
동적(dynamic) 라이브러리 (Dynamic Loading) |
|
실행파일 포함여부 | 포함 | 불포함 (별도파일) | 불포함 (별도파일) |
실행파일 크기 | 크다 | 작다 | 작다 |
확장자 | .a | .so | .so |
라이브러리 로직변경시 | 재컴파일필요 | 컴파일 불필요 | 컴파일 불필요 |
실행파일 컴파일시 | 라이브러리 필요 | 라이브러리 필요 | 라이브러리 불필요 |
실행속도 | 빠름 | 느림 | 빠름 |
로딩시점 | - | 실행시점 | 필요한 시점 |
- 공유 라이브러리의 경우 실행속도가 느린데 이는 별도의 so 파일을 실행시점에 로딩해야 하기 때문이다.
- 정적 라이브러리와 공유 라이브러리의 경우, 실행파일 쪽의 코드는 같다. 빌드방식의 차이만 있다.
- 동적 라이브러리의 경우, 대부분은 공유 라이브러리와 동일하다. 로딩시점과 코드수정 여부의 차이가 있다.
LD_PRELOAD 용법
우선 간단한 사용법은 아래와 같다. 이 경우, main 실행파일을 실행하면서 my.so 라이브러리를 로딩한다.
$ LD_PRELOAD=./my.so ./main
언제 사용할까?
- 공유 라이브러리 실행속도 개선 : 타깃 실행파일 실행 전에 미리 로딩해 주는 것이다.
- 동적 라이브러리 로딩속도 개선 : 필요할 때, 로딩하지 말고 미리 로딩하는 것이다.
- 후킹을 구현 : 동일한 함수명이 존재한다면 LD_PRELOAD로 로딩된 라이브러리가 대상이 된다.
혹시 후킹 할 일이 있으면 다음 링크를 참조하자. (링크)
동적 라이브러리 (Dynamic Loading) 예제 : dlopen, dlsym, dlclose
아래 예제코드를 보자.
#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*);
if (argc != 2) {
fprintf(stderr, "Usage: %s animal_type\n", argv[0]);
return EXIT_FAILURE;
}
// 여기서 라이브러리를 로딩하고...
if (strcmp(argv[1], "dog") == 0) {
handle = dlopen("./libdog.so", RTLD_LAZY);
} else if (strcmp(argv[1], "cat") == 0) {
handle = dlopen("./libcat.so", RTLD_LAZY);
} else {
fprintf(stderr, "Error: unknown animal type: %s\n", argv[1]);
return EXIT_FAILURE;
}
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(argv[1]);
// 라이브러리를 언로딩한다.
dlclose(handle);
return EXIT_SUCCESS;
}
주요 함수는 아래와 같다.
- dlopen : 라이브러리를 메모리로 로딩한다. 참조카운트 +1
- dlsym : 함수이름을 이용해서 주소를 return 해주는 함수이다.
- dlclose : 참조카운트 -1. 만약 참조카운트가 0면 라이브러리를 메모리에서 내린다.
- dlerror : 오류 발생 시, 마지막 오류 내용을 사람이 읽을 수 있는 형태로 리턴해준다.
c++에서의 name mangling과 dlsym
C++로 작성된 so파일의 함수를 호출하기 위해서는 name mangling을 이해해야 한다. 자세한 내용은 여기를 참조하자.
요약하자면 다형성 등을 위해서 컴파일 시점에 메서드/함수명 변경이 발생한다. 이를 name mangling이라고 한다.
아래에 코드를 컴파일해보자.
#include <iostream>
using namespace std;
int def(int a,int b)
{
return a+b;
}
int main() {
cout << "Hello, World!" << endl;
cout << def(1,2) << endl;
return 0;
}
mangling 된 심벌이름은 아래 명령으로 확인할 수 있다. 참고로 mangling 전의 이름을 확인할 수도 있다.
$ nm mybin
0000000000003d78 d _DYNAMIC
00000000000011c9 T _Z3defii
00000000000011e1 T main
$ nm --demangle mybin
0000000000003d78 d _DYNAMIC
00000000000011c9 T def(int, int)
00000000000011e1 T main
name mangling의 문제는 코드상에 구현된 함수명이 실제로 어떤 식으로 변경될지 모른다는 것이다.
name mangling을 강제로 비활성화하는 방법이 extern 'C'이다.
다만 해당 키워드는 C++ 컴파일러에서만 지원됨으로 아래와 같이 사용해야 한다고...
#ifndef __ABC_H__
#define __ABC_H__
#include "other.h"
// 다른 헤더를 내부적으로 포함하면 extern 중첩이 일어난다 --> 컴파일 에러
#ifdef __cplusplus
extern "C" {
#endif
int def(int a,int b);
#ifdef __cplusplus
}
#endif
#endif //__ABC_H__
즉, extern 'C'를 통해서 name mangling을 비활성화하고 dlsym을 통해서 호출해야 한다.
C++에서의 함수에 대한 Dynamic Loading
아래는 main.cpp이다.
#include <iostream>
#include <dlfcn.h>
int main() {
using std::cout;
using std::cerr;
cout << "C++ dlopen demo\n\n";
// open the library
cout << "Opening hello.so...\n";
void* handle = dlopen("hello.so", RTLD_LAZY);
if (!handle) {
cerr << "Cannot open library: " << dlerror() << '\n';
return 1;
}
// load the symbol
cout << "Loading symbol hello...\n";
typedef void (*hello_t)();
hello_t hello = (hello_t) dlsym(handle, "hello");
if (!hello) {
cerr << "Cannot load symbol 'hello': " << dlerror() <<
'\n';
dlclose(handle);
return 1;
}
// use it to do the calculation
cout << "Calling hello...\n";
hello();
// close the library
cout << "Closing library...\n";
dlclose(handle);
}
아래는 hello.cpp이다.
#include <iostream>
extern "C" void hello() {
std::cout << "hello" << '\n';
}
C++에서 클래스에 대한 Dynamic Loading
아래 모든 내용은 다음 링크의 내용을 정리한 것이다.
객체의 생성은 생성자 호출로직이 포함된다.
즉, 실행파일에서 객체를 생성한다는 것은 이미 Dynamic Loading이 아니다. (Dynamic Linking이다)
Dynamic Loading을 위해서는 객체의 생성을 라이브러리에 위임해야 한다.
이를 위해서 아무런 로직이 없는 인터페이스를 실행파일과 라이브러리에서 공유하도록 구현한다.
즉, 실행파일에서는 인터페이스를 사용하고 라이브러리에서는 이를 구현하는 형태이다.
다만 생성을 위한 함수는 mangling 되어서는 안된다.
아래는 공유되는 인터페이스.
#ifndef POLYGON_HPP
#define POLYGON_HPP
class polygon {
protected:
double side_length_;
public:
polygon()
: side_length_(0) {}
void set_side_length(double side_length) {
side_length_ = side_length;
}
virtual double area() const = 0;
};
// the types of the class factories
typedef polygon* create_t();
typedef void destroy_t(polygon*);
#endif
위 인터페이스를 사용하는 실행파일. 당연히 단독으로 컴파일이 가능하다.
#include "polygon.hpp"
#include <iostream>
#include <dlfcn.h>
int main() {
using std::cout;
using std::cerr;
// load the triangle library
void* triangle = dlopen("./triangle.so", RTLD_LAZY);
if (!triangle) {
cerr << "Cannot load library: " << dlerror() << '\n';
return 1;
}
// load the symbols
create_t* create_triangle = (create_t*) dlsym(triangle, "create");
destroy_t* destroy_triangle = (destroy_t*) dlsym(triangle, "destroy");
if (!create_triangle || !destroy_triangle) {
cerr << "Cannot load symbols: " << dlerror() << '\n';
return 1;
}
// create an instance of the class
polygon* poly = create_triangle();
// use the class
poly->set_side_length(7);
cout << "The area is: " << poly->area() << '\n';
// destroy the class
destroy_triangle(poly);
// unload the triangle library
dlclose(triangle);
}
위 인터페이스를 구현하는 라이브러리.
#include "polygon.hpp"
#include <cmath>
class triangle : public polygon {
public:
virtual double area() const {
return side_length_ * side_length_ * sqrt(3) / 2;
}
};
// the class factories
extern "C" polygon* create() {
return new triangle;
}
extern "C" void destroy(polygon* p) {
delete p;
}
아래와 같은 주요 주의점이 있다고 한다.
- create와 delete를 항상 같이 제공하고 delete 또한 라이브러리를 통해서 수행할 것
- 인터페이스의 소멸자는 없거나 virtual 일 것
실제로 위와 유사한 코드는 Android 등 Dynamic Loading을 사용하는 솔루션들에서 사용되고 있다.
ABI(Application Binary Interface)와 name mangling
컴파일러마다 mangling 정책이 다르다고 한다.
그렇다면 C++구현된 라이브러리를 C++ 코드에서 호출할 때도 문제가 발생할 수 있다.
- 다른 컴파일러에 의해서 빌드된 라이브러리를 호출할 때
- 오래된 컴파일러에 의해서 빌드된 라이브러리를 호출할 때 (mangling 정책이 바뀌었다면)
이때 나오는 것이 ABI 콘셉트이다. 자세한 내용은 여기를 참조하자.
요약하자면 아래와 같다.
ABI란 두 개의 바이너리 프로그램 모듈 사이의 인터페이스를 말한다.
ABI는 아래를 포함한다. 즉, mangling을 포함한다.
- CPU 명령어
- 함수 호출, 인자 전달, 결과 리턴
- Mangling
- ...
두 개의 서로 다른 컴파일러가 ABI 호환성이 있다면 mangling 이슈가 없다.
반대로 ABI 호환성이 없다면 mangling 이슈로 linking이 불가능하다.
참고로 버전별로 ABI 호환성이 깨질 수도 있다. (링크)
참고자료
'소프트웨어 > 리눅스' 카테고리의 다른 글
리눅스 부팅시 특정 서비스 제어하기 (systemd) (0) | 2023.08.27 |
---|---|
Boost.Program_options을 통한 명령줄 파싱방법 (0) | 2023.08.27 |
DAC (Discretionary Access Control)와 관련된 명령어들 (0) | 2023.07.26 |