Правила Форума редакция от 22.06.2020 |
|
|
|
|
|
Опции темы | Опции просмотра | Language |
18.12.2006, 01:58 | #1 |
ViP
Пол: Регистрация: 17.09.2006
Сообщений: 1,182
Репутация: 1592
|
windows голыми руками
оригинал на wasm.ru
Как известно, для того, чтобы небрежно называть себя программистом в компании друзей-чайников, необходимо (и достаточно J) написать программу, выдающую тем или иным способом на экран надпись «Hello, World!». Теперь Windows позволяет сделать это очень просто. Наберите в старом добром Блокноте: MsgBox “Hello, World!” - и сохраните файл с расширением .vbs (например, ‘Hello.vbs’), затем запустите его двойным щелчком. Те, у кого установлен Word из MS Office XP, могут использовать более изощренный вариант: Set w = CreateObject("Word.Application") w.Visible = True Set rng = w.Documents.Add.Range(0,0) With rng .InsertBefore "Hello, World!" .ParagraphFormat.Alignment = 1 With .Font .Name = "Arial" .Size = 48 .Italic = True .Color = 200 End With End With Но все это к самой операционной системе имеет лишь отдаленное отношение. Можно ли создать «настоящее» приложение Windows, не используя довольно громоздких сред разработки, на самом обычном компьютере? Оказывается, можно. В Windows XP Pro наберите в командной строке debug, вы весьма «удивитесь» , увидев дефис в консольном окне - знакомое приглашение старого отладчика DOS. Эта любопытная вещица является именно тем инструментом, который нам нужен. Кроме него, нам потребуется немного знаний о формате исполняемых файлов PE и процессе их загрузки в память. Исполняемые файлы Win32 EXE используют формат файла PE. Как и старый формат EXE для DOS, PE-файл состоит из заголовка и собственно образа исполняемой программы. Образ программы составлен из одного или нескольких объектов или секций, которые иногда называют по старинке сегментами. Однако они не имеют ничего общего со старой сегментной моделью, также, впрочем, как и с объектами в том значении, как они используются в языках программирования. Поэтому для обозначения разделов образа программы PE-файла лучше использовать термин «секции». Разделение на секции нацелено главным образом на оптимизацию управления памятью Windows. По этой причине размеры загруженных в оперативную память секций должны быть кратны размеру страницы памяти (обычно 4 Кб) и выровнены по ее границе. Записанные в файл секции должны быть выровнены по границе «файловых страниц», размер которых кратен размеру сектора (512 байт) - это также сделано для оптимизации загрузки. Образ программы загружается в память, начиная с некоторого указанного в заголовке файла базового адреса загрузки, который должен быть выровнен по 64 Кб границе. Для EXE-файлов он обычно равен 400000h. Чтобы наши выкладки не выглядели чересчур абстрактными, приступим сразу к конструированию нашего PE-файла. Примем минимально возможные размеры выравнивания секций - 4000 (1000h) байт - и файла - 512 (200h) байт. Образ нашей программы будет состоять всего из одной секции, в которой будут размещены и данные, и код, и вспомогательные таблицы для импорта. Таким образом, размер файла уместится в 1 Кб, а размер образа в памяти - 8 Кб (2 страницы). Первую страницу по адресу загрузки 400000h займет заголовок PE-файла, а со смещения 1000h будет располагаться первая (и единственная) секция нашего файла. Итак, создадим «болванку». В командной строке набираем «debug». Очистим 1000 (400h - в отладчике debug используется шестнадцатеричная запись чисел) первых байт памяти, заполнив их нулями: f 0 400 0 В этой области мы будем «собирать» наше приложение. Но для удобства набора используем еще одну область по смещению 1000h, чтобы не запутаться с адресами при наборе: f 1000 1200 0 Как и для сценария VBS, работа нашего приложения будет заключаться в отображении окна сообщения «Hello, World!», для чего необходимо использовать функцию Win32 API MessageBoxA (из системного модуля USER32.dll). После этого приложение завершает работу (вызвав еще одну функцию API ExitProcess из модуля KERNEL32.dll). Таким образом, необходимо сначала импортировать указанные две функции в наше приложение. Процесс импорта заключается, во-первых, в отображении (посредством страничной переадресации) нужных нам DLL в адресное пространство нашего процесса, и, во-вторых, в сохранении адресов нужных нам функций из этих DLL в специально отведенных для этого местах - таблицах импортируемых адресов (Import Address Table - IAT). Все это автоматически проделывается системой при загрузке файла на исполнение, но для этого необходимо предусмотреть в образе программы ряд вспомогательных таблиц для импорта. Таблицы импортируемых адресов (IAT) должны располагаться в самом начале секции. Они представляют собой последовательности 4-байтовых (DWORD) полей, в которые загрузчик Windows заносит при связывании с DLL адреса соответствующих импортируемых функций. Порядок расположения функций должен соответствовать порядку функций в таблице поиска (об этом далее). Каждая таблица содержит данные о функциях из одного модуля (DLL); признаком конца таблицы является поле, заполненное нулями. При необходимости несколько таблиц следуют одна за другой. До связывания с DLL таблица импортируемых адресов должна быть полностью идентична соответствующей таблице поиска, в противном случае загрузчик сообщит об ошибке. В нашем случае необходимы две IAT: одна для USER32.dll, другая - для KERNEL32.dll. Из обоих модулей импортируется по одной функции, поэтому размер обоих таблиц будет по 8 байт (4 на адрес, 4 - на завершающие 0). Первая IAT будет располагаться по смещению 1000h относительно базового адреса загрузки, вторая - 1008h. Эти значения мы введем позже. А пока займемся данными. Функция MessageBoxA принимает в числе прочих параметров адреса двух строк (одна - собственно выводимое сообщение, вторая - заголовок). Выровняем адреса по границе параграфа (это не обязательно и сделано лишь для удобства, чтобы не запутаться с адресами). По смещению 1010h поместим ASCII-строку ‘VBScript’ (чтобы заголовок сообщения был аналогичен заголовку сообщения сценария VBS): a 1010 db "WBScript" >Enter&rt; По смещению 1020h поместим строку ‘Hello, World!’ и оставим побольше места (на случай, если потом захотим изменить сообщение): a 1020 db "Hello, World!" >Enter&rt; По смещениям 1040 и 1050 поместим имена импортируемых модулей USER32.dll и KERNEL32.dll соответственно, на которые будет ссылаться таблица импорта: a 1040 db "USER32.dll" >Enter&rt; a 1050 db "KERNEL32.dll" >Enter&rt; Необходимо предусмотреть также имена импортируемых функций, но для них используются строки особого формата: первые два байта строки являются «подсказкой» загрузчику, и лишь после них идет собственно имя. Подсказка представляет собой индекс массива указателей экспортируемых имен DLL, по которому предположительно можно обнаружить искомое имя функции. Если в указанном месте нужное имя отсутствует (например, вследствие изменения версии DLL), загрузчик выполняет поиск данного имени по всему списку экспортируемых имен, на что, естественно, требуется больше времени. В нашем случае придется обойтись без подсказок: на их месте оставим нули. По смещению 1060h поместим имя функции ‘MessageBoxA’, по смещению 1070h - ‘ExitProcess’ (необходимо помнить, что, в отличие от имен DLL, в именах функций учитывается регистр символов): a 1060 db 0,0,"MessageBoxA" >Enter&rt; a 1070 db 0,0,"ExitProcess" >Enter&rt; Поскольку текстовые данные закончились, самое время проверить правильность введенных данных. Для дампа памяти используется команда отладчика d: d 1000 Если данные были введены правильно, в правой части экрана должны отобразиться в текстовом виде введенные нами имена. При ошибке нужно повторно ввести данные по тому же адресу. Далее (по смещению 1080h) разместим таблицы поиска. Аналогично IAT, таблица поиска состоит из последовательного ряда 32-разрядных (DWORD) значений, завершающихся нулевым полем, для импортируемых из одного модуля функций. Поля таблицы указывают на способ поиска функций в списках экспорта DLL: по порядковым номерам или по именам. В последнем случае поле содержит смещение на строку «подсказка-имя» для искомой функции. В нашем случае соответствующие смещения равны 1060h для ‘MessageBoxA’ и 1070h для ‘ExitProcess’ (к сожалению, debug не признает 32-разрядных чисел, поэтому придется вводить их как пары 16-разрядных; при этом надо учесть, что в PC для чисел применяется обратный порядок следования байтов): a 1080 dw 1060,0,0,0,1070,0,0,0 >Enter&rt; Поскольку IAT при загрузке должна быть идентична таблице поиска, теперь мы можем вернуться и заполнить оставленные ранее пустыми поля: a 1000 dw 1060,0,0,0,1070,0,0,0 >Enter&rt; Мы добрались до самой главной таблицы - таблицы импорта (не путать с таблицей импортируемых адресов). Таблица импорта связывает воедино все подготовленные ранее данные. Каждая строка таблицы импорта состоит из пяти 4-байтовых (DWORD) полей и относится к одному импортируемому модулю (DLL). Первое поле содержит смещение (относительно базового адреса загрузки) таблицы поиска для данной DLL; второе и третье не используются и содержат нули; четвертое - смещение на строку с именем DLL; пятое - смещение на соответствующую IAT. Число входов в таблицу импорта равно числу импортируемых модулей плюс одна строка, в которой все поля заполнены нулями для обозначения конца таблицы. В нашем случае таблица импорта будет состоять из 3 входов (для USER32.dll, KERNEL32.dll и один пустой). Таблица располагается по смещению 1090h и имеет размер 3х5х4=60 (3Ch) байт: a 1090 dw 1080,0 (смещение первой таблицы поиска) dw 0,0,0,0 (два пустых поля) dw 1040,0 (смещение на строку с именем USER32.dll) dw 1000,0 (смещение первой IAT) Аналогично заполняем вторую строку: dw 1088,0,0,0,0,0,1050,0,1008,0 >Enter&rt; Последующие 20 байтов оставляем пустыми. Осталось ввести только сам код. Функция MessageBoxA принимает 4 DWORD-параметра: дескриптор окна приложения (в нашем случае окно отсутствует, т.е. 0), указатель на строку сообщения, указатель на строку заголовка, тип окна сообщения (числовая константа; здесь - 0). Параметры функциям Win32 API передаются через стек в обратной последовательности, т.е. первым помещается в стек последний параметр. Поэтому на ассемблере код мог бы выглядеть примерно так: push 0 push offset title ; здесь - 401010h push offset message ; здесь - 401020h push 0 call IAT[1] ; адрес функции MessageBoxA Нужно учесть, что в стек помещаются не смещения, а линейные адреса, поэтому к смещениям строк 1020h и 1010h необходимо добавить базовый адрес загрузки (400000h), получив соответственно 401020h и 401010h; а для импортируемого адреса MessageBoxA - 401000h. Поскольку debug не работает с 32-разрядными смещениями, придется кодировать все самостоятельно (не забыв про обратный порядок байтов в числах): a 10d0 db 6a,0 db 68,10,10,40,0 db 68,20,10,40,0 db 6a,0 db ff,15,0,10,40,0 ExitProcess (адрес которого хранится во второй IAT по линейному адресу 401008h) принимает лишь один аргумент - код завершения (в нашем случае - 0): db 6a,0 db ff,15,8,10,40,0 >Enter&rt; Вот и вся программа. У нас получился как бы «образ в памяти» (по смещению 1000h), теперь надо перенести его на свое место в файле (по смещению 200h): m 1000 1200 200 Осталась самая малость - заполнить заголовок. Заголовок PE-файла можно разделить на «старый» и «новый». «Старый» заголовок, в свою очередь, состоит из несколько модернизированного заголовка EXE-DOS и необязательной программы-заглушки DOS, которая обычно выводит текст «This program cannot be run in DOS mode», когда PE-файл по ошибке пытаются запустить в DOS. Но вместо нее в принципе может быть любая другая DOS-программа. Модернизация DOS-заголовка заключается, во-первых, в резервировании по смещению 20h с начала файла места для идентификатора и имени производителя программы, которое практически всегда остается пустым. Во-вторых, и это уже существенно, по смещению 3Сh находится 32-разрядный указатель на PE-заголовок. Единственное, что нам нужно оставить в заголовке DOS - это сигнатура EXE-файла (ASCII-символы ‘MZ’): a 0 db 4d,5a >Enter&rt; Программу-заглушку мы вообще опустим; таким образом, PE-заголовок будет следовать непосредственно за 4-байтным указателем по смещению 3Ch, т.е. по смещению 40h. Это значение и запишем в качестве указателя: a 3c db 40 >Enter&rt; «Новый» заголовок представлен собственно PE-заголовком и так называемой таблицей объектов. PE-заголовок, в свою очередь, делится на стандартный и NT-заголовок. В конце последнего дополнительно выделяют каталог смещений/размеров. Каждый вход каталога представлен парой DWORD-значений, первое из которых содержит смещение соответствующей служебной таблицы относительно базового адреса загрузки, второе - размер таблицы. Если какая-либо из таблиц не используется, соответствующие поля содержат нули. В следующей таблице приведены только те поля PE-заголовка, без которых запуск программы невозможен: Смещение Размер (байт) Описание Стандартный ------------------------------------------------------------------------- 00h 4 Сигнатура: ASCII-символы ‘PE’ и два нулевых байта 04h 2 Тип процессора (обычно 14Сh для i386) 06h 2 Число секций в образе программы 14h 2 Размер NT-заголовка вместе с каталогом смещений; обычно E0h 16h 2 Флаги программы; для Win32-приложений обычно 10Fh NT-заголовок ------------------------------------------------------------------ 18h 2 «Магическое» значение 10Bh 28h 4 Точка входа в программу (смещение) 34h 4 Базовый адрес загрузки (для EXE обычно 400000h) 38h 4 Выравнивание секций в ОЗУ (размер системной страницы памяти, 4Кб=1000h) 3Ch 4 Файловая страница: выравнивание секций в файле (кратно 512 (200h) байт) 40h 2 Старший номер версии Windows; обычно 4 48h 2 Старший номер версии подсистемы Windows; обычно 4 50h 4 Размер загруженного образа вместе со всеми заголовками; кратен размеру выравнивания секций 54h 4 Общий размер всех заголовков (старых и новых) 5Ch 2 Тип приложения (2 - графическое, 3 - консольное) 74h 4 Число входов в каталоге смещений/размеров (обычно - 10h) Каталог смещений/размеров -------------------------------------------------------- 80h 4 Смещение таблицы импорта 84h 4 Размер таблицы импорта Примечание. Для запуска нашего приложения достаточно заполнить указанные в таблице поля (проверено для трех версий Windows: 98 SE, 2000 Server и XP Pro). Однако для более сложных программ может потребоваться также заполнить 4-байтные (DWORD) поля по смещениям 60h (резервируемый размер стека), 64h (выделенный размер стека), 68h (резервируемый размер кучи), 6Ch (выделенный размер кучи). Таблица объектов следует непосредственно за PE-заголовком и описывает секции (объекты) образа программы. Она фактически является картой отображения записанных на диске секций в память. Число входов в таблицу объектов равно числу секций в образе программы и указывается в PE-заголовке в поле со смещением 6. Каждый вход таблицы имеет следующий формат: Смещение Размер, байт Описание 0 8 Произвольное имя секции (используется при компоновке). Заполняется нулями до 8 байт. 8 4 Количество памяти, отводимое для загрузки секции 0Ch 4 Размещение секции в памяти, смещение относительно базового адреса загрузки 10h 4 Размер секции в файле, выровненный по границе файловой страницы 14h 4 Смещение секции в файле, выровненное по границе файловой страницы 18h 0Ch Зарезервировано; используется в объектных файлах 24h 4 Флаги секции. Наиболее употребляемые: 20h - секция кода; 40h - инициализированные данные; 80h - неинициализированные данные; 20000000h - разрешено исполнение; 40000000h - разрешено чтение; 80000000h - разрешена запись Завершим создание заголовка. По смещению 40h находится сигнатура PE: a 40 db 50,45,0,0 Процессор i386, число секций - 1: dw 14c,1 >Enter&rt; Размер NT-заголовка E0h, флаги программы, «магическое» значение: a 54 dw e0,10f,10b >Enter&rt; Точка входа в программу: a 68 dw 10d0 >Enter&rt; Базовый адрес загрузки 40 0000h (в обратном порядке), выравнивание в памяти - 1000h, в файле - 200h: a 74 dw 0,40,1000,0,200,0 Версия ОС - 4.0, версия подсистемы - 4.0; промежутки заполняются нулями: dw 4,0,0,0,4 >Enter&rt; Размер образа с заголовками в памяти - 2000h, размер заголовка в файле - 200h, подсистема 2: a 90 dw 2000,0,200,0,0,0,2 >Enter&rt; 10h входов в каталоге смещений: a b4 dw 10 >Enter&rt; В каталоге смещений используем лишь один вход: смещение таблицы импорта - 1090h, размер - 3Ch. Остальные входы оставляем пустыми: a c0 dw 1090,0,3c >Enter&rt; По смещению 140h начинается таблица объектов; в нашем случае она имеет лишь один вход. Никакого имени секции давать не будем. Секция занимает в памяти 1000h байт, начиная со смещения 1000h; в файле занимает 200h байт и находится по смещению 200h: a 140 dw 1000,0,1000,0,200,0,200,0 >Enter&rt; И, наконец, флаги: секция является кодовой, имеет разрешения на исполнение, чтение и запись. Сумма всех флагов (в данном случае это эквивалентно побитовому OR) составит E0000020h (порядок слов обратный): a 15с dw 20,e000 Отладчик debug позволяет сохранять файлы лишь в com-формате, при этом первые 100h байт на диск не записываются. Поэтому необходимо сначала «сдвинуть» весь (400h) образ в памяти на 100h байт: m 0 400 100 Озаглавим наш файл; отладчик debug не позволяет записывать файлы в формате exe, поэтому сохраним сначала файл с расширением .bin, потом его переименуем в exe. n hello.bin Количество записываемых байтов заносится в регистр СХ, после чего осуществляется собственно запись командой w: r cx 400 w Для выхода из отладчика используется команда q. Но перед этим нужно еще раз тщательно проверить правильность всех введенных данных. При ошибочном расположении чисел в таблицах попытка запуска может вызвать сообщение: «>Имя программы&rt; не является приложением Win32», а то и вообще вызвать сбой системы. Ну что же, примите мои поздравления: вы создали полноценное приложение Win32 даже не на ассемблере, а в машинных кодах! Последний раз редактировалось Deementor; 13.02.2007 в 02:08.. |
Эти 2 пользователя(ей) сказали cпасибо за это полезное сообщение: |
Реклама: | Рекомендуем гипермаркет KNS - xerox workcentre b315v dni - Подарок каждому покупателю! | обувниц | фильм мегамозг | Conecte Acuity Scheduling a noCRM | Рекомендуем гипермаркет КНС.ру - EFT33-ECPU-04 - Подарок каждому покупателю! |
18.12.2006, 18:08 | #2 |
ViP
Пол: Регистрация: 17.09.2006
Сообщений: 1,182
Репутация: 1592
|
Re: windows голыми руками
DLL
В предыдущей статье было показано, как с помощью debug «вручную» собрать простейшее Win32 exe-приложение с MessageBox’ом. На этот раз предлагается аналогичным образом создать простейшую dll; это тематическое продолжение прошлой статьи и в то же время необходимый фундамент. Я полагаю, что Вы внимательнейшим образом изучил прошлый материал и умеет теперь с лёту создавать PE-заголовки, таблицы импорта и секции кода и данных J. Предполагается также, что благодаря творчеству Свина и последовавшему за этим повальному поветрию увлечения мануалами от Интела никому не составит особого труда разобраться в hex’ах или хотя бы даже и в бинарных кодах. Поэтому все внимание сосредоточим на более полезных вещах - на том, что же отличает dll от обычных exe-файлов. А отличий главных два: появляются экспортируемые функции и, следовательно, таблица экспорта, а также, как ни прискорбно, придется нам разбираться с настройками (relocations), поскольку загрузка нашей dll по нашему любимому базовому адресу 10000000h вовсе не гарантируется L. На этот раз для разнообразия сделаем в нашем PE-файле 4 секции - для кода, данных, импорта с экспортом и настроек. Проявим также, в отличие от прошлого раза, почтение и дадим им имена: .code, .data, .rdata и .reloc соответственно. Они расположатся в памяти по смещениям 1000h, 2000h, 3000h, 4000h, а в файле 200h, 400h, 600h и 800h соответственно. Само содержание будет тем же, т.е. экспортируем лишь одну функцию, вся работа которой будет заключаться в отображении MessageBox’а. В данных всего две строки; создаем файл data.txt: n data.bin r cx 200 f 0 l 200 0 e 0 "Dll" e 10 "Экспортированная функция" m 0 l 200 100 w q Надеюсь, понятно, что в файле записаны команды для debug. На этот раз мы решили несколько автоматизировать процесс J. Итак, строка с заголовком располагается у нас в начале секции данных по смещению 2000h, а «любимый» адрес в памяти будет соответственно 10002000h. Для строки с сообщением цифры будут 2010h и 10002010h соответственно. А теперь - код! Придется снова импортировать MessageBoxA из User32.dll; на этот раз IAT расположится в собственной секции - .rdata - как обычно, в самом начале, т.е. по смещению 3000h; а «любимый» адрес будет 10003000h. Других импортов нет. Заполняем файл code.txt: n code.bin r cx 200 f 0 l 200 0 a 0 ; параметры MessageBox’a db 6a 0 db 68 0 20 0 10 db 68 10 20 0 10 db 6a 0 ; вызов MessageBox db ff 15 0 30 0 10 ; возврат db c3 m 0 l 200 100 w q Пустую строку нельзя убирать! А то debug будет ругаться, а вы получите фуфло вместо классного бинарного блока J. Однако, как мы - не прошло и пяти минут, а уже полфайла составили! На носу новый материал, однако. Посмотрим на код и выделим «топкие» места: 1000: 6a 00 68 00 | 20 00 10 68 | 10 20 00 10 | 6a 00 ff 15 1010: 00 30 00 10 | c3 Я выделил жирным «любимые» адреса, попавшие в состав инструкций. Подлая система может закинуть нашу dll куда-нибудь совсем в другое место - вот тогда-то наши адреса и накроются, а код начнет запихивать в стек всякий хлам и вдобавок отправит все это вместо нашей импортированной функции по невесть какому адресу. Именно эти три 32-разрядных значения и должны быть настроены; а для этого их надо указать в таблице настроек. Каждая настройка представлена всего лишь двумя байтами - 16-разрядным значением, причем 4 старших бита обозначают тип настройки. Для Win32 это практически всегда значение 3, означающее, что надо «поправить» 32-разрядный адрес по указанному смещению. А смещение указывают оставшиеся 12 битов. Но, как вы понимаете, этого хватает лишь на смещения в пределах одной страницы - 4 Кб. Так оно и есть - настройки группируются в блоки; для каждой настраиваемой страницы имеется свой блок, а в начале блока первые 4 байта содержат смещение данной страницы относительно базового адреса загрузки, а следующие 4 байта - размер блока (вместе с первыми 8 байтами). Остальное содержимое блока - набор настроек для данной страницы (см. рис.) При загрузке dll система вычисляет так называемую дельту - разницу между базовым адресом загрузки, указанным в PE-заголовке, и адресом, по которому фактически загружена dll. Естественно, если dll загружена по своей «любимой» базе, дельта равна 0 и никаких настроек не требуется. Если же это не так, дельта добавляется к каждому 32-разрядному значению, для которого имеется настройка. Всего-то и делов. В нашем случае должно быть 3 настройки; их смещения относительно начала страницы - 3, 8 и 10h. С учетом типа (3) получаем числа 3003h, 3008h и 3010h. Блок должен быть выровнен по 32-разрядной границе, поэтому в конец добавим наполнитель из «пустой» настройки (для нее есть даже свой тип - естественно, 0). В итоге получаем: RVA страницы - это смещение настраиваемой, т.е. кодовой страницы - 1000h; размер блока - 8 байт + 3 настройки по 2 байта + 1 «пустая» настройка (2 байта) - всего 10h. Секция настроек готова! Набираем файл reloc.txt: n reloc.bin r cx 200 f 0 l 200 0 a 0 ; RVA страницы dw 1000 0 ; размер блока dw 10 0 ; настройки dw 3003 dw 3008 dw 3010 m 0 l 200 100 w q Остается экспорт - поскольку статью читают специалисты по импорту, о нем больше ни слова. Главная таблица, объединяющая все остальные - таблица экспорта - имеет следующий вид: DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; DWORD Base; DWORD NumberOfFunctions; DWORD NumberOfNames; DWORD AddressOfFunctions; // RVA from base of image DWORD AddressOfNames; // RVA from base of image DWORD AddressOfNameOrdinals; // RVA from base of image При экспорте по ординалам используются всего две таблицы: основная таблица экспорта и таблица экспортируемых адресов. Последняя представляет собой просто массив 32-разрядных смещений реализаций соответствующих функций относительно базового адреса загрузки. Т.е. у каждой экспортированной функции имеется свой индекс в этом массиве; добавив к этому индексу т.н. базу, получаем ординал этой функции. Если функции экспортируются по именам, добавляются еще три таблицы: ординалов, указателей имен и самих имен. С таблицей имен все понятно: она содержит экспортируемые имена. Причем их может быть меньше, чем экспортируемых адресов. «Стыковку» же имен с адресами соответствующих функций осуществляют таблицы ординалов и указателей имен; фактически, это два тесно сопряженных друг с другом массива. Сопряжение осуществляется за счет того, что на одном и том же месте (с одинаковым индексом) в обоих массивах находятся данные для одной функции: в первом массиве - индекс для таблицы экспортируемых адресов, во втором - адрес строки с именем соответствующей функции. Причем функции в этих двух массивах расположены так, что их имена упорядочены по алфавиту (в порядке возрастания их индексов в этих массивах). Заметьте: упорядоченными должны быть не сами имена (их можно даже разбросать по всей секции); упорядоченными должны быть указатели на них в другой таблице. Следует также помнить, что индексы в таблице ординалов 16-разрядные, а указатели имен - 32-разрядные. Пора взяться за последнюю секцию - ‘.rdata’. Сначала прикинем «макет» (см. рис.) Теперь файл rdata.txt: n rdata.bin r cx 200 f 0 l 200 0 a 0 ; Импорт ; IAT dw 3020 0 0 0 ; Таблица поиска dw 3020 0 0 0 ; Имя импортируемого модуля db "User32.dll" 0 a 20 ; Импортируемая функция с hint’ом db 0 0 "MessageBoxA" 0 a 30 ; Таблица импорта: ; смещение таблицы поиска dw 3008 0 ; 2 пустых поля dw 0 0 0 0 ; смещение имени dll dw 3010 0 ; смещение IAT dw 3000 0 a 60 ; Экспорт ; Таблица экспорта: ; 3 пустых поля dw 0 0 0 0 0 0 ; смещение имени dll dw 3094 0 ; база ординалов dw 1 0 ; число адресов dw 1 0 ; число имен dw 1 0 ; смещение адреса dw 3088 0 ; смещение указателя имени dw 3090 0 ; смещение ординала dw 308C 0 ; (Таблица) адресов dw 1000 0 ; (Таблица) ординалов dw 0 0 ; (Таблица) указателей имен dw 30a0 0 ; Имя dll db "Dll.dll" 0 a a0 ; Экспортируемая функция db "Function1" 0 m 0 l 200 100 w q Помните, пустые строки нельзя трогать! Теперь надо лишь слегка подправить PE-заголовок - файл header.txt: n Header.bin r cx 200 f 0 l 200 0 e 0 'MZ' e 3C 40 e 40 'PE' e 44 4C 01 a 46 ; Число секций db 04 a 54 ; Размер дополнительного заголовка db e0 00 ; Тип файла: установить флаг dll и флаг, разрешающий ; загружать образ по базовому адресу, отличному от ; указанного в PE-заголовке db 0E 21 ; "Магическое" значение db 0B 01 a 74 ; Базовый адрес загрузки db 00 00 00 10 ; Выравнивание секций db 00 10 00 00 ; Выравнивание в файле db 00 02 00 00 ; Старшая версия Windows db 04 a 88 ; Старшая версия подсистемы db 04 a 90 ; Размер загруженного файла в памяти db 00 50 00 00 ; Размер всех заголовков в файле db 00 02 a 9C ; Подсистема db 02 00 a A0 ; Зарезервированный размер стека db 00 00 10 00 ; Выделенный размер стека db 00 10 00 00 ; Зарезервированный размер кучи db 00 00 10 00 ; Выделенный размер кучи db 00 10 00 00 a B4 ; Число элементов каталога смещений db 10 00 00 00 ; ; Каталога смещений: ; смещение таблицы экспорта dw 3060 0 ; размер данных экспорта dw 4a 0 ; смещение таблицы импорта dw 3030 0 ; размер таблицы импорта dw 28 0 ; пропускаем 24 байта (3 элемента) dw 0 0 0 0 0 0 0 0 0 0 0 0 ; смещение таблицы настроек dw 4000 0 ; размер таблицы настроек dw 10 a 138 ; Начало таблицы секций ; ; имя первой секции db '.code' 0 0 0 ; размер секции в памяти dw 200 0 ; смещение секции относительно адреса загрузки dw 1000 0 ; размер данных секции в файле dw 200 0 ; смещение начала данных секции в файле dw 200 0 ; Пропускаем 12 байтов dw 0 0 0 0 0 0 ; атрибуты первой секции db 20 00 00 60 ; вторая секция db '.data' 0 0 0 dw 200 0 dw 2000 0 dw 200 0 dw 400 0 dw 0 0 0 0 0 0 db 40 0 0 c0 ; третья секция db '.rdata' 0 0 dw 200 0 dw 3000 0 dw 200 0 dw 600 0 dw 0 0 0 0 0 0 db 40 0 0 40 ; четвертая секция db '.reloc' 0 0 dw 200 0 dw 4000 0 dw 200 0 dw 800 0 dw 0 0 0 0 0 0 db 40 0 0 42 m 0 l 200 100 w q Всё! Собираем все вместе в файле make.bat: @echo off debug < header.txt > report.lst debug < code.txt >> report.lst debug < data.txt >> report.lst debug < rdata.txt >> report.lst debug < reloc.txt >> report.lst copy /b header.bin+code.bin+data.bin+rdata.bin+reloc.bin dll.dll Запускаем этот файл и - ура! - получаем нашу dll. Всенепременно надо заглянуть в заботливо созданный для вас файл отчета - report.lst, чтобы придирчиво поискать там ошибки, о которых сообщает debug. Ведь вы, конечно, прекрасно знаете: метод cut&paste не спасает от самых дебильных ошибок! Да, это, конечно, хорошо; но ведь понадобится еще и тестовое exe-приложение, чтобы проверить работу нашей dll? Я нисколько не сомневаюсь, что ваш уровень теперь позволит с легкостью самостоятельно создать в debug эту тестовую программку J. Ладно, ладно... Вижу вытянувшиеся физиономии некоторых. Вы славно потрудились сегодня (даже если просто прочли это до конца), и в качестве бонуса я решил предоставить «ленивое» тестовое приложение на MASM’е 32-ом. Вот оно: .386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\kernel32.inc include \masm32\include\user32.inc includelib \masm32\lib\kernel32.lib includelib \masm32\lib\user32.lib .data hM1 dword 0 hM2 dword 0 app db "Test dll",0 dll db "dll.dll",0 dll2 db "dll2.dll",0 fname db "Function1",0 err1 db "LoadLibrary (dll1) failed",0 err1_1 db "LoadLibrary (dll2) failed",0 err2 db "GetProcAddress (first) failed",0 err2_1 db "GetProcAddress (second) failed",0 .code start: invoke LoadLibrary,offset dll .if eax==0 invoke MessageBox,0,offset err1,offset app,MB_ICONERROR ret .endif mov hM1,eax invoke LoadLibrary,offset dll2 .if eax==0 invoke MessageBox,0,offset err1_1,offset app,MB_ICONERROR ret .endif mov hM2,eax invoke GetProcAddress,hM1,offset fname .if eax==0 invoke MessageBox,0,offset err2,offset app,MB_ICONERROR ret .endif call eax invoke GetProcAddress,hM2,offset fname .if eax==0 invoke MessageBox,0,offset err2_1,offset app,MB_ICONERROR ret .endif call eax invoke FreeLibrary,hM1 invoke FreeLibrary,hM2 ret end start Надо скопировать созданную нами dll.dll под новым именем dll2.dll в этот же каталог. Вся соль в том, что нам нужно проверить, что наши настройки были правильные, и система может с ними работать. А для этого требуются как минимум две dll, претендующие на одно и то же место в адресном пространстве. Самый ленивый способ, конечно, который только можно придумать - это просто использовать второй переименованный экземпляр. Но на радостях вы можете поэкспериментировать с текстами, хотя бы поменять выводимые MessageBox’ом сообщения и создать другую dll с другими именами модуля и экспортируемой функции. Последний раз редактировалось Deementor; 18.12.2006 в 18:12.. |
Эти 3 пользователя(ей) сказали cпасибо за это полезное сообщение: |
13.08.2008, 17:52 | #3 |
Неактивный пользователь
Пол: Регистрация: 07.08.2008
Сообщений: 13
Репутация: 1
|
Ответ: windows голыми руками
Интересная статейка...
А можно ли раздобыть подробное описание операторов того самого языка... и на сколько он функционален к созданию полноценных приложений? |
26.01.2009, 21:39 | #4 |
Новичок
Регистрация: 21.01.2009
Сообщений: 29
Репутация: 17
|
Re: Ответ: windows голыми руками
Лизер, это ассемблер в байтиках, он функционален даже в драйверах, на "wasm.ru"
полно его разновидностей. |
Похожие темы | ||||
Тема | Автор | Раздел | Ответов | Последнее сообщение |
Защита своими руками ... | megamozg | Защита от угона | 51 | 01.11.2014 06:41 |
Wi-Fi сеть своими руками | dmitry_ua | Локальные сети и их комплектующие | 7 | 27.04.2012 19:48 |
VPN сервер своими руками | Genya_ | Архив | 3 | 03.10.2007 10:22 |
|
|