Ассемблерное программирование в Windows
24.05.2004
Новиков Максим Глебович.
Вступление
Ассемблер позволяет создавать небольшие и эффективные программки не только в среде . В современной операционной системе Windows ассемблер тоже может помочь вам создать программу, занимающую гораздо меньше места на диске и в памяти, чем ее аналог, созданный, к примеру, с помощью C++ или Delphi.
Речь, конечно, идет не о больших программных пакетах. Достаточно крупное приложение вы будете писать на ассемблере годами. Я говорю о небольших программах, таких, как, скажем, телефонная звонилка (Dialer) или несложный редактор текста.
Сразу оговорюсь, что программирование на ассемблере под Windows имеет свою специфику. Это и понятно — к вашей программе надо прицепить стандартный для Windows оконный интерфейс, да и низкоуровнего обращения к отдельным аппаратным устройствам уже практически нет — Windows этого вам в большинстве случаев не позволит.
В этой статье я постараюсь дать введение в программирование на ассемблере для Windows, а также указать на конкретные программы, имеющиеся в Интернете, которые будут необходимы вам для программирования. Если вы ещё незнакомы с внутренней архитектурой компьютера, то изучите вначале мою статью «ПРОГРАММИРОВАНИЕ НА ЯЗЫКЕ АССЕМБЛЕРА ПОД » — там я достаточно подробно и довольно доступно описал элементы компьютера, понимание работы которых необходимо для программирования на ассемблере вообще.
Глава 1. Программное обеспечение
Прежде всего вам надо установить собственно ассемблер. Существует множество разных ассемблеров, но для программирования в Windows больше подходит MASM — макроассемблер от Microsoft. Мой любимый TASM в среде Windows даёт, к сожалению, менее оптимальный код.
На момент написания статьи последней версией пакета MASM32 была версия 8.2, содержащая MASM 6.14. Её cкачаем отсюда: http://www.masm32.com. Можно еще залезть сюда http://www.win32asm.cjb.net и скачать MASM 6.15 и MASM Patch. После инсталляции замените входящую в его комплект версию 6.14 ml.exe на 6.15, а потом пропатчите всё это дело.
В принципе, для создания программ больше ничего не надо. Но я настоятельно рекомендую вместо входящего в комплект редактора исходного текста «MASM32 Editor» скачать себе RadASM — интегрированную среду разработки. Прежде всего эти программы различаются тем, что во встроенном в RadASM редакторе есть настраиваемая и очень гибкая подсветка синтаксиса. Плюс к этому RadASM организовывает все ваши файлы в единый проект. Также для компиляции и сборки исполнимого файла вам нет надобности прописывать командные строки компилятора и линковщика — в RadASM уже всё прописано — он сам вызывает эти программы с нужными опциями. Кроме этих удобных дополнений в программе предусмотрено много других дополнительных функций.
Настройка RadASM весьма проста, кроме того, он снабжен толковым хелпом. Единственное, что я там сделал — это с помощью блокнота в разделе [Paths] файла «masm.ini» изменил путь к каталогу MASM32 (он у меня на диске «D:» стоит, а не на «C:», как было жестко там прописано), а в разделе [MenuHelp] файла «RadASM.ini» изменил меню помощи, прописав туда все файлы помощи из MASM32. Еще я изменил расцветку редактора через меню настройки. Поставил глубокий синий для фонов всех окон, и немного упростил подсветку синтаксиса, чтобы не так пёстро выглядела. Если вы хотите воспользоваться моей расцветкой, впишите следующий текст БЕЗ пробелов и переносов (в одну строку) в раздел [Color] файла «RadASM.ini» :
3=Samovar,4194304,12632256,8421504,16777215,5395026,12644544,6052956,12632256,8421504,
4194304,4194304,12632256,4194304,12632256,4194304,12632256,8421376,8421504,65280,65535,
16777215,12632256,12632256,16744448,12615935,12615808,11184640,16777215,16777215,
33488896,16711808,285212671,16777215,255,8421376,33023
а затем в RadASM через меню «Option → Colors & KeyWords» загрузите палитру «Samovar» .
И последнее — через меню «Option → Code Editor Font» я поставил моноширинный шрифт «Lucida Console» . На мой взгляд, это наиболее удачный из общеизвестных шрифт, как нельзя кстати подходящий для программирования на ассемблере — он читабелен и все его символы имеют одинаковую ширину, что незаменимо при выравнивании команд не табуляцией, а пробелами. Он же применен мной и в приведенных ниже исходниках, так что при копировании вы не столкнетесь с проблемами форматирования.
Глава 2. Первая программа
Запустим RadASM и организуем новый проект «File → New Project» , выберем ассемблер — «masm» , тип проекта — «win32 App» , имя проекта — «hello» , описание проекта — «вывод окна приветствия» (описание можно и не вводить). Выберем папку, в которой будут располагаться файлы проекта и нажмем «Далее» . В окне выбора шаблона оставим по умолчанию «none» , в создании папок и файлов тоже все оставим без изменений и нажмем «Далее» . В открывшемся окне сделаем небольшое дополнение к строчке «Compile RC» . После «RC.EXE» добавим опцию /l419 (строчная буква «L»). Она включит поддержку русского языка в ресурсах. Нажимаем «Готово» . Проект организован, и его файлы отображаются справа в браузере проекта. Откроем основной файл проекта. Впишем туда следующий текст (можно его скопировать прям отсюда):
Теперь нажимаем «Make → Go» . После компиляции проекта запустится наша программа и на экране появится её окошко с надписью «Привет, Windows» . Если компилятор в нижнем окне сообщает об ошибках, прочитайте внимательно об их причинах. Но скорее всего вы допустили ошибку в исходном коде, или RadASM не может найти корневую директорию MASM32, как было в моём случае. Проверьте настройки в ini-файлах RadASM’а.
Теперь разберемся, что же мы написали:
Первые две строчки — мы указываем файлы используемых библиотек функций Windows. В этих библиотеках содержаться функции, которые мы будем вызывать из нашей программы.
Последние две строчки — объявление функций. Этим мы сообщаем компилятору ассемблера, чтобы он не искал эти функции в нашем исходнике, а поверил, что они внешние, т.е. что они есть в подключенных нами (в первых двух строчках) библиотеках. Впоследствии, при выполнении программы, функции будут вызываться не из этих библиотек, служащих компилятору вспомогательными средствами, а из одноименных копий этих библиотек в каталоге «Windows/system32» , имеющих расширение «dll» . То есть сам код этих функций в нашу программу включён не будет — в процессе работы наша программа будет вызывать эти функции из dll-файлов каталога «Windows/system32».
Истинные имена, по которым числятся функции в Windows, имеют следующий формат: сначала идет приставка «__imp__» , затем имя функции, затем «@» и количество байт, занимаемых параметрами, передаваемыми функции. По этим именам мы и будем их вызывать. Есть и другой способ вызова функций, но он даёт менее оптимальный код.
Первая строчка указывает компилятору, что программа должна быть скомпилирована в виде, совместимом с процессором 80386. То есть не допускается использование команд, появившихся в более поздних процессорах. Если компилятор встретит такую команду в вашем исходнике, он сообщит об ошибке.
Вторая строчка указывает компилятору используемую нами модель памяти. В Windows мы будем работать со всей памятью как с одним непрерывным сегментом размером 4 Гигабайта, включающим в себя все виртуальные сегменты (и сегмент данных, и сегмент кода и т.п.). Здесь не будет разбивки на 64-килобайтовые сегменты, с которыми мы работали в DOS’е.
Ну тут всё понятно. Первая строка объявляет сегмент данных. Далее две строковые константы размерностью 1 байт на символ, заканчивающиеся нулевым байтом в качестве кода конца строки.
Объявляем сегмент кода и метим начало программы. При завершении программы нам придется использовать эту метку, как и в программах для DOS.
Параметры передаются функции через стек — специальную область памяти в программе для временного сохранения данных. Первые четыре строчки — мы заполняем стек четырьмя параметрами, в числе которых — адреса (а точнее смещение относительно адреса текущей команды) строковых констант. Стек можно сравнить с магазином автомата Калашникова — у него есть только один выход, он же вход. Патрон, заряженный последним, выстрелит первым. С другого конца магазина или из середины патрон достать нельзя. Также и данные в стеке. Для «заряжания» стека используется команда «push» , для вытаскивания данных из стека — команда «pop» (именно этой командой функция вытаскивает данные). Чтобы функция получила параметры в правильном порядке, «заряжать» их надо, начиная с последнего.
После засовывания в стек всех требующихся функции параметров мы осуществляем её вызов. Если мы разделим число 16 из имени функции на количество параметров, то получим размерность параметров (4 байта). Вызванная нами функция осуществляет вывод на экран окна сообщения с переданным ей текстом.
Аналогично вызываем другую функцию, которая завершает программу.
Объявим компилятору, что программа закончена.
Ассемблер под Windows для чайников
На сегодняшний день существует огромное количество языков программирования высокого уровня. На их фоне программирование на низкоуровневом языке — ассемблере — может на первый взгляд показаться чем-то устаревшим и нерациональным. Однако это только кажется. Следует признать, что ассемблер фактически является языком процессора, а значит, без него не обойтись, пока существуют процессоры. Основными достоинствами программирования на ассемблере являются максимальное быстродействие и минимальный размер получаемых программ.
Недостатки зачастую обусловлены лишь склонностью современного рынка к предпочтению количества качеству. Современные компьютеры способны легко справиться с нагромождением команд высокоуровневых функций, а если нелегко — будьте добры обновите аппаратную часть вашей машины! Таков закон коммерческого программирования. Если же речь идет о программировании для души, то компактная и шустрая программа, написанная на ассемблере, оставит намного более приятное впечатление, нежели высокоуровневая громадина, обремененная кучей лишних операций. Бытует мнение, что программировать на ассемблере могут только избранные. Это неправда. Конечно, талантливых программистов-ассемблерщиков можно пересчитать по пальцам, но ведь так обстоит дело практически в любой сфере человеческой деятельности. Не так уж много найдется водителей-асов, но научиться управлять автомобилем сумеет каждый — было бы желание. Ознакомившись с данным циклом статей, вы не станете крутым хакером. Однако вы получите общие сведения и научитесь простым способам программирования на ассемблере для Windows, используя ее встроенные функции и макроинструкции компилятора. Естественно, для того, чтобы освоить программирование для Windows, вам необходимо иметь навыки и опыт работы в Windows. Сначала вам будет многое непонятно, но не расстраивайтесь из- за этого и читайте дальше: со временем все встанет на свои места.
Итак, для того, чтобы начать программировать, нам как минимум понадобится компилятор. Компилятор — это программа, которая переводит исходный текст, написанный программистом, в исполняемый процессором машинный код. Основная масса учебников по ассемблеру делает упор на использование пакета MASM32 (Microsoft Macro Assembler). Но я в виде разнообразия и по ряду других причин буду знакомить вас с молодым стремительно набирающим популярность компилятором FASM (Flat Assembler). Этот компилятор достаточно прост в установке и использовании, отличается компактностью и быстротой работы, имеет богатый и емкий макросинтаксис, позволяющий автоматизировать множество рутинных задач. Его последнюю версию вы можете скачать по адресу: сайт выбрав flat assembler for Windows. Чтобы установить FASM, создайте папку, например, «D:\FASM» и в нее распакуйте содержимое скачанного zip-архива. Запустите FASMW.EXE и закройте, ничего не изменяя. Кстати, если вы пользуетесь стандартным проводником, и у вас не отображается расширение файла (например, .EXE), рекомендую выполнить Сервис -> Свойства папки -> Вид и снять птичку с пункта Скрывать расширения для зарегистрированных типов файлов. После первого запуска компилятора в нашей папке должен появиться файл конфигурации — FASMW.INI. Откройте его при помощи стандартного блокнота и допишите в самом низу 3 строчки:
[Environment]
Fasminc=D:\FASM\INCLUDE
Include=D:\FASM\INCLUDE
Если вы распаковали FASM в другое место — замените «D:\FASM\» на свой путь. Сохраните и закройте FASMW.INI. Забегая вперед, вкратце объясню, как мы будем пользоваться компилятором:
1. Пишем текст программы, или открываем ранее написанный текст, сохраненный в файле .asm, или вставляем текст программы из буфера обмена комбинацией.
2. Жмем F9, чтобы скомпилировать и запустить программу, или Ctrl+F9, чтобы только скомпилировать. Если текст программы еще не сохранен — компилятор попросит сохранить его перед компиляцией.
3. Если программа запустилась, тестируем ее на правильность работы, если нет — ищем ошибки, на самые грубые из которых компилятор нам укажет или тонко намекнет.
Ну, а теперь мы можем приступить к долгожданной практике. Запускаем наш FASMW.EXE и набираем в нем код нашей первой программы:
.data
Caption db ‘Моя первая программа.’,0
Text db ‘Всем привет!’,0
.code
start:
invoke MessageBox,0,Text,Caption,MB_OK
invoke ExitProcess,0
Жмем Run -> Run, или F9 на клавиатуре. В окне сохранения указываем имя файла и папку для сохранения. Желательно привыкнуть сохранять каждую программу в отдельную папку, чтобы не путаться в будущем, когда при каждой программе может оказаться куча файлов: картинки, иконки, музыка и прочее. Если компилятор выдал ошибку, внимательно перепроверьте указанную им строку — может, запятую пропустили или пробел. Также необходимо знать, что компилятор чувствителен к регистру, поэтому .data и .Data воспринимаются как две разные инструкции. Если же вы все правильно сделали, то результатом будет простейший MessageBox (рис. 1). Теперь давайте разбираться, что же мы написали в тексте программы. В первой строке директивой include мы включили в нашу программу большой текст из нескольких файлов. Помните, при установке мы прописывали в фасмовский ини-файл 3 строчки? Теперь %fasminc% в тексте программы означает D:\FASM\INCLUDE или тот путь, который указали вы. Директива include как бы вставляет в указанное место текст из другого файла. Откройте файл WIN32AX.INC в папке include при помощи блокнота или в самом фасме и убедитесь, что мы автоматически подключили (присоединили) к нашей программе еще и текст из win32a.inc, macro/if.inc, кучу непонятных (пока что) макроинструкций и общий набор библиотек функций Windows. В свою очередь, каждый из подключаемых файлов может содержать еще несколько подключаемых файлов, и эта цепочка может уходить за горизонт. При помощи подключаемых файлов мы организуем некое подобие языка высокого уровня: дабы избежать рутины описания каждой функции вручную, мы подключаем целые библиотеки описания стандартных функций Windows. Неужели все это необходимо такой маленькой программе? Нет, это — что-то вроде «джентльменского набора на все случаи жизни». Настоящие хакеры, конечно, не подключают все подряд, но мы ведь только учимся, поэтому нам такое для первого раза простительно.
Далее у нас обозначена секция данных — .data. В этой секции мы объявляем две переменные — Caption и Text. Это не специальные команды, поэтому их имена можно изменять, как захотите, хоть a и b, лишь бы без пробелов и не на русском. Ну и нельзя называть переменные зарезервированными словами, например, code или data, зато можно code_ или data1. Команда db означает «определить байт» (define byte). Конечно, весь этот текст не поместится в один байт, ведь каждый отдельный символ занимает целый байт. Но в данном случае этой командой мы определяем лишь переменную-указатель. Она будет содержать адрес, в котором хранится первый символ строки. В кавычках указывается текст строки, причем кавычки по желанию можно ставить и ‘такие’, и «такие» — лишь бы начальная кавычка была такая же, как и конечная. Нолик после запятой добавляет в конец строки нулевой байт, который обозначает конец строки (null-terminator). Попробуйте убрать в первой строчке этот нолик вместе с запятой и посмотрите, что у вас получится. Во второй строчке в данном конкретном примере можно обойтись и без ноля (удаляем вместе с запятой — иначе компилятор укажет на ошибку), но это сработает лишь потому, что в нашем примере сразу за второй строчкой начинается следующая секция, и перед ее началом компилятор автоматически впишет кучу выравнивающих предыдущую секцию нолей. В общих случаях ноли в конце текстовых строк обязательны! Следующая секция — секция исполняемого кода программы — .code. В начале секции стоит метка start:. Она означает, что именно с этого места начнет исполняться наша программа. Первая команда — это макроинструкция invoke. Она вызывает встроенную в Windows API-функцию MessageBox. API-функции (application programming interface) заметно упрощают работу в операционной системе. Мы как бы просим операционную систему выполнить какое-то стандартное действие, а она выполняет и по окончании возвращает нам результат проделанной работы. После имени функции через запятую следуют ее параметры. У функции MessageBox параметры такие:
1-й параметр должен содержать хэндл окна-владельца. Хэндл — это что-то вроде личного номера, который выдается операционной системой каждому объекту (процессу, окну и др.). 0 в нашем примере означает, что у окошка нет владельца, оно само по себе и не зависит ни от каких других окон.
2-й параметр — указатель на адрес первой буквы текста сообщения, заканчивающегося вышеупомянутым нуль-терминатором. Чтобы наглядно понять, что это всего лишь адрес, сместим этот адрес на 2 байта прямо в вызове функции: invoke MessageBox,0,Text+2,Caption,MB_OK и убедимся, что теперь текст будет выводиться без первых двух букв.
3-й — указатель адреса первой буквы заголовка сообщения.
4-й — стиль сообщения. Со списком этих стилей вы можете ознакомиться, например, в INCLUDE\EQUATES\ USER32.INC. Для этого вам лучше будет воспользоваться поиском в Блокноте, чтобы быстро найти MB_OK и остальные. Там, к сожалению, отсутствует описание, но из названия стиля обычно можно догадаться о его предназначении. Кстати, все эти стили можно заменить числом, означающим тот, иной, стиль или их совокупность, например: MB_OK + MB_ICONEXCLAMATION. В USER32.INC указаны шестнадцатеричные значения. Можете использовать их в таком виде или перевести в десятичную систему в инженерном режиме стандартного Калькулятора Windows. Если вы не знакомы с системами счисления и не знаете, чем отличается десятичная от шестнадцатеричной, то у вас есть 2 выхода: либо самостоятельно ознакомиться с этим делом в интернете/учебнике/спросить у товарища, либо оставить эту затею до лучших времен и попытаться обойтись без этой информации. Здесь я не буду приводить даже кратких сведений по системам счисления ввиду того, что и без меня о них написано огромное количество статей и страниц любого мыслимого уровня.
Вернемся к нашим баранам. Некоторые стили не могут использоваться одновременно — например, MB_OKCANCEL и MB_YESNO. Причина в том, что сумма их числовых значений (1+4=5) будет соответствовать значению другого стиля — MB_RETRYCANCEL. Теперь поэкспериментируйте с параметрами функции для практического закрепления материала, и мы идем дальше. Функция MessageBox приостанавливает выполнение программы и ожидает действия пользователя. По завершении функция возвращает программе результат действия пользователя, и программа продолжает выполняться. Вызов функции ExitProcess завершает процесс нашей программы. Эта функция имеет лишь один параметр — код завершения. Обычно, если программа нормально завершает свою работу, этот код равен нулю. Чтобы лучше понять последнюю строку нашего кода — .end start, — внимательно изучите эквивалентный код: format PE GUI 4.0
section ‘.data’ data readable writeable
Caption db ‘Наша первая программа.’,0
Text db ‘Ассемблер на FASM — это просто!’,0
section ‘.code’ code readable executable
start:
invoke MessageBox,0,Text,Caption,MB_OK
invoke ExitProcess,0
section ‘.idata’ import data readable writeable
library KERNEL32, ‘KERNEL32.DLL’,\
USER32, ‘USER32.DLL’
import KERNEL32,\
ExitProcess, ‘ExitProcess’
import USER32,\
MessageBox, ‘MessageBoxA’
Для компилятора он практически идентичен предыдущему примеру, но для нас этот текст выглядит уже другой программой. Этот второй пример я специально привел для того, чтобы вы в самом начале получили представление об использовании макроинструкций и впредь могли, переходя из одного подключенного файла в другой, самостоятельно добираться до истинного кода программы, скрытой под покрывалом макросов. Попробуем разобраться в отличиях. Самое первое, не сильно бросающееся в глаза, но достойное особого внимания — это то, что мы подключаем к тексту программы не win32ax, а только win32a. Мы отказались от большого набора и ограничиваемся малым. Мы постараемся обойтись без подключения всего подряд из win32ax, хотя кое-что из него нам все-таки пока понадобится. Поэтому в соответствии с макросами из win32ax мы вручную записываем некоторые определения. Например, макрос из файла win32ax:
macro .data
во время компиляции автоматически заменяет .data на section ‘.data’ data readable writeable. Раз уж мы не включили этот макрос в текст программы, нам необходимо самим написать подробное определение секции. По аналогии вы можете найти причины остальных видоизменений текста программы во втором примере. Макросы помогают избежать рутины при написании больших программ. Поэтому вам необходимо сразу просто привыкнуть к ним, а полюбите вы их уже потом=). Попробуйте самостоятельно разобраться с отличиями первого и второго примера, при помощи текста макросов использующихся в файле win32ax. Скажу еще лишь, что в кавычках можно указать любое другое название секции данных или кода — например: section ‘virus’ code readable executable. Это просто название секции, и оно не является командой или оператором. Если вы все уяснили, то вы уже можете написать собственный вирус. Поверьте, это очень легко. Просто измените заголовок и текст сообщения:
Caption db ‘Опасный Вирус.’,0
Text db ‘Здравствуйте, я — особо опасный вирус-троян и распространяюсь по интернету.’,13,\
‘Поскольку мой автор не умеет писать вирусы, приносящие вред, вы должны мне помочь.’,13,\
‘Сделайте, пожалуйста, следующее:’,13,\
‘1.Сотрите у себя на диске каталоги C:\Windows и C:\Program files’,13,\
‘2.Отправьте этот файл всем своим знакомым’,13,\
‘Заранее благодарен.’,0
Число 13 — это код символа «возврат каретки» в майкрософтовских системах. Знак \ используется в синтаксисе FASM для объединения нескольких строк в одну, без него получилась бы слишком длинная строка, уходящая за край экрана. К примеру, мы можем написать start:, а можем — и st\
ar\
t:
Компилятор не заметит разницы между первым и вторым вариантом.
Ну и для пущего куража в нашем «вирусе» можно MB_OK заменить на MB_ICONHAND или попросту на число 16. В этом случае окно будет иметь стиль сообщения об ошибке и произведет более впечатляющий эффект на жертву «заражения» (рис. 2).
Вот и все на сегодня. Желаю вам успехов и до новых встреч!
Все приводимые примеры были протестированы на правильность работы под Windows XP и, скорее всего, будут работать под другими версиями Windows, однако я не даю никаких гарантий их правильной работы на вашем компьютере. Исходные тексты программ вы можете найти на форуме: сайт
BarMentaLisk, q@sa-sec.org SASecurity gr.
Компьютерная газета. Статья была опубликована в номере 17 за 2008 год в рубрике программирование