Computer Science 기본 지식/운영체제

정적 라이브러리, 동적 라이브러리

로파이 2022. 9. 27. 22:08

라이브러리 관련 지식 정리.

 

Library

라이브러리란 사용하고자하는 함수를 미리 컴파일하여 목적 코드로 만든 이후 목적 코드를 모아 만든 .lib을 링크하여 사용하는 것을 라이브러리라고 한다.


정적 라이브러리 Static Library (.lib 혹은 .a)

정적 라이브러리를 사용하여 자신의 프로젝트 코드를 컴파일 할 때 컴파일러는 사용자 함수에서 라이브러리 함수를 호출하는 경우 해당 함수의 목적 코드를 크게 두 가지 형태로 실행 파일에 담는다.

 

1.  사용자 함수를 목적 코드로 만드는 과정에서 라이브러리 함수 내용도 같이 담을 수 있다. (주로 인라인이 가능한 함수들)

2. 라이브러리 함수나 프로시저의 내용이 클 경우 등 해당 라이브러리 함수나 프로시저 실행 명령어(목적 코드)를 실행 파일에 포함시키되, 특정 주소 오프셋 위치로부터 해당 내용을 모아둔다. 라이브러리 함수 호출 코드는 해당 명령어가 있는 주소로 점프하도록 하는 명령어를 만들어낸다.

 

라이브러리 함수를 참조하는 것을 링커에 의해 수행되는 심볼릭 링크라고도 한다.

 

라이브러리 함수를 모아둔 명령어들의 시작 오프셋 주소는 정적으로 결정되므로 프로세스 실행시 할당 되는 주소 공간의 시작 주소의 일정 오프셋을 통해 참조되어지며 함수를 호출하는 코드에서 해당 주소로 jump하여 라이브러리 함수의 실행 명령어들을 실행하도록 돕는다.

 

최종 실행 파일이 만들어지는 과정은 컴파일러에 따라 다르며 이에 따라 얼마나 링크를 효율적으로 하느냐에 따라 실행 파일의 크기도 다를 수 있다.

 

결국 정적 라이브러리의 함수를 사용할 경우 .lib에 있는 목적 코드 내용을 포함하여 그 실행 내용을 실행 파일 (.exe 혹은 실행 가능한 어떠한 파일)로 만들어 낸다. 

장점과 단점

외부 라이브러리 내용을 직접 실행 파일에 담기 때문에 실행 파일외에 어떠한 것도 필요없게 된다. 그 대신 실행 파일내에 내용이 담기기 때문에 실행 메모리가 커진다는 단점이 있다. 

 

정적 라이브러리를 여러개 참조했어도 컴파일을 성공하였고 실행 파일이 만들어졌다면, 실행 시점에 안전성을 보장받는다. 반에 실제 명령어 내용들을 실행 파일에 담지 않는 동적 라이브러리의 경우 서로 다른 라이브러리의 의존성 때문에 실행 시점에 문제가 생길 수도 있다. 이를 DLL 지옥이라고도 한다.

 

정적 라이브러리 만들기

Visual Studio를 통해서 정적 라이브러리를 만든다.

 

두 라이브러리에 함수들을 정의하고 정적 라이브러리를 만든다.

static_lib1.h/static_lib1.cpp -> static_lib1.lib

#pragma once

#include <string>

namespace lib1 {
	int add_func(int a, int b);
	float add_func(float a, float b);
	double add_func(double a, double b);
	
	// 템플릿의 경우 템플릿 인수 T에 의해 명령어 코드가 만들어 지므로
	// 정적 라이브러리 내용에 포함되지는 않는다. (정적 라이브러리 크기에 영향을 주지 않는다.)
	// 라이브러리가 아닌 프로젝트에서 템플릿 함수/클래스 헤더를 포함하여 사용하는 것과 같다.
	template<typename T>
	static std::string to_my_string(const T& val) {
		return std::to_string(val);
	}
}

템플릿 함수의 경우 cpp 정의 파일에 선언할 수 없기 때문에 정적 라이브러리 사이즈에 영향을 주지 않는다. 실제 명령어 코드는 해당 헤더를 포함하여 템플릿 인수 T를 결정하여 사용하는 시점에 만들어진다. 


static_lib2.h/static_lib2.cpp -> static_lib2.lib

namespace lib2 {
	int sub_func(int a, int b);
	float sub_func(float a, float b);
	double sub_func(double a, double b);
}

두 라이브러리간 의존성은 없는 상태이다.

 

프로젝트 구성

메인 프로젝트에서 두 라이브러리의 헤더 파일을 포함하고 빌드한 두 정적 라이브러리를 링크한다.

CppLibrary 프로젝트에서 두 정적 라이브러리를 링크하고 해당 코드를 빌드한다.

#include <iostream>

#include "static_lib1.h"
#include "static_lib2.h"

#pragma comment(lib, "StaticLib1.lib")
#pragma comment(lib, "StaticLib2.lib")

int main() {

	int result1 = lib1::add_func(10, 11);
	int result2 = lib2::sub_func(6, 5);

	std::cout << lib1::to_my_string(result1) << std::endl;
	std::cout << lib1::to_my_string(result2) << std::endl;

	char c;
	std::cin >> c;

	return 0;
}

CppLibrary를 실행했을 때 잘 실행되며 해당 .exe 파일을 다른 폴더에 옮기거나 정적 라이브러리를 지워도 문제없이 실행된다.

 

static_lib2에서 static_lib1을 참조하는 라이브러리 빌드

static_lib2.cpp에서 static_lib1.h를 포함시키고 static_lib1 라이브러리 함수를 사용한다.

이제 static_lib2 라이브러리는 static_lib1 라이브러리에 의존성이 생긴다.

#include <iostream>

#include "static_lib2.h"

#pragma comment(lib, "StaticLib2.lib")

int main() {

	int result2 = lib2::complex_func(6, 5);

	std::cout << result2 << std::endl;

	char c;
	std::cin >> c;

	return 0;
}

 

라이브러리 의존성 관계

CppLibrary(나의 프로젝트) -> StaticLib2 -> StaticLib1 (위와 같이 빌드 순서를 설정하면 좋다.)

 

라이브러리를 순서대로 잘 빌드만 한다면 컴파일 시점에 자신의 프로젝트를 문제없이 컴파일 가능하고 하나의 독립된 실행 파일을 만들어 낸다.

 

실행 파일 빌드

이제 메인 프로젝트에서 static_lib2.lib만 링크하여 해당 complex_func을 사용하여도 무방하다. (static_lib2 라이브러리에 이미 static_lib1와 관련된 목적 코드를 포함하고 있다.)

 

동적 라이브러리 Dynamic Link Library (.dll 혹은 .so)

 

실행 파일이 사용한 라이브러리 함수들의 실제 내용을 런타임 시점에 확인하게 된다.

 

implicit linking 암묵적 링킹

라이브러리의 어떤 함수를 사용할 것이다라는 정보가 .lib 파일에 담기게된다. 컴파일 시점에 .lib 파일을 참조하여 함수 명세를 포함한 실행파일을 만들어 낸다.

 

실행 시점에는 dll 파일을 참조하여 실제 함수 내용을 메모리에 올리게 된다. 이 때 암묵적 링킹은 위 과정을 전적으로 운영체제에게 맡긴다.

 

explicit linking 명시적 링킹

동적 라이브러리를 다루는 Windows API를 사용하여 DLL를 직접 로드하고 필요한 함수의 주소를 가져와 메모리에 적재하는 방법으로 .lib 파일이 필요없다.

 

장점과 단점

동적 라이브러리는 실행 파일과 독립적이다. 함수 명세가 변경되지 않고 내용만 변경된다면 전체 프로그램 리빌드 없이 DLL 교체만으로도 수정가능하다.

하나의 DLL을 참조하는 여러 프로젝트(프로그램)가 있을 경우 DLL에 포함된 함수 목적 코드 내용은 딱 한 번 메모리에 올라가기 때문에 효율적인 메모리를 사용할 수 있다.

DLL은 실행 시점에 (해당 함수 내용을 메모리로 올리는 과정)링크되므로 성능상 저하가 있을 수 있으며 실행 파일은 실행을 위해 항상 DLL을 필요로 하기 때문에 의존성이 생긴다.

 

동적 라이브러리 만들기

다음 공식 MS의 동적 라이브러리 만들기 내용을 참고하였다.

https://learn.microsoft.com/ko-kr/cpp/build/walkthrough-creating-and-using-a-dynamic-link-library-cpp?view=msvc-170

 

연습: 자체 동적 연결 라이브러리 만들기 및 사용(C++)

C++를 사용하여 Visual Studio에서 Windows DLL(동적 연결 라이브러리)을 만듭니다.

learn.microsoft.com

 

MathLibrary.h

#pragma once

#ifdef MATHLIBRARY_EXPORTS
#define MATHLIBRARY_API __declspec(dllexport)
#else
#define MATHLIBRARY_API __declspec(dllimport)
#endif

// Initialize a Fibonacci relation sequence
// such that F(0) = a, F(1) = b.
// This function must be called before any other function.
extern "C" MATHLIBRARY_API void fibonacci_init(
    const unsigned long long a, const unsigned long long b);

// Produce the next value in the sequence.
// Returns true on success and updates current value and index;
// false on overflow, leaves current value and index unchanged.
extern "C" MATHLIBRARY_API bool fibonacci_next();

// Get the current value in the sequence.
extern "C" MATHLIBRARY_API unsigned long long fibonacci_current();

// Get the position of the current value in the sequence.
extern "C" MATHLIBRARY_API unsigned fibonacci_index();

 

헤더의 핵심은 다음과 같다.

#ifdef MATHLIBRARY_EXPORTS
#define MATHLIBRARY_API __declspec(dllexport)
#else
#define MATHLIBRARY_API __declspec(dllimport)
#endif

MATHLIBRARY_EXPORTS 전처리기 매크로가 정의되어 있다면 MATHLIBARARY_API는 __declspec(dllexport)로 치환된다. 이는 해당 헤더 내용이 현재 동적 라이브러리 빌드를 위한 헤더로 사용되고 있다는 뜻이다.

반대로 매크로가 정의되어 있지 않다면 __declspec(dllimport)로 치환되고 실제 프로젝트를 위해 헤더가 포함되고 있다는 뜻이다.

 

MS 설명 사이트를 참고하여 MathLibrary.cpp도 만들어준다.

동적 라이브러리의 DllMain 진입 프로시저

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

동적 라이브러리에 포함된 DllMain 진입 프로시저는 LoadLibrary 혹은 FreeLibrary와 같은 API를 호출할 때 진입되는 프로시저이다.

 

동적 라이브러리를 빌드하고 메인 프로젝트에서 사용하기

#include <iostream>
#include "MathLibrary.h"
#pragma comment(lib, "MathLibrary.lib")

int main() {

    fibonacci_init(1, 1);

    do {
        std::cout << fibonacci_index() << ": "
            << fibonacci_current() << std::endl;
    } while (fibonacci_next());

    std::cout << fibonacci_index() + 1 <<
        " Fibonacci sequence values fit in an " <<
        "unsigned 64-bit integer." << std::endl;

	return 0;
}

위 방식은 implicit linking을 이용하여 DLL을 사용한 예이다.

MathLibrary.dll은 반드시 실행 파일과 같은 폴더 내에 있어야한다.

 

명시적 링킹 explicit linking으로 동적 라이브러리 사용하기

Windows API LoadLibrary, GetProcAddress, FreeLibrary를 사용한다.

#include <iostream>
#include <Windows.h>
#include "MathLibrary.h"

std::string GetLastErrorAsString();
class FibonacciLibrary {
public:
    typedef void(CALLBACK* LPFN_INIT)(ULONG64, ULONG64);
    typedef UINT(CALLBACK* LPFN_INDEX)();
    typedef ULONG64(CALLBACK* LPFN_CURRENT)();
    typedef bool(CALLBACK* LPFN_NEXT)();

    HMODULE hModule = NULL;
    LPFN_INIT init_func = NULL;
    LPFN_INDEX index_func = NULL;
    LPFN_CURRENT current_func = NULL;
    LPFN_NEXT next_func = NULL;

    FibonacciLibrary() {
        hModule = LoadLibrary(L"C:\\Users\\yhjin\\Desktop\\CppProject\\CppLibrary\\MathLibrary.dll");
        if (hModule == NULL) {
            std::cerr << GetLastErrorAsString() << std::endl;
            return;
        }
      
        init_func = (LPFN_INIT)GetProcAddress(hModule, "fibonacci_init");
        index_func = (LPFN_INDEX)GetProcAddress(hModule, "fibonacci_index");
        current_func = (LPFN_CURRENT)GetProcAddress(hModule, "fibonacci_current");
        next_func = (LPFN_NEXT)GetProcAddress(hModule, "fibonacci_next");
    }
    ~FibonacciLibrary() {
        FreeLibrary(hModule);
    }
};

int main() {

    FibonacciLibrary lib;

    lib.init_func(1, 1);

    do {
        std::cout << lib.index_func() << ": "
            << lib.current_func() << std::endl;
    } while (lib.next_func());

    std::cout << lib.index_func() + 1 <<
        " Fibonacci sequence values fit in an " <<
        "unsigned 64-bit integer." << std::endl;

	return 0;
}