Flat Assembler — инструмент разработчика

Flat Assembler — инструмент разработчика

Споры о самом лучшем языке программирования и самой удобной среде разработки уже довольно давно покинули страницы "Компьютерной газеты". Попытаюсь воспользоваться затишьем и, пока противоборствующие стороны собирают досье на противника, расскажу о не совсем традиционном направлении в разработке приложений. C распространением легкодоступных дистрибутивов Delphi, Visual C++, C++ Builder и прочих средств быстрой разработки приложений ушли в туманное прошлое времена, когда считалось, что вспомогательные утилиты и драйверы должны быть компактными на носителях информации, нетребовательными к памяти при загрузке и быстрыми в своей работе.

К сожалению, исполняемый файл, полученный с помощью перечисленных сред разработки, несет в себе значительный объем балластного кода, выполнение которого не способствует достижению поставленной перед приложением цели. Такова цена за повышение комфорта в виде визуальной среды и объектной модели, которые упрощают программирование, освобождают программиста от необходимости следить за несущественными деталями проекта.
Однако даже сегодня, когда тактовая частота процессоров давно перевалила за гигагерцевый рубеж, а объем оперативной памяти исчисляется сотнями мегабайт, существуют ситуации, в которых объем и скорость исполнения кода являются критическими величинами. Не секрет, что успехи в области разработок аппаратного обеспечения вычислительной техники тонут в новых задачах, которые с помощью этой техники предстоит решать. Обработка звука и изображения в реальном времени, решение задач автоматизированного проектирования, криптография и криптоанализ, работа в качестве контрольно-измерительного комплекса — это только самые очевидные применения персональных компьютеров, в которых требуется максимальная эффективность использования аппаратных ресурсов.
В описанных областях применение средств быстрой разработки может привести к недопустимому снижению производительности продукта из-за постоянного обращения к подпрограммам, размещенным в загрузочном модуле или динамических библиотеках, из-за малоэффективной оптимизации кода. Какой же выход видится в сложившейся ситуации?

Как известно, любой язык программирования служит лишь средством выражения идей человека, возникших в процессе его работы в некоторой предметной области, с целью последующего их формирования транслятором машинного кода, пригодного для выполнения на конкретной платформе. Каждая фраза на языке программирования высокого уровня может быть преобразована в машинные инструкции различными способами, с большей или меньшей степенью оптимизации в каждом конкретном случае. Чтобы полностью держать под контролем процесс трансляции, чтобы получить самый оптимальный код, программист должен обратиться к языку низкого уровня — к ассемблеру.
Сегодня существует несколько популярных компиляторов с языка ассемблера. Наибольшее распространение традиционно получили Microsoft Assembler (MASM) и Turbo Assembler (TASM). Обе эти системы позволяют разрабатывать приложения для различных операционных систем, которые функционируют на процессорах семейства Intel 80x86.
Большинство программистов, работающих с ассемблером, отдает предпочтение продукту фирмы Microsoft по нескольким причинам. Во-первых, он содержит расширенный набор макроинструкций, которые приближают программирование на ассемблере к программированию на языке высокого уровня. Во-вторых, пакет MASM включает максимально полный набор библиотек для обращения к функциям Win32API, что облегчает программирование для 32-разрядных операционных систем семейства Windows. В-третьих, этот продукт является бесплатным, и его можно легально использовать для разработки коммерческих приложений.

Однако при всех достоинствах перечисленных трансляторов у них есть и недостатки. Самый очевидный — размер пакета. В случае MASM он составляет пять с половиной мегабайт, TASM — два с половиной мегабайта. Оба этих продукта требуют установки на жесткий диск, при этом объем занимаемого ими дискового пространства увеличивается в несколько раз.
Наряду с описанными компиляторами мирно сосуществуют и другие, менее именитые разработки. Как читатель уже мог догадаться из названия, об одной из них, а именно о Flat Assembler (FASM), и пойдет речь в этой статье.
Почему именно FASM? Для этого выбора есть несколько причин. Во-первых, он является одним из наиболее динамично развивающихся компиляторов. Его автор Tomasz Grysztar регулярно выкладывает новые версии на свою страничку http://fasm.sourceforge.net/, откуда их может получить любой желающий. Кстати, архив версии 1.46 от 9 апреля 2003 года занимает всего 240 Кб, если предполагается работать в режиме командной строки DOS, и 550 Кб — если разработку планируется вести в среде Windows. И это при том, что в дистрибутив для Windows входит подробная документация в формате PDF, которая содержит описание как самого компилятора, так и машинных инструкций процессоров Intel включая набор команд MMX, SSE, SSE2 и AMD 3DNow! Все перечисленные команды могут быть использованы в программах на FASM.
Стоит отметить, что работать компилятор FASM будет только на компьютерах, оснащенных процессором не хуже Intel 80386, однако сегодня это вряд ли можно отнести к недостаткам. Тем более, что он позволяет генерировать код как для самых современных процессоров, так и для стареньких Intel 8086.

На этом этапе нужно обратить внимание на еще одну особенность рассматриваемого продукта. Дело в том, что FASM является компилятором и компоновщиком "в одном флаконе". То есть программист, использующий его, не нуждается ни в каких дополнительных утилитах. На входе FASM получает текст программы на языке ассемблера, а на выход выдается машинная программа в формате COM или EXE для DOS, DLL или PE (Portable Executable) для Windows, уже готовая к выполнению.
Такой механизм работы FASM вызывает неоднозначную оценку. С одной стороны, это упрощает процесс получения исполняемого файла, с другой — делает невозможным использование традиционных OBJ- и LIB-модулей. Приходится накапливать подпрограммы в текстовых файлах и подключать к основному модулю с помощью директивы INCLUDE. Такая технология ведет к неизбежному замедлению процесса компиляции, однако справедливости ради нужно отметить, что на современной технике это замедление не является критическим. Естественно, эффективность генерируемого машинного кода при этом нисколько не страдает.

В программах на языке ассемблера для FASM могут использоваться машинные инструкции в форме, аналогичной MASM или TASM. Косвенная адресация всегда обозначается заключением операнда в квадратные скобки. Для наглядности покажем несколько примеров.
mov eax, [ebp-4] загружает в регистр eax 32-битное число, находящееся по адресу, который хранится в ячейке памяти с адресом (ebp-4);
mov eax, var_1 загрузит в регистр eax адрес переменной var_1, тогда как для загрузки значения переменной нужно воспользоваться командой mov eax, [var_1]
Описание переменных выполняется традиционным способом в виде инструкции:

var_name <директива_описания> value
или
var_name <директива_резервирования> <количество>

где var_name — метка, задающая имя переменной; <директива_описания> и <директива_резервирования> — зарезервированное слово из таблицы 1, которое определяет размер памяти, отводимой для хранения переменной; value — значение, присваиваемое переменной или символ "?" для неинициализируемых переменных; <количество> — количество резервируемых ячеек памяти, размер которых определяется <директивой_резервирования> .
Если первый способ позволяет создавать инициализированные переменные, то второй удобно использовать в целях резервирования памяти для массивов (вместо оператора DUP, используемого в MASM).



Например:
ARRAY_SIZE = 50
Message db "Hello, world!", 0
ArrayOfHandle rd ARRAY_SIZE

Результатом компиляции указанных строк станет определение строковой переменной Message в формате ASCIIZ (с нулевым символом-ограничителем) и резервирование 4 * 50 = 200 байт памяти для массива ArrayOfHandle.
Чтобы описать структуру, можно воспользоваться оператором struc. Пусть, например, нам нужна структура для хранения информации о колонке списка ListView с целью ее использования в вызовах функций Win32API. Нет ничего проще. Сначала описывается тип структуры:

UINT equ dd ?
int equ dd ?
LPTSTR equ dd ?

struc LV_COLUMN {
.mask UINT
.fmt int
.cx int
.pszText LPTSTR
.cchTextMax int
.iSubItem int
}

а затем — переменная, использующая введенный тип:

lv_Column LV_COLUMN

При работе с FASM нужно помнить, что этот компилятор, в отличие от MASM и TASM, является регистрозависимым, то есть строчные и прописные буквы различаются. Поэтому, например, идентификаторы "lv_Column" и "LV_COLUMN" для него совершенно различны, и конфликта имен не возникает.
Еще одно интересное понятие, используемое в FASM, — виртуальная структура, которая реализуется с помощью директивы virtual следующего формата:

virtual [at <адрес> ]
... ... ...
<описание данных>
... ... ...
virtual end

Виртуальная структура не резервирует места в памяти, она только описывает способ трактовки данных, содержащихся в области памяти, начинающейся с адреса <?????> (или с текущего адреса, если этот параметр опущен). Виртуальные структуры очень удобно использовать в ситуациях, когда адрес реальной структуры, с полями которой нужно работать, находится в регистре, например, bx:

virtual at bx
HiWord dw ?
LoWord dw ?
virtual end
mov dx, [HiWord] ; компилируется как mov dx, [bx]
mov ax, [LoWord] ; компилируется как mov ax, [bx + 2]

Учитывая невозможность компоновки программы из объектных файлов встает вопрос: как обеспечить уникальность меток в подпрограммах, подключаемых с помощью директивы INCLUDE? Здесь FASM предлагает очень удобный способ описания локальных меток, а именно: любая метка, которая начинается с символа "." (точка), является локальной по отношению к предшествующей глобальной метке. Это позволяет сосредоточить внимание только на уникальности меток в контексте подпрограммы. В то же время на локальную метку можно ссылаться из любого места программы по ее полному имени, которое формируется следующим образом: <глобальная_метка> .<локальная_метка>, где <глобальная_метка> — первая глобальная метка, предшествующая описанию локальной метки (своего рода префикс), <локальная_метка> — собственно локальная метка. Поясним сказанное небольшим примером:

; Первая подпрограмма.
;----------------------
Subroutine_1:
...... ...
mov [.var1], ax ; подразумевается Subroutine_1.var1
...... ...
ret
.var1 dw ? ; полное имя — Subroutine_1.var1

; Вторая подпрограмма.
;----------------------
Subroutine_2:
...... ...
xor eax, eax
mov [ax], [Subroutine1.var1]
mov [.var1], eax ; подразумевается Subroutine_2.var1
...... ...
ret
.var1 dd ? ; полное имя — Subroutine_2.var1

Если идентификатор метки начинается не с одной, а с двух точек, такая метка является глобальной, за исключением того, что она не начинает своего контекста локальных меток. То есть локальные метки, следующие за таким описанием, будут продолжать контекст предшествующей "обычной" глобальной метки.
Для организации LOOP-циклов очень удобно использовать так называемые анонимные метки "@@". В программе можно ссылаться на предшествующую или последующую анонимную метку с помощью идентификаторов "@b" (или "@r") и "@f" соответственно (по-видимому, такие идентификаторы произошли от слов "back" ("reverse") — "назад" и "forward" — "вперед"):

mov cx, 255
@@:
...... ...
...... ...
loop @b

В процессе разработки приложений для Windows программист неизбежно сталкивается с необходимостью обращения к функциям API. Для таких целей FASM предлагает invoke — синтаксис, аналогичный используемому в MASM. Вот простейший, но полностью функциональный пример из пакета FASM, реализующий вывод на экран диалогового окна:

; example of simplified Win32 programming
; using complex macro features

include '%include%/win32ax.inc'

.code

start:
invoke MessageBox, HWND_DESKTOP, \
"Hi! I'm the example program!",\
"Win32 Assembly",MB_OK
invoke ExitProcess,0

.end start

Такая простота стала возможной благодаря удачному набору директив описания макроинструкций, реализованных в FASM. Кстати, как читатели уже, наверное, догадались, чтобы записать оператор на нескольких строках, в местах разрыва нужно вставить символ "\" (обратный слэш).
Макроинструкции в ассемблере FASM описываются с помощью директивы MACRO, имеющей следующий синтаксис:

macro <name> <arg1>,..., <argN>, [<gr_arg1>, ..., <gr_argM> ] {
common
... ... ...
<операторы секции common>
... ... ...
forward
... ... ...
<операторы секции forward>
... ... ...
reverse
... ... ...
<операторы секции reverse>
... ... ...
}

Внимание! Квадратные скобки в описателе макроинструкции являются элементом синтаксической конструкции, а не признаком необязательных параметров.
Как же это описание работает? Если не используются групповые аргументы (группа аргументов, заключенная в квадратные скобки), то необходимость в использовании секций отпадает, и описание макроинструкции напоминает определение подпрограммы в языках высокого уровня: после директивы macro указывается имя макроинструкции и список аргументов, которые играют роль формальных параметров.
При использовании групповых аргументов все несколько сложнее. В этом случае при записи макроинструкции первые N аргументов обрабатываются как обычные аргументы. Оставшиеся аргументы разбиваются на группы по M штук (это означает, что количество оставшихся аргументов должно быть кратно M), и способ обработки каждой такой группы определяется секцией.
Содержимое секции common разворачивается только один раз, для всех аргументов сразу. Содержимое секции forward разворачивается столько раз, сколько получилось групп аргументов. Причем сначала берутся первые M аргументов, следующие за <argN>, затем — следующие M аргументов — и так до тех пор, пока список не будет исчерпан. Секция reverse обрабатывается аналогично секции forward, только список аргументов просматривается с конца: сначала берутся последние M аргументов, затем — предыдущие M аргументов, и так далее, пока не останется N обычных аргументов.
Реализованный в FASM механизм макроинструкций, использующий групповые аргументы, является мощным и удобным средством и позволяет, например, написать макроинструкции для описания подпрограмм с произвольным порядком обработки как входных параметров, так и локальных переменных для подключения всевозможных ресурсов к приложениям Windows, реализовать такие конструкции языков высокого уровня, как ветвление и циклы. Все перечисленное уже создано автором компилятора и поставляется в виде подключаемых файлов в составе дистрибутивного пакета. Используя же прилагающуюся документацию, которая уже упоминалась в начале статьи, при наличии небольшой толики терпения и базовых знаний английского языка читателям не составит труда разобраться с примерами и изучить возможности директив, которые не были рассмотрены в этой обзорной статье.

В заключение несколько слов о впечатлениях, полученных в процессе знакомства с компилятором FASM. Автор считает, что наиболее целесообразно его использование в качестве учебного компилятора на практических занятиях по системному программированию для процессоров семейства Intel. Аргументом в пользу такого выбора может служить простота входного текста на языке ассемблера по сравнению с компиляторами MASM и TASM, которая позволяет сразу перейти к разработке программ и сосредоточить внимание на предмете изучения, не отвлекаясь на процедуры компиляции/компоновки, которые на первых шагах обучения только загружают учащихся избыточными и хаотичными сведениями. С этой точки зрения он может служить низкоуровневым аналогом пакета Turbo Pascal. Кроме того, он поддерживает расширенные наборы инструкций, присущие последним моделям процессоров Intel.
Так же, как Turbo Pascal, компилятор FASM позволяет создавать реальные коммерческие приложения, причем для нескольких широко распространенных операционных систем. Большим плюсом в этой связи является его бесплатность. FASM имеет средства, позволяющие создавать модульные программы с использованием директив INCLUDE и локальных меток, однако не поддерживает объектных модулей.
Последний факт может насторожить профессионалов, и они сделают свой выбор в пользу продукта фирмы Microsoft.
В любом случае FASM является интересной разработкой, на которую стоит обратить внимание как профессиональным программистам, так и всем желающим познакомиться с увлекательным процессом программирования на языке ассемблера, который никогда не утратит своей актуальности.

Игорь Орещенков, 2003 г.


Компьютерная газета. Статья была опубликована в номере 22 за 2003 год в рубрике программирование :: разное

©1997-2024 Компьютерная газета