Перейти из форума на сайт.

НовостиФайловые архивы
ПоискАктивные темыТоп лист
ПравилаКто в on-line?
Вход Забыли пароль? Первый раз на этом сайте? Регистрация
Компьютерный форум Ru.Board » Компьютеры » Прикладное программирование » Win32 в машинных кодах

Модерирует : ShIvADeSt

 Версия для печати • ПодписатьсяДобавить в закладки

Открыть новую тему     Написать ответ в эту тему

RoustemRuBoard

Newbie
Редактировать | Профиль | Сообщение | Цитировать | Сообщить модератору
Есть 10 небольших статей - введение в программирование под Windows (Win32) прямо в машинных кодах. Описывается создание исполняемых файлов Windows (PE-exe) "вручную", с использованием debug.exe (хотя в принципе можно использовать любой 16-ричный редактор). Цикл завершается на создании общего шаблона оконного приложения (generic.exe).  
Мне хотелось бы знать, интересна ли кому-нибудь здесь подобная тема - если да, готов поделиться. И в случае, если это будет интересно, вопрос администраторам-модераторам - куда эти статьи нужно отправлять, чтобы опубликовать здесь?..

Всего записей: 12 | Зарегистр. 06-06-2007 | Отправлено: 10:34 06-06-2007
Hiken



Advanced Member
Редактировать | Профиль | Сообщение | Цитировать | Сообщить модератору
Предлагаю разместить статьи у нас Реклама Если не будешь против, переведем их также и на украинский язык.




не рекламируем ресурсы

Всего записей: 1343 | Зарегистр. 14-01-2006 | Отправлено: 16:14 06-06-2007 | Исправлено: ShIvADeSt, 01:59 07-06-2007
WiseAlex



Софтовых дел М...
Редактировать | Профиль | Сообщение | Цитировать | Сообщить модератору
RoustemRuBoard
можно конечно как посты оформить и в путеводитель, но тематика несколько специфическая
Я бы сказал, что тема больше для wasm.ru, чем для местного форума

Всего записей: 1001 | Зарегистр. 02-03-2003 | Отправлено: 20:30 06-06-2007
RoustemRuBoard

Newbie
Редактировать | Профиль | Сообщение | Цитировать | Сообщить модератору
WiseAlex
На wasm.ru уже есть мои статьи подобной тематики
Идея (может быть, безумная) была в том, чтобы попытаться заинтересовать подобной темой аудиторию побольше - возможно, тех, кто об этом вообще не подозревал и не собирался этим заниматься, и посмотрел материал чисто из любопытства. Соответственно, и писалось все так, чтобы работающие приложения мог собрать даже тот, кто вообще не занимался программированием - обычный пользователь, на самом обычном компьютере, без всяких сред разработки или т.п.  
Собс-но, многие низкоуровневики сами довольно скептически отнеслись в этой идее. Мне же просто хотелось предоставить выбор - чтобы программист, знающий о программировании в машинных кодах лишь понаслышке и считающий это безнадежно устаревшим делом, попробовал создать реально работающее приложение своими руками, потом сравнил это с работой в средах программирования и уже сам, на основе этого собственного - хоть и небольшого - опыта принял осознанное, а не навязанное чужими мнениями и стереотипами, решение - что это такое и что можно с этим делать.

Всего записей: 12 | Зарегистр. 06-06-2007 | Отправлено: 14:21 07-06-2007
WiseAlex



Софтовых дел М...
Редактировать | Профиль | Сообщение | Цитировать | Сообщить модератору
RoustemRuBoard
Ну тогда, можно постить прямо в этой теме. по посту на статью - я поставлю на эту тему ссылку в путеводителе

Всего записей: 1001 | Зарегистр. 02-03-2003 | Отправлено: 17:46 07-06-2007
dmka



Advanced Member
Редактировать | Профиль | Сообщение | Цитировать | Сообщить модератору
RoustemRuBoard
Согласен с низкоуровневиками, так как на основе собственного опыта не вижу реального применения этой технологии "в мирных целях". Да и при желании изучить вопрос, добраться до wasm-а не сложнее чем до ру-борда.
 
С другой стороны, не хочу никого отговаривать - чем больше интересных статей, тем лучше

Всего записей: 947 | Зарегистр. 23-04-2003 | Отправлено: 02:55 08-06-2007
RoustemRuBoard

Newbie
Редактировать | Профиль | Сообщение | Цитировать | Сообщить модератору
1. Введение в машинные коды для Win32
 
Мир машинных кодов для процессоров Intel IA-32 захватывающий и фантастический. Он предоставляет такие богатые возможности для творчества, что будет неудивительно, если через некоторое время станут проводить чемпионаты по спортивному программированию в машинных кодах, а лучшие творения кодеров представлять на выставках, как произведения искусства. Множество интересных находок было накоплено за прошедшие годы кодокопателями, среди которых есть как законные системные программисты, так и подпольные авторы вирусов, хакеры и кракеры.  
 
Как когда-то великие путешественники-первопроходцы открывали новые земли, кодеры исследуют бурно разросшееся виртуальное пространство информационных технологий. Несмотря на то, что ее создавали сами люди, эта матрица нашего времени стремительно развивается по каким-то своим законам. Накопились огромные пласты неосвоенных знаний. Развилась целая философия "быстрой разработки приложений" - своего рода "информационный фастфуд". Но разве может забегаловка заменить собой изысканный ресторан?  
 
Можно сказать, информационные технологии проходят сейчас период массового производства, как когда-то автомобильная и другие виды промышленности. Конвейер штампует однотипные универсальные изделия. Но посмотрите на исторические тенденции. Сначала автомобили собирали поштучно. Потом появился конвейер. Но сейчас самые дорогие и качественные машины опять собирают вручную! А разве механические часы исчезли с появлением электронных? Напротив, стали только качественнее и дороже. А когда их сравнивают с электронными, последние презрительно именуют "штамповкой". И как сравнить массовую бижутерию с синтетическими камнями с филигранной ювелирной работой?..  
 
Как бы то ни было, но и в компьютерной индустрии постепенно развилась особая субкультура низкоуровневого программирования. Долгое время она варилась в собственном соку, оставаясь достоянием узкого круга посвященных, интенсивно осмысливавших накопленные знания. Вероятно, был пройден некий порог, и мы вплотную приблизились к моменту, когда начинает зарождаться элитарное штучное ручное производство и в данной высокотехнологичной области. И делать это, естественно, могут лишь специалисты высочайшей квалификации, понимающие значение каждого используемого байта. Однако для дальнейшего развития в этом направлении нужно не только ознакомить более широкую аудиторию с накопленным в узких кругах опытом, но и развенчать некоторые уже устаревшие стереотипы наподобие того, что современные системы программировать на низком уровне невозможно вообще.  
 
Вот с этой целью и появилась задумка систематически рассмотреть с уровня машинных кодов работу наиболее популярной ОС - Windows, чтобы это оказалось доступным самому широкому кругу заинтересовавшихся читателей - от простых пользователей до искушенных программистов. Это и программирование, и изучение работы ОС "изнутри", причем проводимое без всяких посредников в виде языков программирования, вспомогательных библиотек и сред разработки, напрямую, "как есть" в самой ОС. Для работы специально будут использоваться простейшие и даже примитивные инструменты, входящие в состав любой версии Windows от 95 до XP и даже 2003 Server - любой, кто захочет, сможет повторить описываемые эксперименты на самом обычном компьютере.  
 
Хочу добавить пару слов о пользователях, никогда до этого не программировавших. Идея научить их программировать - причем сразу в машинных кодах и сразу под Windows - может, и несколько авантюрная (даже многие низкоуровневики отнеслись к ней скептически), тем не менее, мне кажется, это вполне посильная задача. Особенно если учесть, сколько сил и времени надо затратить, чтобы научиться работать в интегрированной среде разработки, скажем, в том же VisualBasic'е, не говоря уже о том, что надо еще выучить язык. А если, не приведи господи, в набранном из самоучителя тексте окажется опечатка и система выдаст кучу сообщений об ошибках - для новичка продраться через это, по моему глубокому убеждению, гораздо более нереально, чем построить собственными руками подобное же, но работоспособное приложение в машинных кодах.  
 
Не надо бояться окунуться в джунгли машинных кодов. На самом деле, здесь уже есть и проторенные дороги, и тайные заветные тропинки - надо всего лишь их знать и уметь по ним ходить. И я хочу просто показать, как это можно сделать; а уж каждый пусть сам сравнивает, оценивает и решает, сложно это или элементарно, нужно это ему или нет - это будет осознанный выбор, основанный на его собственных знаниях и опыте, а не на чьих-то стереотипах из прошлого.  
 
Что ж, пора перейти от вступлений к сути. Архитектура процессоров Intel IA-32 относится к CISC-модели (с усложненным набором инструкций). Одна из самых примечательных особенностей этих процессоров - формат команды с переменным размером. Команда процессора может быть от 1 до 15 байтов длиной (включая возможные префиксы). Любители комбинаторики могут подсчитать количество возможных инструкций при такой схеме. Но и без подсчетов ясно, что число астрономическое. Команда может иметь один или несколько так называемых префиксов; собственно код операции (он называется опкодом) состоит из 1 или 2 байтов, а дальше идут байты, описывающие операнды - данные (или ссылки на данные), над которыми производится соответствующая операция. Даже если считать командой лишь байты опкода, то возможны 255 однобайтных команд и столько же двухбайтных (в двухбайтных опкодах первый байт всегда одинаков и равен 0Fh). Т.е. получаем свыше 500 команд процессора (на самом деле, не все возможные опкоды используются в настоящее время; кроме того, некоторые опкоды могут иметь дополнительные поля в байтах для операндов и т.п., но это уже тонкости, которые мы можем пока опустить).  
 
Пугаться этого не следует. На самом деле, для программирования под Windows требуется весьма ограниченный набор инструкций, и скоро мы сможем в этом убедиться. Мы будем изучать нужные нам инструкции по мере необходимости. А сейчас кратко рассмотрим "суть" программирования в машинных кодах, а она довольно проста.  
 
Компьютер - это машина для обработки информации. Для этой цели вся информация, которую нужно обработать, делится на более-менее элементарные "кусочки". Необходимая обработка тоже подразделяется на более-менее элементарные действия. Элементарный "кусочек" обрабатываемой информации называется операндом, а элементарное действие - командой. Таким образом, инструкция процессора представляет собой команду и связанные с ней операнды (которые, кстати, могут подразумеваться, а не быть явно заданными в инструкции). А сама программа представляет собой набор инструкций.  
 
Все уже знают, что информация в компьютере представлена в виде двоичных чисел. Обычно в этом месте положено рассказывать об основах двоичного и шестнадцатеричного счислений и способах перевода чисел из одной формы представления в другую, но мы этого делать не будем. Во-первых, это несколько отвлекает от нашей непосредственной темы; во-вторых, кому надо, без труда найдет соответствующие сведения; а в-третьих, все это и так запомнится при практической работе. А если на первых порах будут проблемы, в Windows есть стандартное приложение - калькулятор, который можно использовать для перевода чисел из одной формы в другую. Только в меню "Вид" калькулятора установите "Научный", и в верхнем ряду слева увидите 4 кнопки-переключателя "Hex", "Dec", "Oct", "Bin", которыми и нужно пользоваться.  
 
Windows сильно упрощает программирование - это относится к машинным кодам в значительно большей степени, чем к любому языку программирования на высоком уровне (обстоятельство, которое упускают из виду противники низкоуровневого программирования). Для программирования под Windows нам вполне достаточно рассматривать процессор, как обычный калькулятор. В свое время был такой программируемый калькулятор - Б3-34. Он имел 14 регистров для хранения чисел. В процессоре тоже есть набор 32-разрядных регистров общего пользования, и их всего 8. На ассемблере их обозначают как EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI. Понятное дело, в машинных кодах никаких букв нет, и регистры кодируются тремя битами (в указанном выше порядке - от 000 до 111). Но в разговоре для удобства мы будем использовать и их "названия".  
 
Еще одна особенность интеловских процессоров - они несут на себе "печать своего детства": когда-то регистры были 16-разрядными, и именовались соответственно как AX, CX, DX, BX, SP, BP, SI, DI (причем с такими же кодами, как для 32-разрядных регистров). Еще раньше микропроцессоры были 8-разрядными, и регистров у них было поменьше; очевидно, это тоже оставило свой след, поскольку к четырем 16-разрядным регистрам (AX, CX, DX, BX) можно обращаться побайтно, т.е. отдельно к старшему и младшему байтам. Эти отдельно взятые байты четырех общих регистров обозначаются как AL (младший байт AX), CL, DL, BL, AH (старший байт AX), CH, DH, BH; а коды их тоже соответственно от 000 до 111 (совпадают со значениями для "полных" регистров.  
 
   
 
На рисунке показано взаимоотношение адресуемых частей для регистра EAX; регистры ECX, EDX и EBX имеют подобную же схему. Регистры ESP, EBP, ESI и EDI "включают" в свой состав лишь 16-разрядные SP, BP, SI, DI и не допускают обращения к отдельным байтам.  
 
Как же узнать, к какой именно части регистра происходит обращение, тем более, если коды регистров одни и те же (как в случае EAX, AX и AL)? Эта информация заложена в саму инструкцию. Многие опкоды имеют так называемый бит w, который указывает на размер используемого регистра (или операнда в целом): если он равен 0, это байт, если 1, "полный" регистр. В 16-разрядном режиме бит w обозначает размер операнда 8 или 16 бит. Но современная Windows работает в 32-разрядном режиме, и состояние бита w обозначает размер операнда 8 или 32 бита. Обращение к 16 младшим битам регистра тоже возможно, но для этого используется другая схема с применением префиксов (об этом поговорим в другой раз).  
 
Есть еще два регистра, с которыми придется иметь дело: это регистр флагов EFLAGS и указатель инструкций EIP. Состояние регистра флагов может меняться после каждой инструкции в зависимости от полученного результата; подробнее об этом поговорим в другой раз. Регистр EIP содержит адрес начала следующей инструкции в памяти. Его значение увеличивается каждый раз, когда из памяти извлекается для исполнения очередная инструкция, на величину размера этой инструкции.  
 
Обрабатываемые инструкцией данные могут находиться не только в регистре, но и в памяти, а также входить в состав самой инструкции. При обращении к памяти в инструкции указывается адрес, по которому расположены данные. Рассмотрим различные способы доступа к данным на примере инструкции (а вернее, группы инструкций) перемещения данных, которыми мы будем очень активно пользоваться. На ассемблере группа данных инструкций обозначается мнемоникой MOV.  
 
Начнем с команды, которая перемещает непосредственное значение (являющееся частью самой инструкции) в регистр общего назначения. Формат команды следующий:  
 

Код:
1011 w reg <байты данных>

 
В зависимости от значения бита w за опкодом следует либо 1, либо 4 байта, содержащих непосредственное значение (и это значение попадет соответственно либо в 1-байтную часть регистра, либо заполнит регистр целиком). В архитектуре IA-32 используется так называемый "little-endian" порядок следования байтов (его называют обратным): сначала (по младшим адресам в памяти) размещаются младшие байты числа. Т.е. 16-ричное ABCDh будет представлено как байты CDh ABh, а 12345678h - как 78h 56h 34h 12h. Подробнее об этом поговорим в следующей статье, а пока пример: загрузим в регистр EAX единицу. Регистр 000, бит w=1 (полный регистр), а данные - внимание - 4 байта для одной единицы!  
 

Код:
10111000 00000001 00000000 00000000 00000000

 
Или в 16-ричном виде: B8 01 00 00 00. А вот как то же значение передается в младший байт регистра EAX (т.е. AL): регистр тот же - 000, бит w=0 (1 байт), а вот данные уже - 1 байт - 01:  
 
 

Код:
10110000 00000001 (B0 01)

 
Обратите внимание - если в регистре EAX до этого содержался 0, последняя инструкция будет равносильна первой. Но в общем случае это не так.  
 
Теперь эту же единицу загрузим в старший байт регистра AX (2-й байт EAX): тоже один байт (w=0), но код регистра AH уже другой (100):  
 

Код:
10110100 00000001 (B4 01)

 
Удовольствие составления различных инструкций с данным опкодом оставим вам для самостоятельных упражнений и перейдем к другой команде, которая перемещает данные между памятью и регистром EAX (AX, AL):  
 

Код:
101000 d w <байты адреса>

 
Этот опкод содержит бит w, но не содержит кодов регистров, поскольку он предполагает работу лишь с регистром EAX (или его частью). Зато есть другой характерный бит - d (direction), указывающий направление перемещения данных - из памяти в регистр (0) или из регистра в память (1).  
 
В этом примере мы видим одну важную особенность обращения к данным в памяти: размер операнда и размер его адреса в памяти - разные вещи. В данном случае операнд находится в памяти и может занимать 1, 2 или 4 байта, тогда как адрес (входящий в состав самой инструкции) в любом случае занимает 4 байта. Составим инструкцию для перемещения в регистр EAX значения, которое хранится по адресу 1. Используется полный регистр (w=1), направление - из памяти в регистр (d=0):  
 

Код:
10100001 00000001 00000000 00000000 00000000 (A1 01 00 00 00)

 
А теперь то же значение загрузим в регистр AL (w=0, d=0):  
 

Код:
10100000 00000001 00000000 00000000 00000000 (A0 01 00 00 00)

 
Изменился всего один бит инструкции! Между тем результат операции будет разительно отличаться: в первом случае в регистр EAX будут скопированы четыре (!) байта, начиная с адреса 1, тогда как во втором случае - в регистр AL будет скопирован лишь один байт по тому же адресу, остальные 3 байта регистра EAX останутся без изменений.  
 
Архитектура IA-32 предоставляет очень богатый набор способов адресации памяти. Сейчас отметим лишь, что возможна еще и косвенная адресация, когда адрес операнда в памяти находится в регистре, а инструкция ссылается на соответствующий регистр. Для работы с такими случаями, а также для перемещения данных между регистрами используется так называемый байт способа адресации (ModR/M). Этот байт следует непосредственно за опкодом, который предполагает его использование, и содержит следующие поля:  
 

Код:
2 бита MOD - 3 бита REG - 3 бита R/M

 
Байт ModR/M предполагает, что имеются два операнда, причем один из них всегда находится в регистре (код которого содержится в поле REG), а второй может находиться (в зависимости от значения поля MOD) либо тоже в регистре (при MOD = 11; при этом поле R/M содержит код регистра), либо в памяти (R/M="register or memory"). В последнем случае адрес памяти, по которому находится операнд, вычисляется следующим образом (см. табл.):  
 
   
 
SIB означает, что после байта ModR/M следует еще один байт способа адресации (Scale-Index-Base - SIB), который мы рассматривать не будем. При MOD=00 нужный адрес памяти находится в соответствующем регистре, кроме R/M=101, когда 4 байта адреса следуют непосредственно после опкода и байта ModR/M (как в случае команды 101000dw). В ассемблере для указания того, что в регистре содержится адрес операнда, а не его значение, регистр заключают в квадратные скобки.  
 
Если MOD=01, за байтом ModR/M следует байт, значение которого добавляется к значению соответствующего регистра и таким образом вычисляется адрес операнда. При MOD=10 за ModR/M следуют уже 4 байта; значение этого числа тоже суммируются со значением соответствующего регистра для вычисления адреса.  
 
Присутствие байта ModR/M обычно требует также наличия битов d и w. Рассмотрим еще одну команду:  
 

Код:
100010 d w

 
При d=0 данные перемещаются из регистра, закодированного в REG, в регистр или память, определяемые по R/M. При d=1 наоборот - из R/M в REG. Составим, например, инструкцию для копирования данных из EAX в EBX. Сначала "составим" байт ModR/M: оба операнда в регистрах, поэтому MOD=11; 1-й операнд в EAX - REG=000; 2-й операнд в EBX - R/M=011; итого - 11000011 (C3). Опкод: полные регистры - w=1; копирование от REG к R/M - d=0. Итоговая инструкция - 10001001 11000011 (89 C3).  
 
Теперь фишка: 1-й операнд в EBX (REG=011), 2-й - в EAX (MOD=11, R/M=000), бит d установим (1). Итог: 10001011 11011000 (8B D8) - но эта инструкция делает абсолютно то же самое, что и предыдущая! На ассемблере обе инструкции записываются одинаково: MOV EBX, EAX. Аналогичные примеры можно привести с инструкциями (A1 78 56 34 12) и (8B 05 78 56 34 12), (89 D7) и (8B FA) и т.д. Проверьте! Да и сами вы теперь сможете составить кучу таких же. А что делают инструкции (88 E4) и (8A C9)?  
 
Это характерная особенность работы с машинными кодами. Подобные этим трюки могут использоваться для создания защит и антиотладочных приемов. Между тем даже ассемблер генерирует для подобных команд лишь один вид кода, тем самым значительно вас обкрадывая, не говоря уже о компиляторах с языков высокого уровня.  
 
Только не надо пугаться и думать, что при программировании в машинных кодах все время придется делать выбор из сотен возможных вариантов. На самом деле в Win32-программировании постоянно будут встречаться одни и те же инструкции, так что мы их помимо своей воли выучим наизусть. Хотя в этой статье оказалось много разнообразного материала, вы можете считать его одой свободе и богатству выбора, которую несут с собой машинные коды. В будущих статьях мы непременно сможем убедиться, насколько простым может быть программирование под Windows в машинных кодах, особенно если вы сумели уловить логику построения инструкций.  
 
2005 © Roustem  

Всего записей: 12 | Зарегистр. 06-06-2007 | Отправлено: 14:49 08-06-2007 | Исправлено: RoustemRuBoard, 10:39 11-06-2007
RoustemRuBoard

Newbie
Редактировать | Профиль | Сообщение | Цитировать | Сообщить модератору
2. Инструменты
 
Чтобы вводить двоичные значения в компьютер, необходим шестнадцатеричный редактор. Поскольку мы решили обходиться стандартными средствами, имеющимися в любой типичной поставке Windows, используем в качестве шестнадцатеричного редактора старый досовский отладчик debug. Рассмотрим лишь те возможности этого отладчика, которые нам понадобятся в работе.  
 
Сначала имеет смысл создать отдельную папку для проводимых экспериментов, например, \exp. Теперь запустим командную строку DOS, перейдем в созданный каталог (cd \exp) и наберем: debug. Появляется черточка - приглашение отладчка; можно набирать команды. Сразу о том, как завершить работу debug: для этого служит команда q (quit).  
 
Debug позволяет создавать и записывать на диск файлы, но у этого процесса есть некоторые особенности. Дело в том, что создаваемые файлы будут в старом досовском формате com. Для нас это означает, что при записи на диск отладчик использует данные, начиная со смещения 100h кодового сегмента (адрес которого содержится в регистре CS), это надо учитывать. Если наши данные будут начинаться со смещения 0, первые 256 (100h) байтов окажутся утерянными (для содержимого регистров CS и DS по умолчанию). Либо надо вручную изменить (увеличить на 10h) значение регистра DS.  
 
Попробуем создать простейший файл. Запускаем debug. Для записи служит команда w (write); однако вначале должно быть определено имя файла с помощью команды n (name). В принципе, имя может быть любым досовским именем (в коротком формате 8.3), но расширение не может быть exe или hex. Лучше использовать расширение bin, а потом переименовать файл. Набираем:  
 

Код:
n first.bin

 
Теперь необходимо указать размер создаваемого файла. Это значение должно быть в регистрах BX:CX, причем младшее слово содержится в CX, старшее слово - в BX (отладчик debug 16-разрядный, поэтому он не работает с 32-разрядными регистрами и смещениями). Для начала запишем лишь 1 байт; введем с помощью команды r (register) 1 в регистр CX (в BX по умолчанию содержится 0):  
 

Код:
r cx
1

 
Таким способом можно изменять значения любых регистров. Собственно запись осуществляется командой w. Смотрим - в нашем каталоге должен появиться файл 'first.bin' размером в 1 байт.  
 
Перейдем к формированию наших данных. Одна из полезных команд - f (fill), она позволяет заполнить участок памяти указанными данными. После f первым параметром идет смещение (начальный адрес) заполняемого блока, затем либо параметр l (length) и число, указывающее на длину заполняемого участка в байтах, либо смещение его конца. После этого - собственно данные, которыми будет заполняться данный участок. Причем данные могут быть как в виде 16-ричных чисел, так и в виде заключенных в апострофы или кавычки строк, причем их можно чередовать. Например, заполним первые 256 (100h) байт строкой "This is the filling string":  
 

Код:
f 0 l 100 'This is the filling string'

 
Чтобы просмотреть содержимое памяти, служит команда d (dump). Как и в случае с командой f, первый параметр указывает смещение начала отображения данных (дампа), а за ним - либо l с указанием размера дампа, либо конечное смещение. Используем для разнообразия второй вариант (учтите, все используемые числа в debug - 16-ричные):  
 

Код:
d 0 ff

 
Как видим, указанный участок заполнен повторяющейся строкой, которую мы указали в качестве параметра команды f. Разумеется, это лишь пример, а в реальности мы будем эту команду использовать для очистки (заполнения нулями) блоков памяти. Например, очистим первый килобайт (400h байт):  
 

Код:
f 0 400 0

 
Если в команде d указать лишь один параметр, она по умолчанию отображает 80h байт, начиная с данного смещения. А если не указать и его, то отображаются очередные 80h байт с того места, на котором остановились в прошлый раз. Поэтому мы можем набрать:  
 

Код:
d 0

 
Посмотрев первые 80h байт дампа, набираем d и смотрим следующую порцию и т.д.  
 
Теперь проделаем эксперимент, демонстрирующий особенность сохранения файлов в debug. Создадим файл, первые 100h байт которого заполнены символами '0', вторые 100h байт - символами '1' и т.д. до, скажем, '9'. Дадим файлу имя 'first.txt' (или любое другое с расширением .txt), а размер его будет a00h (2,5 Кб).  
 

Код:
n first.txt
r cx
a00
f 0 l 100 30
f 100 l 100 31

 
и т.д. до f 900 l 100 39. 16-ричные числа 30, 31, ... , 39 являются ASCII-кодами цифр 0-9. После этого набираем w и смотрим, что получилось.  
 
Открываем 'first.txt' в Блокноте. Но что это? Файл начинается с единиц, а в конце какой-то мусор? Смотрим в debug'е: d 0 ff - все нормально, заполнено цифрами 0 (30h). Вот это и есть та особенность отладчика, о которой мы говорили в начале. В файл записываются данные начиная со смещения 100h относительно кодового сегмента.  
 
Исправить эту ситуацию можно попытаться двумя способами. Рассмотрим еще одну команду отладчика: m (move). Она позволяет копировать данные из одной области памяти в другую. Первый параметр, как и ранее, является смещением начала участка памяти, который необходимо скопировать, второй - либо смещением конца копируемого участка, либо (при наличии буквы l) его длиной, третий параметр - смещение места назначения, куда надо скопировать данные. С помощью этой команды мы можем "передвинуть" весь наш блок данных так, чтобы он начинался со смещения 100h:  
 

Код:
m 0 l a00 100

 
Теперь снова попробуем записать эти данные в тот же файл. Открываем в Блокноте - то что надо! Начинается нулями, заканчивается девятками, ничего лишнего, только то, что мы сами вводили.  
 
Второй способ - изменить значение регистра сегмента данных DS таким образом, чтобы он указывал на область со смещением 100h относительно начала кодового сегмента. Т.е. надо просто добавить к старому значению DS 10h. Допустим, в DS было значение 2020. Изменим его на 2030:  
 

Код:
r ds
2030

 
Теперь затрем старые данные, скажем, числом ff:  
 

Код:
f 0 l a00 ff

 
Запишем это в старый файл командой w и убедимся, что файл изменился. И повторим старую операцию:  
 

Код:
f 0 l 100 30
f 100 l 100 31
...
f 900 l 100 39
w

 
Результат аналогичный ранее сделанному.  
 
Команда e (enter) позволяет вводить данные по конкретным адресам. Первый параметр указывает начальный адрес, остальные рассматриваются как данные для ввода. Причем здесь тоже можно использовать как 16-ричные числа, так и символьные строки, чередуя их между собой произвольным образом.  
 
В связи с данной командой рассмотрим особенность процессоров IA-32, о которой говорилось в прошлой статье. Речь идет об "обратном" представлении чисел в памяти; хотя по внимательном рассмотрении этого вопроса представление чисел в процессарах IA-32 как раз является естественным ("нормальным"), а "обратным" оказывается наша традиционная запись. Попробуем разобраться.  
 
Мы читаем и записываем слева направо. Если записать порядковые номера, они будут увеличиваться тоже слева направо. Естественно таким же образом нумеровать объекты, скажем, байты памяти: 1, 2, 3, 4 и т.д. Значения возрастают слева направо. Теперь посмотрите на числа, у которых увеличиваются разряды: 1, 10, 100, 1000. Каждый новый разряд мы добавляем слева, т.е. возрастание числа получается справа налево - порядок, противоположный традиционному письму. Если сохранять в памяти текст, т.е. строку символов, при добавлении новых символов они будут помещаться "правее", т.е. по возрастающим адресам памяти (поскольку мы нумеруем их слева направо). А как быть, если увеличивается значение числа и оно перестает помещаться на старом месте? Скажем, вместо байта требуется уже слово (два байта)? Новый байт можно добавить "слева" (с меньшим адресом) или "справа" (с большим адресом). Поскольку адресом многобайтной конструкции по соглашению считают самый младший адрес, он может указывать либо на байт, в котором хранятся старшие разряды числа, либо на байт, в котором хранятся младшие разряды. Первый способ называется "big-endian", второй - "little-endian". Так вот, в процессорах IA-32 используется "little-endian", т.е. старшие разряды добавляются "справа" (по старшим адресам памяти) - порядок, обратный нашей записи чисел. Говорят, в свое время Фибоначчи, заимствуя цифры у арабов, не учел особенностей их письма: арабы пишут справа налево, в отличие от нас. И так же располагались разряды их цифр. Фибоначчи использовал тот же порядок, хотя европейцы писали в обратном направлении - вот где корень всех наших бед .  
 
Таким образом, если мы хотим разместить по адресу 10h число 12h, мы набираем:  
 

Код:
e 10 12

 
Если же мы хотим разместить по этому же адресу число 1234h, два байта, его составляющих, нам придется вводить следующим образом:  
 

Код:
e 10 34 12

 
А если по тому же адресу нужно записать число 12345678h, ввод будет таким:  
 

Код:
e 10 78 56 34 12

 
Только в этом случае в результате исполнения инструкции копирования данных из памяти (по адресу 10h) в регистр EAX, которую мы рассматривали в прошлой статье, в регистре EAX окажется нужное нам значение 12345678h.  
 
Как вы уже, очевидно, заметили, в 16-разрядной системе используется сегментная модель памяти. Это создает дополнительные проблемы; в частности, команды заполнения (f) и перемещения (m) не работают через границы сегментов. Поэтому, хотя debug в принципе позволяет сохранять файлы размером более одного 16-разрядного сегмента (64 Кб), при составлении таких файлов у нас могут возникнуть проблемы. Их можно решить другим путем - собирая в debug отдельные "модули", не превышающие 64 Кб, и соединяя их с помощью команды DOS copy.  
 
Для доказательства такой возможности соберем простой текстовый файл размером в 1 Мб. Собирать будем из 16 модулей в 64 Кб, сохраненных средствами debug; каждый модуль будет заполнен единственным символом - 16-ричной цифрой, значение которой равно номеру модуля (для контроля).  
 
Сначала настроим регистр DS (если он не был настроен ранее), увеличив его значение на 10h. В регистр CX должно быть значение 0, в BX - 1 (это соответствует размеру файла 10000h байт, или ровно 64 Кб):  
 

Код:
r ds
<ввести значение на 10h большее прежнего>
r cx
0
r bx
1

 
Если параметр с буквой l в командах равен 0, длина участка памяти считается равной размеру полного сегмента, т.е. 64 Кб. Будем последовательно заполнять весь сегмент символом очередной 16-ричной цифры (от 0 (30h) до 9 (39h) и далее от A (41h) до F (46h)) и сохранять его под новым именем:  
 

Код:
n 0.bin
f 0 l 0 30
w
n 1.bin
f 0 l 0 31
w
. . .
n 15.bin
f 0 l 0 46
w

 
В нашем каталоге должны появиться 16 файлов с расширением bin и размером 64 Кб каждый. Теперь выходим из debug (q) и набираем в командной строке:  
 

Код:
copy /b 0.bin+1.bin+2.bin+3.bin+...+15.bin 16.txt

 
Естественно, вместо "..." здесь должны быть имена остальных файлов, соединенных знаком "+". Откроем итоговый файл 16.txt в WordPad (Блокнот для этой цели не годится - слишком большой файл) и убедимся, что он заполнен введенными нами символами и что в нем нет ничего лишнего.  
 
Осталось рассмотреть лишь некоторые методы автоматизации нашей работы. Работать с debug, все время вводя данные в интерактивном режиме, может оказаться утомительным - удобнее использовать заранее подготовленные шаблоны, внося в них каждый раз небольшие изменения. Для этого воспользуемся еще одной возможностью ОС - перенаправлением ввода-вывода.  
 
Все необходимые команды для debug записываются в текстовый файл, который затем подается на вход отладчика при его запуске следующим образом:  
 

Код:
debug < batch.txt

 
Для испытания этого способа повторим тот же алгоритм, который мы использовали при создании файла "first.txt". В Блокноте создаем файл "batch.txt" со следующим содержимым:  
 

Код:
n first.txt
r cx
a00
f 0 l 100 30
f 100 l 100 31
. . .
f 900 l 100 39
m 0 l a00 100
w
q

 
В конце файла надо не забыть поставить q - иначе мы останемся в отладчике. Результаты работы все еще выводятся на экран. Их можно записать в файл (иногда это бывает полезно), использовав второе перенаправление:  
 

Код:
debug < batch.txt > batch.lst

 
Теперь в консольном окне сообщения не выводятся, зато в файле batch.lst оказались записанными введенные нами команды и ответы на них отладчика. Заметим, что таким способом мы не можем использовать команды, требующие анализа ответов отладчика. Например, мы не сможем воспользоваться изменением значения регистра DS, поскольку заранее (в общем случае) неизвестно его значение.  
 
Наконец, рассмотрим еще одну команду - a (assemble). Эта команда позволяет войти в режим ассемблирования, т.е. ввода инструкций на ассемблере, которые debug автоматически преобразует в машинные коды. Однако делает это он в 16-разрядном режиме, что нам совершенно не подходит. Но мы можем воспользоваться в этом режиме директивой db, позволяющей вводить отдельные байты, как в команде e. Это может напомнить путешествие из Петербурга в Москву через Владивосток; однако, удобство этого метода в том, что отладчик будет автоматически подсчитывать смещение следующей вводимой инструкции (в нашем случае - байта), и можно не считать все самим.  
 
Параметром команды a является адрес (смещение), с которого мы начинаем вводить инструкции. Чтобы выйти из режима ассемблирования, необходимо просто нажать 'Enter' еще раз (в тексте пакетного файла в этом месте должна быть пустая строка). Потренируемся в использовании этой команды с использованием инструкций в машинных кодах, которые мы составляли в прошлый раз (впрочем, ничто не мешает составить и новые). Сначала поработаем в интерактивном режиме:  
 

Код:
a 100

 
В ответ слева появится что-то типа 2020:0100. Старшее слово (сегмент) нам неинтересно, а младшее (справа) как раз и является текущим смещением от начала сегмента. Набираем:  
 

Код:
db b8 01 00 00 00

 
После нажатия 'Enter' появляется новое смещение - 105. К старому смещению автоматически прибавилась длина введенных нами данных. Вводим:  
 

Код:
; конец инструкции

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

Код:
db b4 01
; конец второй инструкции
db "Some text"

 
Как видим, длина текстовых строк тоже подсчитывается автоматически и добавляется к смещению. Запишем все в файл, выйдя из режима ассемблирования (для этого просто нажимаем на 'Enter' еще раз):  
 

Код:
<Enter>
n second.bin
r cx
10
w

 
Заметим, что длину введенных данных подсчитать теперь очень просто: отнимаем от конечного смещения (в данном случае 110h) начальное: 110-100=10h.  
 
Но преимущества режима ассемблирования станут очевидными при работе с перенаправлениями. Создадим файл "second.txt" и наберем в нем те же данные (не забыв про пустую строку в соответствующем месте и команду q в конце). В командной строке DOS запишем:  
 

Код:
debug < second.txt > second.lst

 
В данном случае нас особо интересует именно выходной файл - second.lst. Теперь все смещения записаны в файле. Это дает возможность при "первом проходе" (черновом) вводить приблизительные значения (здесь это могло бы быть, например, значение регистра CX). Выходной файл используется затем для получения точных значений смещений и подстановки их в исходный файл с командами для "второго прохода" (чистового).  
 
Завершим знакомство с отладчиком способами загрузки созданных заранее шаблонов. Для загрузки файлов служит команда l (load). При этом имя файла должно быть уже указано командой n. Файл загружается по смещению 100h. Либо имя файла можно указать в качестве параметра при вызове отладчика:  
 

Код:
debug first.txt

 
Произведя нужные изменения, файл можно сохранить под другим именем. При этом, если работа с debug ведется со смещениями, меньшими 100h, новое имя файла нужно вводить заранее, т.к. debug записывает имя в эту область, и данные могут оказаться испорчеными.  
 
Для примера рассмотрим, как можно загрузить в качестве шаблона созданный ранее файл "first.txt" и сохранить его после сделанных изменений. Сначала создаем "автоматизирующий" файл с командами ("third.txt"):  
 

Код:
n third.bin
m 100 l a00 0
f 100 l 100 ff
m 0 l a00 100
w
q

 
Теперь в командной строке набираем:  
 

Код:
debug first.txt < third.txt > third.lst

 
В данном случае отладчик загружается вместе с файлом "first.txt", затем он исполняет команды, содержащиеся в "third.txt" (создавая в процессе работы файл "third.bin"), а отчет записывает в файл "third.lst".  
 
Финальный штрих - полная автоматизация создания исполняемого файла. Для этого создается bat-файл, в котором записываются вызов самого debug, а также другие необходимые действия, например, составление одного большого файла из отдельных модулей с помощью команды copy или переименование файла с расширением bin в файл с расширением exe. Создадим в Блокноте файл "make.bat":  
 

Код:
@echo off
debug < second.txt
ren second.bin second.exe

 
Теперь можно запустить этот файл на исполение (двойным щелчком по его имени в Проводнике или набрав имя в командной строке). Строка "@echo off" нужна для того, чтобы команды в bat-файле не выводились на экран. Однако, результат работы debug все равно будет отображаться на экране; чтобы его не было, можно использовать второе перенаправление - либо в файл, либо, если файл с результатами работы не нужен, сюда можно записать nul:  
 

Код:
debug < second.txt > nul

 
Результатом работы будет файл second.exe. Кстати, можете попробовать запустить его - ничего страшного не произойдет, система просто сообщит, что это не настоящий исполняемый файл Windows. Отметим, что это простейший случай; на самом деле в bat-файле может быть записано множество вызовов debug с различными заранее подготовленными файлами для создания сразу нескольких модулей и последующего их объединения в один результирующий.  
 
Каков же итог? Любой файл - будь то картинка, векторная или трехмерная графика, музыка, видео или исполняемый - это всего лишь сохраненный набор двоичных чисел. А итог таков, что мы умеем теперь создавать файлы практически любого размера и с любым содержанием. Единственное, что при этом надо - это изучить формат соответствующего типа файла. Этим мы и займемся в следующей статье применительно к исполняемым файлам Windows.  
 
2005 © Roustem  

Всего записей: 12 | Зарегистр. 06-06-2007 | Отправлено: 11:21 10-06-2007 | Исправлено: RoustemRuBoard, 10:49 11-06-2007
RoustemRuBoard

Newbie
Редактировать | Профиль | Сообщение | Цитировать | Сообщить модератору
3. Исполняемые файлы Windows
 
Как сделать, чтобы программа заработала? Работа приложения начинается с того, что операционная система создает процесс. Это не просто загруженная в память программа пользователя; процесс предполагает создание множества внутренних системных структур для обеспечения работы программы и предоставления ей различных ресурсов, таких как память, процессорное время, доступ к установленному в системе оборудованию и т.д.  
Важнейшим ресурсом являетcя виртуальная память. Каждый процесс получает в свое распоряжение собственное виртуальное адресное пространство памяти размером 4 Гб. Это значит, что он может обращаться по любому из адресов памяти от 0 до FFFFFFFFh. Но это значит также и то, что различные процессы могут использовать одни и те же адреса, не мешая друг другу. Система работает с памятью в виде блоков фиксированного размера, называемых страницами (обычно по 4 Кб; на современных процессорах могут быть страницы также по 2 Мб) и использует страничную переадресацию для отображения одних и тех же виртуальных адресов различных процессов в разные области физической памяти. Кроме того, при недостатке физической памяти временно неиспользуемые данные могут сохраняться на диске, освобождая физическую память для других виртуальных адресов (это называется подкачкой).  
 
В адресном пространстве процесса резервируются области для динамически выделяемой памяти ("кучи") и стека (о нем мы подробнее поговорим в следующей статье). Затем образ программы загружается из файла на диск по базовому адресу загрузки. Образ программы состоит из одной или нескольких секций. Для каждой секции выделяется несколько страниц памяти, имеющих одинаковые атрибуты. Например, это могут быть исполняемые страницы, страницы только для чтения или для чтения и записи. Это сделано для уменьшения количества возможных ошибок; например, случайный запуск на исполнение страницы, содержащей не код, а данные, может привести к непредсказуемым результатом. Если же в атрибутах страницы не указана возможность исполнения, это приведет к сообщению об ошибке. Точно так же атрибут "только для чтения" позволяет перехватить попытку случайной или преднамеренной записи на страницу, содержание которой не должно изменяться (допустим, если она содержит константы).  
 
Расширение "exe" осталось в наследство от старых досовских исполняемых (executable) файлов. Используемый в настоящее время формат исполняемых файлов Windows называется "Portable Executable" (PE), поскольку один и тот же формат используется для разных платформ. Более того, он построен на основе шаблонов, являющихся общими и для объектных файлов формата COFF (используемых в том числе в мире Unix), а также построенных на их основе библиотечных файлов и файлов импорта (.lib). Формат PE в системе Win32 является универсальным: его используют не только исполняемые файлы (exe), но и динамические библиотеки (dll) и их особые разновидности -элементы ActiveX (ocx) и системные драйверы (sys и drv).  
 
Как и старый формат exe для DOS, PE-файл состоит из заголовка и собственно образа исполняемой программы. Образ программы, как уже отмечалось, может быть составлен из одной или нескольких секций. Заголовок же можно условно разделить на "старый" и "новый" (см. рис.)  
 
   
 
"Старый" заголовок, в свою очередь, составлен из слегка модифицированного DOS-заголовка и т.н. программы-заглушки, и фактически представляет собой небольшую программу DOS, выводящую простое текстовое сообщение наподобие "This program cannot be run in DOS mode". Это сделано для того, чтобы при ошибочной попытке запуска программы Windows под DOS она могла сообщить об ошибке. Модификация заголовка DOS заключается в том, что по смещению 3Ch от начала файла расположено 32-разрядное смещение PE-заголовка.  
 
"Новый" заголовок составлен из собственно PE-заголовка и таблицы секций, которая фактически является картой отображения записанных в файле секций образа программы в память. В PE-заголовке выделяют также несколько составных частей, но для нашего рассмотрения они несущественны. Отметим лишь каталог смещений-размеров, который указывает на расположение и размеры специальных служебных таблиц. Для размещения последних могут быть выделены отдельные секции в образе программы, но это не является обязательным; в принципе, для всей программы можно использовать одну единственную секцию, разместив в ней и данные, и код, и все необходимые вспомогательные структуры.  
 
Теперь рассмотрим все подробнее. Поскольку попытка запуска создаваемых нами программ под DOS маловероятна, можно без особых проблем обойтись без программы-заглушки DOS. PE-заголовок в этом случае будет следовать сразу за старым заголовком DOS, а именно - непосредственно после 4-байтного поля со смещением 3Ch, т.е. по смещению 40h (само поле 3Ch будет содержать в данном случае это же значение). Единственное, что нужно еще оставить в старом заголовке - это сигнатуру в виде 2 ASCII-символов 'MZ' в начале файла (байты 4Dh 5Ah). Остальные поля могут содержать нули - загрузчик Windows их не использует.  
 
Поля PE-заголовка приведены в таблице 1. Смещения указаны относительно начала заголовка, а жирным шрифтом выделены те поля, при неверных значениях которых Windows откажется загружать программу. Остальные поля либо содержат необязательные данные (например, указатель на размещение и размер отладочных данных), либо для них предусмотрены значения по умолчанию (как для размеров кучи и стека), либо используются лишь для определенных видов файлов (например, флаги dll или контрольная сумма).  
 
Таблица 1. PE-заголовок  
   
 
Далее в PE-заголовке следует каталог размещения вспомогательных таблиц: первые 4 байта для каждого элемента являются смещением начала соответствующих данных относительно базового адреса загрузки, следующие 4 байта - размером этих данных. Хотя число элементов в каталоге указывается в поле PE-заголовка, Windows 9* не допускает значения, меньшего 10h. Структура каталога фиксирована; указатели на соответствующие данные должны следовать в следующем порядке:  
 

  1. таблица экспорта;  
  2. таблица импорта;  
  3. таблица ресурсов;  
  4. таблица исключений;  
  5. таблица сертификатов;  
  6. таблица настроек;  
  7. отладочные данные;  
  8. специфичные для архитектуры данные;  
  9. глобальный указатель;  
  10. таблица локального хранилища потоков (TLS);  
  11. таблица конфигурирования загрузки;  
  12. таблица связанного импорта;  
  13. таблица импортируемых адресов (IAT);  
  14. дескриптор отложенного импорта;  
  15. зарезервировано;  
  16. зарезервировано.

 
Это не значит, что все перечисленные данные должны присутствовать. Если те или иные данные отсутствуют, соответствующие поля каталога содержат нули. Мы будем рассматривать эти структуры по мере того, как начнем с ними работать.  
 
Таблица секций следует непосредственно после PE-заголовка (после каталога смещений). Каждый вход таблицы имееет следующий формат (см. табл. 2).  
 
Таблица 2. Строка таблицы секций  
   
 
Таблица секций имеет столько входов, сколько секций в образе программы. Расположение секций в файле и в виртуальной памяти созданного процесса может не совпадать. Данные различных секций как в файле, так и в памяти располагаются не вплотную друг к другу - они должны быть соответствующим образом выровнены. Например, если код занимает всего 2 байта, следующая за ним секция (допустим, данных) располагается не по смещению +2 байта, а на границе следующей страницы, т.е. как минимум через 4 Кб, если это образ в памяти, и минимум через 512 байт для образа в файле. Значения для выравнивания в файле и в памяти указаны в PE-заголовке, причем они обязательны.  
 
Секция может содержать т.н. неинициализированные данные. Фактически, это просто резервирование определенных адресов памяти под будущие переменные. Для таких данных место в файле не отводится; память резервируется лишь при загрузке на исполнение. Если вся секция содержит лишь неинициализированные данные, поля размера данных секции в файле и смещения начала данных секции в файле равны нулю. В любом случае, когда размер секции в файле меньше указанного размера секции в памяти, остаток заполняется до нужного размера нулями.  
 
Поле флагов секции - то самое, где задаются атрибуты страниц памяти, отводимых под секцию. Возможно использование до 32 флагов (по одному на каждый бит 4-байтного значения), но часть из них зарезервирована, другая часть используется лишь в объектных файлах. Биты нумеруются от младшего к старшему, начиная от 0 (самый младший бит - 0, самый старший - 31). Наиболее употребительные для исполняемых файлов следующие:  
 
бит 5 - секция кода;
бит 6 - инициализированные данные;
бит 7 - неинициализированные данные;
бит 28 - секция может быть общей (разделяемой - shared);
бит 29 - разрешено исполнение;
бит 30 - разрешено чтение;
бит 31 - разрешена запись.  
 
Например, в секции кода с разрешениями на чтение и исполнение установлены следующие флаги:  
 

Код:
01100000 00000000 00000000 00100000 (60 00 00 20 h)

Секция с инициализированными данными с разрешениями на чтение и запись:  
 

Код:
11000000 00000000 00000000 01000000 (C0 00 00 40 h)

 
Та же секция, но с разрешением только для чтения:  
 

Код:
01000000 00000000 00000000 01000000 (40 00 00 40 h)

 
Перейдем, наконец, к практике и составим шаблон заголовка PE-файла, имеющего 3 секции с минимальными размерами. Тогда в памяти каждая будет занимать 1000h (1 страница - отвести меньше памяти невозможно), а в файле - 200h байт (1 сектор диска). Такими же будут и значения выравнивания. Первой пусть идет секция кода; назовем ее '.code' (см. рис.) Она будет располагаться по смещению 200h от начала файла, а в памяти - по смещению 1000h от адреса загрузки (первую страницу памяти и первые 200h байтов файла занимает заголовок). Секция кода будет иметь флаги, которые мы вычислили ранее (60000020h)  
 
   
 
Следующей будет секция с данными только для чтения; назовем ее '.rdata'. Она будет расположена в файле по смещению 400h, а в памяти - по смещению 2000h. Флаги: 40000040h. За ней - секция данных с разрешениями на чтение и запись: '.data', расположение в файле - 600h, в памяти - 3000h; флаги: C0000040h.  
 
Теперь составим командный файл для отладчика debug. Имеет смысл сначала создать специальную папку "Шаблоны". В ней сохраним этот файл для использования в дальнейшем. Открываем Блокнот и набираем:  
 

Код:
n Header.bin
r cx
200
f 0 l 200 0
e 0 'MZ'
e 3C 40
e 40 'PE'
e 44 4C 01

 
Бинарный файл с заголовом будет называться 'Header.bin', его размер - 200h байт. Сначала очищаем "область сборки" - первые 200h байт, затем набираем стандартные сигнатуры. Программы-заглушки у нас не будет - PE-заголовок следует непосредственно за DOS-заголовком; заодно это сэкономит размер заголовка.  
 
А вот дальше пойдут поля PE-заголовка, которые нужно будет настраивать для каждого отдельного exe-файла. Чтобы было удобнее редактировать этот файл в дальнейшем, оставим здесь комментарии - а для этого нам придется изменить способ ввода и перейти в режим ассемблирования.  
 

Код:
a 46
; Здесь должно быть число секций (2 байта) *****
db 03 00
<пустая строка>

 
Режим ассемблирования начинается с команды 'a', за которой следует смещение, по которому нужно вводить данные. В нашем случае, PE-заголовок начинается со смещения 40h от начала файла, поэтому к значениям смещения в таблице 1 нужно добавлять 40h. Близко отстоящие друг от друга поля можно набирать "в один заход"; когда же разрыв большой, можно выйти из режима ассемблирования (оставив для этого пустую строку) и вновь набрать 'a' уже с новым смещением. В "разрыве" при этом останутся нули. Учтите, что комментарии можно оставлять лишь "внутри" режима ассемблирования - вне его отладчик выдаст ошибку.  
 
Имеет смысл также выделить те участки, которые нужно будет в дальнейшем редактировать (как этот случай - число секций может каждый раз быть разным); для этого удобно выделять каким-либо способом строку с комментарием, чтобы она сразу бросалась в глаза. Оставшуюся часть файла для debug приведем, как есть; она не должна вызвать проблем (обратите внимание на пустые строки - их нельзя удалять; и помните про обратный порядок байтов в числах, требующих более 1 байта):  
 

Код:
a 54
; Размер дополнительного заголовка
db e0 00
; Тип файла
db 0F 01
; "Магическое" значение
db 0B 01
 
a 68
; Здесь должно быть смещение точки входа  
; относительно адреса загрузки (4 байта) *****
db 00 10 00 00
 
a 74
; Начальный адрес загрузки (4 байта) *****
db 00 00 40 00
; Выравнивание секций (4 байта)
db 00 10 00 00  
; Выравнивание в файле (4 байта)
db 00 02 00 00
; Старшая версия Windows (2 байта)
db 04 00
 
a 88
; Старшая версия подсистемы (2 байта)
db 04 00
 
a 90
; Здесь должен быть размер загруженного файла  
; в памяти (4 байта) *****
db 00 40 00 00
; Размер всех заголовков в файле (4 байта)
db 00 02 00 00  
 
a 9C
; Подсистема: 02 - графическая, 03 - консольная (2 байта)
db 02 00  
 
a A0
; Зарезервированный размер стека (4 байта)
db 00 00 10 00
; Выделенный размер стека (4 байта)
db 00 10 00 00
; Зарезервированный размер кучи (4 байта)
db 00 00 10 00
; Выделенный размер кучи (4 байта)
db 00 10 00 00
 
a B4
; Число элементов каталога смещений (4 байта)
db 10 00 00 00
;************
; Здесь начинается первый элемент каталога:
; но у нас пока ничего нет - оставляем нули
 
a 138
; Начало таблицы секций
;
; имя первой секции (8 символов)
db '.code' 0 0 0
; размер секции в памяти (4 байта)
db 00 10 00 00
; смещение секции относительно адреса загрузки (4 байта)
db 00 10 00 00
; размер данных секции в файле (4 байта)
db 00 02 00 00
; смещение начала данных секции в файле (4 байта)
db 00 02 00 00
; Пропускаем 12 байтов
db 0 0 0 0 0 0 0 0 0 0 0 0
; атрибуты первой секции (4 байта):
; код, разрешено исполнение и чтение
db 20 00 00 60
;
; данные второй секции - аналогично:
db '.rdata' 0 0
db 00 10 00 00
db 00 20 00 00
db 00 02 00 00
db 00 04 00 00
db 0 0 0 0 0 0 0 0 0 0 0 0
db 40 00 00 40
;
; данные третьей секции
db '.data' 0 0 0
db 00 10 00 00
db 00 30 00 00
db 00 02 00 00
db 00 06 00 00
db 0 0 0 0 0 0 0 0 0 0 0 0
db 40 00 00 C0
 
m 0 l 200 100
w
q

 
Перед записью созданного "образа заголовка" сдвигаем его на 100h байт, чтобы все записалось правильно. Сохраним этот текст в файле "Header.txt".  
 
Теперь у нас есть шаблон, который можно вставлять в начало exe-файла с 3 секциями, размеры которых не превышают 200h байт каждая. Чтобы протестировать его, нужно собрать "настоящий" exe-файл с его использованием. Для этого немного схитрим: вставим две пустые секции (содержащие лишь нули) в качестве секций данных; а в секции кода используем всего 2 байта: EB FE. Это инструкция, передающая управление на себя (как мы узнаем в дальнейшем). Т.е. наша программа просто зацикливается; но пока нам большего и не надо.  
 
В блокноте создадим еще 2 простых файла. Первый - "s1.txt" (содержит наш "код"):  
 

Код:
n s1.bin
r cx
200
f 100 l 200 0
e 100 eb fe
w
q

 
Второй - "s2.txt" (секция в 200h байт, заполненная нулями):  
 

Код:
n s2.bin
r cx
200
f 100 l 200 0
w
q

 
А теперь в том же Блокноте создаем файл "make.bat":  
 

Код:
@echo off
debug < header.txt > report.lst
debug < s1.txt >> report.lst
debug < s2.txt >> report.lst
copy /b header.bin+s1.bin+s2.bin+s2.bin nil.exe

 
Первый вызов debug исполняет команды, записанные в файле header.txt (при этом создается файл header.bin). Отчет выводится в файл report.lst; это необходимо для того, чтобы можно было проверить, не были ли допущены ошибки.  
 
Второй вызов debug исполняет команды в файле s1.txt, создавая файл s1.bin с нашей "секцией кода". Перенаправление с двумя знаками >> означает, что отчет записывается не с начала указанного файла (затирая его содержимое), а добавляется в его конец. Третий вызов debug выполняет s2.txt, создавая пустую секцию в файле s2.bin. Наконец, мы объединяем эти секции в единый файл с расширением exe, причем заметьте - файл s2.bin использован дважды (2 пустые секции).  
 
Теперь полученный файл можно попытаться запустить. Но перед этим неплохо бы еще раз тщательно проверить все исходные файлы - вероятность допущенной ошибки довольно велика. Просмотрите файл report.lst - нет ли сообщений отладчика об ошибках. В частности, типичной ошибкой является случайное использование в командах вместо латинских букв кириллицы (особенно одинаковых - c и с, e и е и т.д.) Если файл создан правильно, ничего не произойдет - сообщения Windows будут лишь при наличии ошибки. Зато нажав Ctl-Alt-Del, вы увидите исполняющуюся задачу 'nil'. Выделите ее и нажмите кнопку "Завершить процесс" - пока мы можем закрыть эту программу только таким способом.  
 
2005 © Roustem  
 

Всего записей: 12 | Зарегистр. 06-06-2007 | Отправлено: 10:13 11-06-2007 | Исправлено: RoustemRuBoard, 17:35 11-06-2007
RoustemRuBoard

Newbie
Редактировать | Профиль | Сообщение | Цитировать | Сообщить модератору
4. Простейшее приложение
 
В первой статье мы получили представление о строении машинных инструкций. Во второй статье научились составлять с использованием подручных средств файлы любого уровня сложности. Наконец, в прошлой статье начали создавать исполняемые файлы, которые система "признает" в качестве своих. Теперь мы вплотную стоим перед дверями, открывающими доступ к неисчислимым сокровищам мира Windows.  
 
Это API - Application Programming Interface - интерфейс прикладного программирования, огромная библиотека уже готовых наборов инструкций, входящая в состав самой системы и служащая для выполнения разнообразнейших задач почти на все случаи жизни - имеется в виду, жизни в мире Windows .  
 
Сокровища эти упакованы в виде процедур и функций и размещены в системных модулях - dll. Чтобы получить к ним доступ, необходимо связаться с соответствующими модулями и импортировать из них нужные нам функции. Попробуем сегодня создать элементарное приложение, выводящее простое сообщение с набранным нами текстом и на этом завершающее свою работу. Эту работу осуществляет функция MessageBoxA, находящаяся в модуле User32.dll. Чтобы не пришлось "прибивать" наше приложение из менеджера задач, как в прошлый раз, оно должно также содержать для своего нормального завершения вызов функции ExitProcess из модуля Kernel32.dll.  
 
Обычно для своей работы эти системные функции используют какие-то наши данные, которые мы им должны передать в виде параметров. Например, функции MessageBoxA мы должны предоставить текст, который хотим отобразить. В первой статье мы рассматривали инструкции, позволяющие копировать данные из одного места в другое. Однако, в данном случае мы ничего не знаем о внутреннем "устройстве" вызываемых функций и о том, в каком месте они хранят данные, с которыми работают. Как раз для подобных случаев был изобретен в свое время механизм, получивший название стека.  
 
Стек - это некоторая область в виртуальном адресном пространстве процесса, специально предназначенная для временного хранения данных. Если помните, при создании PE-заголовка мы заполняли поля с размером выделяемого стека. Место, где разместить стек, выбирает система, а соответствующий адрес памяти сохраняет в регистре ESP. Этот регистр специально предназначен для работы со стеком и поэтому не должен использоваться ни для каких других данных.  
 
Доступ же к стеку осуществляется последовательным образом, причем операнд всегда должен иметь размер в 4 байта (для 32-разрядного режима). Для записи очередного числа значение ESP уменьшается на 4 и указывает на "чистую" область размером в 4 байта. В эту область и копируется единственный операнд соответствующей инструкции. При извлечении сохраненного числа из стека копируется 4-байтное значение, адрес которого находится в настоящий момент в ESP, а затем значение ESP увеличивается на 4 и указывает уже на предыдущее сохраненное число. Единственное, за чем необходимо следить - чтобы каждому случаю помещения данных в стек соответствовал ровно один случай извлечения этих данных из стека (это называется сбалансированностью стека).  
 
Рассмотрим инструкции помещения данных в стек (инструкции извлечения данных из стека нам пока не понадобятся, и мы займемся ими в другой раз). На ассемблере группа данных инструкций обозначается мнемоникой PUSH. Сначала инструкция помещения в стек непосредственного значения:  
 

Код:
011010 s 0 <байты данных>

 
Эта инструкция имеет бит знакового расширения s. Если s = 0, то за опкодом следуют 4 байта, которые необходимо скопировать в стек. Если же s = 1, за опкодом следует всего один байт. Но перед тем, как поместить его в стек, производится его т.н. знаковое расширение до 4 байтов: старшие 3 байта заполняются значением старшего бита данного байта. Например, если байт данных был 01000000, он становится 00000000 00000000 00000000 01000000. Если же он был 10000000, то получается 11111111 11111111 11111111 10000000. Получившиеся 4 байта и помещаются в стек. Это позволяет сохранить знак числа, записанного в виде двоичного дополнения, и в то же время сэкономить 3 байта при операциях с небольшими числами (от -128 до +127). Например, команда помещения в стек нулевого значения (PUSH 0) кодируется так:  
 

Код:
01101010 00000000 (6Ah 00h)

 
Следующая инструкция сохраняет в стеке значение общего регистра:  
 

Код:
01010 reg

 
Инструкция сохранения в стеке значения регистра EAX (PUSH EAX) займет всего 1 байт: 01010000 (50h).  
 
Наконец, еще одна инструкция, использующая уже знакомый нам по первой статье байт ModR/M. Этот байт позволяет записывать в стек значения, хранящиеся в памяти. Но в данном случае есть одна особенность использования этого байта. Вспомните, что байт ModR/M предполагает наличие двух операндов (один из которых всегда находится в регистре). Здесь же необходимо указывать лишь один операнд - другой все время один и тот же и задается неявно (адрес в ESP). Поэтому поле REG байта ModR/M, служившее для обозначения регистра, теперь используется в качестве расширения опкода и для данной конкретной инструкции всегда одно и то же (постоянно). А сама инструкция вместе с байтом ModR/M выглядит так:  
 

Код:
11111111 Mod 110 R/M

 
Обратите внимание, что у нас снова появляется альтернативное кодирование - теперь для команды помещения в стек значений регистров (при Mod=11). Например, указанная выше инструкция PUSH EAX может быть закодирована и таким образом:  
 

Код:
11111111 11110000 (FFh F0h)

 
Данные в виде параметров передаются функциям Windows именно через стек. Рассмотрим функцию MessageBoxA, которая принимает 4 параметра. Первым в стек помещается число, указывающее на стиль создаваемого окна сообщения. Это число представляет собой битовую структуру (см. рис.)  
 
   
 
Поля структуры могут содержать следующие значения, определяющие внешний вид и поведение окна сообщения. Тип окна:  
 
0 - содержит одну кнопку OK;
1 - содержит две кнопки: OK и Cancel;
2 - содержит три кнопки: Abort, Retry, Ignore;
3 - содержит три кнопки: Yes, No, Cancel;
4 - содержит две кнопки: Yes и No;
5 - содержит две кнопки: Retry и Cancel.  
 
Поле значок содержит значение, определяющее вид отображаемой пиктограммы в окне:  
 
0 - нет значка;
1 - красный кружок с крестиком (значок стоп);
2 - вопросительный знак;
3 - восклицательный знак;
4 - кружочек с буквой i.  
 
Поле кнопка определяет кнопку по умолчанию, т.е. ту, которая ассоциируется с нажатием на клавишу 'Enter' при появлении окна сообщения на экране:  
 
0 - 1-я кнопка;
1 - 2-я кнопка;
2 - 3-я кнопка;
3 - 4-я кнопка.  
 
Значение поля режим определяет способ взаимодействия окна сообщения с другими окнами. Кроме этих полей, могут быть установлены некоторые другие биты. Например, установка 14-го бита добавляет к окну сообщения кнопку 'Help'; установка 18-го бита заставляет окно все время находиться сверху и т.д.  
 
Таким образом, число 0 в качестве стиля означает простое окно сообщения с одной кнопкой 'OK' и без всяких значков. Инструкция для помещения 0 в стек нам уже знакома: 6Ah 00h.  
 
Вторым в стек должен быть помещен адрес начала строки, являющейся заголовком окна сообщения. Эту строку разместим в секции данных нашей программы; используя ту же схему, что и в прошлый раз, это будет третья секция, начинающаяся со смещения 3000h относительно базового адреса загрузки, который равен 400000h. В качестве заголовка можно выбрать любой текст; пусть это будет просто "Заголовок". В конце строки обязательно должен быть нулевой байт. В результате адрес нашей строки в памяти будет 403000h; поместим это непосредственное значение в стек (на этот раз мы указываем все 4 байта, поэтому бит s = 0): 68 00 30 40 00 (h).  
 
Третьим в стек помещается адрес начала другой строки, которая является собственно выводимым сообщением. Пусть будет "Текст сообщения для вывода"; расположим ее сразу после первой строки (после ее завершающего 0) - по адресу 4030A0h: 68 A0 30 40 00 (h).  
 
Последний параметр для функции MessageBoxA - описатель (handle) другого окна, являющегося владельцем нашего окна сообщения. Если такое окно имеется, оно (в зависимости от значения в поле "Режим") может быть "заморожено" на время отображения нашего окна сообщения. В данном случае других окон в приложении нет, поэтому оставим 0: 6A 00 (h).  
 
Функция ExitProcess принимает единственный аргумент - т.н. код завершения. Он обычно равен 0 при нормальном завершении приложения. Мы также оставим это значение: 6A 00 (h).  
 
Больше данных в нашем приложении не предвидится; поэтому мы можем сразу построить секцию данных. Создадим отдельную папку и разместим в ней файл "data.txt" со следующим содержимым:  
 

Код:
n data.bin
r cx
200
f 0 l 200 0
e 0 "Заголовок" 0
e a0 "Текст сообщения для вывода" 0
m 0 l 200 100
w
q

 
Все аргументы подготовлены; настало время рассмотреть вызовы самих функций. При нормальном ходе исполнения очередная инструкция извлекается из памяти по адресу, содержащемуся в регистре EIP. В процессе декодирования сначала определяется длина инструкции, и это значение прибавляется к содержимому регистра EIP, который, таким образом, указывает на следующую инструкцию.  
 
Но существуют отклонения от этого последовательного хода исполнения; одним из таких случаев является вызов функций. В этом случае содержимое регистра EIP автоматически сохраняется в стеке (это значение называется адресом возврата), а в регистр EIP заносится операнд инструкции вызова функции, который является адресом первой команды вызываемой функции. В свою очередь, последней командой функции должна быть инструкция возврата управления, которая восстанавливает сохраненный ранее в стеке адрес возврата в регистре EIP, и управление переходит на следующую после вызова функции инструкцию. В результате вызов функции выглядит так, будто это обычная одиночная инструкция и не было всего этого "путешествия" куда-то в другие области адресного пространства.  
 
Рассмотрим инструкции вызова функций (эта группа обозначается на ассемблере мнемоникой CALL). Инструкция с непосредственным смещением:  
 

Код:
11101000 <4 байта смещения>

 
Следующие за опкодом 4 байта являются знаковым смещением относительно адреса следующей инструкции: это значение добавляется к содержимому EIP, и полученное число является конечным адресом. Причем если самый старший бит смещения равен 1, число рассматривается как отрицательное (представленное в виде двоичного дополнения). Не вдаваясь в детали, преобразование положительных двоичных чисел в отрицательные и обратно осуществляется так: все нули исходного числа меняются на единицы, а единицы - на нули, и к этому числу добавляется единица. Например, в случае байта, 00000001 - это +1, меняем на 11111110 и добавляем 1, получая 11111111 - это будет -1 в двоичном виде.  
 
Есть также инструкция с косвенной адресацией; она использует байт ModR/M:  
 

Код:
11111111 Mod 010 R/M

 
Операнд в виде адреса назначения находится в этом случае в регистре или в памяти. Обратите внимание, поскольку у этой инструкции единственный операнд (на самом деле, второй операнд - регистр EIP - задан неявно), поле REG байта ModR/M также используется в качестве расширения опкода. Более того, сам опкод (FFh) совпадает с опкодом для инструкции помещения данных в стек с байтом R/M - процессор различает эти команды как раз по полю REG байта R/M: в случае стековой инструкции это 110, а для инструкции вызова функции - 010. Забегая вперед, отметим, что здесь возможны и другие значения, создающие соответственно другие инструкции.  
 
Хорошо; но как получить адрес нужной нам функции? Это делается посредством процесса, который называется импортом. Загрузчик Windows осуществляет его автоматически при запуске приложения, нужно только указать ему, какие функции и из каких модулей нам потребуются. Вот для этой цели и служат таблица импорта и сопутствующие ей данные, о которых упоминалось в прошлой статье. Теперь познакомимся с ними поближе.  
 
Число записей в таблице импорта равно числу импортируемых модулей. Последняя запись должна содержать нули для обозначения конца таблицы. Каждая запись имеет следующий формат:  
 
   
 
Записи этой таблицы ссылаются на другие вспомогательные таблицы и строки; их взаимоотношение показано на рисунке. Импортировать функции можно по именам или по их порядковым номерам в соответствующем модуле. Все импортируемые из одного модуля функции должны быть указаны в таблице поиска, на которую ссылается таблица импорта, следующим образом. Каждой импортируемой функции соответствует 32-разрядное значение. Если старший бит этого значения установлен (1), импорт осуществляется по номерам, и оставшийся 31 бит является значением этого номера. Если же он сброшен (0), оставшийся 31 бит является смещением (относительно базового адреса загрузки) на соответствующую строку таблицы имен. Первые две байта этой строки являются "подсказкой" загрузчику, в каком месте импортируемого модуля может находиться соответствующее имя (если "подсказки" нет, они равны 0). За ними следует сама строка с именем импортируемой функции. Таблица поиска завершается 4-байтным полем, содержащим нули.  
 
   
 
Таблица импортируемых адресов должна находится в самом начале секции. При загрузке она должна быть идентична таблице поиска. В процессе загрузки система заполняет поля этой таблицы адресами соответствующих функций. Таким образом, мы должны указать в инструкциях вызова функций именно эти адреса.  
 
Теперь мы можем составить нужные для нашего приложения данные. Разместим их в секции .rdata (со смещением 2000h относительно адреса загрузки). Создадим файл rdata.txt. Это как раз тот случай, когда могут оказаться полезными два прохода, чтобы узнать относительные взаимные смещения различных таблиц. Учтите, что все смещения должны указываться относительно базового адреса загрузки. Я приведу здесь уже готовый вариант:  
 

Код:
n rdata.bin
r cx
200
f 2000 l 200 0
a 2000
; 1 IAT
db 2A 20 0 0 0 0 0 0  
; 2 IAT
db 38 20 0 0 0 0 0 0
; имя 1 модуля
db "User32.dll" 0 0
; имя 2 модуля
db "Kernel32.dll" 0 0
; имя 1 функции
db 0 0 "MessageBoxA" 0
; имя 2 функции
db 0 0 "ExitProcess" 0
; таблица поиска 1
db 2A 20 0 0 0 0 0 0
; таблица поиска 2
db 38 20 0 0 0 0 0 0
; таблица импорта:
; 1 модуль
; указатель на 1 таблицу поиска
db 46 20 0 0
; 2 пустых поля
db 0 0 0 0 0 0 0 0  
; указатель на имя 1 модуля
db 10 20 0 0
; указатель на 1 IAT
db 0 20 0 0
; 2 модуль
; указатель на 2 таблицу поиска
db 4E 20 0 0
; 2 пустых
db 0 0 0 0 0 0 0 0
; указатель на имя 2 модуля
db 1C 20 0 0
; указатель на 2 IAT
db 08 20 0 0
; последняя запись - все нули
 
m 2000 l 200 100
w
q

 
Теперь мы можем закончить и кодовую секцию. Адрес функции MessageBoxA будет находиться в поле первой IAT по адресу 402000, используем в инструкции ModR/M с непосредственным смещением (Mod = 00, R/M = 101; затем следуют 4 байта адреса, где находится операнд):  
 

Код:
11111111 00 010 101 <4 байта адреса 402000h>, или FF 15 00 20 40 00.

 
Аналогично адрес функции ExitProcess будет по адресу 402008, а инструкция выглядит так: FF 15 08 20 40 00.  
 
Составим файл code.txt:  
 

Код:
n code.bin
r cx
200
f 100 l 200 0
a 100
; помещаем в стек параметры MessageBoxA
db 6a 00
db 68 00 30 40 00
db 68 a0 30 40 00
db 6a 00
; вызываем MessageBoxA
db ff 15 00 20 40 00
; помещаем в стек параметр (0)
db 6a 00
; вызываем ExitProcess
db ff 15 08 20 40 00
 
w
q

 
Готово почти все; единственное, что осталось - подправить PE-заголовок в нашем шаблоне. Скопируем файл Header.txt, созданный в прошлый раз, в рабочий каталог. Теперь в нашем приложении есть таблица импорта, и надо указать ее смещение (2056h) и размер (3Ch) в каталоге. Найдите в файле Header.txt строку "; Здесь начинается первый элемент каталога:". Теперь переделайте его начало следующим образом:  
 

Код:
; смещение таблицы экспорта (4 байта)
db 0 0 0 0
; размер таблицы экспорта (4 байта)
db 0 0 0 0
; Второй элемент каталога:
; смещение таблицы импорта (4 байта)
db 56 20 0 0
; размер таблицы импорта (4 байта)
db 3c 0 0 0

 
Все! Пишем заключительный файл сборки (make.bat):  
 

Код:
@echo off
debug < header.txt > report.lst
debug < code.txt >> report.lst
debug < rdata.txt >> report.lst
debug < data.txt >> report.lst
copy /b header.bin+code.bin+rdata.bin+data.bin msg.exe

 
Запустив make.bat, мы получим файл msg.exe. Прежде чем запускать его, внимательно проверьте файл отчета report.lst на предмет сообщений об ошибках. Всего один неверно введенный символ (например, русская буква е в команде e) вызовет ошибку, отладчик не выполнит соответствующую команду, в результате создаваемая нами структура окажется неверной, что может привести к совершенно неожиданным результатам и даже вызвать сбой всей системы (особенно если это ошибка в PE-заголовке).  
 
Если же все нормально - хлопайте в ладоши! Вот оно, наше окно, собственноручно созданное самым честным образом в самых что ни на есть настоящих машинных кодах. Теперь самое время изучить функцию Win32 API MessageBoxA подробнее, давая ей в качестве параметров другие строки и значения стилей - для этого они и были здесь приведены. Успехов!  
 
2005 © Roustem  
 

Всего записей: 12 | Зарегистр. 06-06-2007 | Отправлено: 17:33 11-06-2007 | Исправлено: RoustemRuBoard, 23:22 11-06-2007
RoustemRuBoard

Newbie
Редактировать | Профиль | Сообщение | Цитировать | Сообщить модератору
5. Консольное приложение
 
Существует разновидность приложений Windows, которые называются консольными. По своим "внешним" проявлениям они напоминают приложения DOS, запущенные в Windows. Тем не менее, это настоящие Win32-приложения, которые под DOS работать не будут; для них также доступен Win32 API, а кроме того, они могут использовать консоль - окно, предоставляемое системой, которое работает в текстовом режиме и в которое можно вводить данные с клавиатуры.  
 
Особенность консольных приложений в том, что они работают не в графическом, а в текстовом режиме. Для этого используются унаследованые от DOS так называемые стандартный ввод и стандартный вывод. Все, что пользователь вводит с клавиатуры (когда консольное окно имеет фокус), попадает в буфер стандартного ввода, откуда данные можно читать, как из файла. Выходные же данные можно записать, как в файл, в буфер стандартного вывода, и они будут отображены в консольном окне.  
 
Еще одной особенностью стандартных ввода и вывода является возможность их перенаправления в файл. Этим мы уже пользовались при создании наших приложений, используя в командной строке знаки '<' для перенаправления ввода и '>' для перенаправления вывода. Debug, будучи приложением DOS, использует для приема команд стандартный ввод, а для отображения данных - стандартный вывод. Когда мы писали: 'debug < code.txt', содержащиеся в файле code.txt данные поступали в debug так, как будто их набирают на клавиатуре. При команде же 'debug > result.lst' вся информация, которая выводилась бы на экран, попадала в файл 'result.lst'.  
 
Из приложений, подобных debug, т.е. использующих для ввода данных стандартный ввод, а для вывода - соответственно стандартный вывод, можно строить даже своеобразные "конвейеры". Для этого данные из стандартного вывода одного приложения подают на стандартный ввод другого посредством специального знака командной строки '|' (вертикальной черты). В команде  
 

Код:
приложение1 | приложение 2 | приложение3

 
данные проходят последовательную обработку этими тремя приложениями. Например, приложение1 могло бы составлять список из данных источника, приложение2 - сортировать его, а приложение3 - форматировать нужным образом. Таким образом, у консольных приложений есть свои преимущества; их удобно использовать в качестве "строительных блоков" для автоматизации многих рутинных задач, не требующих интерактивного взаимодействия с пользователем.  
 
Работа со стандартными вводом и выводом "изнутри" подобна работе с файлами. Стандартный ввод выглядит как файл с разрешением "только для чтения", а стандартный вывод - как файл с разрешением "только для записи". Для работы с ними используют соответствующие функции API - ReadFile и WriteFile из модуля Kernel32.dll. Рассмотрим их подробнее.  
 
При вызове функции ReadFile в стек помещаются 5 параметров в следующем порядке:  
 

  1. адрес структуры, использующейся при асинхронном вводе-выводе. При обычном (синхронном) вводе значение этого параметра равно нулю;  
  2. адрес переменной (4 байта), по которому будет записано количество действительно прочитанных функцией байтов (это значение может быть меньше заявленного - например, если кончились данные);  
  3. число байтов, которые нужно прочесть ("заявка");  
  4. адрес, по которому нужно разместить прочитанные данные (буфер);  
  5. описатель файла, из которого производится чтение.

 
Внимания заслуживает последний аргумент. Для работы с файлом используется так называемый описатель (handle) - это некий идентификатор, который система присваивает файлу после его открытия. На самом деле, при открытии файла создается внутренняя системная структура, в которой хранятся различные вспомогательные данные, такие как текущая позиция, с которой нужно читать или записывать данные, и т.п. Все обращения к файлам возможны только после их открытия и только по их описателям.  
 
Функция WriteFile также принимает 5 схожих параметров:  
 

  1. адрес структуры для асинхронного вывода;  
  2. адрес переменной (4 байта), в которую будет помещено количество действительно записанных байтов;  
  3. число байтов, которые нужно записать ("заявка");  
  4. адрес начала буфера, где находятся предназначенные для записи данные;  
  5. описатель файла, в который производится запись.

 
Но как получить нужные описатели? В случае файлов существуют специальные функции API (наподобие CreateFile) для их открытия. Для стандартного ввода-вывода тоже существует своя функция - GetStdHandle, тоже из модуля Kernel32.dll. Она принимает лишь один аргумент - число, указывающее на тип нужного описателя (для стандартного ввода или вывода). На самом деле, существует еще и третий тип - стандартная ошибка, он, как и стандартный вывод, служит для отображения сообщений на экране. Его можно использовать в тех случаях, когда нужно как-то разделить обычные сообщения и сообщения об ошибках (например, можно использовать перенаправление только для стандартной ошибки - тогда эти сообщения будут записаны в файл и не попадут на экран). В качестве параметра функции GetStdHandle используются 0FFFFFFF6h для стандартного ввода, 0FFFFFFF5h для стандартного вывода и 0FFFFFFF4h для стандартной ошибки.  
 
Настало время обсудить один важный вопрос. Функции не только принимают параметры, часто они еще возвращают значения. Результат работы функции по ее возвращении (т.е. перед выполнением следующей после вызова функции инструкции) оказывается в регистре EAX. Это общее соглашение: когда мы начнем создавать свои функции, мы тоже будем должны записывать в регистр EAX значение, которое должно быть возвращено как результат функции. В случае функции GetStdHandle таким результатом как раз и является нужный нам описатель. Его можно либо сохранить где-то в памяти (переписав туда значение из регистра), либо использовать прямо в регистре, если вызов нужной функции непосредственно следует после получения описателя.  
 
Здесь нужно отметить еще один момент. Мы уже знаем, что из 8 общих регистров один (ESP) используется в качестве указателя стека, и его трогать нельзя. На самом деле, при работе со стеком используется еще и второй регистр - EBP, поэтому число доступных для манипуляций регистров сокращается до 6. Теперь задумайтесь над вопросом: а что случается с данными, которые находились в регистрах, после вызова функции? Особенно, если это "чужие" функции, являющиеся для нас "черными ящиками" (как в случае с функциями API). Значения в регистрах могут быть перезаписаны (ведь надо с чем-то работать!), а могут остаться без изменения. Чтобы внести ясность в этот вопрос, для работы с функциями Win32 API было принято следующее соглашение: при вызовах любых функций значения регистров EBX, ESI и EDI остаются без изменений - какие были перед вызовом функции, такие будут и после; значения же регистров EAX, ECX и EDX могут быть изменены произвольным образом. В регистре EAX, как мы уже знаем, будет находиться результат работы функции (если функция возвращает результат). Если функция не имеет возвращаемого результата, значение в EAX не определено.  
 
Практический же вывод такой. Если нам нужно, чтобы значение в "изменяемых" регистрах (EAX, ECX или EDX) сохранилось после вызова функции, перед ее вызовом необходимо поместить значение соответствующего регистра в стек, а после вызова функции - извлечь его оттуда (в тот же регистр). И наоборот: если мы создаем свою функцию, которую может вызвать система (например, забегая вперед, это относится к главной функции окна), и если в работе этой функции нам приходится использовать регистры, которые не должны изменяться (EBX, ESI или EDI), мы должны в самом начале функции сохранить значение этого используемого регистра в стеке, а перед возвратом из функции - восстановить его. В случае "изменяемых" регистров этого делать не нужно.  
 
Что ж, необходимый теоретический минимум мы прошли; теперь можно применить его на практике. Попробуем создать простое консольное приложение, выводящее сообщение. "Макет" нашего приложения будет следующий: сначала идет PE-заголовок, затем секции кода (.code), данных (.data) и вспомогательных данных для импорта (.rdata). По сравнению с прошлым разом для разнообразия переставлены местами секции .data и .rdata. Как и раньше, секции располагаются в памяти по смещениям 1000h, 2000h и 3000h соответственно, а в файле - 200h, 400h и 600h.  
 
Начнем с секции данных. Их немного - в начале секции (по смещению 2000h, который после загрузки превратится в виртуальный адрес 402000h) разместим переменную в 4 байта для вывода количества записанных байтов. Сразу за ней (по адресу 402004h) будет выводимая текстовая строка. Набираем файл data.txt:  
 

Код:
n data.bin
r cx
200
f 0 l 200 0
a 0
; 4 байта для числа выведенных байтов
db 0 0 0 0
; выводимая строка
db "Greetings from console window" 0a 0d 0
 
m 0 l 200 100
w
q

 
Числа 0Ah и 0Dh после строки являются ASCII-символами перехода на новую строку; сама строка должна завершаться нулем.  
 
Теперь надо заняться секцией импорта. Нам нужно импортировать три функции, и все из модуля Kernel32.dll: GetStdHandle, WriteFile и ExitProcess. В начале секции, как обычно, таблица импортируемых адресов (IAT); на этот раз она имеет, по числу функций, 3 поля и четвертое нулевое. Сразу вслед за IAT расположим таблицу поиска, тем более, что они при загрузке должны быть идентичны. Затем будет таблица импорта, содержащая одну запись для единственного импортируемого модуля и одну завершающую нулевую запись (общий размер 28h байт). Затем последуют строки с именами модуля и функций. Здесь удобно использовать два прохода в режиме ассемблирования - при "черновом" содержимое структур можно просто заполнять нулями (сохраняя лишь размер полей), а для второго "чистового" прохода подставить из полученного файла отчета нужные значения. Для этой же цели лучше выбрать для "сборки" образа в debug то же смещение, что и у загруженной в память секции (в данном случае - 3000h; файл rdata.txt):  
 

Код:
n rdata.bin
r cx
200
f 3000 l 200 0
a 3000
; Таблица импортируемых адресов: до загрузки  
; идентична таблице поиска
; будущий адрес GetStdHandle
db 55 30 0 0
; будущий адрес WriteFile
db 64 30 0 0
; будущий адрес ExitProcess
db 70 30 0 0  
; завершение таблицы нулями
db 0 0 0 0
; Таблица поиска
; смещение строки с именем GetStdHandle
db 55 30 0 0
; смещение строки с именем WriteFile
db 64 30 0 0
; смещение строки с именем ExitProcess
db 70 30 0 0
; завершающие нули
db 0 0 0 0
; Таблица импорта
; строка для импорта из Kernel32.dll:
; смещение таблицы поиска
db 10 30 0 0
; 2 пустых поля
db 0 0 0 0 0 0 0 0
; смещение имени модуля
db 48 30 0 0
; смещение таблицы импортируемых адресов
db 0 30 0 0
; завершение таблицы - пустая строка (20 нулевых байтов)
db 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0  
; Имя импортируемого модуля
db "Kernel32.dll" 0
; Имена импортируемых функций; вместо "подсказок" нули
db 0 0 "GetStdHandle" 0
db 0 0 "WriteFile" 0
db 0 0 "ExitProcess" 0
 
m 3000 l 200 100
w
q

 
Переходим к секции кода. Освежим знания по инструкциям: "короткая" команда помещения в стек 6Ah + 1 байт, "длинная" - 68h + 4 байта; команда вызова функции - опкод 0FFh, байт ModR/M 15h, указывающий, что операнд находится в памяти по 4-байтному адресу, который включен в инструкцию. Обратите внимание, что для помещения в стек числа 0FFFFFFF5h мы можем использовать "короткий" вариант инструкции со знаковым расширением, т.к. это на самом деле отрицательное число, представимое в виде 1 байта (-11 = 0F5h. Функцию WriteFile мы вызываем непосредственно после функции GetStdHandle, значит, нужный нам описатель файла будет находиться в регистре EAX; поэтому в этот раз придется использовать также инструкцию помещения в стек значения регистра EAX (если помните, это 50h). Указатели на нужные нам функции будут находиться после загрузки в соответствующих полях IAT, по адресам 403000h, 403004h и 403008h. Итак, файл code.txt:  
 

Код:
n code.bin
r cx
200
f 0 l 200 0
a 0
; параметр для стандартного вывода
db 6a f5
; вызов GetStdHandle
db ff 15 0 30 40 0
; параметры для WriteFile:
; не используется - 0
db 6a 0
; адрес переменной для числа выведенных символов
db 68 0 20 40 0
; длина строки = 30 (1Eh)
db 6a 1e
; адрес выводимой строки
db 68 4 20 40 0
; содержимое регистра EAX
db 50
; вызов WriteFile
db ff 15 4 30 40 0
; параметр кода завершения (0)
db 6a 0
; вызов ExitProcess
db ff 15 8 30 40 0
 
m 0 l 200 100
w
q

 
Осталось лишь добавить PE-заголовок. Сначала скопируем его шаблон (header.txt) в рабочий каталог, а затем слегка его подправим. Потребуются изменения всего в трех местах. Самое главное - нужно изменить подсистему: вместо графической (2) поставить консольную (3). Собственно, это единственное, чем консольные приложения отличаются от графических! Находим в шаблоне строки:  
 

Код:
a 9C
; Подсистема: 2 - графическая, 03 - консольная (2 байта)

 
Сразу после нее должно быть:  
 

Код:
db 03 00

 
Теперь надо указать расположение таблицы импорта. Находим строку:  
 

Код:
; Здесь начинается первый элемент каталога:

 
За ней должен следовать текст:  
 

Код:
; смещение таблицы экспорта (4 байта)
db 0 0 0 0
; размер таблицы экспорта (4 байта)
db 0 0 0 0
; Второй элемент каталога:
; смещение таблицы импорта (4 байта)
db 20 30 0 0
; размер таблицы импорта (4 байта)
db 28 0 0 0

 
Наконец, мы поменяли местами секции .data и .rdata (хотя в принципе этого можно было и не делать). Находим начало второй секции:  
 

Код:
; вторая секция

 
И заменяем оставшийся текст на следующий:  
 

Код:
db '.data' 0 0 0
db 0 2 0 0
db 0 20 0 0
db 0 2 0 0
db 0 4 0 0
db 0 0 0 0 0 0 0 0 0 0 0 0
db 40 0 0 c0
;
; третья секция  
db '.rdata' 0 0
db 0 2 0 0
db 0 30 0 0
db 0 2 0 0
db 0 6 0 0
db 0 0 0 0 0 0 0 0 0 0 0 0
db 40 0 0 40
 
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
copy /b header.bin+code.bin+data.bin+rdata.bin cnsl.exe

 
Проверив файл отчета report.lst, можно запускать cnsl.exe. Если вы запускаете его не из консоли, создаваемое окно будет мелькать - закрываться сразу после завершения программы. Поэтому можно запустить сначала консоль командной строки DOS и уже из него - наше приложение, набрав его имя (и путь, если требуется).  
 
Еще одно примечание - в консольных приложениях используется кодировка DOS. Поэтому если вы набрали текст для вывода в Блокноте и на русском, то в консольном окне прочесть его не сможете - в Windows используется другая кодировка (ANSI).  
 
На самом деле, возможности текстового вывода шире, чем можно было бы подумать. Попробуйте в качестве примера использовать такой файл data.txt:  
 

Код:
n data1.bin
r cx
200
f 0 l 200 0
a 0
db 0 0 0 0
db c9 cd cd cb cd cd bb 0a 0d  
db ba 20 20 ba 20 20 ba 0a 0d
db c7 c4 c4 d7 c4 c4 b6 0a 0d
db ba 20 20 ba 20 20 ba 0a 0d
db c8 cd cd ca cd cd bc 0a 0d 0
 
m 0 l 200 100
w
q

 
Чтобы пример работал правильно, надо еще подправить кодовую секцию - там, где строка:  
 

Код:
; длина строки = 30 (1Eh)

 
надо заменить  
 

Код:
db 6a 1e

 
на  
 

Код:
db 6a 2e

 
Впрочем, это лишь подсказка - экспериментируйте сами!  
 
2005 © Roustem  
 

Всего записей: 12 | Зарегистр. 06-06-2007 | Отправлено: 11:43 12-06-2007 | Исправлено: RoustemRuBoard, 12:29 12-06-2007
RoustemRuBoard

Newbie
Редактировать | Профиль | Сообщение | Цитировать | Сообщить модератору
6. Окна Windows
 
Основной "действующей единицей" в ОС Windows является окно. Графически оно представляет собой прямоугольную область на экране, через который осуществляется весь ввод и вывод. Каждое окно имеет свой логический номер - описатель (handle), по которому операционная система отличает одно окно от другого. Любой ввод и вывод может осуществляться только через окно; более того, сообщения также адресуются именно окнам. На самом деле, окно является исходным элементом, на котором построена вся операционная система Windows.  
 
Для пользователя окна выступают прежде всего областями экрана, занимаемого разными программами. В "классическом" окне выделяют различные области и зоны - строку заголовка со значком и кнопками свертывания, разворачивания и восстановления окна; строку меню; обрамление; различные дополнения в виде панелей инструментов, полос прокрутки, строки состояния и т.п. Однако окна отнюдь не ограничиваются подобным "классическим" типом. Любая более или менее функционирующая область экрана является, как правило, окном. Более того, сами элементы окна часто тоже являются, в свою очередь, дочерними окнами.  
 
Давайте посмотрим, сколько окон можно насчитать, скажем, запустив наш любимый текстовый редактор "Word". Во-первых, это, конечно, главное окно программы, которое содержит в себе все остальные, со значком W и надписью "Microsoft Word". Во-вторых, многочисленные окна документов (для переключения между которыми имеется специальное меню "Окно"). Панели инструментов с кнопками наверху и строка состояния внизу - это тоже окна, но уже дочерние, принадлежащие главному окну. Это же относится к полосам прокрутки справа и внизу окна документа, а также горизонтальной и вертикальной линейке (если вы их включили). Особой разновидностью окна является и строка меню, а также всплывающие меню, появляющиеся при щелчке правой клавишей мыши. При выборе различных пунктов меню появляются диалоговые окна, содержащие, в свою очередь, большое количество собственных дочерних окон - вкладок, элементов управления и т.д. Даже область пользователя окна документа является, как это ни странно, самостоятельным окном! При использовании мастера приложений MS Visual C++ программа будет построена именно таким образом - основное окно является только обрамлением, а всю рабочую область занимает дочернее окно (представляющее собой закрывающий всю область пользователя главного окна белый прямоугольник), в которое и осуществляется вывод.  
 
Однако, окно - это не только прямоугольная область экрана для ввода и вывода. Окно является основным системным объектом, обеспечивающим взаимодействие приложения с пользователем, другими приложениями и самой операционной системой. Как и другие объекты, окно принадлежит определенному классу, который называется классом окна, имеет свои свойства, не все из которых находят непосредственное графическое отражение на экране, а также специальную функцию, которая, называется главной процедурой окна.  
 
Класс окна представляет собой набор атрибутов, которые система использует в качестве шаблона при создании окна. Сюда относятся такие внешние атрибуты окна, как значок, форма курсора, фон окна, меню окна по умолчанию, а также набор стилей, называемых стилями класса окна и определяющих наиболее общие особенности поведения и отображения окна. Все эти атрибуты хранятся в специальной структуре WNDCLASSEX.  
 
Каждый класс окна имеет связанную с ним процедуру окна, которую разделяют все окна одного класса. Процедура окна обрабатывает сообщения для всех окон данного класса и тем самым контролирует их поведение и отображение.  
 
Каждое окно имеет также свою внутреннюю структуру CREATESTRUCT, которая заполняется во время создания окна с помощью функции CreateWindowExA. Это такие атрибуты окна, как его расположение и размеры, название, меню, отношения владения-подчинения с другими окнами, а также набор индивидуальных стилей, определяющих конкретный тип окна.  
 
Рассмотрим функцию CreateWindowExA подробнее. Эта функция из модуля User32.dll принимает аж целых 12 параметров, которые должны быть размещены в стеке в следующем порядке:  
 

  1. адрес переменной, в которой находится дополнительное значение для передачи некоторым типам окон. Если окну не требуется дополнительное значение, этот параметр равен нулю;  
  2. описатель экземпляра приложения, которому принадлежит окно. Это значение может быть получено с помощью функции GetModuleHandleA из модуля Kernel32.dll;  
  3. в зависимости от стиля окна, этот параметр является либо идентификатором дочернего окна, либо описателем меню. Если создаваемое окно - дочернее, это идентификатор окна; если нет - описатель меню окна (при отсутствии меню параметр равен нулю);  
  4. описатель родительского окна или окна-владельца (если окно самостоятельное, параметр равен нулю);  
  5. высота окна в пикселах;  
  6. ширина окна в пикселах;  
  7. начальная вертикальная координата окна. Если окно дочернее, вертикальное положение отсчитывается от левого верхнего угла клиентской области родительского окна; если окно самостоятельное - от левого верхнего угла экрана;  
  8. начальная горизонтальная координата окна. Аналогично вертикальной координате, за точку отсчета для дочерних окон принимается левый верхний угол клиентской области родительского окна, для самостоятельных окон - левый верхний угол экрана;  
  9. флаги, указывающие стиль окна;  
  10. адрес строки с именем окна;  
  11. адрес строки с именем класса окна;  
  12. флаги, указывающие расширенный стиль окна.

При успешном создании окна в регистре EAX возвращается его описатель. Если произошла ошибка, EAX будет содержать 0. Постепенно мы разберем каждый параметр этой функции более подробно. Сейчас же попробуем создать приложение с использованием этой функции.  
 
Сначала с помощью функции GetModuleHandleA нужно получить значение описателя для экземпляра нашего приложения. GetModuleHandleA принимает всего один аргумент - адрес строки с именем модуля, для которого нужно возвратить описатель. Подразумевается, что модуль уже загружен в адресное пространство того процесса, который вызывает эту функцию. (Например, так можно получать описатели для модулей загруженных dll). Если параметр равен нулю, возвращается описатель для самого вызывающего приложения (как в нашем случае).  
 
Многие параметры CreateWindowExA будут равны нулю, например, дополнительное значение окна, описатель меню, описатель родительского окна, а также параметр расширенных стилей. Поскольку вызов CreateWindowExA следует непосредственно за вызовом GetModuleHandleA, значение описателя экземпляра приложения можно поместить в стек прямо из регистра EAX. Начальные координаты и размеры окна можно выбрать произвольные; пусть будут, например, такие: высота - 100h, ширина - 150h, начальная координата y - 100h, x - 150h пикселей. Нужно указать также стиль окна. Подробнее разбираться со стилями мы будем в другой раз, а сейчас просто используем значение 10CF0000h.  
 
Остались 2 параметра: адреса строк с именами окна и класса окна. Имя окна тоже может быть произвольным (например, просто "Моё окно") или даже вовсе отсутствовать (в этом случае параметр равен 0). А вот имя класса окна должно быть предварительно зарегистрировано в системе вместе с соответствующей структурой WNDCLASSEX. Этим мы займемся в следующий раз, а сейчас используем один из предопределенных в системе классов - "BUTTON". Правда, окна этого класса должны использоваться лишь в качестве дочерних в составе других окон; но мы ради эксперимента создадим самостоятельное окно и посмотрим, что из этого получится. Соответствующие строки должны находиться в секции данных, с создания которой мы и начнем конструирование нашего приложения.  
 
"Макет" сделаем по нашему стандартному шаблону, т.е. первой будет секция кода .code со смещениями в памяти и файле 1000h и 200h соответственно; второй - секция данных импорта .rdata (2000h и 400h); третьей - секция данных .data (3000h и 600h). Создаем файл data.txt:  
 

Код:
n data.bin
r cx
200
f 0 l 200 0
e 0 "BUTTON" 0
e 10 "Моё окно" 0
m 0 l 200 100
w
q

 
Переходим к секции .rdata. Нам нужно импортировать 2 функции из модуля Kernel32.dll (GetModuleHandleA и ExitProcess) и одну функцию (CreateWindowExA) из модуля User32.dll. Напомним, что данные для импортируемых из одного модуля функций должны располагаться в одних и тех же таблицах поиска и импортируемых адресов (IAT), а для функций из разных модулей нужно использовать разные таблицы. Поэтому у нас будут по две IAT и таблицы поиска, но одна общая таблица импорта с тремя записями (по одной на каждый модуль плюс завершающая, заполненная нулями). Создание вспомогательных таблиц не должно вызвать проблем (файл rdata.txt):  
 

Код:
n rdata.bin
r cx
200
f 2000 l 200 0
a 2000
; 1-я IAT (для Kernel32.dll)
; GetModuleHandleA
db 7C 20 0 0
; ExitProcess
db 90 20 0 0
db 0 0 0 0
; 2-я IAT (User32.dll)
; CreateWindowExA
db 9E 20 0 0
db 0 0 0 0
; таблица поиска для Kernel32.dll
; GetModuleHandleA
db 7C 20 0 0
; ExitProcess
db 90 20 0 0
db 0 0 0 0
; таблица поиска для User32.dll
; CreateWindowExA
db 9E 20 0 0
db 0 0 0 0
; Таблица импорта: 2 записи + завершающая (0)
; запись для Kernel32.dll
; смещение таблицы поиска
db 14 20 0 0
db 0 0 0 0 0 0 0 0
; смещение строки "Kernel32.dll"
db 64 20 0 0
; смещение IAT(1)
db 0 20 0 0
; запись для User32.dll
; смещение таблицы поиска
db 20 20 0 0
db 0 0 0 0 0 0 0 0
; смещение строки "User32.dll"
db 71 20 0 0
; смещение IAT(2)
db 0C 20 0 0
; завершение таблицы
db 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
db "Kernel32.dll" 0
db "User32.dll" 0
db 0 0 "GetModuleHandleA" 0 0
db 0 0 "ExitProcess" 0
db 0 0 "CreateWindowExA" 0
 
m 2000 l 200 100
w
q

 
Секция, как обычно, строится "в два прохода"; чтобы сразу получить нужные смещения в режиме ассемблирования, образ в debug собирается по тому же смещению, что и образ в памяти (2000h), а затем для записи переносится по смещению 100h. После строки "GetModuleHandleA" дополнительный 0 поставлен для выравнивания, т.к. строки в таблице имен должны начинаться по четному адресу. Теперь секция кода (файл code.txt):  
 

Код:
n code.bin
r cx
200
f 0 l 200 0
a 0
; параметр GetModuleHandleA = 0
db 6a 0
; вызов GetModuleHanldeA (по адресу в IAT(1) 402000h)
db ff 15 0 20 40 0
; параметры CreateWindowExA
; дополнительное число (0)
db 6a 0
; описатель модуля (в EAX)
db 50
; описатель меню (0)
db 6a 0
; описатель окна-владельца (0)
db 6a 0
; высота окна
db 68 0 1 0 0
; ширина окна
db 68 50 1 0 0
; координата y
db 68 0 1 0 0
; координата x
db 68 50 1 0 0
; стиль окна
db 68 0 0 cf 10
; адрес имени окна (в секции данных - 403010h)
db 68 10 30 40 0
; адрес имени класса (в секции данных - 403000h)
db 68 0 30 40 0
; расширенный стиль окна (0)
db 6a 0
; вызов CreateWindowEx (по адресу в IAT(2) 40200Ch)
db ff 15 c 20 40 0
; параметр ExitProcess (код завершения = 0)
db 6a 0
; вызов ExitProcess (по адресу в IAT(1) 402004h)
db ff 15 4 20 40 0
 
m 0 l 200 100
w
q

 
Осталось слегка подправить наш шаблон заголовка. Скопируем файл header.txt в рабочий каталог. Изменения требует лишь начало каталога смещений. Находим строку "Здесь начинается первый элемент каталога:" и вставляем такой кусок:  
 

Код:
; смещение таблицы экспорта (4 байта)
db 0 0 0 0
; размер таблицы экспорта (4 байта)
db 0 0 0 0
; Второй элемент каталога:
; смещение таблицы импорта (4 байта)
db 28 20 0 0
; размер таблицы импорта (4 байта)
db 3c 0 0 0

 
Файл "make.bat" традиционный:  
 

Код:
@echo off
debug < header.txt > report.lst
debug < code.txt >> report.lst
debug < rdata.txt >> report.lst
debug < data.txt >> report.lst
copy /b header.bin+code.bin+rdata.bin+data.bin wnd.exe

 
Проверив файл report.lst на наличие ошибок, можно запускать wnd.exe. Что-то мелькает? Хлопайте в ладоши! Это не ошибка - так и должно быть. Ошибка, если появится сообщение от Windows или вообще ничего не появится. Наше приложение создает окно, но завершается раньше, чем мы успеваем что-либо рассмотреть. Как "остановить" приложение? Один раз мы это уже делали, когда создавали самый первый PE-файл (в статье "Исполняемые файлы Windows"): нужно зациклить программу с помощью инструкции EB FE. Ее можно ввести вместо параметра функции ExitProcess (т.е. в файле code.txt вместо последнего 'db 6a 0' записать 'db eb fe'). Изменив файл code.txt, нужно снова запустить make.bat.  
 
Теперь, запустив wnd.exe, можно полюбоваться на созданное окно (хотя на самом деле оно пока выглядит не так, как должно). Все, что надо, на месте - и три кнопки в правом верхнем углу, и строка с именем, и даже значок слева. Только вот сделать с ним ничего не удастся - ни сдвинуть, ни изменить размеры, ни даже закрыть. Обратите внимание - когда указатель мыши попадает в область нашего окна, курсор принимает ждущую форму. Если переключиться на другое окно, которое перекроет наше, оно исчезнет и больше не появится.  
 
Все это происходит потому, что окно должно обрабатывать сообщения, которые система начинает посылать в очередь сообщений приложения, создавшего окно, сразу после его создания. Мы же обработку сообщений не предусмотрели. Но этим мы займемся уже в следующий раз. А пока можно поэкспериментировать с изменением значений тех параметров функции CreateWindowExA, которые можно пронаблюдать - т.е. размеров и расположения окна. Учтите, что данные в коде - 16-ричные. А чтобы завершить приложение, придется снова "прибивать" его из менеджера задач, нажав Ctrl-Alt-Del .  
 
2005 © Roustem  

Всего записей: 12 | Зарегистр. 06-06-2007 | Отправлено: 10:57 13-06-2007 | Исправлено: RoustemRuBoard, 09:54 15-06-2007
RoustemRuBoard

Newbie
Редактировать | Профиль | Сообщение | Цитировать | Сообщить модератору
7. Сообщения Windows
 
Windows является многозадачной средой - в ней одновременно (параллельно) могут быть запущены сразу несколько приложений. Если в однозадачной среде приложение само отслеживает действия, происходящие с устройствами ввода и вывода, то в многозадачной среде эту функцию полностью берет на себя операционная система. Она осуществляет это централизованно для всех выполняющихся в данное время задач. Изменения в состоянии аппаратных средств, самой операционной системе, других исполняющихся в данный момент приложениях, которые могут оказать воздействие на ход выполнения задачи, называются событиями. Приложения извещаются о событиях через сообщения, которые им посылает Windows.  
 
Вся система Windows построена на передаче, приеме и обработке сообщений. Вместо непосредственного контроля за состоянием устройств ввода-вывода приложение должно ожидать поступления соответствующего сообщения от операционной системы. Значительная часть программирования в среде Windows заключается в выборе сообщений, которые будут обрабатываться, и написании соответствующего кода для их обработки. Windows создает специальные очереди сообщений, в которые направляются сообщения для приложений. Приложения, в свою очередь, должны выбирать сообщения из этой очереди и обрабатывать их соответствующим образом; кроме того, они сами могут при необходимости посылать сообщения операционной системе и другим выполняющимся в данный момент приложениям.  
 
В прошлый раз мы построили приложение, которое создает окно и входит в бесконечный цикл. Но в этом цикле оно не делает ничего полезного; а система тем временем создает для приложения очередь сообщений и начинает посылать туда для созданного окна сообщения, которые не только не обрабатываются, но даже и не забираются из этой очереди. Именно поэтому указатель мыши в пределах нашего окна принимал форму ожидания - Windows "наивно" полагала, что сообщения не забираются из очереди, потому что приложение занято какой-то длительной операцией.  
 
Настало время исправить это положение. Для получения сообщения из очереди существует функция GetMessageA из модуля User32.dll. Эта функция перемещает очередное сообщение из очереди в структуру типа MSG, адрес которой мы указываем в параметрах. Структура MSG имеет следующее строение:  
 
   
 
Как указывалось в прошлой статье, основной рабочей единицей в Windows является окно, и сообщения посылаются именно окнам. Чтобы самому послать сообщение, нужно указать описатель окна, для которого сообщение предназначено. Эта информация записывается в первое поле структуры. Параметры wParam и lParam содержат дополнительную информацию, которая специфична для каждого кода сообщения. Система добавляет также к каждому сообщению информацию о времени и координатах курсора мыши в момент отправки сообщения.  
 
Функция GetMessageA принимет 4 параметра, которые размещаются в стеке в следующем порядке:  
 

  1. максимальный код сообщения, который принимает функция;  
  2. минимальный код сообщения, который принимает функция;  
  3. описатель окна, для которого нужно получить сообщение;  
  4. адрес структуры MSG, куда должно быть скопировано сообщение.

Первые три параметра образуют фильтр - GetMessageA может отбирать лишь те сообщения из очереди, которые попадают в заданный диапазон или предназначены лишь указанному окну. Если параметры максимального и минимального кодов равны нулю, из очереди извлекаются все типы сообщений. Если равен нулю параметр описателя окна, извлекаются сообщения для всех окон. Обычно первые три параметра всегда равны нулю - фильтрация сообщений не используется.  
 
Переделаем созданное в прошлый раз приложение так, чтобы включить в него вызов функции GetMessageA. Причем сделаем это с некоторым "запасом" - оставим место (в первую очередь, в структурах для импорта) еще для 2 функций, которые нам пригодятся в дальнейшем.  
 
В секции данных нам потребуется лишь зарезервировать место для структуры MSG размером 28 байтов; но, поскольку мы ее разместим после строк по смещению 3020h, а после нее ничего нет, никаких изменений в файл data.txt вводить не потребуется - просто надо запомнить, что адрес структуры (после загрузки ее в память) будет 403020h. Сюда в процессе работы приложения будут копироваться сообщения системы.  
 
Зато изменить потребуется файл rdata.txt. GetMessageA и другие функции, которые мы собираемся добавить потом, находятся в одном модуле User32.dll, поэтому вторую IAT и вторую таблицу поиска придется увеличить на 12 байтов, а таблицу импорта и следующие за ней строки сместить. Соответственно потребуется также изменить смещения строк во всех таблицах. Файл rdata.txt будет выглядеть следующим образом:  
 

Код:
n rdata.bin
r cx
200
f 2000 l 200 0
a 2000
; 1-я IAT (для Kernel32.dll)
; GetModuleHandleA
db 94 20 0 0
; ExitProcess
db a8 20 0 0
db 0 0 0 0
; 2-я IAT (User32.dll)
; CreateWindowExA
db b6 20 0 0
; GetMessageA
db c8 20 0 0
; запас для 2-й ф-ции
db 0 0 0 0
; запас для 3-й ф-ции
db 0 0 0 0
db 0 0 0 0
; таблица поиска для Kernel32.dll
; GetModuleHandleA
db 94 20 0 0
; ExitProcess
db a8 20 0 0
db 0 0 0 0
; таблица поиска для User32.dll
; CreateWindowExA
db b6 20 0 0
; GetMessageA
db c8 20 0 0
; запас для 2-й ф-ции
db 0 0 0 0
; запас для 3-й ф-ции
db 0 0 0 0
db 0 0 0 0
; Таблица импорта: 2 записи + завершающая (0)
; запись для Kernel32.dll
; адрес таблицы поиска
db 20 20 0 0
db 0 0 0 0 0 0 0 0
; адрес строки "Kernel32.dll"
db 7c 20 0 0
; адрес IAT(1)
db 0 20 0 0
; запись для User32.dll
; адрес таблицы поиска
db 2c 20 0 0
db 0 0 0 0 0 0 0 0
; адрес строки "User32.dll"
db 89 20 0 0
; адрес IAT(2)
db 0C 20 0 0
; завершение таблицы
db 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
db "Kernel32.dll" 0
db "User32.dll" 0
db 0 0 "GetModuleHandleA" 0 0
db 0 0 "ExitProcess" 0
db 0 0 "CreateWindowExA" 0
db 0 0 "GetMessageA" 0
 
m 2000 l 200 100
w
q

 
В секции кода после вызова функции CreateWindowExA добавляется вызов функции GetMessageA (предварительно в стек помещаются 4 параметра - три нулевых и адрес структуры MSG в секции данных). Цикл также меняется: теперь внутри него находится функция GetMessageA, и соответствующая инструкция должна передать управление команде помещения в стек первого параметра для этой функции. Теперь это уже не просто цикл, а цикл обработки сообщений - один из главных элементов приложения Windows (хотя сама обработка пока не совсем полноценная).  
 
Рассмотрим подробнее инструкцию безусловного перехода, которая на ассемблере обозначается мнемоникой JMP. Опкод можно представить следующим образом:  
 

Код:
1110 10 s 1 <байт(ы) смещения>

 
"Буквально" эта инструкция добавляет с учетом знака непосредственное значение (следующее за опкодом) к текущему значению регистра EIP. Обратите внимание на наличие бита знакового расширения s. Напомним, что если он равен 1, за опкодом следует всего один байт данных, который расширяется со знаком (т.е. старший бит этого байта заполняет 3 оставшихся старших байта). Если s = 0, за опкодом следуют 4 байта.  
 
Как вы уже знаете, в регистре EIP находится адрес начала следующей инструкции. Таким образом, данная инструкция изменяет адрес следующей инструкции и тем самым осуществляет безусловный переход на исполнение кода где-то в другом месте. Указанный операнд является смещением этого "другого места" относительно начала следующей инструкции. Если смещение положительно, переход осуществляется "вперед"; если отрицательно (старший бит = 1), переход "назад". Сама инструкция перехода в короткой форме занимает 2 байта, поэтому смещение -2 означает переход на себя (отсчет ведется от следующей инструкции!); переведем это отрицательное число в бинарную форму: 2 = 00000010b; двоичное дополнение = 11111101b, добавив единицу, получим: -2 = 11111110b. Полная инструкция будет (s = 1):  
 

Код:
11101011 11111110, или EB FE  

 
- то, что мы уже использовали в прошлый раз.  
На этот раз нам нужно подсчитать число байтов, которые следует "перескочить" - это будет прыжок "назад", как вы уже поняли (и смещение соответственно отрицательное). В файле code.txt находим строку "параметр ExitProcess (код завершения = 0)" и заменяем ее следующим кодом:  
 

Код:
; цикл
; параметры GetMessageA
db 6a 0
db 6a 0
db 6a 0
; 4-й параметр - адрес структуры MSG (в секции данных - 403020h)
db 68 20 30 40 0
; вызов GetMessageA (по адресу в IAT(2) 402010h)
db ff 15 10 20 40 0

 
Подсчитаем число получившихся байтов: 17. Еще 2 займет сама команда перехода; поэтому, чтобы перейти на начало первой инструкции '6a 0', нужно добавить к значению EIP смещение -19 (11101101b = EDh). Сама же инструкция перехода будет иметь вид FEh EDh. Остаток файла должен выглядеть так:  
 

Код:
; возврат на "цикл" (-19 байтов)
db eb ed
 
m 0 l 200 100
w
q

 
В файле 'header.txt' необходимо изменить лишь смещение таблицы импорта:  
 

Код:
; Второй элемент каталога:
; смещение таблицы импорта (4 байта)
db 40 20 0 0

 
Размер таблицы остался тем же. Файл 'make.bat' также остался без изменений. Строим новый файл wnd.exe и запускаем его.  
 
Отличий у данного приложения по сравнению с предыдущим немного - оно также не перемещается, не изменяет размеров и не отвечает на попытки закрыть окно. Тем не менее, одно отличие все же есть: обратите внимание на форму указателя мыши в пределах окна. Он больше не принимает ждущей формы - сообщения забираются из очереди. Но они лишь копируются в одно и то же место и никак не обрабатываются.  
 
Чтобы от сообщений был какой-то толк, их нужно не просто забирать из очереди, но и направлять соответствующему окну. Для этой цели служит еще одна функция из модуля User32.dll - DispatchMessageA. Эта функция принимает лишь один аргумент - адрес структуры MSG, содержащей соответствующее сообщение. Добавим в наше приложение эту функцию.  
 
В секции данных никаких изменений не будет. В секцию .rdata нужно добавить после имеющихся строк строку 'DispatchMessageA', а также указать ее смещение в IAT(2) и таблице поиска в специально оставленных для этого местах. Найдем строку:  
 

Код:
; запас для 2-й ф-ции
db 0 0 0 0

 
- и заменим ее на следующую:  
 

Код:
; DispatchMessageA
db d6 20 0 0

 
Это нужно сделать в двух местах - в IAT(2) и в таблице поиска для User32.dll. В конец файла нужно добавить строку 'DispatchMessageA':  
 

Код:
db 0 0 "GetMessageA" 0
db 0 0 "DispatchMessageA" 0  
 
m 2000 l 200 100
w
q

 
Теперь изменим секцию кода. В файле 'code.txt' найдем строки:  
 

Код:
; вызов GetMessageA (по адресу в IAT(2) 402010h)
db ff 15 10 20 40 0

 
Теперь вставим после них инструкции помещения в стек параметра и вызова функции DispatchMessageA:  
 

Код:
; параметр DispatchMessageA - адрес MSG (403020h)
db 68 20 30 40 0
; вызов DispatchMessageA (по адресу в IAT(2) 402014h)
db ff 15 14 20 40 0

 
Дальше следует безусловный переход на начало цикла. Однако, мы добавили 11 новых байтов, поэтому смещение будет теперь не -19, а -30 (или E2h). Поэтому конец файла должен выглядеть так:  
 

Код:
; возврат на "цикл" (-30 байтов)
db eb e2
 
m 0 l 200 100
w
q

 
Файлы header.txt и make.bat не изменились. Строим очередную версию wnd.exe.  
 
Вот это другое дело! Теперь окно выглядит так, как оно должно было выглядеть - это одна большая кнопка! Причем реагирует на щелчки мышью! А само окно можно перемещать, изменять его размеры, сворачивать, разворачивать и даже закрывать! Правда, в последнем случае не спешите радоваться - окно-то вы закрыли, но приложение осталось работать (оно все еще крутится в цикле обработки сообщений). Убедиться в этом несложно, заглянув в менеджер задач (нажав Ctrl-Alt-Del) - вы увидите выполняющуюся задачу 'wnd' (если файл не был переименован). Ее снова придется "прибивать" отсюда. Это происходит потому, что класс окна "BUTTON", как уже говорилось, не предназначен для создания самостоятельных окон. При закрытии главного окна приложения в цикл сообщений посылается соответствующее уведомление о завершении приложения; в нашем случае этого не происходит. К тому же наш цикл вообще не предусматривает возможности выхода из него - в дальнейшем мы реализуем и такую возможность.  
 
Чтобы рассмотреть еще одну функцию, использующуюся в цикле сообщений, изменим класс нашего окна на "EDIT". Для этого в файле 'data.txt' просто поменяем строку "BUTTON" на "EDIT". И все! Снова строим wnd.exe, запустив make.bat.  
 
И какая разительная перемена! Поэкспериментируйте с этим приложением сами. В качестве подсказки: воспользуйтесь контекстным меню, щелкнув в окне правой клавишей мыши; а для копирования текста в буфер можно воспользоваться Блокнотом.  
 
Я думаю, вы уже заметили особенность нового окна: хотя можно пользоваться командами редактирования текста из всплывающего меню и двигать текстовый курсор с помощью стрелок, набрать текст на клавиатуре не удается. Это происходит потому, что окно получает лишь сообщения о нажатиях клавиш, но они не преобразуются в сообщения о поступлении соответствующих символов. Как раз для решения этой задачи и служит последняя функция - TranslateMessage, тоже из модуля User32.dll. Эта функция принимает один параметр - конечно же, опять все тот же адрес структуры MSG, и помещается в цикл обработки сообщений как раз перед вызовом функции DispatchMessageA.  
 
Добавим эту функцию. Для этого в конце файла 'rdata.txt' добавляем соответствующую строку:  
 

Код:
db 0 0 "GetMessageA" 0
db 0 0 "DispatchMessageA" 0 0
db 0 0 "TranslateMessage" 0
 
m 2000 l 200 100
w
q

 
Второй 0 после "DispatchMessageA" нужен для выравнивания начала следующей строки по четному адресу. Необходимо также изменить (тоже в двух местах - для IAT(2) и таблицы поиска User32.dll) строки:  
 

Код:
; запас для 3-й ф-ции
db 0 0 0 0

 
на строки:  
 

Код:
; TranslateMessage
db ea 20 0 0

 
В файле code.txt очередные 11 байтов нужно добавить до вызова DispatchMessageA (не забыв изменить смещение в инструкции безусловного перехода на начало цикла), так что конец файла будет выглядеть так:  
 

Код:
; вызов GetMessageA (по адресу в IAT(2) 402010h)
db ff 15 10 20 40 0
; параметр TranslateMessage - адрес MSG (403020h)
db 68 20 30 40 0
; вызов TranslateMessage (по адресу в IAT(2) 402018h)
db ff 15 18 20 40 0
; параметр DispatchMessageA - адрес MSG (403020h)
db 68 20 30 40 0
; вызов DispatchMessageA (по адресу в IAT(2) 402014h)
db ff 15 14 20 40 0
; возврат на "цикл" (-41 байт)
db eb d7
 
m 0 l 200 100
w
q

 
Вот теперь, построив очередную версию wnd.exe, мы получим почти полноценный текстовый редактор! Хотя пока он работает лишь с одной строкой; но зато создан полностью в машинных кодах!  
 
На сегодня этого достаточно. Не забудьте "прибить" работающие приложения - закрытия окна недостаточно для их завершения (пока). В заключение хочу заметить, что подобный способ завершения приложений, конечно, не является нормальным, и система может начать (и, возможно, у вас уже не раз начинала) работать нестабильно, так что придется ее перезагружать. Поэтому перед началом таких экспериментов лучше закрыть все остальные приложения, особенно, если вы работаете в них с важными данными.  
 
2005 © Roustem  

Всего записей: 12 | Зарегистр. 06-06-2007 | Отправлено: 19:19 14-06-2007
RoustemRuBoard

Newbie
Редактировать | Профиль | Сообщение | Цитировать | Сообщить модератору
8. Класс окна
 
До сих пор все создаваемые нами окна использовали стандартные системные классы окон. Однако практически каждое самостоятельное приложение создает и регистрирует свои собственные классы (хотя бы для главного окна), дающие возможность реализовать уникальные особенности именно этого приложения. Настало время и нам создать собственный класс окна.  
 
Для регистрации класса окна необходимо заполнить специальную структуру - WNDCLASSEX, в которой собираются вместе все необходимые для создания класса сведения. Структура имеет 12 полей, каждое размером 4 байта:  

  1. размер данной структуры в байтах (30h);  
  2. флаги, указывающие стили класса;  
  3. адрес главной функции окна (по традиции ее называют также главной процедурой окна);  
  4. количество дополнительных байтов класса (эти байты, если имеются, следуют непосредственно за данной структурой);  
  5. количество дополнительных байтов окна (эти байты, если имеются, следуют после внутренней структуры окна);  
  6. описатель экземпляра приложения. Это должно быть то приложение, в адресном пространстве которого размещена главная функция окна (поскольку одна и та же функция используется для всех окон данного класса);  
  7. описатель ресурса значка, отображаемого в строке заголовка окна и на панели задач. Для получения описателя ресурс значка должен быть предварительно загружен;  
  8. описатель ресурса курсора, ассоциированного с данным окном. При попадании указателя мыши в рабочую область окна система автоматически меняет форму курсора на данную. Ресурс курсора также должен быть предварительно загружен;  
  9. описатель кисти, использующейся при создании фона окна. Объект кисти должен быть предварительно создан; либо должно использоваться число, соответствующее одному из предопределенных системных цветов;  
  10. адрес строки с именем ресурса меню. Данное меню будет использоваться в создаваемом окне по умолчанию, если в функции CreateWindowExA не предусмотрено использование другого меню. Если это поле равно 0, у класса окна нет меню по умолчанию;  
  11. адрес строки с именем класса. Это имя используется функцией CreateWindowExA при создании окна данного класса;  
  12. описатель мелкого значка, связанного с данным окном. Если его нет, мелкий значок ищется через описатель ресурса значка.

Для начала в большинстве полей структуры можно оставить нулевое значение. В обязательном порядке должны быть заполнены лишь поля размера (всегда 30h), описателя приложения, имени класса и адреса главной процедуры окна. Мы будем постепенно знакомиться с полями данной структуры, проводя с ними эксперименты. Главная процедура окна сама по себе представляет собой отдельную большую тему, поскольку фактически вся функциональность нашего приложения определяется именно этой процедурой. Пока мы просто используем предоставляемую системой процедуру окна по умолчанию (DefWindowProcA), которую необходимо импортировать из модуля User32.dll. Там же расположена и другая нужная нам функция - RegisterClassExA, которая используется для регистрации заполненной нами структуры. Таким образом, нам придется добавить в созданный ранее файл rdata.txt две новые функции. С учетом всего этого, файл rdata.txt будет выглядеть следующим образом:  
 

Код:
n rdata.bin
r cx
200
f 2000 l 200 0
a 2000
; 1-я IAT (для Kernel32.dll)
; GetModuleHandleA
db a4 20 0 0
; ExitProcess
db b8 20 0 0
db 0 0 0 0
; 2-я IAT (User32.dll)
; CreateWindowExA
db c6 20 0 0
; GetMessageA
db d8 20 0 0
; DispatchMessageA
db e6 20 0 0
; TranslateMessage
db fa 20 0 0
; DefWindowProc
db e 21 0 0
; RegisterClassExA
db 20 21 0 0
db 0 0 0 0
; таблица поиска для Kernel32.dll
; GetModuleHandleA
db a4 20 0 0
; ExitProcess
db b8 20 0 0
db 0 0 0 0
; таблица поиска для User32.dll
; CreateWindowExA
db c6 20 0 0
; GetMessageA
db d8 20 0 0
; DispatchMessageA
db e6 20 0 0
; TranslateMessage
db fa 20 0 0
; DefWindowProcA
db e 21 0 0
; RegisterClassExA
db 20 21 0 0
db 0 0 0 0
; Таблица импорта: 2 записи + завершающая (0)
; запись для Kernel32.dll
; смещение таблицы поиска
db 28 20 0 0
db 0 0 0 0 0 0 0 0
; смещение строки "Kernel32.dll"
db 8c 20 0 0
; смещение IAT(1)
db 0 20 0 0
; запись для User32.dll
; смещение таблицы поиска
db 34 20 0 0
db 0 0 0 0 0 0 0 0
; смещение строки "User32.dll"
db 99 20 0 0
; смещение IAT(2)
db 0C 20 0 0
; завершение таблицы
db 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
db "Kernel32.dll" 0
db "User32.dll" 0
db 0 0 "GetModuleHandleA" 0 0
db 0 0 "ExitProcess" 0
db 0 0 "CreateWindowExA" 0
db 0 0 "GetMessageA" 0
db 0 0 "DispatchMessageA" 0 0
db 0 0 "TranslateMessage" 0 0
db 0 0 "DefWindowProcA" 0 0
db 0 0 "RegisterClassExA" 0
 
m 2000 l 200 100
w
q

 
Двойные нули после строк, как и ранее, предназначены для выравнивания по четным адресам. Для удобства выпишем здесь адреса полей IAT, в которых после загрузки приложения на исполнение будут находиться адреса соответствующих функций (это не адреса самих функций - это адреса, по которым будут размещены адреса этих функций!)  

  • GetModuleHandle - 402000h <-- (начало 1-й IAT);  
  • ExitProcess - 402004h;  
  • CreateWindowExA - 40200Ch <-- (начало 2-й IAT);  
  • GetMessageA - 402010h;  
  • DispatchMessageA - 402014h;  
  • TranslateMessage - 402018h;  
  • DefWindowProcA - 40201Ch;  
  • RegisterClassExA - 402020h.

Теперь можем заняться секцией данных. Как и ранее, там будут две строки - имя класса окна и заголовок окна, за ними - место для структуры MSG, а за ней расположим структуру WNDCLASSEX. Обратите внимание, что необязательно располагать данные вплотную друг к другу (в данном случае) - нам просто нужно следить, чтобы они не перекрывали друг друга, и знать их адреса в памяти (в т.ч. и адреса отдельных полей структур). Поэтому может оказаться удобным и здесь использовать "два прохода" в режиме ассемблирования, чтобы нужные смещения были вычислены автоматически. Я приведу уже готовый файл data.txt (нужные адреса указаны в скобках):  
 

Код:
n data.bin
r cx
200
f 3000 l 200 0
a 3000
; имя класса окна
db "MYCLASS" 0
 
a 3010
; заголовок окна
db "Моё окно" 0
 
a 3020
; структура MSG (28 байт)
 
a 3040
; структура WNDCLASSEX:
; размер структуры
db 30 0 0 0
; стили класса окна
db 0 0 0 0
; (403048h) адрес главной функции окна  
db 0 0 0 0
; дополнительные байты класса
db 0 0 0 0
; дополнительные байты окна
db 0 0 0 0
; (403054h) описатель экземпляра приложения
db 0 0 0 0
; описатель ресурса значка
db 0 0 0 0
; описатель ресурса курсора
db 0 0 0 0
; фон окна
db 6 0 0 0
; адрес имени меню
db 0 0 0 0
; адрес имени класса окна
db 0 30 40 0
; описатель ресурса мелкого значка
db 0 0 0 0
 
m 3000 l 200 100
w
q

 
Не забудьте, что пустые строки должны быть на своих местах - их нельзя удалять. Значения для размера структуры, адреса имени класса, а также фона окна уже указаны в секции данных. Однако, значения для еще двух обязательных полей - описатель экземпляра приложения и адрес главной процедуры окна - будут известны лишь после загрузки программы на исполнение, и мы должны предусмотреть инициализацию соответствующих полей (по адресам в памяти 403048h и 403054h) в секции кода.  
 
Как получить описатель приложения, мы уже знаем - он будет возвращен в регистре EAX после успешного вызова GetModuleHandle. Это значение нужно скопировать из EAX по адресу памяти 403054h. Вспомним, что для копирования данных между регистром EAX и памятью используется инструкция с опкодом 101000dw и четырьмя байтами адреса памяти (это мы рассматривали в самой первой статье). В данном случае направление копирования - из EAX в память (d = 1), используется полный регистр (w = 1); вся инструкция будет иметь вид: A3 54 30 40 00.  
 
Обратите внимание на особенность главной процедуры окна: хотя в данном конкретном случае мы используем импортируемую функцию, ее будет вызывать сама система, а не мы. Поэтому передавать ей параметры нам не придется; нужно лишь указать ее адрес в соответствующем поле структуры WNDCLASSEX (по адресу 403048h). Получить же этот адрес (импортированный) можно из второй IAT по адресу 40201Ch. (Если бы мы сами написали эту процедуру, мы знали бы ее адрес непосредственно). Таким образом, нам нужно скопировать значение из одного места в памяти (по адресу 40201Ch) в другое (по адресу 403048h). Поскольку нет непосредственной инструкции, производящей эту операцию, придется воспользоваться посредником в виде того же регистра EAX, и использовать две последовательные команды: копируем значение из [40201Ch] в EAX (A1 1C 20 40 00), затем из EAX в [403048h] (A3 48 30 40 00).  
 
Теперь структура WNDCLASSEX инициализирована, и можно передать ее адрес функции RegisterClassExA (этот аргумент у функции единственный): 68 40 30 40 00 (поместить адрес в стек); FF 15 20 20 40 00 (вызвать функцию по адресу в IAT). После успешного завершения этой функции новый класс окна зарегистрирован, и мы можем использовать его имя ("MYCLASS" - можно было использовать и другое имя) в вызове CreateWindowExA. Дальнейший код нам уже известен.  
 
Единственный момент - описатель экземпляра приложения теперь находится в памяти, а не в EAX, поэтому нужно использовать инструкцию косвенного помещения значения в стек (по адресу памяти). Ее мы рассматривали в статье "Простейшее приложение": 11111111 Mod 110 R/M. Используем непосредственный 4-байтный адрес после байта ModR/M (R/M = 101 при Mod = 0): FF 35 54 30 40 00. Полностью файл code.txt будет выглядеть следующим образом:  
 

Код:
n code.bin
r cx
200
f 1000 l 200 0
a 1000
; параметр GetModuleHandleA = 0
db 6a 0
; вызов GetModuleHanldeA (по адресу в IAT(1) 402000h)
db ff 15 0 20 40 0
; скопировать описатель из EAX по адресу 403054h
db a3 54 30 40 0
; скопировать в EAX адрес функции DefWindowProcA
; (из поля IAT(2) с адресом 40201Ch)
db a1 1c 20 40 0
; скопировать адрес функции из EAX  
; в поле структуры WNDCLASSEX с адресом 403048h
db a3 48 30 40 0
; параметр для RegisterClassExA -  
; адрес WNDCLASSEX (403040h)
db 68 40 30 40 0
; вызов RegisterClassExA (в IAT(2) - 402020h)
db ff 15 20 20 40 0
; параметры для CreateWindowExA
; дополнительное число (0)
db 6a 0
; описатель модуля (сохранен по адресу 403054h)
db ff 35 54 30 40 0
; описатель меню (0)
db 6a 0
; описатель окна-владельца (0)
db 6a 0
; высота окна
db 68 0 1 0 0
; ширина окна
db 68 50 1 0 0
; координата y
db 68 0 1 0 0
; координата x
db 68 50 1 0 0
; стиль окна
db 68 0 0 cf 10
; адрес имени окна (403010h)
db 68 10 30 40 0
; адрес имени класса (403000h)
db 68 0 30 40 0
; расширенный стиль окна (0)
db 6a 0
; вызов CreateWindowExA (по адресу в IAT(2) 40200Ch)
db ff 15 c 20 40 0
; цикл
; параметры GetMessageA
db 6a 0
db 6a 0
db 6a 0
; 4-й параметр - адрес структуры MSG (403020h)
db 68 20 30 40 0
; вызов GetMessageA (по адресу в IAT(2) 402010h)
db ff 15 10 20 40 0
; параметр TranslateMessage - адрес MSG (403020h)
db 68 20 30 40 0
; вызов TranslateMessage (по адресу в IAT(2) 402018h)
db ff 15 18 20 40 0
; параметр DispatchMessageA - адрес MSG (403020h)
db 68 20 30 40 0
; вызов DispatchMessageA (по адресу в IAT(2) 402014h)
db ff 15 14 20 40 0
; возврат на "цикл" (-41 байт)
db eb d7
; параметр ExitProcess (код завершения = 0)
db 6a 0
; вызов ExitProcess (по адресу в IAT(1) 402004h)
db ff 15 4 20 40 0
 
m 1000 l 200 100
w
q

 
В файле "header.txt" необходимо изменить лишь значение смещения таблицы импорта:  
 

Код:
; Здесь начинается первый элемент каталога:
; смещение таблицы экспорта (4 байта)
db 0 0 0 0
; размер таблицы экспорта (4 байта)
db 0 0 0 0
; Второй элемент каталога:
; смещение таблицы импорта (4 байта)
db 50 20 0 0
; размер таблицы импорта (4 байта)
db 3c 0 0 0

 
Имя исполняемого файла можно изменить, скажем, на "class.exe" (в файле make.bat). Строим приложение и запускаем его.  
 
Мы получили почти нормальное окно. Поэкспериментируйте с ним сами. "Почти нормальное" потому, что при его закрытии приложение все равно не завершается - его опять нужно "прибивать" из менеджера задач. Чтобы программа завершалась при закрытии его главного окна, нам придется написать собственную главную функцию окна и реализовать в ней эту возможность. Но этим мы займемся уже в другой раз.  
 
2005 © Roustem  

Всего записей: 12 | Зарегистр. 06-06-2007 | Отправлено: 09:28 15-06-2007
RoustemRuBoard

Newbie
Редактировать | Профиль | Сообщение | Цитировать | Сообщить модератору
9. Главная функция окна
 
Сегодня мы впервые будем создавать собственную функцию, поэтому освежим в памяти особенности функций.  
 
Функция - это некий код, который находится где-то по другому адресу от того места, из которого вызывается. Поэтому адрес возврата сохраняется в стеке. Кроме того, мы уже работали с передаваемыми функции данными - ее параметрами - и знаем, что они тоже помещаются в стек. А значит, при возвращении из функции мы должны компенсировать изменения в стеке, иначе он будет несбалансированным.  
 
Процедура окна - это функция, которая вызывается самой системой; поэтому она должна удовлетворять соглашениям вызова функций API. Т.е. мы можем внутри функции свободно работать с регистрами EAX, ECX, EDX и изменять их значения; но если потребуется работа с регистрами EBX, ESI или EDI, нужно сначала сохранить их содержимое в стеке, а перед возвращением из функции - восстановить старые значения. Кроме того, в EAX должен быть помещен результат работы функции, если она возвращает какое-то значение.  
 
Теперь разберем все эти детали подробнее. Нужно каким-то образом получить доступ к параметрам, помещенным в стек. Мы уже знаем, что указатель стека находится в ESP, и можно было бы воспользоваться косвенной адресацией через этот регистр. Кстати, современные оптимизирующие компиляторы используют именно этот способ. Но если функция сама будет активно использовать стек, например, сохранять и восстанавливать регистры EBX, ESI или EDI, смещение параметров относительно ESP будет все время меняться и работать с ним будет неудобно - особенно при "ручном" кодировании. Поэтому для работы с параметрами (и локальными переменными, с которыми мы познакомимся позже) создается т.н. фрейм стека: значение ESP в самом начале функции копируется в регистр EBP, который и используется в дальнейшем в качестве базового указателя при обращении к параметрам (и локальным переменным). Предварительно в стек заносится старое значение EBP, которое восстанавливается перед завершением функции.  
 
Главная функция окна строится по единому шаблону. Эта функция получает в качестве параметров значения 4 из 7 полей, содержащихся в структуре MSG. Параметры помещаются в стек в следующем порядке:  
 

  1. дополнительный параметр lParam, зависящий от типа сообщения;  
  2. дополнительный параметр wParam, зависящий от типа сообщения;  
  3. код сообщения (msg);  
  4. описатель окна (hWnd), для которого предназначено сообщение.

На рисунке показаны состояния стека в различные моменты при вызове главной функции окна. На первой картинке показан момент непосредственно перед инструкцией вызова функции: все параметры уже помещены в стек. Вторая картинка изображает момент непосредственно после исполнения инструкции вызова функции: мы находимся "внутри" функции. В стек только что был помещен адрес возврата. Для создания фрейма стека помещаем в него содержимое регистра EBP, затем значение ESP копируем в EBP. В дальнейшем значение ESP может меняться, в EBP же "замораживается" то состояние стека, которое было в самом начале функции.  
 
   
 
Этим и можно воспользоваться для получения доступа к параметрам. На следующем рисунке показан механизм этого процесса. Текущее значение в EBP является адресом, по которому в стеке сохранено старое значение EBP. По смещению +4 от этого адреса будет находиться адрес возврата из функции, а после него - параметры, которые были помещены в стек перед вызовом функции. Параметр, который мы помещали в стек первым, будет иметь наибольшее смещение (в данном случае, +14h).  
 
   
 
Получив доступ к параметрам, мы можем использовать их для своих целей. Действия, которые совершает главная процедура окна, определяются кодом полученного сообщения. Поэтому нам нужно начать с анализа именно этого параметра (находящегося в стеке по адресу EBP+0Ch), и предпринимать те или иные действия в зависимости от его значения. А для этого придется изучить инструкции проверки условий и условных переходов.  
 
На ассемблере группа инструкций для сравнения значений обозначается мнемоникой CMP. В данном случае код сообщения представляет собой обычное число, поэтому нам будет нужно использовать вариант инструкции, сравнивающий значение в памяти с непосредственным значением. Она имеет вид, приведенный на следующем рисунке.  
 
   
 
Сразу обращаем внимание, что опкод содержит биты s и w. Бит w определяет размер сравниваемого операнда в памяти - 1 байт или 4. Поскольку сравниваемые операнды всегда должны иметь одинаковый размер, это определяет и длину непосредственного значения. Однако, в случае, когда s = 1 (и w = 1), в качестве непосредственного значения в инструкцию записывается лишь 1 байт, который затем расширяется с учетом знака до 4 байт. Непосредственное значение в инструкции всегда располагается в самом конце, после всех прочих полей.  
 
После байта ModR/M, в зависимости от значения поля Mod, могут следовать 1 или 4 байта смещения, значение которых добавляется к значению закодированного в R/M регистра для формирования адреса памяти (где находится сравниваемое число). При Mod = 00 в соответствующем регистре содержится полный адрес; при Mod = 01 к значению регистра добавляется 1 байт смещения (с учетом знака); при Mod = 10 к значению регистра добавляются 4 байта смещения.  
 
В нашем случае к адресу в EBP нужно добавить смещение 0Ch - для него достаточно одного байта, откуда имеем: Mod = 01, R/M = 101 (адрес в EBP). В стеке все хранящиеся значения являются 32-разрядными, поэтому w = 1. Значение кода сообщения будем сравнивать с 81h - это самое первое сообщение (WM_NCCREATE), которое получает окно при своем создании. 81h (десятичное 129) не укладывается в диапазон представимых в виде 1 байта знаковых значений (от -128 до +127), поэтому "сократить" его с 4 до 1 байта не удастся - бит s = 0, а непосредственное значение придется кодировать четырьмя байтами. С учетом всего этого получаем инструкцию:  
 

Код:
10000001 01111101 00001100 10000001 00000000 00000000 00000000,
или 81 7D 0C 81 00 00 00 (h).

 
Рассмотрим теперь механизм работы этой команды. Инструкция сравнения делает "пробное" вычитание второго операнда из первого. Значения операндов при этом не изменяются; зато, как и в случае многих других инструкций, меняются отдельные поля регистра флагов EFLAGS, о котором мы упоминали в самой первой статье. Настало время рассмотреть его подробнее.  
 
Отдельные поля (биты) регистра EFLAGS служат в качетве своего рода переключателей, используемых другими инструкциями (в частности, инструкциями условных переходов) для запуска тех или иных действий. Широко используются т.н. флаги состояния, которые приведены на следующем рисунке.  
 
   
 
Инструкция сравнения в зависимости от полученного результата операции изменяет все эти 6 флагов. В данном случае нас интересует состояние одного флага, а именно: флага нуля. Если сравниваемые величины равны, этот флаг будет установлен (1), если нет - сброшен (0).  
 
Изменить ход выполнения программы в зависимости от значений флагов состояния позволяют инструкции условного перехода, имеющие следующий общий формат:  
 

Код:
0111 ttt n <1 байт смещения>

 
Где ttt является кодом состояния определенных флагов (условием), а n указывает, нужно использовать само условие (при n = 0) или его отрицание (при n = 1). При выполнении условия (или его отрицания, если n = 1) к значению EIP прибавляется (с учетом знака) следующий за опкодом 1 байт смещения. В результате осуществляется переход на исполнение команд в другом месте. Если условие не выполняется - ничего не происходит, выполнение продолжается со следующей инструкции (как будто команды условного перехода не было).  
 
Как видим, эта инструкция действует подобно короткой инструкции безусловного перехода, за тем исключением, что "работает" она избирательно - лишь при заданном состоянии определенных флагов. Коды состояния флагов приведены в следующей таблице:  
 
   
 
Таблица отражает работу команды на низком уровне. Использование состояния различных флагов, их комбинаций и создаваемые этим условия мы будем изучать по мере необходимости. Сейчас не будем углубляться в детали, рассмотрим лишь конкретный пример.  
 
Допустим, два сравниваемых операнда равны. В результате вычитания второго операнда из первого будет установлен флаг нуля (ZF=1). Мы можем составить инструкцию, которая осуществляет переход при этом условии - код условия (ZF=1) 010, n = 0, получаем опкод 01110100 (74h), за которым следует 1 байт смещения. В то же время можно составить противоположную инструкцию - которая не делает перехода при этом условии, а делает его при ZF=0 (т.е. когда операнды не равны); для этого нужно всего лишь установить бит n = 1: 01110101 (75h).  
 
У нас теперь есть необходимый теоретический минимум для модернизации нашего приложения путем добавления в него собственной процедуры окна. Пока она будет просто выводить MessageBox в ответ на сообщение Windows 81h (в самом начале создания окна). Скопируем созданные в прошлый раз файлы в новый рабочий каталог. Чтобы переделок было не слишком много, заменим в файле "rdata.txt" строку "DefWindowProc" на "MessageBoxA", дополнив ее нужным числом нулей (для сохранения длины прежнего названия) - благо что эта функция из того же модуля (User32.dll):  
 

Код:
db 0 0 "TranslateMessage" 0 0
db 0 0 "MessageBoxA" 0 0 0 0 0
db 0 0 "RegisterClassExA" 0

 
Теперь именно ее адрес окажется в поле IAT 40201Ch. В файле "code.txt" найдите и удалите следующие строки:  
 

Код:
; скопировать в EAX адрес функции DefWindowProcA
; (из поля IAT(2) с адресом 40201Ch)
db a1 1c 20 40 0
; скопировать адрес функции из EAX  
; в поле структуры WNDCLASSEX с адресом 403048h
db a3 48 30 40 0

 
У нас будет своя главная функция окна - копировать адрес импортированной функции не нужно. Саму же процедуру окна разместим в конце секции кода (чтобы узнать адрес процедуры, удобно использовать предварительный "черновой" проход в режиме ассемблирования). Итак, составляем функцию, добавляя код в конце файла code.txt после вызова ExitProcess:  
 

Код:
; вызов ExitProcess (по адресу в IAT(1) 402004h)
db ff 15 4 20 40 0
;-----------------------------------------------
; Процедура окна

 
Сначала - на входе функции - сохраняем в стеке значение регистра EBP. Вспоминаем инструкцию копирования в стек значений регистров:  
 

Код:
; создание фрейма стека
db 55

 
Затем копируем ESP в EBP: инструкция с использованием байта ModR/M, причем Mod = 11 (оба операнда - регистры). В зависимости от состояния бита d, команду можно закодировать двумя способами; выберем этот:  
 

Код:
db 89 e5

 
Теперь сравниваем параметр кода сообщения с числом 81h - эту инструкцию уже разбирали:  
 

Код:
; сравнить значение в [EBP+0Ch] c 81h
db 81 7d c 81 0 0 0

 
И сразу за ней должен следовать условный переход: если флаг нуля не установлен (операнды не равны, т.е. получено сообщение с кодом, отличным от 81h) - переход к завершению функции. А если это "наше" сообщение - выведем MessageBox; код нам уже знаком, причем используем в качестве заголовка название класса окна, а в качестве сообщения - заголовок самого окна (чтобы не возиться с лишними строками). Инструкция условного перехода (75h + смещение) должна "перепрыгнуть" именно этот участок кода.  
 

Код:
; если не равно - перескочить 14h байтов
db 75 14
; параметры MessageBoxA
db 6a 0
db 68 0 30 40 0
db 68 10 30 40 0
db 6a 0
; вызов MessageBoxA
db ff 15 1c 20 40 0

 
Перед выходом из функции следует восстановить значение регистра ESP, если он был изменен внутри функции (например, из-за создания в стеке пространства для локальных переменных) - для этого обычно просто копируют сохраненное в EBP значение обратно в ESP, а затем извлекают из стека старое значение EBP. Существует одна короткая инструкция для всех этих действий - C9, ее мы и используем.  
 

Код:
; удаление фрейма стека
db c9

 
Осталось вернуться из функции обратно по сохраненному в стеке адресу. При этом надо еще удалить из стека помещенные в него ранее параметры (в нашем случае - 4 параметра общим размером в 10h байт). Для этого просто увеличивают значение ESP на соответствующую величину. Здесь также существует специальная инструкция возврата, которая автоматически выравнивает стек:  
 

Код:
11000010 <2 байта>

 
Значение следующих за опкодом 2 байтов при возвращении из функции добавляется к ESP. Нам нужно добавить 10h:  
 

Код:
; возврат из процедуры с очисткой стека
db c2 10 0
<пустая строка>

 
Конец файла обычный; после процедуры не забудьте оставить пустую строку.  
 

Код:
m 1000 l 200 100
w
q

 
После "чернового" прохода узнаем смещение начала нашей функции (1080h); полученный адрес нужно вставить в структуру WNDCLASSEX в файле data.txt:  
 

Код:
; (403048h) адрес главной функции окна  
db 80 10 40 0

 
В заголовок никаких изменений вносить не требуется. Собираем файл и запускаем его. Если ошибок не было, сразу появится окно сообщения. А вот после щелчка на "OK" поведение системы станет странным: в определенной области экрана указатель мыши начнет принимать ждущую форму, но окно нашего приложения так и не появится. Мы как бы опять вернулись к одному из предыдущих зацикленных приложений, которое не обрабатывало сообщений. В данном случае наша главная функция окна обрабатывает лишь самое первое сообщение, игнорируя все последующие. В результате окно не создается - т.к. для его создания должны быть специальным образом обработаны некоторые сообщения, посылаемые окну при его создании. Если процедура окна не обрабатывает эти (и вообще любые другие) сообщения самостоятельно, она должна переправлять их системной процедуре окна по умолчанию (DefWindowProc), которую мы в этот раз не использовали.  
 
Но это мы оставим уже на следующий раз. Сегодня было много нового материала; мы будем двигаться небольшими шажками. Это позволяет почувствовать вкус бесконечности.  
 
2005 © Roustem  

Всего записей: 12 | Зарегистр. 06-06-2007 | Отправлено: 11:36 16-06-2007
Hiken



Advanced Member
Редактировать | Профиль | Сообщение | Цитировать | Сообщить модератору
Супер! Только я не уразумею: чем асм плох?

Всего записей: 1343 | Зарегистр. 14-01-2006 | Отправлено: 18:11 16-06-2007 | Исправлено: Hiken, 18:13 16-06-2007
RoustemRuBoard

Newbie
Редактировать | Профиль | Сообщение | Цитировать | Сообщить модератору
10. Шаблон оконного приложения
 
В четвертой статье мы создали первый шаблон приложения, возможности которого потом постепенно наращивали. Сегодня важный момент: мы подошли к очередному рубежу. Наконец-то мы сможем создать полноценное работоспособное окно. Мы построим законченное приложение и сохраним его в качестве общего шаблона ("generic"), а затем будем проводить с ним эксперименты.  
 
Для "доведения до ума" предыдущего приложения осталось уже совсем немного. Нужно реализовать возможность завершения цикла сообщений, обеспечить обработку сообщений по умолчанию и, наконец, дать возможность приложению закончить работу при закрытии окна.  
 
Цикл сообщений завершается очень просто. На самом деле, функция GetMessageA возвращает значение 0, если получено сообщение с кодом 12h (WM_QUIT), и ненулевое значение для любого другого сообщения. Для отправки сообщения с кодом 12h служит функция PostQuitMessage (из модуля User32.dll), которая принимает единственный аргумент - код завершения приложения (при нормальном завершении обычно 0). Эту функцию можно вызвать при обработке другого сообщения от главного окна приложения - 2 (WM_DESTROY). В результате при закрытии окна приложение будет автоматически завершаться (на самом деле, просто выходить из цикла сообщений; но за ним традиционно стоит вызов функции ExitProcess. Любители кошмариков могут, конечно, вставить сюда переход в начало программы, чтобы создать "вечное" приложение, которое никак невозможно завершить).  
 
Итак, в файле rdata.txt нужно восстановить функцию DefWindowProc, а также импортировать одну новую - PostQuitMessage. Но сначала изменим общую схему нашего приложения. Оставим больше места для последующих экспериментов; кроме того, минимизируем необходимость внесения в файлы правок в будущем и разместим структуры импорта "рыхлым" образом, с промежутками между ними (чтобы не править каждый раз смещения строк и таблиц). Размеры заголовка и секций в файлах сделаем по 400h (в памяти по-прежнему оставим 1000h - пока этого достаточно). С прицелом на будущее добавим также одну новую секцию ".rsrc" - для ресурсов (придется внести изменения в таблицу секций PE-заголовка). Т.о., макет будет следующим: заголовок (400h в файле, 1000h в памяти), секция кода ".code" по смещению 1000h в памяти и 400h в файле, затем секция данных импорта ".rdata" (2000h и 800h), секция данных ".data" (3000h и C00h), секция ресурсов (4000h и 1000h).  
 
Файл "rdata.txt" будет иметь теперь следующий вид:  
 

Код:
n rdata.bin
r cx
400
f 2000 l 400 0
a 2000
; 1-я IAT (для Kernel32.dll)
; GetModuleHandleA
db f0 21 0 0
; ExitProcess
db 4 22 0 0
db 0 0 0 0
; 2-я IAT (User32.dll)
; CreateWindowExA
db 12 22 0 0
; GetMessageA
db 24 22 0 0
; DispatchMessageA
db 32 22 0 0
; TranslateMessage
db 46 22 0 0
; DefWindowProc
db 5a 22 0 0
; RegisterClassExA
db 6c 22 0 0
; PostQuitMessage
db 80 22 0 0
db 0 0 0 0
 
a 2088
; таблица поиска для Kernel32.dll
; GetModuleHandleA
db f0 21 0 0
; ExitProcess
db 4 22 0 0
db 0 0 0 0
; таблица поиска для User32.dll
; CreateWindowExA
db 12 22 0 0
; GetMessageA
db 24 22 0 0
; DispatchMessageA
db 32 22 0 0
; TranslateMessage
db 46 22 0 0
; DefWindowProcA
db 5a 22 0 0
; RegisterClassExA
db 6c 22 0 0
; PostQuitMessage
db 80 22 0 0
db 0 0 0 0
 
a 2110
; Таблица импорта: 2 записи + завершающая (0)
; запись для Kernel32.dll
; смещение таблицы поиска
db 88 20 0 0
db 0 0 0 0 0 0 0 0
; смещение строки "Kernel32.dll"
db a0 21 0 0
; смещение IAT(1)
db 0 20 0 0
; запись для User32.dll
; смещение таблицы поиска
db 94 20 0 0
db 0 0 0 0 0 0 0 0
; смещение строки "User32.dll"
db ad 21 0 0
; смещение IAT(2)
db 0C 20 0 0
; завершение таблицы
 
a 21a0
; имена модулей
db "Kernel32.dll" 0
db "User32.dll" 0
 
a 21f0
; имена функций
db 0 0 "GetModuleHandleA" 0 0
db 0 0 "ExitProcess" 0
db 0 0 "CreateWindowExA" 0
db 0 0 "GetMessageA" 0
db 0 0 "DispatchMessageA" 0 0
db 0 0 "TranslateMessage" 0 0
db 0 0 "DefWindowProcA" 0 0
db 0 0 "RegisterClassExA" 0 0
db 0 0 "PostQuitMessage" 0
 
m 2000 l 400 100
w
q

 
Обратите внимание на сделанные изменения. Размер файла увеличен до 400h. Оставлено свободное место между IAT и таблицами поиска; между таблицами поиска и таблицей импорта; оставлен резерв для расширения самой таблицы импорта, а также место между именами модулей и таблицей имен импортируемых функций. Все соответствующие смещения в таблицах изменены.  
 
Необходимо изменить также смещение таблицы импорта в PE-заголовке. Кроме того, в файле "header.txt" нужно изменить число секций (теперь 4), размер загруженного файла в памяти (5000h) и общий размер заголовков (400h), а также добавить данные для четвертой секции в соответствующей таблице. Размер самого файла "header.txt" также увеличен до 400h. Вот новый заголовок полностью:  
 

Код:
n Header.bin
r cx
400
f 0 l 400 0
e 0 'MZ'
e 3C 40
e 40 'PE'
e 44 4C 01
a 46
; Число секций  
db 04 00
 
a 54
; Размер дополнительного заголовка
db e0 00
; Тип файла
db 0F 01
; "Магическое" значение
db 0B 01
 
a 68
; Смещение точки входа  
; относительно адреса загрузки
db 00 10 00 00
 
a 74
; Начальный адрес загрузки
db 00 00 40 00
; Выравнивание секций
db 00 10 00 00  
; Выравнивание в файле
db 00 02 00 00
; Старшая версия Windows
db 04 00
 
a 88
; Старшая версия подсистемы
db 04 00
 
a 90
; Размер загруженного файла в памяти
db 00 50 00 00
; Размер всех заголовков в файле
db 00 04 00 00  
 
a 9C
; Подсистема: 2 - графическая, 03 - консольная
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
;
; Каталог смещений/размеров
; смещение таблицы экспорта
db 0 0 0 0
; размер данных экспорта
db 0 0 0 0
; смещение таблицы импорта
db 10 21 0 0
; размер таблицы импорта
db 3c 0 0 0
; смещение таблицы ресурсов
; db 0 0 0 0
; размер таблицы ресурсов
; db 0 0 0 0
 
a 138
; Начало таблицы секций
;
; Первая секция (имя - 8 символов)
db '.code' 0 0 0
; размер в памяти
db 0 4 0 0
; смещение в памяти
db 0 10 0 0
; размер в файле
db 0 4 0 0
; смещение начала данных секции в файле
db 0 4 0 0
; Пропускаем 12 байтов
db 0 0 0 0 0 0 0 0 0 0 0 0
; атрибуты
db 20 0 0 60
;
; Вторая секция
db '.rdata' 0 0
; размер в памяти
db 0 4 0 0
; смещение в памяти
db 0 20 0 0
; размер в файле
db 0 4 0 0
; смещение в файле
db 0 8 0 0
db 0 0 0 0 0 0 0 0 0 0 0 0
; атрибуты
db 40 0 0 40
;
; Третья секция
db '.data' 0 0 0
; размер в памяти
db 0 4 0 0
; смещение в памяти
db 0 30 0 0
; размер в файле
db 0 4 0 0
; смещение в файле
db 0 c 0 0
db 0 0 0 0 0 0 0 0 0 0 0 0
; атрибуты
db 40 0 0 C0
;
; Четвертая секция
db '.rsrc' 0 0 0
; размер в памяти
db 0 4 0 0
; смещение в памяти
db 0 40 0 0
; размер в файле
db 0 4 0 0
; смещение в файле
db 0 10 0 0
db 0 0 0 0 0 0 0 0 0 0 0 0
; атрибуты
db 40 0 0 c0
 
m 0 l 400 100
w
q

 
Файл "code.txt" также претерпел изменения. В начале изменяется размер сохраняемого файла (тоже до 400h). Целесообразно "собирать" секцию в debug по "родному" смещению (1000h), чтобы при черновом проходе можно было сразу определить нужный адрес главной функции окна.  
 

Код:
n code.bin
r cx
400
f 1000 l 400 0
a 1000
; параметр GetModuleHandleA = 0
db 6a 0
; вызов GetModuleHandleA (по адресу в IAT(1) 402000h)
db ff 15 0 20 40 0
; скопировать описатель из EAX по адресу 403054h
db a3 54 30 40 0
; параметр для RegisterClassExA -  
; адрес WNDCLASSEX (403040h)
db 68 40 30 40 0
; вызов RegisterClassExA (в IAT(2) - 402020h)
db ff 15 20 20 40 0
; параметры для CreateWindowExA
; дополнительное число (0)
db 6a 0
; описатель модуля (сохранен по адресу 403054h)
db ff 35 54 30 40 0
; описатель меню (0)
db 6a 0
; описатель окна-владельца (0)
db 6a 0
; высота окна
db 68 0 1 0 0
; ширина окна
db 68 50 1 0 0
; координата y
db 68 0 1 0 0
; координата x
db 68 50 1 0 0
; стиль окна
db 68 0 0 cf 10
; адрес имени окна (в секции данных - 403010h)
db 68 10 30 40 0
; адрес имени класса (в секции данных - 403000h)
db 68 0 30 40 0
; расширенный стиль окна (0)
db 6a 0
; вызов CreateWindowExA (по адресу в IAT(2) 40200Ch)
db ff 15 c 20 40 0
; цикл
; параметры GetMessageA
db 6a 0
db 6a 0
db 6a 0
; 4-й параметр - адрес структуры MSG (в секции данных - 403020h)
db 68 20 30 40 0
; вызов GetMessageA (по адресу в IAT(2) 402010h)
db ff 15 10 20 40 0

 
После вызова GetMessageA нужно проверить возвращенное функцией значение. Если EAX не равен 0, выполняется обычная последовательность действий с вызовом TranslateMessage и DispatchMessageA и последующим возвратом на начало цикла. Если же возрвращен 0, нужно перескочить этот участок кода прямо на вызов функции ExitProcess. Для сравнения используется инструкция 100000sw с байтом Mod111R/M; напомним, что она содержит второй операнд в виде непосредственного значения в коде самой инструкции (в данном случае, это число 0). Мы сравниваем содержимое всего регистра EAX, поэтому w = 1; но непосредственное значение помещается в одном байте - потом оно будет расширено до 4 байтов, поэтому s тоже равен 1. Первый операнд находится в регистре (Mod = 11), код EAX - 000. Результирующая инструкция:  
 

Код:
10000011 11111000 00000000, или 83 F8 00 (h)  

 
Продолжим:  
 

Код:
; EAX = 0?
db 83 f8 00
; да - перескочить 18h байтов вперед
db 74 18
; параметр TranslateMessage - адрес MSG (403020h)
db 68 20 30 40 0
; вызов TranslateMessage (по адресу в IAT(2) 402018h)
db ff 15 18 20 40 0
; параметр DispatchMessageA - адрес MSG (403020h)
db 68 20 30 40 0
; вызов DispatchMessageA (по адресу в IAT(2) 402014h)
db ff 15 14 20 40 0
; возврат на "цикл" (-46 байт)
db eb d2

 
Здесь в стек помещается параметр для функции ExitProcess. Раньше мы использовали в качестве кода выхода 0; но теперь мы завершаем цикл с помощью сообщения WM_QUIT, а оно само заносит в параметр wParam структуры MSG код завершения (тот самый, который был передан функции PostQuitMessage). Нам нужно передать функции ExitProcess именно это значение. Вспомним инструкцию помещения в стек значений, находящихся в памяти; используется байт ModR/M. Мы познакомились с этой командой в статье "Простейшее приложение"; напомним ее формат:  
 

Код:
11111111 Mod 110 R/M

 
Для указания непосредственного адреса памяти используется комбинация Mod = 00 и R/M = 101, затем следуют 4 байта адреса. Структура MSG у нас располагается по адресу 403020h, а ее поле wParam - по адресу 403028h:  
 

Код:
; параметр ExitProcess (код завершения = MSG.wParam)
db ff 35 28 30 40 0
; вызов ExitProcess (по адресу в IAT(1) 402004h)
db ff 15 4 20 40 0

 
Дальше начинается главная функция окна. Сначала, как обычно, создаем фрейм стека для обращения к параметрам:  
 

Код:
;-----------------------
; Процедура окна
; создать фрейм стека:
db 55
db 89 e5

 
Затем проверяем код сообщения, переданный в качестве параметра. На этот раз мы будем пересылать системной процедуре окна по умолчанию (DefWindowProc) все сообщения, кроме одного с кодом 2. Как уже говорилось в начале, это сообщение посылается системой окну при его разрушении (т.е. когда пользователь закрывает окно). Если это так, вызываем PostQuitMessage с параметром 0 и завершаем функцию. Если нет, нужно перескочить этот участок кода:  
 

Код:
; сравнить значение в [EBP+0Ch] и 2
db 83 7d c 2
; не равны = перескочить 0Ch байтов (на "по умолчанию")
db 75 c
; параметр PostQuitMessage (0)
db 6a 0
; вызов PostQuitMessage
db ff 15 24 20 40 0
; "выход"
; удалить фрейм стека
db c9
; возврат из процедуры с очисткой стека
db c2 10 0
; "по умолчанию"

 
Дальше следует код обработки сообщений по умолчанию. Он сводится к упаковке параметров для DefWindowProc (тех самых, которые были переданы нашей функции окна) и вызову ее. В нашем случае операнд находится в памяти (в стеке), адрес соответствующего фрейма стека находится в регистре EBP; причем дополнительно необходимо использовать смещение. Размер фрейма стека небольшой, поэтому для смещения достаточно использовать 1 байт (при этом Mod будет равен 01). Код R/M для передачи адреса через EBP - 101, откуда получаем результирующую инструкцию:  
 

Код:
11111111 01110101 <1 байт смещения> (FFh 75h <...>)

 
Параметры должны передаваться в том же порядке, в котором они были переданы нашей функции. Поэтому первый параметр будет иметь наибольшее смещение относительно текущего значения EBP (14h), затем соответственно следуют 10h, 0Ch и 8:  
 

Код:
; параметры DefWindowProc
db ff 75 14
db ff 75 10
db ff 75 c
db ff 75 8
; вызов DefWindowProc
db ff 15 1c 20 40 0

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

Код:
; на "выход" - назад -24 байта
db eb e8
 
m 1000 l 400 100
w
q

 
Файл кода составлен. Остался файл данных "data.txt". Здесь, помимо размера самого файла, необходимо изменить лишь одну вещь - адрес главной функции окна в соответствующем поле структуры WNDCLASSEX:  
 

Код:
n data.bin
r cx
400
f 3000 l 400 0
a 3000
; имя класса окна
db "MYCLASS" 0
 
a 3010
; заголовок окна
db "Моё окно" 0
 
a 3020
; структура MSG (28 байт)
 
a 3040
; структура WNDCLASSEX:
; размер структуры
db 30 0 0 0
; стили класса окна
db 0 0 0 0
; (403048h) адрес главной функции окна  
db 89 10 40 0
; дополнительные байты класса
db 0 0 0 0
; дополнительные байты окна
db 0 0 0 0
; (403054h) описатель экземпляра приложения
db 0 0 0 0
; описатель ресурса значка
db 0 0 0 0
; описатель ресурса курсора
db 0 0 0 0
; фон окна
db 6 0 0 0
; адрес имени меню
db 0 0 0 0
; адрес имени класса окна
db 0 30 40 0
; описатель ресурса мелкого значка
db 0 0 0 0
 
m 3000 l 400 100
w
q

 
У нас появилась также и новая секция - но пока для нее создадим лишь пустой шаблон (файл rsrc.txt):  
 

Код:
n rsrc.bin
r cx
400
f 4000 l 400 0
 
m 4000 l 400 100
w
q

 
Осталось лишь слегка подправить файл сборки (make.bat):  
 

Код:
@echo off
debug < header.txt > report.lst
debug < code.txt >> report.lst
debug < rdata.txt >> report.lst
debug < data.txt >> report.lst
debug < rsrc.txt >> report.lst
copy /b header.bin+code.bin+rdata.bin+data.bin+rsrc.bin generic.exe

 
Имя результирующего файла "generic.exe" должно находится на одной строке с командой copy - здесь нет разрыва строки. Вот и все. Опечатки можно проверить в файле "report.lst"; если их нет, можно запускать получившийся файл. Это уже настоящее оконное приложение Win32, правда, пока с ограниченными возможностями. В дальнейшем мы будем их наращивать. Поэтому имеет смысл сохранить все исходные файлы для этой заготовки в отдельной папке - и до новых встреч!  
 
2005 © Roustem  

Всего записей: 12 | Зарегистр. 06-06-2007 | Отправлено: 11:31 17-06-2007
Retro222

Member
Редактировать | Профиль | Сообщение | Цитировать | Сообщить модератору
Ждём продолжение цикла статей.

Всего записей: 260 | Зарегистр. 21-09-2018 | Отправлено: 10:16 18-05-2020
akaGM

Platinum Member
Редактировать | Профиль | Сообщение | Цитировать | Сообщить модератору

Цитата:
Отправлено: 11:31 17-06-2007
ты лучше книжку купи, а то не дождёшься...

Всего записей: 24120 | Зарегистр. 06-12-2002 | Отправлено: 12:28 18-05-2020
Открыть новую тему     Написать ответ в эту тему

Компьютерный форум Ru.Board » Компьютеры » Прикладное программирование » Win32 в машинных кодах


Реклама на форуме Ru.Board.

Powered by Ikonboard "v2.1.7b" © 2000 Ikonboard.com
Modified by Ru.B0ard
© Ru.B0ard 2000-2024

BitCoin: 1NGG1chHtUvrtEqjeerQCKDMUi6S6CG4iC

Рейтинг.ru