9. Dynamic-Link Library

Dynamic-Link Library (DLL)

Introduction

Оба .exe и .dll считаются переносимыми форматами исполняемых файлов, однако между ними есть различия. В этом модуле объясняется разница между этими двумя типами файлов.

What is a DLL?

DLL - это разделяемые библиотеки исполняемых функций или данных, которые могут использоваться несколькими приложениями одновременно. Они используются для экспорта функций для использования процессом. В отличие от EXE-файлов, DLL-файлы не могут самостоятельно выполнять код. Вместо этого библиотеки DLL должны вызываться другими программами для выполнения кода. As previously mentioned, the CreateFileW is exported from kernel32.dll, therefore if a process wants to call that function it would first need to load kernel32.dll into its address space.

Some DLLs are automatically loaded into every process by default since these DLLs export functions that are necessary for the process to execute properly. A few examples of these DLLs are ntdll.dllkernel32.dll and kernelbase.dll. The image below shows several DLLs that are currently loaded by the explorer.exe process.

System-Wide DLL Base Address

The Windows OS uses a system-wide DLL base address to load some DLLs at the same base address in the virtual address space of all processes on a given machine to optimize memory usage and improve system performance. The following image shows kernel32.dll being loaded at the same address (0x7fff9fad0000) among multiple running processes.

Why Use DLLs?

There are several reasons why DLLs are very often used in Windows:

  1. Modularization of Code - Instead of having one massive executable that contains the entire functionality, the code is divided into several independent libraries with each library being focused on specific functionality. Modularization makes it easier for developers during development and debugging.
  1. Code Reuse - DLLs promote code reuse since a library can be invoked by multiple processes.
  1. Efficient Memory Usage - When several processes need the same DLL, they can save memory by sharing that DLL instead of loading it into the process's memory.

DLL Entry Point

DLLs can optionally specify an entry point function that executes code when a certain task occurs such as when a process loads the DLL library. There are 4 possibilities for the entry point being called:

Sample DLL Code

The code below shows a typical DLL code structure.

BOOL APIENTRY DllMain(
    HANDLE hModule,             // Handle to DLL module
    DWORD ul_reason_for_call,   // Reason for calling function
    LPVOID lpReserved           // Reserved
) {

    switch (ul_reason_for_call) {
        case DLL_PROCESS_ATTACHED: // A process is loading the DLL.
        // Do something here
        break;
        case DLL_THREAD_ATTACHED: // A process is creating a new thread.
        // Do something here
        break;
        case DLL_THREAD_DETACH: // A thread exits normally.
        // Do something here
        break;
        case DLL_PROCESS_DETACH: // A process unloads the DLL.
        // Do something here
        break;
    }
    return TRUE;
}

Exporting a Function

DLLs can export functions that can then be used by the calling application or process. To export a function it needs to be defined using the keywords extern and __declspec(dllexport). An example exported function HelloWorld is shown below.

////// sampleDLL.dll //////

extern __declspec(dllexport) void HelloWorld(){
// Function code here
}

Dynamic Linking

It's possible to use the LoadLibraryGetModuleHandle and GetProcAddress WinAPIs to import a function from a DLL. This is referred to as dynamic linking. This is a method of loading and linking code (DLLs) at runtime rather than linking them at compile time using the linker and import address table.

There are several advantages of using dynamic linking, these are documented by Microsoft here.

This section walks through the steps of loading a DLL, retrieving the DLL's handle, retrieving the exported function's address and then invoking the function.

Загрузка DLL

Calling a function such as MessageBoxA in an application will force the Windows OS to load the DLL exporting the MessageBoxA function into the calling process's memory address space, which in this case is user32.dll. Loading user32.dll was done automatically by the OS when the process started and not by the code.

However, in some cases such as the HelloWorld function in sampleDLL.dll, the DLL may not be loaded into memory. For the application to call the HelloWorld function, it first needs to retrieve the DLL's handle that is exporting the function. If the application doesn't have sampleDLL.dll loaded into memory, it would require the usage of the LoadLibrary WinAPI, as shown below.

HMODULE hModule = LoadLibraryA("sampleDLL.dll"); // hModule now contain sampleDLL.dll's handle

Получение хэндла DLL

If sampleDLL.dll is already loaded into the application's memory, one can retrieve its handle via the GetModuleHandle WinAPI function without leveraging the LoadLibrary function.

HMODULE hModule = GetModuleHandleA("sampleDLL.dll");

Получение адреса функции

Once the DLL is loaded into memory and the handle is retrieved, the next step is to retrieve the function's address. This is done using the GetProcAddress WinAPI which takes the handle of the DLL that exports the function and the function name.

PVOID pHelloWorld = GetProcAddress(hModule, "HelloWorld");

Вызов функции

Once HelloWorld's address is saved into the pHelloWorld variable, the next step is to perform a type-cast on this address to HelloWorld's function pointer. This function pointer is required in order to invoke the function.

// Constructing a new data type that represents HelloWorld's function pointer
typedef void (WINAPI* HelloWorldFunctionPointer)();

void call(){
    HMODULE hModule = LoadLibraryA("sampleDLL.dll");
    PVOID pHelloWorld = GetProcAddress(hModule, "HelloWorld");
    // Type-casting the 'pHelloWorld' variable to be of type 'HelloWorldFunctionPointer'
    HelloWorldFunctionPointer HelloWorld = (HelloWorldFunctionPointer)pHelloWorld;
    HelloWorld();   // Calling the 'HelloWorld' function via its function pointer
}

Dynamic Linking Example

The code below demonstrates another simple example of dynamic linking where MessageBoxA is called. The code assumes that user32.dll, the DLL that exports that function, isn't loaded into memory. Recall that if a DLL isn't loaded into memory the usage of LoadLibrary is required to load that DLL into the process's address space.

typedef int (WINAPI* MessageBoxAFunctionPointer)( // Constructing a new data type, that will represent MessageBoxA's function pointer
  HWND          hWnd,
  LPCSTR        lpText,
  LPCSTR        lpCaption,
  UINT          uType
);

void call(){
    // Retrieving MessageBox's address, and saving it to 'pMessageBoxA' (MessageBoxA's function pointer)
    MessageBoxAFunctionPointer pMessageBoxA = (MessageBoxAFunctionPointer)GetProcAddress(LoadLibraryA("user32.dll"), "MessageBoxA");
    if (pMessageBoxA != NULL){
        // Calling MessageBox via its function pointer if not null
        pMessageBoxA(NULL, "MessageBox's Text", "MessageBox's Caption", MB_OK);
    }
}

Указатели функций

В оставшейся части курса типы данных указателей функций будут иметь соглашение об именовании, в котором используется имя WinAPI с префиксом fn, что означает "указатель функции". Например, приведенное выше MessageBoxAFunctionPointer тип данных будет представлен в виде fnMessageBoxA. Это сделано для упрощения и повышения наглядности всего курса.

Rundll32.exe

Существует несколько способов запуска экспортированных функций без использования программных методов. Одним из распространенных способов является использование двоичного файла rundll32.exe. Rundll32.exe это встроенный в Windows двоичный файл, который используется для запуска экспортируемой функции DLL-файла. Для запуска экспортируемой функции используйте следующую команду:

rundll32.exe <dllname>, <function exported to run>

К примеру, User32.dll экспортирует функцию LockWorkStation которая блокирует машину. Чтобы запустить функцию, выполните следующую команду:

rundll32.exe user32.dll,LockWorkStation

Создание DLL-файла с помощью Visual Studio

Для создания DLL-файла запустите Visual studio и создайте новый проект. В списке шаблонов проекта выберите Dynamic-Link Library (DLL) опция.

Далее выберите место сохранения файлов проекта. После этого должен появиться следующий код на языке Си.

Предоставляемый шаблон DLL поставляется с framework.hpch.h и pch.cpp которые известны как прекомпилированные заголовки. Эти файлы используются для ускорения компиляции больших проектов. Вряд ли они понадобятся в данной ситуации, поэтому рекомендуется удалить эти файлы. Для этого выделите файл, нажмите клавишу delete и выберите опцию 'Delete'.

После удаления прекомпилированных заголовков необходимо изменить настройки компилятора по умолчанию, чтобы подтвердить, что прекомпилированные заголовки не должны использоваться в проекте.

Go to C/C++ > Advanced Tab

Измените опцию 'Precompiled Header' на 'Not Using Precompiled Headers' и нажмите 'Apply'.

Finally, change the dllmain.cpp file to dllmain.c. Это необходимо, поскольку в представленных в Maldev Academy фрагментах кода используется язык C, а не C++. Чтобы скомпилировать программу, нажмите Build > Build Solution, и в зависимости от конфигурации компиляции будет создана DLL в папке Release или Debug.