Пишем DLL на FASM'e - FASM - Ответ 12999244

Всё описанное здесь - это личное видение,
если не оговорено обратное.

Если вас мучает вопрос: "Как создать DLL-библиотеку?", то вы пришли по адресу.

Небольшой ликбез для новичков позволит разобраться с такими вопросами как:

Что такое DLL и зачем они нужны.
Системная поддержка и проецирование в память.
Точка входа в DLL-библиотеку.
Пример простейшей DDL на FASM.
Подводные камни при их создании.
Заключение.

--------------------------------------------------

Dynamic Link Library

в простонародье DLL - часть исполняемого файла в виде внешнего модуля. Модуль представляет из-себя библиотеку уникальных процедур и функций, которых нет в составе системных WinAPI, но необходимых нашей программе. Разделение исполняемого файла на подгружаемые модули позволяет эффективно использовать память, т.к. услугами одной библиотеки могут пользоваться сразу несколько приложений.

По своей природе, DLL не отличается от EXE.. те-же секции кода/данных/импорта, только добавляется ещё одна - секция экспорта, благодаря которой приложения могут импортировать из библиотеки имеющиеся в ней функции. Есть и ещё одно/важное отличие *dll от *exe - они не способны сами исполнять код, поскольку каждый участок обёрнут в отдельную процедуру. Эти процедуры просто ждут своего часа, пока exe-модуль

(или другая dll)

не запросит какую-нибудь из них.

Системная поддержка и проецирование в память

Когда система запускает юзерский процесс, ядро проделывает огромную работу. Помимо прочего, это выделение памяти для юзерской кучи TLS - Thread Local Storage, и создание в этой куче служебных структур типа PEB -

Process Environment Block

(блок окружения процесса)

, и дочернюю к РЕВ структуру TEB -

Thread Environment Block

(блок окружения основного потока)

. Это т.н. "процесс инициализации приложения", подготавливающий юзерский контекст и окружение. Занимается этим "загрузчик образа", который находится в Ntdll.dll.

Поскольку процедура инициализации стартует до нашего приложения, алгоритм её действий остаётся для нас прозрачным. Однако после загрузки exe, в стеке кое-что остаётся и если прокрутить его в отладчике назад

(в сторону меньших адресов)

, то можно найти там много интересного. Инструкции POP и RET не затирают данные в стеке, а просто смещают указатель ESP, в результате чего всё тайны всплывают наружу.

Проделав глобальную работу, загрузчик приступает к разбору таблицы импорта нашего EXE-модуля, чтобы подтянуть в юзерскую память внешние библиотеки, динамически скомпонованные во время компиляции EXE. Каждая импортируемая DLL может иметь свою/собственную таблицу импорта, поэтому эта операция будет продолжаться в рекурсивном режиме до тех пор, пока не будет найден весь импорт. По мере загрузки каждой DLL, загрузчик будет сохранять её информацию в виде базы, в структурах PEB и TEB.

В поисках библиотек, загрузчик сначала открывает в реестре ветку:

HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs

где прописаны системные либы пользовательского режима. Если в таблице импорта нашего exe имеются и сторонние

(по-факту самописные)

dll'ки, то загрузчик ищет их в следующих директориях:

1. Текущий каталог, от куда запускается приложение;

2. C:\Windows\system32;

3. C:\Windows\system

(для 16-бит приложений, но находит и 32-бит)

.

Другими словами, мы можем располагать свои DLL в любую из указанных диров и загрузчик подхватит их на-лету, спроецировав на нашу память.

Добавлено через 3 минуты

База данных загруженных модулей

Загрузчик ведёт список всех модулей, которые были загружены нашим процессом. Инфа хранится в структуре PEB, ..а именно в её подструктуре

PEB_LDR_DATA

. В ней загрузчик ведёт три списка 'list', в которых содержится одинаковые, но по разному выстроенные листы: по порядку загрузки(Load), по размещению в памяти(Memory) и в порядке инициализации(Init):

typedef struct _PEB_LDR_DATA {
  ULONG               Length;
  BOOLEAN             Initialized;
  PVOID               SsHandle;
  LIST_ENTRY          InLoadOrderModuleList;
  LIST_ENTRY          InMemoryOrderModuleList;
  LIST_ENTRY          InInitializationOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;

Эти списки представляют из-себя дочерние структуры LDR_MODULE, в которых и хранится инфа о каждом/подгружаемом модуле

(для всех импортируемых DLL создаётся свой лист)

. Посмотрим на эти поля и дадим им короткие пояснения:

typedef struct _LDR_MODULE {
  LIST_ENTRY          LoadOrderList     ; см. PEB_LDR_DATA
  LIST_ENTRY          MemoryOrderList   ;  ...
  LIST_ENTRY          InitOrderList     ;    ...
  PVOID               BaseAddress       ; Базовый адрес загрузки в память
  PVOID               EntryPoint        ; Указатель на точку-входа в библиотеку
  ULONG               SizeOfImage       ; Её размер в памяти
  UNICODE_STRING      FullDllName       ; Полный путь к либе
  UNICODE_STRING      BaseDllName       ; Имя без пути
  ULONG               Flags             ; Атрибуты доступа
  SHORT               LoadCount         ; Важно! кол-во обращений к DLL
  SHORT               TlsIndex          ; Слот либы в юзерской куче
  LIST_ENTRY          HashTableEntry    ; Хеш для быстрого запуска/остановки
  ULONG               TimeDateStamp     ;
} LDR_MODULE, *PLDR_MODULE;

Нужно сказать, что это детали загрузчика пользовательского режима в системной Ntdll.dll. Ядро использует свой/собственный загрузчик для нативных драйверов и зависимых к ним DLL, с похожей структурой записей. Важно понять, что РЕВ создаётся загрузчиком для каждой задачи (процесса) в отдельности: сколько процессов - столько и структур РЕВ.

Посмотрим, чем занимается загрузчик на финальном этапе, после настройки окружения:

Загружает каждую DLL, на которую есть линк в таблице импорта EXE-модуля.
Проверяет в базе LDR_MODULE, не была-ли DLL уже загружена (поле LoadCount). Если нет, загрузчик создает структуру для этой DLL и вставляет её в базу данных РЕВ.
При поиске, загрузчик сначала проверяет DDL на соответсвие Known (в реестре). Попадание означает, что DLL уже в памяти и была загружена системой при запуске. Если в реестре прописки нет, то ищется рядом с EXE или в папках System.
После того как DLL найдена, загрузчик передаёт управление ядерной функции LdrLoadDLL() для её загрузки в память. Эта fn. возвращает управление опять загрузчику, который проверяет, не загрузило-ли ядро нашу DLL'ку в какое-нибудь другое место - механизм перемещения, или Relocation.
Если загрузчик обнаружит перемещение проекции файла от базового значения, он проводит анализ секции ".reloc" в DLL-модуле и выполняет поправки в структурах РЕВ. Если инфа о перемещении в либе отсутствует, встречаем сбой загрузки DLL.
Теперь загрузчик проводит анализ IAT (таблицы импорта) для поиска конкретных функций по их именам или ординалам (порядковым номерам).
На этом загрузчик завершает свою работу и передаёт управление на точку входа в EXE-приложение, которая находится в стеке. Процесс запускается.. но загрузчик не уходит в тень, а всегда ожидает нас на низком старте. К его услугам мы обращаемся всякий раз, когда динамически вызываем функции DLL-библиотек, например через LoadLibrary(). Загрузчику приходится повторять все/перечисленные пункты сначала, что сказывается на времени выполнения программы.

Добавлено через 1 минуту

Точка входа в DLL-библиотеку

Когда EXE-модуль вызывает какую-нибудь функцию из DLL, системный загрузчик прибегает к услугам API

LdrLoadDLL()

из либы ntdll. Эта-же API принимает управление от функций динамического вызова процедур LoadLibrary(), GetModuleFileName() etc, ожидающих получить дескрипторы модулей. Вот её прототип:

LdrLoadDll(
   IN PWCHAR               PathToFile OPTIONAL,
   IN ULONG                Flags OPTIONAL,
   IN PUNICODE_STRING      ModuleFileName,
  OUT PHANDLE              ModuleHandle );

Запрос от LdrLoadDLL() принимает на грудь необязательная функция

DllEntryPoint()

(аналог DllMain)

, которая по-факту является точкой-входа в любую библиотеку. Необязательная она тем, что мы можем игнорировать её аргументы и это не приведёт к краху. Однако чего нельзя ингорить, так это возвращаемое ею значение TRUE в регистре EAX, поскольку LdrLoadDLL() рассчитывает его получить. Ldr передаёт в DllEntryPoint() три аргумента в таком порядке, а обрабатывать их или нет - дело хозяйское, ..главное вернуть EAX=1:

DllEntryPoint (
***IN HINSTANCE            hinstDLL,
***IN DWORD                fdwReason,
***IN LPVOID               lpvReserved *);

hinstDLL - в теории дескриптор DLL, а в реале - её VA-база в памяти.
fdwReason - одна из четырёх причин вызова точки входа:
• DLL_PROCESS_ATTACH - модуль подключается к пространству текущего процесса при его запуске.
• DLL_PROCESS_DETACH - модуль удаляется из адресного пространства процесса.
• DLL_THREAD_ATTACH - процесс создаёт новый поток, причем на основной поток процесса это не распространяется.
• DLL_THREAD_DETACH - созданный поток отработал своё и завершается.
lpvReserved - резерв для системных нужд.

Здесь видно, что интересным является лишь параметр

fdwReason

, который передаёт нам LdrLoadDLL(). Благодаря ему мы можем провести какие-нибудь подготовительные операции init, перед вызовом конкретной функции из нашей тушки. Но повторюсь, что никто нас к этому не принуждает и мы можем просто вернуть единицу и всё. Нужно сказать и о том, что Ldr ожидает TRUE только при обращение к DDL'ке с флагом 'DLL_PROCESS_ATTACH', а в остальных случаях возврат не критичен.

Кроме этого нужно учитывать, что на входе либа ещё не определена, и фактически мы находимся внутри обработчика LdrLoadDLL(). Такая картина накладывает определённые ограничения на вызов некоторых/вложенных Win-API внутри DllEntryPoint(), пока не выйдем из точки-входа по RET. Не сбрасывайте это со-счетов!

С деталями можно ознакомиться в статье КК:

"Динамические библиотеки для гурманов" -

http://citforum.ru/book/cook/dll1.shtml

Вернуться к обсуждению:

Пишем DLL на FASM'e FASM