Ассемблер под windows для чайников часть 1
В данной главе я намерен дать некоторую вводную информацию по средствам программирования на языке ассемблера. Данная глава предназначена для начинающих программирование на ассемблере, поэтому программистам более опытным ее можно пропустить без особого ущерба для себя.
Прежде всего замечу, что в названии главы есть некоторая натяжка, т.к. технологии трансляции и в MS DOS, и в Windows весьма схожи. Однако программирование в MS DOS уходит в прошлое.
Рис.1.1.1. Схема трансляции ассемблерного модуля.
Двум стадиям трансляции (Рис.1.1.1) соответствуют две основные программы: ассемблер ML.EXE и редактор связей LINK.EXE 7 (или TASM32.EXE и TLINK32.EXE в Турбо Ассемблере).
Пусть файл с текстом программы на языке ассемблера называется PROG.ASM, тогда, не вдаваясь в подробный анализ, две стадии трансляции будут выглядеть следующим образом: c:\masm32\bin\ml /c /coff PROG.ASM — в результате появляется модуль PROG.OBJ, а также c:\masm32\bin\link /SUBSYSTEM:WINDOWS PROG.OBJ — в результате появляется исполняемый модуль PROG.EXE. Как Вы, я надеюсь, догадались /с и /coff являются параметрами программы ML.EXE, a /SUBSYSTEM:WINDOWS является параметром для программы LINK.EXE. О других ключах этих программ более подробно см. Гл. 1.5.
Чем больше я размышляю об этой схеме трансляции, тем более совершенной она мне кажется. Действительно, формат конечного модуля зависит от операционной системы. Установив стандарт на структуру объектного модуля, мы получаем возможность: а) использовать уже готовые объектные модули, б) стыковать между собой программы, написанные на разных языках. Но самое прекрасное здесь то, что если стандарт объектного модуля распространить на разные операционные системы, то можно использовать модули, написанные в разных операционных системах 8 .
Чтобы процесс трансляции сделать для Вас привычным, рассмотрим несколько простых, «ничего не делающих» программ.
На Рис. 1.1.2 представлена «ничего не делающая» программа. Назовем ее PROG1. Сразу отмечу на будущее: команды микропроцессора и директивы макроассемблера будем писать заглавными буквами.
Итак, чтобы получить загружаемый модуль, выполним следующие команды:
или для Турбо Ассемблера
Примем пока параметры трансляции программ как некую данность и продолжим наши изыскания.
Часто удобно разбить текст программы на несколько частей и объединять эти части еще на 1-й стадии трансляции. Это достигается посредством директивы INCLUDE. Например, один файл будет содержать код программы, а константы, данные (определение переменных) и прототипы внешних процедур помещаются в отдельные файлы. Часто такие файлы записывают с расширением .INC.
Именно такая разбивка демонстрируется в следующей программе (Рис.1.1.3).
Программа на Рис. 1.1.3 также достаточно бессмысленна (как и все программы данной главы), но зато демонстрирует удобства использования директивы INCLUDE. Напомню, что мы не останавливаемся в книге на очевидных командах микропроцессора. Замечу только по поводу команды IDIV. В данном случае команда IDIV осуществляет операцию деления над операндом, находящемся в паре регистров EDX:EAX. Обнуляя EDX, мы указываем, что операнд целиком находится в регистре EAX.
Трансляция программы осуществляется так, как это было указано ранее для ассемблеров MASM и TASM.
Замечание о типах данных. В данной книге Вы встретитесь в основном с тремя типами данных (простых): байт, слово, двойное слово. При этом используются следующие стандартные обозначения. Байт — BYTE или DB, слово — WORD или DW, двойное слово — DWORD или DD. Выбор, скажем, в одном случае DB, а в другом BYTE, продиктован лишь желанием автора несколько разнообразить изложение.
7 Программу LINK.EXE называют также компоновщиком или просто линковщиком.
8 Правда, весьма ограниченно, т.к. согласование системных вызовов в разных операционных системах может весьма сильно различаться.
Перейдем теперь к вопросу о подсоединении других объектных модулей и библиотек во второй стадии трансляции. Прежде всего замечу, что, сколько бы ни подсоединялось объектных модулей, один объектный модуль является главным. Смысл этого весьма прост: именно с этого модуля начинается исполнение программы. На этом различие между модулями заканчивается. Условимся далее, что главный модуль всегда в начале сегмента кода будет содержать метку START, ее мы указываем после директивы END — транслятор должен знать точку входа программы, чтобы указать ее в заголовке загружаемого модуля (см. Гл.5.1).
Обычно во второстепенные модули помещаются процедуры, которые будут вызываться из основного и других модулей. Рассмотрим такой модуль. Этот модуль Вы можете видеть на Рис. 1.4.
Прежде всего, обращаю Ваше внимание на то, что после директивы END не указана никакая метка. Ясно, что это не главный модуль, процедуры его будут вызываться из других модулей.
Второе, на что я хотел бы обратить Ваше внимание, это то, что процедура, которая будет вызываться, должна быть объявлена как PUBLIC. В этом случае это имя будет сохранено в объектном модуле и далее может быть связано с вызовами из других модулей.
Итак, выполняем команду ML /coff /c PROG1.ASM. В результате на диске появляется объектный модуль PROG2.OBJ.
А теперь проведем маленькое исследование. Просмотрим объектный модуль с помощью какого-нибудь простого Viewer’a, например того, что есть у программы Far. И что же мы обнаружим: вместо имени PROC1 мы увидим имя _PROC1@0. Это особый разговор — будьте сейчас внимательны! Во-первых, подчеркивание спереди отражает стандарт ANSI, предписывающий всем внешним именам (доступным нескольким модулям) автоматически добавлять символ подчеркивания. Здесь ассемблер будет действовать автоматически, и у нас по этому поводу не будет никаких забот.
Сложнее с припиской @0. Что она значит? На самом деле все просто: цифра после знака @ означает количество байт, которые необходимо передать в стек в виде параметров при вызове процедуры. В данном случае ассемблер понял так, что наша процедура параметров не требует. Сделано это для удобства использования директивы INVOKE. Но о ней речь пойдет ниже, а пока попытаемся сконструировать основной модуль PROG1.ASM.
Как Вы понимаете, процедура, вызываемая из другого модуля, объявляется как EXTERN. Далее, вместо имени PROC1 нам приходится использовать имя PROC1@0. Здесь пока ничего нельзя сделать. Может возникнуть вопрос о типе NEAR. Дело в том, что в операционной системе MS DOS тип NEAR означал, что вызов процедуры (или безусловный переход) будет происходить в пределах одного сегмента. Тип FAR означал, что процедура (или переход) будет вызываться из другого сегмента. В операционной системе Windows реализована так называемая плоская модель, когда всю память можно рассматривать как один большой сегмент. И здесь логично использовать тип NEAR.
Выполним команду ML /coff /c PROG1.ASM, в результате получим объектный модуль PROG1.OBJ. Теперь можно объединить модули и получить загружаемую программу PROG1.EXE:
При объединении нескольких модулей первым должен идти главный, а остальные — в произвольном порядке.
Обратимся теперь к директиве INVOKE. Это довольно удобная команда, правда, по некоторым причинам (которые станут понятными позже) я почти не буду употреблять ее в своих программах.
Удобство ее заключается, во-первых, в том, что мы сможем забыть о добавке @N. Во-вторых, эта команда сама заботится о помещении передаваемых параметров в стек. Последовательность команд
Причем параметрами могут являться регистр, непосредственно значение или адрес. Кроме того, для адреса может использоваться как оператор OFFSET, так и оператор ADDR. Видоизменим теперь модуль PROG1.ASM (модуль PROG2.ASM изменять не придется).
Как видите, внешняя процедура объявляется теперь при помощи директивы PROTO. Данная директива позволяет при необходимости указывать и наличие параметров. Например, строка
будет означать, что процедура требует два параметра длиной в четыре и два байта (всего 6, т.е. @6).
Как уже говорилось, я буду редко использовать оператор INVOKE. Теперь я назову первую причину такого пренебрежения к данной возможности. Дело в том, что я сторонник чистоты языка ассемблера и любое использование макросредств вызывает у меня чувство несовершенства. На мой взгляд, и начинающим программистам не стоит увлекаться макросредствами, иначе не чувствуется вся красота этого языка. О второй причине Вы узнаете ниже.
На нашей схеме, на Рис. 1.1, говорится не только о возможности подсоединения объектных модулей, но и библиотек. Собственно, если объектных модулей несколько, то это по понятным причинам вызовет неудобства. Поэтому объектные модули объединяются в библиотеки. Для подсоединения библиотеки в MASM удобнее всего использовать директиву INCLUDELIB, которая сохраняется в объектном коде и используется программой LINK.EXE.
Но как создать библиотеку из объектных модулей? Для этого имеется специальная программа, называемая библиотекарем. Предположим, мы хотим создать библиотеку LIB1.LIB, состоящую из одного модуля — PROG2.OBJ. Выполним для этого следующую команду: LIB /OUT:LIB1.LIB PROG2.OBJ.
Если необходимо добавить в библиотеку еще один модуль (MODUL.OBJ), то достаточно выполнить команду: LIB LIB1.LIB MODUL.OBJ.
Вот еще два полезных примера использования библиотекаря:
LIB /LIST LIB1.LIB — выдает список модулей библиотеки.
LIB /REMOVE:MODUL.OBJ LIB1.LIB — удаляет из библиотеки модуль MODUL.OBJ.
Вернемся теперь к нашему примеру. Вместо объектного модуля мы используем теперь библиотеку LIB1.LIB. Видоизмененный текст программы PROG1.ASM представлен на Рис. 1.7.
Рассмотрим теперь менее важный (для нас) вопрос об использовании данных (переменных), определенных в другом объектном модуле. Здесь читателю, просмотревшему предыдущий материал, должно быть все понятно, а модули PROG2.ASM и PROG1.ASM, демонстрирующие технику использования внешних 9 переменных, приводятся на Рис.1.8-1.9.
Заметим, что в отличие от внешних процедур, внешняя переменная не требует добавки @N, поскольку размер переменной известен.
А теперь проверим все представленные в данной главе программы на предмет их трансляции средствами пакета TASM.
С программами на Рис. 1.2-1.3 дело обстоит просто. Для их трансляции достаточно выполнить команды:
Обратимся теперь к модулям PROG2.ASM и PROG1.ASM, приведенным на Рис. 1.4 и 1.5 соответственно.
Получение объектных модулей происходит без каких-либо трудностей. Просматривая модуль PROG2.OBJ, мы увидим, что внешняя процедура представлена просто именем PROC1.
Следовательно, единственное, что нам следует сделать, это заменить в модуле PROC1.ASM имя PROC1@0 на PROC1.
Объединение модулей далее производится элементарно:
Для работы с библиотекой в пакете TASM имеется программа — библиотекарь TLIB.ЕХЕ. Создание библиотеки, состоящей из модуля PROG2.OBJ, производится по команде
В результате на диске появится библиотека LIB1.LIB. Далее компонуем модуль PROG1.OBJ с этой библиотекой:
В результате получается загружаемый модуль PROG1.EXE.
Вообще, стоит разобраться с командной строкой TLINK32 более подробно. В расширенном виде 11 эта строка выглядит следующим образом:
OBJFILES — один или несколько объектных файлов (через пробел). Первый главный модуль.
EXEFILE — исполняемый модуль.
MAPFILE — МАР-файл, содержащий информацию о структуре модуля.
LIBFILES — одна или несколько библиотек (через пробел).
В TASM отсутствует директива INVOKE, поэтому в дальнейшем я буду избегать ее использования 12 .
В начале книги я объявил о своем намерении примирить два ассемблера. Поскольку различие между ними заключается в директивах и макрокомандах (см. Гл.1.5), то напрашивается вывод, что совместимости можно добиться, избегая таких директив и макрокоманд. Основой программы для Windows является вызов API-функций (см. Гл. 1.2), а мы знаем, что различие в вызове внешней процедуры заключается в том, что в именах для MASM в конце есть добавка @N. И здесь не обойтись без макроопределений, и начинается самое интересное. Но об этом, дорогой читатель, Вы узнаете в свое время.
И MASM и TASM поддерживают так называемую упрощенную сегментацию. Я являюсь приверженцем классической структуры ассемблерной программы и должен признаться, что упрощенная сегментация довольно удобная штука, особенно при программировании под Windows. Суть такой сегментации в следующем: начало сегмента определяется директивой .CODE, а сегмента данных — .DATA 13 . Причем обе директивы могут появляться в тексте программы несколько раз. Транслятор затем собирает код и данные вместе, как положено. Основной целью такого подхода, по-видимому, является возможность приблизить в тексте программы данные к тем строкам, где они используются. Такая возможность, как известно, в свое время была реализована в C++. На мой взгляд, она приводит к определенному неудобству при чтении текста программы. Кроме того, не сочтите меня за эстета, но когда я вижу данные, перемешанные в тексте программы с кодом, у меня возникает чувство дискомфорта.
Ниже представлена программа, демонстрирующая упрощенный режим сегментации.
В заключительной части главы я хотел дать краткий обзор ряда других программ, которые часто используются при программировании на ассемблере. С некоторыми из перечисленных программ мы познакомимся в дальнейшем более подробно, а некоторые более упоминаться не будут.
Редакторы. Хотя сам я никогда не использую специализированные редакторы для написания программ на ассемблере, полноты ради кратко расскажу о двух известных мне редакторах. Начну с редактора QEDITOR.EXE, который поставляется вместе с пакетом MASM32. Сам редактор и все сопутствующие ему утилиты написаны на ассемблере. Анализ их размера и возможностей действительно впечатляет. Например, сам редактор имеет длину всего 27 Кб, а утилита, используемая для просмотра отчетов о трансляции — всего 6 Кб. Редактор вполне годится для работы с небольшими одномодульными приложениями. Для работы с несколькими модулями он не очень удобен. Работа редактора основана на взаимодействии с различными утилитами посредством пакетных файлов. Например, трансляцию программ осуществляет пакетный файл ASSMBL.BAT, который использует ассемблер ML.EXE, а результат ассемблирования направляется в текстовый файл ASMBL.TXT. Далее для просмотра этого файла используется простая утилита THEGUN.EXE. Аналогично осуществляется редактирование связей. Для дизассемблирования исполняемого модуля используется утилита DUMPPE.EXE, результат работы этой утилиты помещается в текстовый файл DISASM.TXT. Аналогично осуществляются и другие операции. Вы легко сможете настроить эти операции, отредактировав соответствующий пакетный файл с модификацией (при необходимости) используемых утилит (заменив, например, ML.EXE на TASM32.EXE и т.п.).
Вторая программа, с которой я хочу познакомить читателя, это EAS.EXE (Easy Assembler Shell). Редактор, а точнее оболочка, позволяет создавать и транслировать довольно сложные проекты, состоящие из ASM-,OВJ-,RC-,RES-,DEF-файлов. Программа позволяет работать как с TASM, так и MASM, а также с другими утилитами (отладчиками, редакторами ресурсов и т.д.). Непосредственно в программе можно настроить компиляторы и редакторы связей на определенный режим работы путем задания ключей этих утилит.
Отладчики позволяют исполнять программу в пошаговом режиме. В IV части книги мы более подробно будем рассматривать отладчики и дизассемблеры. Приведу несколько наиболее известных отладчиков 14 : CodeView (Микрософт), Turbo Debugger (Borland), Ice.
Дизассемблеры переводят исполняемый модуль в ассемблерный код. Примером простейшего дизассемблера является программа DUMPPE.EXE, работающая в строковом режиме. Пример работы программы DUMPPE.EXE представлен на Рис. 1.1.11. Здесь дизассемблируется программа, приведенная на Рис.1.1.5. Ну как, узнали нашу программу? Смысл обозначений будет ясен из дальнейшего изложения.
Рис. 1.1.11. Пример дизассемблированш программы с помощью DUMPPE.EXE.
Отмечу также дизассемблер W32Dasm, который подробно будет описан в последней части книги, и знаменитый дизассемблер IDA Pro. В части IV мы будем подробно рассматривать и сами дизассемблеры, и методику их использования.
Нех-редакторы позволяют просматривать и редактировать загружаемые модули в шестнадцатеричном виде. Их великое множество, к тому же отладчики и дизассемблеры, как правило, имеют встроенные НЕХ-редакторы. Отмечу только, весьма популярную в хакерских кругах программу HIEW.EXE. Эта программа позволяет просматривать загружаемые модули как в шестнадцатеричном виде, так и в виде ассемблерного кода. И не только просматривать, но и редактировать.
В пакетах MASM32 и TASM32 есть компиляторы ресурсов, которые будут описаны ниже. Это программы RC.EXE и BRC32.EXE соответственно.
Обычно я пользуюсь редактором ресурсов из пакета ВС5 (Borland C++ 5.0) Простые ресурсы можно создавать в обычном текстовом редакторе. Язык описания ресурсов будет подробно рассмотрен далее.

