9 minute read

동적 로딩(Dynamic loading)은 런타임에 프로그램에서 코드 모듈을 로드하고 언로드할 수 있는 C++의 강력한 기능입니다. 이 기능은 공유 라이브러리의 동적 로딩을 가능하게 하는 함수 집합인 dlopen API를 통해 제공됩니다.

dlopen API는 네 가지 주요 함수인 dlopen, dlsym, dlclose 및 dlerror로 구성됩니다. 이러한 함수를 사용하면 각각 공유 라이브러리를 열고, 함수나 변수 등의 심볼을 조회하고, 라이브러리를 닫고, 발생할 수 있는 오류를 처리할 수 있습니다.

동적 로딩은 유연한 모듈형 소프트웨어를 제작할 때 특히 중요합니다. 동적 로딩을 통해 개발자는 실행 중인 프로그램을 수정하거나 중지하지 않고도 프로그램의 기능을 확장할 수 있습니다. 이는 새로운 기능이나 모듈을 즉시 추가하고 제거할 수 있는 플러그인 아키텍처와 같은 많은 애플리케이션에서 매우 중요합니다.

이 블로그 게시물에서는 C++에서 동적 로딩의 복잡성에 대해 자세히 살펴보고, 이를 통해 발생하는 문제와 이를 극복하는 방법에 대해 논의합니다. 또한 이러한 개념을 설명하기 위해 실제 예제를 제공합니다. 숙련된 C++ 개발자이든 이제 막 시작한 개발자이든 동적 로딩을 이해하는 것은 프로그래밍 툴킷에 유용한 추가 기능이 될 것입니다.

동적 로딩 이해

동적 로딩은 프로그램이 런타임에 주소 공간에 코드를 로드한 다음 해당 코드를 실행할 수 있도록 하는 메커니즘입니다. 이는 실행이 시작되기 전에 모든 코드가 메모리에 로드되는 정적 로딩과 대조적입니다. 동적 로딩은 정적 로딩이 따라올 수 없는 수준의 유연성과 효율성을 제공합니다. 동적 로딩을 사용하면 프로그램이 필요할 때 필요한 리소스만 사용하고 더 이상 필요하지 않을 때는 해당 리소스를 해제할 수 있습니다.

C++의 경우 동적 로딩은 공유 라이브러리(Linux의 경우 .so 파일, Windows의 경우 .dll 파일)를 로드하는 데 자주 사용됩니다. 이러한 공유 라이브러리에는 프로그램에서 로드된 후 사용할 수 있는 함수, 클래스 또는 변수가 포함될 수 있습니다.

dlopen API는 C++의 동적 로딩에서 중요한 역할을 합니다. 네 가지 주요 기능으로 구성되어 있습니다:

  1. dlopen: 이 함수는 공유 라이브러리를 메모리에 로드하는 데 사용됩니다. 이 함수는 공유 라이브러리의 이름을 인수로 받고 dlopen API의 다른 함수와 함께 사용할 수 있는 핸들을 반환합니다.

  2. dlsym: 공유 라이브러리가 로드되면 이 함수는 라이브러리 내의 심볼(예: 함수 또는 변수)을 조회하는 데 사용됩니다. 이 함수는 dlopen이 반환한 핸들과 심볼 이름을 인자로 받아 심볼에 대한 포인터를 반환합니다.

  3. dlclose: 이 함수는 공유 라이브러리를 메모리에서 언로드하는 데 사용됩니다. 이 함수는 dlopen이 반환한 핸들을 인자로 받습니다. 라이브러리가 언로드되면 해당 심볼은 더 이상 프로그램에서 사용할 수 없습니다.

  4. dlerror: 이 함수는 dlopen API의 다른 함수가 실패할 경우 오류를 설명하는 사람이 읽을 수 있는 문자열을 검색하는 데 사용됩니다.

이러한 함수와 이들이 함께 작동하는 방식을 이해하면 C++ 프로그램에서 동적 로딩의 강력한 기능을 활용할 수 있습니다. 다음 섹션에서는 C++에서 동적 로딩을 사용할 때 발생할 수 있는 몇 가지 문제와 이를 극복하는 방법에 대해 설명합니다.

C++에서 동적 로딩의 문제점

동적 로딩은 C++의 강력한 기능이지만, 여기에는 몇 가지 문제가 있습니다. 두 가지 주요 문제는 이름 맹글링과 클래스 로딩과 관련이 있습니다.

이름 맹글링

이 맹글링은 C++ 컴파일러가 프로그램의 각 함수에 고유한 이름을 생성하는 데 사용하는 기법입니다. C++는 이름은 같지만 매개변수가 다른 여러 함수를 허용하는 함수 오버로딩을 지원하기 때문에 이 기능이 필요합니다. 컴파일러는 이러한 함수를 구분할 방법이 필요하므로 함수의 매개변수와 반환 유형에 대한 정보를 추가하여 이름을 “망글링”합니다.

다음은 간단한 예입니다:

1
2
3
// Original C++ code
void foo(int a) { }
void foo(double a) { }

위 코드가 컴파일되면 컴파일러는 함수 이름을 다음과 같이 변경할 수 있습니다:

1
2
3
// Mangled names
void _Z3fooi(int a) { }
void _Z3food(double a) { }

맹글링된 이름의 정확한 형식은 컴파일러와 컴파일러가 사용하는 호출 규칙에 따라 다릅니다. 이 예제에서 _Z3fooi_Z3food는 각각 foo(int)foo(double)의 맹글링된 이름으로, 유닉스 계열 시스템에서 일반적으로 사용되는 Itanium C++ ABI를 사용하는 컴파일러에서 생성된 이름입니다.

C++ 코드를 -c 옵션(객체 파일을 생성하지만 링크하지는 않음)으로 컴파일한 다음 nm 명령을 사용하여 객체 파일의 심볼을 나열하면 이름 뒤섞기가 실제로 작동하는 것을 확인할 수 있습니다. 예를 들어

1
2
g++ -c myfile.cpp
nm myfile.o

그러면 myfile.o에 있는 심볼 목록이 출력되며, 여기에는 맹글린된 함수 이름도 포함됩니다.

1
2
0000000000000000 T _Z3fooi
000000000000000a T _Z3food

이 출력에서:

  • 첫 번째 열은 심볼의 주소입니다. 이 경우 심볼은 함수이므로 객체 파일에 있는 함수의 주소입니다.
  • 두 번째 열은 심볼의 유형입니다. T는 심볼이 텍스트(코드) 섹션에 있음을 의미합니다.
  • 세 번째 열은 심볼의 이름입니다. _Z3fooi와 ‘_Z3food는 각각 foo(int)foo(double)`의 망글링된 이름입니다.

실제 출력은 컴파일러와 사용 중인 플랫폼에 따라 다를 수 있다는 점에 유의하세요.

이름 맹글링은 C++의 필수 기능이지만 동적 로딩을 복잡하게 만듭니다. 공유 라이브러리에서 심볼을 조회하는 데 사용되는 dlsym 함수는 심볼의 정확한 이름을 필요로 합니다. 그러나 이름 뒤섞임으로 인해 컴파일된 코드의 심볼 이름이 소스 코드의 이름과 다릅니다. 또한 컴파일러마다, 심지어 같은 컴파일러의 다른 버전에서도 이름이 다르게 뒤섞일 수 있으므로 뒤섞인 이름을 예측하기가 어렵습니다.

클래스 로딩

C++에서 동적 로딩의 또 다른 문제는 클래스 로딩입니다. dlopen API는 클래스가 없는 C 언어를 염두에 두고 설계되었습니다. 따라서 API는 클래스를 로드하는 간단한 방법을 제공하지 않습니다.

C++에서 클래스는 단순한 함수(메서드) 모음 그 이상입니다. 데이터(멤버 변수)와 다른 클래스(베이스 클래스)도 포함할 수 있습니다. 공유 라이브러리에서 클래스를 로드할 때는 해당 클래스의 인스턴스를 생성해야 하며, 여기에는 클래스 데이터에 대한 메모리를 할당하고 초기화하는 작업이 포함됩니다. 그러나 dlopen API는 클래스 인스턴스를 생성하는 함수를 제공하지 않습니다.

다음 섹션에서는 이러한 문제를 극복하고 C++에서 동적 로딩을 성공적으로 사용하는 방법에 대해 설명합니다.

C++의 동적 로딩에 대한 해결책

이러한 문제에도 불구하고 C++에서 동적 로딩을 효과적으로 사용할 수 있는 방법이 있습니다. 이 솔루션에는 extern "C" 키워드 사용, dlsym 함수의 영리한 사용, 다형성 및 클래스 팩토리 함수의 적용이 포함됩니다.

동적 로딩에 extern “C” 사용

C++에서 external "C" 키워드는 컴파일러가 지정된 코드를 C로 작성된 것처럼 취급하도록 지시하는 데 사용됩니다. 이렇게 하면 해당 코드에 대한 이름 망글링을 해제하는 효과가 있어 동적으로 로드하기가 훨씬 쉬워집니다.

external "C"로 함수를 선언하면 컴파일러가 이름을 뒤섞지 않으므로 dlsym을 사용하여 원래 이름을 사용하여 함수를 조회할 수 있습니다. 하지만 몇 가지 제한 사항이 있습니다. 비회원 함수만 external "C"로 선언할 수 있으며, 오버로드할 수 없습니다.

dlsym으로 함수 로드하기

함수를 external "C"로 선언한 후에는 dlsym을 사용하여 함수를 동적으로 로드할 수 있습니다. dlsymdlopen이 반환한 핸들과 함수 이름을 전달하면 dlsym은 함수에 대한 포인터를 반환합니다. 그런 다음 이 포인터를 통해 함수를 호출할 수 있습니다.

dlopen API로 클래스 로드하기

클래스를 동적으로 로드하는 것은 조금 더 복잡하지만 다형성 및 클래스 팩토리 함수를 사용하여 수행할 수 있습니다. 기본 아이디어는 프로그램에서 기본 클래스를 정의하고 공유 라이브러리에서 파생 클래스를 정의하는 것입니다. 기본 클래스에는 파생 클래스가 재정의하는 가상 함수가 있어야 합니다.

공유 라이브러리에는 파생 클래스 외에도 파생 클래스의 인스턴스를 생성하고 이에 대한 포인터를 반환하는 “create” 함수와 파생 클래스의 인스턴스에 대한 포인터를 가져와 삭제하는 “destroy” 함수의 두 가지 external "C" 함수가 포함되어야 합니다.

공유 라이브러리에서 클래스를 사용하려면 일반 함수를 로드할 때와 마찬가지로 dlsym을 사용하여 “create” 및 “destroy” 함수를 로드합니다. 그런 다음 “create” 함수를 호출하여 클래스의 인스턴스를 생성하고, 작업이 끝나면 “destroy” 함수를 호출하여 삭제할 수 있습니다.

다음 섹션에서는 C++에서 함수와 클래스를 동적으로 로드하는 방법을 보여주는 몇 가지 코드 예제를 살펴보겠습니다.

실용적인 예제

이제 dlopen API를 사용하여 C++에서 함수와 클래스를 동적으로 로드하는 방법을 보여주는 몇 가지 실용적인 예제를 살펴 보겠습니다.

함수 로드하기

먼저 동적으로 로드하려는 함수가 포함된 간단한 공유 라이브러리를 고려해 보겠습니다. 공유 라이브러리는 다음과 같이 보일 수 있습니다:

1
2
3
4
5
extern "C" {
    void hello() {
        std::cout << "Hello, world!" << std::endl;
    }
}

이 함수를 동적으로 로드하려면 다음 코드를 사용할 수 있습니다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <dlfcn.h>
#include <iostream>

int main() {
    void* handle = dlopen("./libhello.so", RTLD_LAZY);
    if (!handle) {
        std::cerr << "Cannot open library: " << dlerror() << '\n';
        return 1;
    }

    typedef void (*hello_t)();

    dlerror();
    hello_t hello = (hello_t) dlsym(handle, "hello");
    const char *dlsym_error = dlerror();
    if (dlsym_error) {
        std::cerr << "Cannot load symbol 'hello': " << dlsym_error << '\n';
        dlclose(handle);
        return 1;
    }

    hello();

    dlclose(handle);
}

이 코드는 공유 라이브러리를 열고 hello 함수를 조회하여 호출한 다음 라이브러리를 닫습니다.

클래스 로드

클래스를 로드하는 것은 조금 더 복잡합니다. 동적으로 로드하려는 파생 클래스가 포함된 공유 라이브러리를 고려해 보겠습니다. 공유 라이브러리는 다음과 같이 보일 수 있습니다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base {
public:
    virtual void hello() = 0;
};

class Derived : public Base {
public:
    void hello() override {
        std::cout << "Hello, world!" << std::endl;
    }
};

extern "C" Base* create() {
    return new Derived;
}

extern "C" void destroy(Base* p) {
    delete p;
}

이 클래스를 동적으로 로드하려면 다음 코드를 사용할 수 있습니다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <dlfcn.h>
#include <iostream>

class Base {
public:
    virtual void hello() = 0;
};

typedef Base* create_t();
typedef void destroy_t(Base*);

int main() {
    void* handle = dlopen("./libhello.so", RTLD_LAZY);
    if (!handle) {
        std::cerr << "Cannot open library: " << dlerror() << '\n';
        return 1;
    }

    create_t* create = (create_t*) dlsym(handle, "create");
    destroy_t* destroy = (destroy_t*) dlsym(handle, "destroy");

    Base* p = create();
    p->hello();
    destroy(p);

    dlclose(handle);
}

이 코드는 공유 라이브러리를 열고, ‘create’ 및 ‘destroy’ 함수를 조회한 다음, 이를 사용하여 파생 클래스의 인스턴스를 생성 및 소멸한 다음 라이브러리를 닫습니다.

이 예제를 통해 자체 C++ 프로그램에서 동적 로딩을 사용하기 위한 좋은 출발점이 될 것입니다. 다음 섹션에서는 C++의 동적 로딩에 대해 자주 묻는 몇 가지 질문을 다루겠습니다.

자주 묻는 질문

C++의 동적 로딩은 복잡한 주제일 수 있으므로 질문이 생기는 것은 당연합니다. 다음은 C++의 동적 로딩에 대한 가장 일반적인 질문과 오해에 대한 답변입니다.

Q1: 공유 라이브러리에서 함수나 클래스를 로드할 수 있나요?

답변: 정확히는 아닙니다. 외부 “C”`로 선언된 함수만 로드할 수 있으며, 클래스는 클래스의 인스턴스를 생성하는 팩토리 함수를 통해서만 로드할 수 있습니다. 이는 dlopen API가 C 언어를 염두에 두고 설계되었으며, C는 클래스나 함수 오버로딩을 지원하지 않아 이름 변경이 필요하기 때문입니다.

Q2: 동적 로딩을 사용하여 실행 중인 프로그램을 중지하지 않고 기능을 추가하거나 제거할 수 있나요?

답변: 예, 동적 로딩의 주요 이점 중 하나입니다. 프로그램이 실행되는 동안 공유 라이브러리를 로드하고 해당 함수나 클래스를 사용한 다음 언로드할 수 있습니다. 이는 새로운 기능을 즉시 추가하고 제거할 수 있는 플러그인 아키텍처에서 일반적으로 사용됩니다.

Q3: 동적 로딩이 효율적인가요?

답변: 동적 로딩은 프로그램이 필요할 때 필요한 리소스만 사용할 수 있기 때문에 정적 로딩보다 더 효율적일 수 있습니다. 하지만 공유 라이브러리를 로드하고 언로드하는 데 약간의 오버헤드가 발생하므로 항상 최상의 솔루션은 아닙니다. 프로그램의 특정 요구 사항에 따라 다릅니다.

Q4: 모든 운영 체제에서 C++의 동적 로딩을 사용할 수 있나요?

답변: dlopen API는 POSIX 표준의 일부이므로 Linux, macOS, BSD와 같은 모든 유닉스 계열 운영 체제를 포함한 모든 POSIX 호환 운영 체제에서 사용할 수 있습니다. Windows에서는 유사한 LoadLibrary 및 GetProcAddress 함수를 사용할 수 있습니다.

Q5: 공유 라이브러리에서 C++ 코드를 C 프로그램으로 로드할 수 있나요?

답변: 가능하지만 까다로울 수 있습니다. C++ 코드는 외부 세계에 C 인터페이스를 제공하는 방식으로 작성되어야 합니다. 즉, 내보낸 모든 함수에 external "C"를 사용하고, C 코드로 전파될 수 있는 예외를 던지지 않아야 합니다. 또한 C++ 코드가 정적 생성자나 C++ 표준 라이브러리와 같은 C++ 런타임 기능에 의존해서는 안 되며, 이러한 기능이 제대로 초기화되고 C 환경에서 사용 가능하다는 것을 보장할 수 없다면 C++ 코드에 의존하지 않아야 합니다.

다음 섹션에서는 논의된 주요 사항을 요약하여 이 블로그 게시물을 마무리하겠습니다.

결론

동적 로딩은 프로그램이 런타임에 코드 모듈을 로드하고 언로드할 수 있도록 하는 C++의 강력한 기능입니다. 이 기능은 공유 라이브러리의 동적 로딩을 가능하게 하는 함수 집합인 dlopen API를 통해 제공됩니다.

이 블로그 게시물에서는 C++에서 동적 로딩의 복잡성을 살펴보고, 이를 통해 발생하는 문제와 이를 극복하는 방법에 대해 논의했습니다. 네임 맹글링과 이것이 동적 로딩을 복잡하게 만드는 방식에 대해 알아보고, dlopen API를 사용하여 클래스를 로드할 때의 문제점에 대해서도 논의했습니다.

이러한 문제를 극복하기 위해 동적 로딩을 위해 C++에서 external "C"를 사용하는 방법을 소개했으며, dlsym을 사용하여 함수를 로드하는 방법에 대해 논의했습니다. 또한 다형성과 클래스 팩토리 함수를 통해 dlopen API를 사용하여 클래스를 로드하는 방법도 살펴봤습니다.

이러한 개념을 설명하기 위해 실제 예제를 제공했으며, dlopen API를 사용해 C++에서 함수와 클래스를 동적으로 로드하는 방법을 시연했습니다. 또한 C++의 동적 로딩에 대한 몇 가지 일반적인 질문과 오해를 해결했습니다.

동적 로딩은 복잡한 주제이지만, 이 블로그 게시물에 제공된 정보를 통해 C++ 프로젝트에서 동적 로딩을 실험해 볼 수 있는 준비가 되어 있을 것입니다. 동적 로딩은 소프트웨어를 더욱 유연하고 효율적으로 만들 수 있는 강력한 도구이므로 두려워하지 말고 바로 학습을 시작하세요.

가장 좋은 학습 방법은 실행하는 것임을 기억하세요. 이제 C++ 프로젝트에서 동적 로딩을 실험해 보세요. 즐거운 코딩이 되시길 바랍니다!

References

  1. DocbookSgml/C++-dlopen
  2. Linux dynamic library dlopen, dlsym, dlclose, dlerror 사용법
  3. Dynamic Class Loading
  4. C++ how to use dlopen in C++
  5. IT-Note
  6. C++-dlopen

Tags: , , , , , , , , , , , , , , , , , , ,

Categories:

Source File: 2023-06-02-dynamic-loading-in-cpp.md

Updated:

Comments