Тема: створення та використання динамічних бібліотеки dll, за допомогою мови асемблер Мета: навчитися створювати та використовувати динамічні бібліотеки dll за допомогою миви асемблер. Теоретична частина тільки задати її початок, а всю інформацію із керування стеком визначає компілятор із використанням адресації щодо покажчика стека (який буде встановлено завантажувачем). Динамічне компонування Поняття динамічної бібліотеки Динамічна бібліотека - набір функцій, скомпонованих разом у вигляді бінарного файлу, який може бути динамічно завантажений в адресний простір процесу, що використовує ці функції. Динамічне завантаження (dynamic Loading) - завантаження під час виконання процесу (зазвичай реалізоване як відображення файлу бібліотеки в його адресний простір), Динамічне компонування (dynamic linking) - компонування образу виконуваного файлу під час виконання процесу із використанням динамічних бібліотек. Переваги використання динамічних бібліотек: ¦ Оскільки бібліотечні функції містяться в окремому файлі, розмір виконуваного файлу стає меншим і так заощаджують дуже багато дискового простору. ¦ Якщо динамічну бібліотеку використовують кілька процесів, у пам'ять завантажують лише одну її копію, після чого сторінки коду бібліотеки відображаються в адресний простір кожного з цих процесів. Це дає змогу ефективніше використовувати пам'ять. ¦ Оновлення застосування може бути зведене до встановлення нової версії динамічної бібліотеки без необхідності перекомпонування тих його частин, які не змінилися. ¦ Динамічні бібліотеки дають змогу застосуванню реалізувати динамічне завантаження модулів на вимогу. На базі цього може бути реалізований розширюваний АРІ застосування. Для додавання нових функцій до такого АРІ стороннім розробникам достатньо буде створити і встановити нову динамічну бібліотеку. Динамічні бібліотеки дають можливість спільно використовувати ресурси застосування (наприклад, набір піктограм), спростити локалізацію застосування, якщо всі машинно-залежні фрагменти програми, помістити в окрему DLL. Оскільки динамічні бібліотеки є двійковими файлами, можна організувати спільну роботу бібліотек, розроблених із використанням різних мов програмування і програмних засобів, що спрощує створення застосувань на основі програмних компонентів. Недоліки при використанні динамічних бібліотек: ¦ Використання DLL сповільнює завантаження застосування. Що більше таких бібліотек потрібно процесу, то більше файлів треба йому відобразити у свій адресний простір під час завантаження. Для прискорення завантаження рекомендують укрупнювати DLL, об'єднуючи кілька взаємозалежних бібліотек в одну загальну. ¦ У деяких ситуаціях (наприклад, під час аварійного завантаження системи із дискети) використання спільних системних DLL неприйнятне через нестачу дискового простору для їхнього зберігання (такі системні DLL можуть займати кілька мегабайтів дискового простору, при цьому застосування часто потребують усього по кілька функцій із них). ¦ Найбільшою проблемою у використанні динамічного компонування є проблема зворотної сумісності динамічних бібліотек. Неявне і явне зв'язування Є два основні способи завантаження динамічних бібліотек в адресний простір процесу - неявне і явне зв'язування (implicit і explicit binding). Неявне - основний спосіб завантаження динамічних бібліотек у сучасних ОС. При цьому бібліотеку завантажують автоматично до початку виконання застосування під час завантаження виконуваного файлу, за це відповідає завантажувач виконуваних файлів ОС. У деяких системах такий завантажувач є частиною ядра ОС, у деяких - окремим застосуванням. Список бібліотек, потрібних для завантаження, зберігають у виконуваному файлі. До переваг цього методу належать: простота і прозорість з погляду програміста (йому не потрібно писати код завантаження бібліотек, а достатньо у налаштуваннях компонувальника вказати список потрібних бібліотек); висока ефективність роботи процесу після початкового завантаження (усі необхідні бібліотеки до цього часу вже завантажені у його адресний простір). Недоліком неявного зв'язування можна вважати зниження гнучкості (так, наприклад, якщо хоча б однієї з необхідних бібліотек не буде на місці, процес завантаження не обійдеться без проблем, навіть коли для виконання конкретної задачі ця бібліотека не потрібна). Крім того, збільшуються час завантаження і початковий обсяг необхідної пам'яті. Альтернативним для неявного є явне зв'язування, коли динамічну бібліотеку завантажують в адресний простір процесу виконанням системного виклику із його коду. Після цього, використовуючи інший системний виклик, застосування отримує адресу необхідної йому функції бібліотеки і може її викликати. Після використання бібліотеку можна вилучити з пам'яті. Здебільшого неявне зв'язування зводиться до автоматичного виконання тих самих викликів, які сам програміст виконує за явного. Такий підхід вимагає від програміста додаткових зусиль, але має більшу гнучкість. Однак складність реалізації призводить до того, що його використовують лише тоді, коли застосуванню справді потрібно завантажувати і вивантажувати додаткові бібліотеки під час виконання. Динамічні бібліотеки та адресний простір процесу. Особливості об'єктного коду динамічних бібліотек Після того, як динамічна бібліотека була відображена в адресний простір процесу, вона стає майже прозорою для програмного коду, що тут виконується. Усі функції бібліотеки стають доступними для всіх потоків цього процесу, фактично її код і дані набувають вигляду доданих до адресного простору процесу. Зазначимо, що під час відображення бібліотеки у пам'ять використовують технологію копіювання під час записування, тому кожен процес матиме свою копію стека і даних бібліотеки. З іншого боку, для коду бібліотечної функції будуть доступні такі ресурси, як дескриптори відкритих файлів процесу і стек потоку, що викликав дану функцію. Не слід, однак, забувати про те, що під час роботи із даними потоку із коду бібліотеки потрібно виявляти обережність, зокрема, ніколи не вивільняти пам'ять, розподілену не в цій бібліотеці. Іншими словами, код бібліотек, розрахованих на використання у багато потокових застосуваннях, має бути безпечним з погляду потоків (thread-safe). Код динамічних бібліотек звичайно зберігають у виконуваних файлах формату, стандартного для цієї ОС, але з погляду характеру цього коду є одна важлива відмінність між кодом DLL і кодом звичайних виконуваних файлів. Вона полягає в тому, що код DLL в один і той самий час повинен мати можливість завантажуватися за різними адресами. Для того щоб це було можливо, такий код потрібно робити позиційно-незалежним. Позиційно-незалежний код завжди використовує відносну адресацію (базову адресу додають до зсуву). Базову адресу налаштовують у момент завантаження DLL в адресний простір процесу і називають також базовою адресою бібліотеки; такі адреси відрізняються для різних процесів. Зсув у цьому разі називають внутрішнім зсувом об'єкта. Одна із функцій динамічної бібліотеки може бути позначена як її точка входу. Така функція автоматично виконуватиметься завжди, коли цю DLL відображають в адресний простір процесу (явно або неявно); у неї можна поміщати код ініціалізації структур даних бібліотеки. Багато систем дають змогу задавати також і функцію, що викличеться в разі вивантаження DLL із пам'яті. Структура виконуваних файлів У сучасних ОС є тенденція до спрощення процедури завантаження виконуваних файлів через наближення їхнього формату до образу процесу у пам'яті. Описати загальну структуру виконуваного файлу доволі складно, тому лише зупинимося на деяких спільних компонентах таких файлів, а потім розглянемо конкретні формати. Оскільки виконувані файли створює компонувальник на базі об'єктних файлів, то у структурі цих файлів є багато спільного. Насамперед це стосується того, що обидва види файлів складаються з набору секцій різного призначення. Деякі спільні елементи структури виконуваних файлів (їхній порядок і точний зміст розрізняють для різних форматів цих файлів) наведено нижче. Насамперед виконувані файли мають заголовок. Він найчастіше містить «магічні символи», які дають змогу ОС швидко визначити його тип; базову адресу відображення виконуваного коду у пам'ять; ознаку відмінності між незалежним виконуваним файлом і DLL; адреси найважливіших елементів файлу. Майже завжди в такі файли включають інформацію для динамічного компонувальника. Звичайно ця інформація складається зі списку імпорту, що містить інформацію про всі DLL, потрібні для виконання цього файлу (для неявного зв'язування) та списку експорту, що містить інформацію про всі функції, доступні для використання іншими виконуваними файлами. Деякі формати виконуваних файлів використовують зовнішній динамічний завантажувач; у цьому разі всередині файлу також зберігають інформацію про його місцезнаходження. Інформацію про всі секції файлу зберігають у списку секцій, який називають таблицею секцій (section table). Елементи цієї таблиці описують різні секції файлу. Нарешті, у файлі розташовані самі секції, що містять дані різного призначення. Назви секцій та їхній вміст розрізняються для різних форматів, але майже завжди є секції для коду та ініціалізованих даних. Створення dll за допомогою мови асемблер Тепер маючи достатню кількість знань про функціонування динамічних бібліотек dll, можна приступити до розгляду практичного створення та використання цих бібліотек за допомогою мови асемблер. Отже перш за все давайте розглянемо каркас коду бібліотеки ;---------------------------------------------------------------------------- ; my_dll.asm ;---------------------------------------------------------------------------- .386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib .data
msg db "hello it is dll function",0
.code ;-------------------- Стертова функція ---------------------------- DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD mov eax,TRUE ret DllEntry Endp ;------------- Тестова функція в бібліотеці ------------------ TestFunction proc invoke MessageBox,NULL,addr msg,addr msg,MB_OK ret TestFunction endp End DllEntry В даному коді містяться дві функції DllEntry та TestFunction. Перша з нинх є стартовою функцією і повинна обов’язково бути присутньою в бібліотеці, при загрузці даної бібліотеки в оперативну пам'ять, операційна система автоматично створить ще один потів в межах процесу що викликав двну функції і передасть йому точку входженя вказівник саме на цю функцію. DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD mov eax,TRUE ret DllEntry Endp Дана функція отримує 3 параметри від операційної системи: hInstDLL – хендл модуля dll reason – прапорець що може приймати одне із наступних значень: DLL_PROCESS_ATTACH - DLL отримує значення прапорця при першій загрузці у оперативну память DLL_PROCESS_DETACK - DLL отримує цей прапорець у мемент вигризки з оперативної памяті DLL_THREAD_ATTACK - DLL отримує даний прапорець у момент створення нового потоку. DLL_THREAD_DETACK - DLL отримує даний код у момент знищення одного із потоків По завершенню виконання цієї функції визначається чи продовжувати роботу цієї бібліотеки (TRUE FALSE), результати вератається через eax. Окрім стартової функції у бібліотеці можуть знаходитися і інші функції, деякі з них можу формувати інтерфейс бібліотеки, а інші бути службовими і доступними лише для функцій даної бібліотеки. Для того щоб сформувати інтерфейс бібліотеки (тобто функції які буду доступні ззовні) треба описати їх у спеціальному файлі, вміст цього файла наступний: LIBRARY my_dll // назва бібліотеки EXPORTS TestFunction // назви функцій її інтерфейсу Тепер спробуємо скомпілювати нашу бібліотеку виконавши наступні дії: ml.exe /c /coff /Cp my_dll.asm link.exe /dll /subsystem:windows /def:my_dll.def my_dll.obj В результаті ми отримуємо my_dll.lib та my_dll.dll Створення програми що використовує dll статично за допомогою мови асемблер Для того щоб внести у програму інформацію про функцію, що знаходиться в dll на етапі лінкування окрім об’єктних файлів лінкеру задається також файл з розширенням lib який був отриманий в результаті компілювання бібліотеки, або можна прописати в коді асемблерної програми наступний рядок - includelib my_dll.lib Після цього всі функції які належать до інтерфейсу даної бібліотеки стануть доступними в програмі. ;---------------------------------------------------------------------------- ; my_test_1.asm ;---------------------------------------------------------------------------- .386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\kernel32.inc includelib my_dll.lib ; підключаю бібліотеку includelib \masm32\lib\kernel32.lib TestFunction PROTO .code start: invoke TestFunction ; викликаю функцію з бібліотеки invoke ExitProcess,NULL end start ;---------------------------------------------------------------------------- Щоб скомпілювати програму необхідно виконати наступні дії: ml.exe /c /coff mt_test_1.asm link.exe /subsystem:windows my_test_1.obj В результаті буде створено виконавчий файл my_test_1.exe результатом виконання якого буде вивід повідомлення:
Створення програми що використовує dll динамічно за допомогою мови асемблер Для того щоб динамічно використовувати бібліотеку, необхідно загружати та вигружати її з оперативної памяті по ходу виконання програми. Для цього існують спеціальні апі функції: LoadLibrary – функція для загрузки бібліотеки в оперативну пам'ять GetProcAddress – функція визначення адреси функції після загрузки бібліотеки в память FreeLibrary – функція вигризки бібліотеки з памяті Отже використовуючи ці три функції можна побудувати програму здатну загружати, викликати функції та вигружити бібліотеку з памяті, ось код такої програми: ;---------------------------------------------------------------------------- ; my_test_2.asm ;---------------------------------------------------------------------------- .386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib includelib \masm32\lib\user32.lib .data LibName db "my_dll.dll",0 FunctionName db "TestFunction",0 DllNotFound db "Cannot load library",0 AppName db "Load Library",0 FunctionNotFound db "TestFunction function not found",0 .data? hLib dd ? TestFunctionAddr dd ? .code start: invoke LoadLibrary,addr LibName ; загружаю бібліотеку .if eax==NULL invoke MessageBox,NULL,addr DllNotFound,addr AppName,MB_OK ; якщо загрузка не вдалася .else mov hLib,eax ; зберігаю хендел бібліотеки invoke GetProcAddress,hLib,addr FunctionName ; знаходжу адресу функції .if eax==NULL invoke MessageBox,NULL,addr FunctionNotFound,addr AppName,MB_OK ; якщо не вдалося вичислити адресу .else mov TestFunctionAddr,eax call [TestFunctionAddr] ; викликаю функцію .endif invoke FreeLibrary,hLib ; вигружаю бібліотеку ; вигружаю бібліотеку .endif invoke ExitProcess,NULL end start ;---------------------------------------------------------------------------- Для того щоб скомпілювати дану прогараму необхідно виконати наступні дії: ml.exe /c /coff mt_test_2.asm link.exe /subsystem:windows my_test_2.obj В результаті буде створено виконавчий файл my_test_2.exe в результаті виконання якого буде виведено наступне повідомлення:
Завдання: Створити dll бібліотеку, за допомогою мови асемблер, що містить функцію згідно з варіантом, та дві прикладні програми, на мові асемблер, які використовують дану бібліотеку, з статичним та динамічним зв’язуванням, у випадку динамічного зв’язування передбачити виведення повідомлень про відсутність dll бібліотеки чи необхідної функції у ній. Варіанти завдань: Варіант Опис функції
1 Знайти скільки разів заданий символ входить в рядок
2 Перевірити чи кількість буквених символів у заданому рядку не перевищує заданої кількості
3 Обрізати заданий рядок до заданого символа ти вивести його на екран
4 Перевірити чи кількість цифрових символів у заданому рядку не перевищує задану кількість
5 Замінити в заданому рядку всі символи переходу на новий рядок пробілами та вивести рядок на екран
6 Перевірити чи перший та останній символи в рядку не є пробільними Вивести окреме повідомлення якщо перший символ пробільний, окреме якщо останній, та окреме якщо обидва.
7 Перевірити чи останній символ першого слова заданого рядка дорівнює заданому символу (важати що слова розділяються лише пробілами)
8 Порахувати кількість слів у рядку, вважаючи що слова розділені лише пробілами, кількість пробілів між словами може бути довільна
9 Перевірити чи заданий рядок завершується переходом на новий рядок
10 Визначити скільки перших символів з першого заданого рядка співпадає з відповідними їм символами другого заданого рядка, припинити пошу після знаходження першого не співпавшого символа.
11 Обчислити на скільки один з заданих рядків довший від іншого
12 Обрахувати кількість великих букв у заданому рядку
13 Перевірити чи заданий рядок не починається з переходу на новий рядок
14 Перевірити чи розмір заданого рядка не перевищує задане значеня
15 Перевірити чи перше слово в заданому рядку починається маленькою буквою
16 Перевірити чи останній символ першого слова заданого рядка є буквиним символом (важати що слова розділяються лише пробілами)
17 Перевірити чи символи в заданому рядку є лише цифровими
18 Обрахувати, який з заданих рядків коротший і вивести його не екран
19 Перевірити чи в заданому рядку нема пробілів
20 Перевірити чи перше слово в заданому рядку починається цифровим символом
21 Перевірити чи перше слово в заданому рядку починається великою буквою
22 Перевірити чи заданий рядок описує число дійсного типу (наприклад «3,14»)
23 Знайти та вивести на екран перше слово з вказаного рядка
23 Обрахувати кількість цифрових символів у заданому рядку
24 Перевірити чи останній символ першого слова заданого рядка є цифровим символом (важати що слова розділяються лише пробілами)
25 Обчислити який номер по порядку займає у заданому рядку перший найдений заданий символ.
26 Обрахувати кількість малих букв у рядку
27 Обрахувати, який з заданих рядків довший і вивести його не екран
28 Перевірити чи рядок не порожній, тобто чи містить він хоч один алфавітноцифровий символ.