Лабораторна робота № 5

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 6

Лабораторна робота № 5.

Бібліотеки, що динамічно підключаються


(DLL).

Мета роботи: отримання навичок створення та підключення DLL,


засвоєння механізмів завантаження DLL.
Постановка задачі:
1. Розробити дві DLL-бібліотеки, перша з яких повинна експортувати
якусь функцію, наприклад, функцію сортування масиву, а друга –
експортувати цілий клас, наприклад, клас черги або стеку.
2. Розробити застосунок, який буде імпортувати функцію та клас зі
створених бібліотек. Причому DLL з функцією треба завантажувати
явно, а DLL з класом – неявно.

Методичні вказівки і теоретичні відомості

Компонування бібліотеки із застосунком


Можливі два варіанти компонування бібліотеки: статичне та
динамічне.
Коли використовується статичне компонування, необхідно включати
у файл проекту програми відповідний lib-файл, що містить потрібну вам
бібліотеку об'єктних модулів. Така бібліотека містить виконуваний код
модулів, який на етапі статичного компонування включається в exe-файл
завантажувального модуля.
Якщо ж використовується динамічне компонування, в
завантажувальний exe-файл програми записується не виконуваний код
функцій, а посилання на відповідну DLL-бібліотеку та функцію всередині
неї. Це посилання може бути організовано з використанням або імені
функції, або її порядкового номера в бібліотеці DLL.
При динамічному компонуванні бібліотеки її підключення може
відбуватись неявно і явно. При неявному підключенні завантаження
бібліотеки в адресний простір процесу відбувається під час запуску
застосунку. При явному підключенні завантаження відбувається під час
роботи застосунку (на вимогу).

Функция DllMain
Більшість бібліотек DLL – це колекції практично незалежних одна
від одної функцій, що експортуються до застосунків і використовуються в
них. Крім функцій, призначених для експортування, у кожній бібліотеці
DLL є функція DllMain. Ця функція призначена для ініціалізації та
очищення DLL. Якщо Вашій DLL подібні дії не потрібні, Ви не зобов'язані
реалізовувати цю функцію. Приклад — DLL, що містить лише ресурси.
Структура найпростішої функції DllMain може виглядати, наприклад,
так:
BOOL WINAPI DllMain(HINSTANCE hinstDll,DWORD fdwReason,
PVOID fImpLoad)
{
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
// DLL проектується на адресний простір процесу
break;
case DLL_THREAD_ATTACH:
// створюється потік
break;
case DLL_THREAD_DETACH:
// потік коректно завершується
break;
case DLL_PROCESS_DETACH
// DLL відключається від адресного простору процесу
break;
}
return(TRUE);
// використовується тільки для DLL_PROCESS_ATTACH
}
Функція DllMain викликається у кількох випадках. Причина її
виклику визначається параметром fdwReason, який може приймати одне з
наступних значень: DLL_PROCESS_ATTACH, DLL_PROCESS_DETACH,
DLL_THREAD_ATTACH або DLL_THREAD_DETACH.
При першому завантаженні бібліотеки DLL процесом викликається
функція DllMain з fdwReason, що дорівнює DLL_PROCESS_ATTACH.
Щоразу під час створення процесом нового потоку DllMain викликається з
fdwReason, рівним DLL_THREAD_ATTACH (крім першого потоку, тому
що в цьому випадку fdwReason дорівнює DLL_PROCESS_ATTACH).
Після завершення процесу з DLL функція DllMain викликається з
параметром fdwReason, рівним DLL_PROCESS_DETACH. При знищенні
потоку (крім першого) fdwReason дорівнюватиме
DLL_THREAD_DETACH.
Параметр hinstDll містить описувач екземпляра DLL. Це значення —
віртуальна адреса проекції файлу DLL на адресний простір процесу.
Останній параметр, fImpLoad, відмінний від 0, якщо DLL завантажена
неявно, і рівний 0, якщо вона завантажена явно.
У разі успішного завершення, функція DllMain повинна повертати
TRUE. У разі помилки повертається FALSE, і подальші дії припиняються.
Експорт/імпорт функцій
Якщо перед змінною, прототипом функції або C++-классом DLL
стоїть модифікатор __declspec(dllexport), компілятор Microsoft С/c++
вбудовує в кінцевий OBJ-файл додаткову інформацію. Вона знадобиться
компонувальнику при збірці DLL з OBJ-файлів.
Виявивши таку інформацію, компонувальник створює LIB-файл із
списком ідентифікаторів, що експортуються з DLL. Цей LIB-файл
потрібний при збірці будь-якого EXE-модуля, що посилається на такі
ідентифікатори. Компонувальник також вставляє в кінцевий DLL-файл
таблицю ідентифікаторів, що експортуються, — розділ експорту, в якому
міститься список (в алфавітному порядку) ідентифікаторів функцій, що
експортуються, змінних і класів. Туди ж поміщається відносна віртуальна
адреса (relative virtual address, RVA) кожного ідентифікатора всередині
DLL-модуля.
Хай ми формуємо вихідний код EXE-модуля, який імпортує
ідентифікатори, що експортуються певною DLL, і посилається на них в
процесі виконання. Рекомендується користуватися ключовим словом
__declspec(dllimport) для функцій, що імпортуються, і ідентифікаторів
даних. Використовуючи посилання на ідентифікатори, що імпортуються,
компонувальник створює в кінцевому EXE-модулі розділ імпорту (imports
section). У ньому перераховуються DLL, необхідні цьому модулю, і
ідентифікатори, на які є посилання зі всіх використовуваних DLL.

Неявне підключення DLL


У всі модулі вихідного кода, де є посилання на зовнішні функції,
змінні, структури даних або ідентифікатори, треба включити заголовний
файл, наданий розробником DLL.
Ви пишете на С/С++ модуль (або модулі) вихідного коду з тілами
функцій і визначеннями змінних, які повинні знаходитися в EXE-файлі.
Природно, ніщо не заважає Вам посилатися на функції і змінні, визначені в
заголовному файлі DLL-модуля.
Компілятор перетворить вихідний код модулів EXE в OBJ-файли
(поодинці на кожен модуль).
Компонувальник збирає всі OBJ-модули в єдиний завантажувальний
EXE-модуль, в який зрештою поміщаються двійковий код і змінні
(глобальні і статичні), що відносяться до даного EXE. У нім також
створюється розділ імпорту, де перераховуються імена всіх необхідних
DLL-модулів. Крім того, для кожної DLL в цьому розділі вказується, на які
символьні імена функцій і змінних посилається двійковий код
виконуваного файлу. Ці відомості буде потрібно завантажувачу
операційної системи.

Явне підключення DLL


Завантаження DLL, що явно підклюається, у адресний простір
процесу, може відбуватись за допомогою однієї з функції:
HINSTANCE LoadLibrary(PCTSTR pszDLLPathName);
HINSTANCE LoadLibraryEx( PCTSTR pszDLLPathName, HANDLE hFile,
DWORD dwFlags);
Обидві функції шукають образ DLL-файла і намагаються
спроектувати його на адресний простір процесу. Значення типа
HINSTANCE, що повертають ці функції, повідомляє адресу віртуальної
пам'яті, по якій спроектований образ файлу. Якщо спроектувати DLL на
адресний простір процесу не вдалося, функції повертають NULL.
DONT_RESOLVE_DLL_REFERENCES

Цей прапор вказує системі спроектувати DLL на адресний простір


процесу. Проектуючи DLL, система зазвичай викликає з неї функцію
DllMain і з її допомогою ініціалізує бібліотеку. Але даний прапор заставляє
систему проектувати DLL, не звертаючись до DllMain.
Якщо необхідність в DLL відпадає, її можна вивантажити з
адресного простору процесу, викликавши функцію:
BOOL FreeLibrary(HINSTANCE hinstDll);

Ви повинні передати в FreeLibrary значення типа HINSTANCE, яке


ідентифікує вивантажувану DLL. Це значення Ви маєте після виклику
LoadLibrary(Ex).
Необхідно також пам'ятати, що насправді LoadLibrary і
LoadLibraryEx лише збільшують лічильник числа користувачів вказаної
бібліотеки, а FreeLibrary його зменшує.
Поток отримує адресу ідентифікатора, що експортується, з явно
завантаженої DLL викликом GetProcAddress:
FARPROC GetProcAddress( HINSTANCE hinstDll, PCSTR pszSymbolName);

Параметр hinstDll — дескриптор, повернений LoadLibrary(Ex).


Параметр pszSymbolName повинен містити ім'я функції, що цікавить Вас:
FARPROC pfn = GetProcAddress(hinstDll, "SomeFuncInDll");
Відмітьте, тип параметра pszSymbolName — PCSTR, а не PCTSTR.
Це означає, що функція GetProcAddress приймає лише ANSI-рядки — їй не
можна передати Unicode-рядок. А причина в тому, що ідентифікатори
функцій і змінних в розділі експорту DLL завжди зберігаються як ANSI-
рядки.
В результаті Ви отримуєте адресу ідентифікатора, що міститься в
DLL. Якщо ідентифікатор не знайдений, GetProcAddress повертає NULL.
Перед тим, як використовувати функції бібліотеки, необхідно
отримати їх адресу. Для цього спочатку слід скористатися директивою
typedef для визначення типа вказівника на функцію і визначити змінну
цього нового типа, наприклад:
// тип PFN_MyFunction оголошуватиме вказівник на функцію
// що приймає вказівник на буфер і видає значення типа int
typedef int (WINAPI *PFN_MyFunction)(char *);
PFN_MyFunction pfnMyFunction;
Потім слід отримати дескриптор бібліотеки, за допомогою якого і
визначити адреси функцій, наприклад, адресу функції з ім'ям MyFunction:
hMyDll=LoadLibrary("MYDLL");
pfnMyFunction=(PFN_MyFunction)::GetProcAddress(hMyDll,"MyFunction");
int iCode=(*pfnMyFunction)("Hello");//виклик функції

Приклад створення
і неявного завантаження DLL по крокам
1. Створення DLL:
- створюємо застосунок, вказавши тип програми – DLL;
- у застосунку створюємо заголовний файл "MyDLL.h" з прототипами
функцій, які будуть експортуватися з DLL, використовуючи в якості
модифікатора extern "C" _declspec(dllexport);
extern "C" _declspec(dllexport) int fact(int)
- пишемо реалізацію експортованих функцій у файлі “MyDLL.cpp”:
extern "C" _declspec(dllexport) int fact(int n)
{
int rez(1);
for (int i=1;i<=n;i++) rez=rez*i;
return rez;
}
- збираємо DLL, в результаті чого з'являються фай “MyDLL.dll” і
“MyDLL.lib”
2. Підключення DLL:
- створюємо застосукнок, який буде використовувати функції з DLL;
вміст файлу “TestDLL.cpp”:
#include "stdafx.h"
#include "TestDLL.h"
#include <iostream>
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
int m; cin>>m;
cout<<m<<"!="<<fact(m)<<endl;
return 0;
}
- розміщуємо копію заголовного файлу "MyDLL.h" в папці з
заголовними файлами нового проекту і підключаємо його в файлі
"TestDLL.h":
#include "MyDLL.h"
- розміщуємо файли "MyDLL.dll" і "MyDLL.lib" в папці Debug нового
проекту;
- додаємо повне ім'я файлу бібліотеки “MyDLL.lib” до списку Project-
>Properties->Configuration Properties->Linker->Input->Additional
Dependencies

Приклад явного завантаження DLL


#include <Windows.h>
int _tmain(int argc, _TCHAR* argv[])
{
int n;
HINSTANCE hDll;
typedef int (*PFN_fact) (int);
PFN_fact pfn_fact;
hDll=LoadLibraryEx("DLL.dll",0,DONT_RESOLVE_DLL_REFERENCES);
pfn_fact = (PFN_fact)GetProcAddress(hDll, "fact");
cin>>n;
cout<<n<<"!="<<pfn_fact(n)<<endl;
FreeLibrary(hDll);
return 0;
}

Контрольні запитання

1. Які існують способи компонування бібліотеки?


2. Які способи завантаження dll Ви знаєте?
3. Що таке розділ імпорту? Навіщо він потрібен?
4. Що таке розділ експорту? Навіщо він потрібен?
5. В чому перевага бібліотек, що динамічно завантажуються?
6. Як підключити бібліотеку, що буде завантажуватись явно?
7. Як підключити бібліотеку, що буде завантажуватись неявно?

You might also like