FLAT ASSEMBLER 1.64 Мануал программера
Глава 1. Введение
Эта глава содержит всю важнейшую информацию, которая понадобится вам, чтобы начать использовать flat assembler. Если у вас уже есть опыт программирования на ассемблере, вам достаточно прочитать лишь первую главу перед использованием этого компилятора.
1.1 Обзор компилятора
Flat assembler — это быстрый компилятор ассемблера для процессоров с архитектурой х86, который делает множественные проходы для оптимизации размера сгенерированного машинного кода. Он способен скомпилировать сам себя и существуют версии для разных операционных систем. Все версии созданы для использования с помощью системной командной строки и в обращении с ними нет разницы.
1.1.1 Системные требования
Для работы всех версий требуется 32-битный процессор с архитектурой х86 (как минимум 80386), хотя также он должен обрабатывать программы для 16-битных процессоров с архитектурой х86. DOS-версия требует ОС, совместимую с MS DOS 2.0, Windows-версия требует консоль Win32, совместимую с версией 3.1.
1.1.2 Работа с компилятором из командной строки
Для запуска FASMа из командной строки вам понадобится ввести два параметра: первый — это путь к файлу с кодом, второй — путь к файлу-адресату информации. Если второй параметр не задан, название файла вывода будет создано автоматически. После показа короткой информации о названии программы и её версии, компилятор считает информацию из файла с кодом и скомпилирует её. После успешной компиляции FASM запишет сгенерированный код в файл-адресат и выведет краткую информацию о завершенном процессе; в противном случае он выведет информацию о первой ошибке.
Исходник должен быть текстовым файлом и может быть создан в любом текстовом редакторе. Обрыв строки допускается и в стандарте DOS, и в стандарте Unix, табуляции обрабатываются как пробелы.
В командную строку вы также можете включить опцию «-m», за которой должно следовать число, указывающее, сколько килобайт памяти может быть максимально задействовано flat assembler'ом. В случае DOS-версии эта опция ограничивает лишь использование расширенной памяти. Опция «-p» со следующим за ним числом может быть использована для того, чтобы ограничить количество проходов, которое будет делать ассемблер. Если код не может быть создан заданным числом проходов, ассемблирование прекратится с сообщением об ошибке. Максимальное значение этой опции равно 65536, а значение по умолчанию равно 100.
Не существует опций, оказывающих воздействие на выходные данные компилятора, вся необходимая flat assembler'у информация должна содержаться в исходном коде. Например, для установки формата файла-адресата используется директива «format» в начале кода.
1.1.3 Сообщения компилятора
Как было сказано выше, после успешной компиляции FASM выводит на экран сводку о проделанной работе. Она включает информацию о том, сколько было сделано проходов, сколько времени это заняло, и сколько байт записано в файл-адресат.
Вот пример такой сводки:
flat assembler version 1.64
38 passes, 5.3 seconds, 77824 bytes.
В случае ошибки во время компиляции, программа выведет на экран сообщение об ошибке. Например, когда компилятор не может найти файл ввода, он покажет следующее сообщение:
flat assembler version 1.64
error: source file not found.
Если ошибка связана с определенной частью кода, будет выведена строка, которая её вызвала. Также, чтобы помочь вам найти эту ошибку, будет показано место этой строки в коде, например:
flat assembler version 1.64
example.asm [3]:
mob ax,1
error: illegal instruction.
Это значит, что в третьей строке файла «example.asm» компилятор встретил неопознанную инструкцию. Если строка, вызвавшая ошибку, содержит макрос, также будет выведена строка в формулировке макроса, которая сгенерировала ошибочную инструкцию:
flat assembler version 1.64
example.asm [6]:
stoschar 7
example.asm [3] stoschar [1]:
mob al,char
error: illegal instruction.
Это значит, что макрос в шестой строке файла «example.asm» создал неопознанную инструкцию в первой строке своей формулировки.
1.1.4 Форматы вывода
По умолчанию, если в исходнике нет директивы «format», flat assembler направляет сгенерированный код на вывод, создавая таким образом простой двоичный файл. По умолчанию он создает 16-битный код, но вы всегда можете переключить его в 32-битный или 16-битный режим, используя директивы «use32» или «use16». Выбор некоторых форматов файла-адресата автоматически переключает компилятор в 32-битный режим. Подробнее читайте о форматах, которые можете выбрать, в 2.4.
Весь сгенерированный код в файле-адресате всегда идет в том же порядке, что и написанный в исходнике.
1.2 Синтаксис ассемблера
Информация, изложенная ниже, предназначена главным образом программистам, которые прежде использовали другие компиляторы ассемблера. Если вы новичок, читайте учебники по программированию на ассемблере.
Flat assembler по умолчанию использует Интеловский синтаксис ассемблерных инструкций, однако вы можете переделать их, используя возможности препроцессора (макросы и символьные константы). Он также имеет собственный набор директив — инструкций для компилятора.
Все символы, определенные внутри кода, чувствительны к регистру.
1.2.1 Синтаксис инструкций
Инструкции в ассемблере разделяются разрывами строк, и одна инструкция должна располагаться на одной строке. Если строка содержит точку с запятой, не считая точек с запятой, заключенных в кавычки, остаток её считается комментарием и компилятор её проигнорирует. Если строка содержит символ «\» (в конце концов точка с запятой и комментарий могут за ней следовать), то следующая срока прикрепляется к этой точке. После символа «\» строка не должна содержать ничего, кроме комментариев, следующих за точкой с запятой.
Каждая строка в коде — это последовательность отдельных предметов, каждый из которых может принадлежать к одному из трек типов. Первый тип — это знаки символов, которыми являются специальные знаки, которые трактуются отдельно, даже если не отделены пробелами о других символов. Любой из «+-*/=<>()[]{}:,|&~#`» — это знак символа. Последовательность других знаков, отделенная от остальных либо пробелами, либо знаками символов, это символ. Если первый знак такого символа двойная либо одинарная кавычка, он объединяет любую последовательность символов, даже специальных, следующих за ним, в строку. Она должна заканчиваться таким же знаком, каким начиналась (двойной либо одинарной кавычкой). Однако если встречаются две кавычки подряд (без знаков между ними), они также включаются в строку и она продолжается. Символы, отличные от знаков символов и строк, заключенных в кавычки, могут использоваться как имена, поэтому они также называются символами имен.
Таблица 1.1 Размеры операторов
Каждая инструкция состоит из мнемоника и различного числа операндов, разделенных запятыми. Операндом может быть регистр, непосредственное значение или адрес в памяти, он также может предварен оператором размера, чтобы определить или изменить его размер (таблица 1.1). Названия возможных регистров вы можете найти в таблице 1.2, их размеры не могут быть изменены. Непосредственные значения могут быть определены любым числовым значением.
Таблица 1.2 Регистры
Если операнд — это данные в памяти, адрес этих данных (также любого числового выражения, но оно может содержать регистры) должен быть заключен в квадратные скобки или предварен оператором «ptr». Например, инструкция «mov eax,3» поместит число 3 в регистр EAX, а инструкция «mov eax,[7]» поместит 32-битное значение из адреса 7 в EAX, и инструкция «mov byte [7],3» поместит число 3 в байт по адресу 7, это можно записать еще так: «mov byte ptr 7,3». Для того, чтобы установить, какой сегментный регистр будет использоваться для адресации, нужно поставить его название с двоеточием перед адресом внутри квадратных скобок или после оператора «ptr».
1.2.2 Описание данных
Чтобы описать данные или зарезервировать для них место, используйте одну из директив, перечисленных в таблице 1.3. За директивой описания данных должно следовать одно или несколько числовых значений, разделенных запятыми. Эти выражения определяют значения для простейших элементов данных, размер которых зависит от того, какая директива используется. Например «db 1,2,3» описывает три байта со значениями 1, 2 и 3 соответственно.
Таблица 1.3 Директивы данных
Директивы «du» и «db» также поддерживают сроки любой длины, заключенные в кавычки, которые будут конвертированы в последовательность байтов, если использована директива «db», или в последовательность слов с нулевым верхним байтом, если использована директива «du». Например, «db 'abc'» определяет три байта со значениями 61, 62 и 63.
Директива «dp» или её синоним «df» допускают, чтобы значения состояли из двух числовых выражений, разделенных двоеточием, где первое значение — это верхнее слово, а второе — это нижнее двойное слово значения дальнего указателя. Также «dd» допускает такие указатели, состоящие из двух слов, разделенных двоеточием, и «dt» допускает слово и четверное слово, разделенные двоеточием, четверное слово запоминается первым. Директива «dt» с одним параметром допускает только значения с плавающей точкой и создает данные в FPU-формате двойной расширенной точности.
Все вышеперечисленные директивы поддерживают использование специального оператора «dup» для создания копий данных значений. Количество дубликатов должно стоять перед этим оператором, а их значение должно стоять после — это может быть даже цепь значений, разделенных запятыми, но эта цепь должна быть заключена в скобки, например «db 5 dup (1,2)» определяет пять копий данной последовательности из двух байтов.
«file» — это специальная директива и её синтаксис может быть различным. Эта директива включает цепь байтов из файла. В качестве параметра за ней должно идти в кавычках имя файла, далее, опционально, двоеточие и числовое выражение, указывающее начало цепочки байтов, далее, также опционально, запятая и числовое выражение, определяющее количество байтов в этой цепочке (если этот параметр не определен, то будут включены все данные до конца файла). Например, «file 'data.bin'» включит весь файл как двоичные данные, а «file 'data.bin':10h,4» включит только четыре байта, начиная со смещения 10h.
За директивой резервирования данных должно следовать одно числовое выражение, значение которого определяет количество резервируемых ячеек установленного размера. Все директивы описания данных также поддерживают значение «?», которое значит, что этой ячейке не должно быть присвоено какое-то значение. Эффект от этой директивы такой же, как от директивы резервирования данных. Неинициализированные данные не могут быть включены в файл вывода, и, таким образом, их значения всегда будут считаться неизвестными.
1.2.3 Константы и метки
В числовых выражениях вместо чисел вы также можете использовать константы и метки. Чтобы назначить их, используйте специальные директивы. Каждая метка может быть определена только однажды и она будет доступна из любой части кода (даже перед местом, где она была определена). Константа может быть переопределена много раз, но в этом случае она будет доступна только после присвоения значения и всегда будет равна значению из последнего определения перед местом, в котором она использована. Если константа определена лишь однажды, она, так же как и метка, доступна из любой части кода.
Определение константы состоит из имени константы, знака «=» и числового выражения, которое после вычисления становится значением константы. Это значение всегда вычисляется в то же время, что и определение константы. Например, с помощью директивы «count = 17» вы можете определить константу «count» и после использовать её в инструкциях ассемблера, таких как «mov cx,count» — которая превратится в «mov cx,17» во время процесса компиляции.
Существуют разные способы определения меток. Простейший из них — двоеточие после названия метки. За этой директивой на той же строке даже может следовать другая инструкция. Она определяет метку, значение которой равно смещению точки, в которой она определена. Этот метод обычно используется, чтобы пометить места в коде. Другой способ — это следование за именем метки (без двоеточия) какой-нибудь директивы описания данных. Метке присваивается значение адреса начала определенных в директиве данных и запоминается компилятором как метка для данных с размером ячейки, заданной директивой из таблицы 1.3.
Метка может быть обработана как константа со значением, равным смещению помеченного кода или данных. Например, если вы определяете данные, используя помеченную директиву «char db 224», для того, чтобы поместить адрес начала этих данных в регистр BX, вам нужно использовать инструкцию «mov bx,char», а для того, чтобы поместить в регистр DL значение байта, на который ссылается «char», нужно использовать «mov dl,[char]» (или «mov dl,ptr char»). Если вы попытаетесь ассемблировать «mov ax,[char]», FASM выдаст ошибку, так как он сравнивает размеры операндов, которые должны быть равны. Вы можете принудительно проассемблировать эту инструкцию, изменяя размер операнда: «mov ax, word [char]», но помните, что эта инструкция прочитает два байта, начинающихся с адреса «char», тогда как он был определен как один байт.
Последний и самый гибкий способ задания меток — это использование директивы «label». За этой директивой должно следовать имя метки, далее, опционально, размер оператора (может предваряться двоеточием), и далее, также опционально, оператор «at» и числовое выражение, определяющее адрес, на который данная метка должна ссылаться. Например, «label wchar word at char» определяет новую метку для 16-битных данных по адресу «char». Теперь инструкция «mov ax,[wchar]» после компиляции будет выглядеть так же, как «mov ax,word [char]». Если адрес не указан, директива «label» будет ссылаться на текущий адрес. Таким образом, «mov [wchar],57568» скопирует два байта, тогда как «mov [char],224» скопирует один байт на тот же адрес.
Метка, имя которой начинается с точки, обрабатывается как локальная, и её имя прикрепляется к имени последней глобальной метки (с названием, начинающемся с чего угодно, кроме точки) для создания полного имени этой метки. Так, вы можете использовать короткое имя (начинающееся с точки) где угодно перед следующей глобальной меткой, а в других местах вам придется пользоваться полным именем. Метки, начинающиеся с двух точек — исключения. Они имеют свойства глобальных, но не создают новый префикс для локальных меток.
«@@» обозначает анонимную метку, вы можете определить её множество раз. Символ «@b» (или эквивалент «@r») ссылается на ближайшую предшествующую анонимную метку, а символ «@f» ссылается на ближайшую после неё анонимною метку. Эти специальные символы нечувствительны к регистру.
1.2.4 Числовые выражения
В предыдущих примерах все числовые выражения были обычными числами, константами или метками. Но они могут быть более сложными, использовать арифметические или логические операторы для вычисления во время компиляции. Все эти операторы с их значениями приоритета перечислены в таблице 1.4.
Таблица 1.4 Арифметические и логические операторы в порядке приоритета
Операции с высшим приоритетом выполняются первыми, однако вы, конечно, можете изменить такой образ действий, заключив некоторые части выражения в скобочки. «+», «-", «*» и «/» — это стандартные арифметические операции, «mod» вычисляет остаток от деления нацело. «and», «or», «xor», «shl», «shr» и «not» совершают те же логические операции, что и инструкции ассемблера с такими же названиями. «rva» характерна только для формата вывода PE и производит превращение адреса в RVA.
Числа в выражениях по умолчанию обрабатываются как десятичные, двоичные числа должны иметь «b» в конце, восьмеричные числа должны заканчиваться на букву «o», шестнадцатеричные цифры должны начинаться символами «0x» (как в языке C), или символом «$» (как в языке Pascal) или должны заканчиваться буквой «h». Также заключенная в кавычки строка при включении в выражение будет конвертирована в число — первый символ станет минимальным значащим байтом числа. Числовые выражения, используемые как значения адреса, могут также содержать любой из общих регистров, используемых для адресации, они могут быть сложены или умножены на подходящие значения так, как это позволено в инструкциях архитектуры x86.
Также есть несколько специальных символов, которые могут быть использованы в числовом выражении. Первое — это «$», которое всегда равно значению текущего смещения, тогда как «$$» равно базовому адресу текущего диапазона адресов. Следующий символ — «%» — это номер текущего повтора в частях кода, которые повторяются, благодаря использованию некоторых специальных директив (смотрите 2.2). Также существует символ «%t», который всегда равен текущей отметке времени.
Любое численное выражение также может состоять из одного значения с плавающей точкой (flat assembler не может производить во время компиляции операции с плавающей точкой) в научной записи. Для распознания компилятором, эти значения должны содержать в конце букву «f», либо включать в себя по крайней мере один символ ".» или «E». Так, «1.0», «1E0» и «1f» определяют одно и то же значение с плавающей точкой, когда как просто «1» определяет целочисленное значение.
1.2.5 Переходы и вызовы
Операнд любого перехода или инструкция вызова может предваряться не только операторами размера, но также одним из операторов, определяющих тип перехода: «near» или «far». Например, если ассемблер в 16-битном режиме, инструкция «jmp dword [0]» станет далеким переходом, а если ассемблер в 32-битном режиме, она станет близким переходом. Чтобы заставить эту инструкцию обрабатываться по-разному, используйте формы «jmp near dword [0]» или «jmp far dword [0]».
Если операнд близкого перехода это немедленное значение, ассемблер, если возможно, сгенерирует кратчайший вариант этой инструкции перехода (но не будет создавать 32-битную инструкцию в 16-битном режиме или 16-битную инструкцию в 32-битном режиме, если оператор размера точно её не определит). Заданием оператора размера вы можете заставить ассемблер всегда генерировать длинный вариант (например, «jmp word 0» в 16-битном режиме или «jmp dword 0» в 32-битном режиме) или всегда создавать короткий вариант и завершаться с ошибкой, когда это невозможно (например «jmp byte 0»).
1.2.6 Установки размера
Если инструкция использует некоторую адресацию в памяти, по умолчанию будет генерироваться кратчайшая 8-битная форма, если значение адреса попадает в нужный диапазон, но он может быть изменен с помощью операторов «word» и «dword» перед адресом в квадратных скобках (или после оператора «ptr»). Такое размещение оператора размера также может быть использовано для установки размера адреса, отличного от размера, установленного в данном режиме по умолчанию.
Инструкции «adc», «add», «and», «cmp», «or», «sbb», «sub» и «xor» с первым 16-ти или 32-битным операндом по умолчанию генерируются в укороченной 8-битной форме, если второй операнд — это непосредственное значение, применимое для предписанных 8-битных значений. Она также может быть изменена операторами «word» и «dword» перед такими значениями. Сходные правила применимы к инструкции «imul» с непосредственным значениям в качестве последнего операнда.
Непосредственное значение как операнд для инструкции «push» без оператора размера, по умолчанию обрабатывается как слово, если ассемблер 16-битном режиме, и как двойное слово, если FASM в 32-битном режиме. Короткая 8-битная форма используется по возможности, операторы размера «word» и «dword» могут заставить инструкцию «push» быть сгенерированной в более длинной форме. Мнемоники «pushw» и «pushd» указывают ассемблеру сгенерировать 16-битный или 32-битный код без принуждения его использовать длинную форму инструкции.
Глава 2. Описание инструкций
Эта глава содержит детальную информацию об инструкциях и директивах, поддерживаемых FASMом. Директивы определения констант и меток уже описаны в 1.2.3, все остальные директивы будут описаны ниже в этой главе.
2.2 Директивы управления
Этот параграф описывает директивы, которые управляют процессом ассемблирования. Эти директивы выполняются во время ассемблирования и могут делать так, чтобы некоторые блоки инструкций ассемблировались по-разному или не ассемблировались вовсе.
2.2.1 Условное ассемблирование
С помощью директивы «if» можно ассемблировать или не ассемблировать блок инструкций в зависимости от выполнения условия. За ней должно следовать логическое выражение, определяющее условие. Инструкции на следующих строках ассемблируются, только если это условие выполняется, иначе они пропускаются. Опциональная директива «elseif» со следующим за ней логическим выражением, определяющим дополнительное условие, начинает следующий блок инструкций, который ассемблируется, если предыдущие условия не выполняются, а данное дополнительное условие выполняется. Опциональная директива «else» начинает блок инструкций, которые ассемблируются, если не выполняется ни одно из условий. «end if» заканчивает последний блок инструкций.
Вы должны помнить, что директива «if» обрабатывается на стадии ассемблирования и поэтому не влияет на директивы препроцессора, такие как определения символьных констант и макроинструкции — когда ассемблер распознает директиву «if», весь препроцессинг уже закончен.
Логическое выражение состоит из логических значений и логических операторов. Логические операторы выглядят так: «~» для логического отрицания, «&» для логического И, «|» для логического ИЛИ. Отрицание имеет высший приоритет. Логическое значение может быть числовым выражением, оно будет считаться ложным в случае равенства нулю, иначе оно будет истинным. Для создания логического значения можно сравнить два числовых выражения, используя один из следующих операторов: «=» (равно), «<» (меньше), «>» (больше), «<=» (меньше или равно), «>=» (больше или равно), «<>» (не равно).
«used» со следующим за ним символом имени, это логическое значение, которое проверяет, использовался ли где-нибудь данный символ (он возвращает правильный результат даже если символ используется только после этой проверки). За оператором «defined» может следовать любое выражение, обычно это только одно символьное имя; этот оператор проверяет, содержит ли данное выражение исключительно символы, определенные в коде, и доступные из текущей позиции.
Следующий простой пример использует константу «count» которая должна быть определена где-то в коде:
if count>0
mov cx,count
rep movsb
end if
Эти две инструкции будут ассемблированы только если константа «count» больше нуля. Следующий пример показывает более комплексную условную структуру:
if count & ~ count mod 4
mov cx,count/4
rep movsd
else if count>4
mov cx,count/4
rep movsd
mov cx,count mod 4
rep movsb
else
mov cx,count
rep movsb
end if
Первый блок инструкций ассеблируется, если константа «count» не равна нулю и кратна четырем, если это условие не выполняется, оценивается второе логическое условие, следующее за «else if», и если оно верно, ассемблируется второй блок инструкций, иначе ассемблируется последний блок, который следует за строкой, содержащей только «else».
Также есть операторы, которые позволяют сравнивать значения, которые представляют собой последовательности символов. «eq» проверяет такие значения на тождественность. Оператор «in» проверяет, принадлежит ли данное значение к списку значений, следующему за оператором. Список должен быть заключен между символами «<» и «>», а его члены должны быть разделены запятыми. Символы считаются одинаковыми, если они имеют одно и то же значение для ассемблера — например, «pword» и «fword» для ассемблера одинаковы поэтому не различаются вышеуказанными операторами. Так же «16 eq 10h» является истиной, однако «16 eq 10+4» нет.
Оператор «eqtype» имеют ли сравниваемые значения одинаковую структуру, и принадлежат ли структурные элементы одному типу. Различаемые типы включают в себя числовые выражения, строки, заключенные в кавычки, значения с плавающей точкой, адресные выражения (выражения в квадратных скобках или предваренные оператором «ptr»), мнемоники инструкций, регистры, операторы размера, операторы перехода и операторы типа кода. И каждый из специальных символов, действующих как разделители, такой как запятая или двоеточие, это отдельный тип сам по себе. Например, два значения, каждое из которых состоит из имени регистра и числового выражения, разделенных запятой, будут распознаны как один тип, независимо от вида регистра и сложности числового выражения; за исключением строк, заключенных в кавычки и значений с плавающей точкой, которые относятся к специальным видом числовых выражений и распознаются как разные типы. Поэтому условие «eax,16 eqtype fs,3+7» является истиной, но «eax,16 eqtype eax,1.6» — ложь.
2.2.2 Повторение блоков инструкций
«times» повторяет одну инструкцию указанное количество раз. За ней должно следовать числовое выражение, определяющее количество повторений, и инструкция, которую нужно повторять (опционально для того, чтобы отделить число и инструкцию, можно использовать двоеточие). Специальный символ «%», использующийся внутри инструкции, эквивалентен номеру текущего повтора. Например, «times 5 db %» определит пять байтов со значениями 1, 2, 3, 4, 5. Поддерживается также рекурсивное использование директивы «times», например, «times 3 times % db %» определит шесть байтов со значениями 1, 1, 2, 1, 2, 3.
«repeat» повторяет целый блок инструкций. За ней должно следовать числовое выражение, определяющее количество повторений. Инструкции для повторения предполагаются на следующих строках, а заканчиваться блок должен директивой «end repeat», например:
repeat 8
mov byte [bx],%
inc bx
end repeat
Сгенерированный код сохраняет байты со значениями от одного до восьми в памяти, адресованной регистром BX.
Количество повторений может быть равным нулю, и в таком случае инструкции не будут ассемболироваться вовсе.
«break» позволяет остановить повторение раньше и продолжить ассемблирование с первой строки после «end repeat». В сочетании с директивой «if» она позволяет остановить повторение при выполнении некоторого особого условия, например:
s = x/2
repeat 100
if x/s = s
break
end if
s = (s+x/s)/2
end repeat
«while» повторяет блок инструкций, пока выполняется следующее за ней условие, определенное логическим выражением. Блок инструкций для повторения должен заканчиваться директивой «end while». Перед каждым повторением логическое выражение вычисляется и если его значение ложь, ассемблирование продолжается, начиная с первой строки после «end while». Также в этом случае символ «%» содержит номер текущего повторения. Директива «break» может быть использована для остановки этого типа цикла так же, как с директивой «repeat». Предыдущий пример может быть переписан с использованием «while» вместо «repeat» таким образом:
s = x/2
while x/s
s
s = (s+x/s)/2
if % = 100
break
end if
end while
Блоки, определенные с использованием «if», «repeat» и «while» могут быть вложены в любом порядке, однако и закрыты в обратном. Директива «break» всегда останавливает обработку бока, который был начат последним либо директивой «repeat», либо «while».
2.2.3 Адресные пространства
«org» устанавливает адрес, по которому следующий за ней код должен появиться в памяти. За ней должно следовать числовое выражение, указывающее адрес. Эта директива начинает новое адресное пространство, следующий код сам по себе никуда не двигается, но все метки, определенные в нем и значение символа «$» изменяются как если бы он был бы помещен по этому адресу. Тем не менее обязанность поместить во время выполнения код по правильному адресу лежит на программисте.
«load» позволяет определить константу двоичным значением, загруженным из уже сассемблированного кода. За директивой должно следовать имя константы, затем опционально оператор размера, затем оператор «from» и числовое выражение, определяющее валидный адрес в текущем адресном пространстве. Оператор размера здесь имеет необычное значение — он определяет, сколько байтов (до 8) должно быть загружено из двоичного значения константы. Если оператор размера не определен, загружается один байт (таким образом значение оказывается в пределах от 0 до 255). Загруженные данные не могут превосходить текущее смещение.
«store» может модифицировать уже сгенерированный код заменой некоторых ранее сгенерированных байтов значением, задаваемым следующим за инструкцией числовым выражением. Перед этим выражением может идти оператор размера, определяющий, насколько длинное значение оно задает, то есть сколько будет сохранено байт. Если оператор размера не задан, подразумевается длина в один байт. Далее должен следовать оператор «at» и числовое выражение, указывающее валидный адрес в текущем адресном пространстве кода. По этому адресу будет сохранено задаваемое значение. Это директива для продвинутого применения и её следует использовать осторожно.
Обе директивы «load» и «store» ограничены оперированием только в пределах текущего адресного пространства. Символ «$$» всегда равен базовому адресу в текущем адресном пространстве, а символ «$» — это адрес текущей позиции в нём, то есть эти два значения определяют границы действия директив «load» и «store».
Сочетая директивы «load» и «store» можно делать вещи, такие как шифрование некоторого из уже сгенерированного кода. Например, для шифрования всего кода, сгенерированного в текущем адресном пространстве вы можете использовать такой блок директив:
repeat $-$$
load a byte from $$+%-1
store byte a xor c at $$+%-1
end repeat
и каждый байт коза будет проксорен со значением, определенным константой «c».
«virtual» определяет виртуальные данные по указанному адресу. Эти данные не будут включены в файл вывода, но но метки, определенные здесь, могут использоваться в других частях кода. За этой директивой может следовать оператор «at» и числовое выражение, определяющее адрес виртуальных данных, иначе будет использован текущий адрес, что равносильно директиве «virtual at $». Инструкции определяемых данных должны быть расположены на следующих строках и заканчиваться директивой «end virtual». Блок виртуальных инструкций сам по себе независимое адресное пространство, и после того, как оно заканчивается, восстанавливается контекст предыдущего адресного пространства.
Директива «virtual» может быть использована для создания объединения нескольких переменных, например:
GDTR dp?
virtual at GDTR
GDT_limit dw?
GDT_address dd?
end virtual
Здесь определяются две части 48-битной переменной по адресу «GDTR».
Директива также может быть использована для определения меток некоторых структур, адресованных регистром, например:
virtual at bx
LDT_limit dw?
LDT_address dd?
end virtual
С таким определением инструкция «mov ax,[LDT_limit]» будет сассемблирована в «mov ax,[bx]».
Также может быть полезно объявление инструкций и значений данных внутри виртуально блока, так как директиву «load» можно использовать для загрузки в константы значений из виртуально сгенерированного кода. Эта директива должна быть использована после загружаемого кода, но до окончания виртуального блока, так как она может загружать значения только из того же адресного пространства. Например:
virtual at 0
xor eax,eax
and edx,eax
load zeroq dword from 0
end virtual
Этот кусок кода определяет константу «zeroq», которая будет содержать четыре байта машинного кода инструкций, указанных внутри виртуального блока. Этот метод также может быть использован для загрузки некоторых бинарных значений из внешнего файла. Например этот код:
virtual at 0
file 'a.txt':10h,1
load char from 0
end virtual
загружает один байт со смещением 10h из файла «a.txt» в константу «char».
Все директивы «section», описанные в 2.4, также начинают новое адресное пространство.
2.2.4 Другие директивы
«align» выравнивает код или данные по указанной границе. За ней должно следовать числовое выражение, определяющее количество байтов, на кратность которому должен быть выровнен текущий адрес. Значение границы должно быть степенью двойки.
Директива «align» заполняет байты, которые должны быть пропущены, чтобы совершить выравнивание, инструкциями «nop», и в это же время маркирует эту область как неинициализированные данные, то есть если её поместить среди других неинициализированных данных, это не займет места в файле вывода, выравнивание байтов происходит таким же образом. Если вам нужно заполнить область выравнивания какими-то другими значениями, вы можете сочетать «align» и «virtual», чтобы получить требуемый размер выравнивания и далее создать выравнивание самостоятельно, например:
virtual
align 16
a = $ — $$
end virtual
db a dup 0
Константа «a» определяется как разница между адресом после выравнивания и адресом блока «virtual» (смотрите предыдущий параграф), то есть она равна размеру требуемого пространства выравнивания.
«display» во время ассемблирования показывает сообщение. За ней должны следовать строка в кавычках или значения байтов, разделенные запятыми. Директива может быть использована для показа значений некоторых констант, например:
bits = 16
display 'Current offset is 0x'
repeat bits/4
d = '0' + $ shr (bits-%*4) and 0Fh
if d> '9'
d = d + 'A'-'9'-1
end if
display d
end repeat
display 13,10
Этот блок директив рассчитывает четыре цифры 16-битного значения и конвертирует их в знаки для показа. Помните что это не будет работать, если адреса в текущем адресном пространстве перемещаемы (как это может быть с объектным форматом вывода и форматом PE), так как таким образом могут быть использованы только абсолютные значения. Абсолютное значение может быть получено вычислением относительного адреса, например «$-$$» или «rva $» в случае формата PE.
2.2.5 Множественные проходы
Так как ассемблер позволяет ссылаться на некоторые метки и константы перед тем, как они фактически определены, приходится прогнозировать значения этих меток и если есть даже подозрение, что прогноз окажется неверным хотя бы один раз, делается еще один проход, ассемблирующий весь код, и в это время делается лучший прогноз, базирующееся на значениях меток, полученных в предыдущий проход.
Изменение значений меток может быть причиной того, что некоторые инструкции перекодируются с другими длинами и это снова повлечет изменение меток. И так как метки и константы ещё могут использоваться внутри выражений, которые влияют на поведение директив управления, весь блок инструкций в новый проход может ассемблироваться абсолютно по-другому. Поэтому ассемблер делает проходы снова и снова, каждый раз пытаясь создать лучшие прогнозы, чтобы приблизиться к финальному решению, когда все значения спрогнозированы правильно. Для прогнозов используются разные методы, которые выбираются с тем, чтобы найти с как можно меньшим количеством проходов решение наименьшей возможной длины для большинства программ.
О некоторых ошибках, таких как непопадание значений в заданные границе, не сигнализируется во время этих промежуточных проходов, пока может случиться такое, что если какие-то значения будут спрогнозированы лучше, эти ошибки исчезнут сами собой. Однако, если ассемблер встречает какую-то недопустимую синтаксическую конструкцию или неизвестную инструкцию, он всегда останавливается немедленно. Такую же ошибку вызывает определение метки более, чем один раз, так как это делает прогнозы необоснованными.
Если в коде встречается директива «display», фактически отображаются только сообщения, созданные в последний совершённый проход. В случае, если ассемблер остановился из-за ошибки, эти сообщения могут отражать спрогнозированные значения, которые еще не разрешены правильно.
Разрешение иногда может не создаться и в таких случаях ассемблер никогда не сумеет создать правильные прогнозы — по этой причине существует предел количества походов, и когда ассемблер исчерпает этот лимит, он остановится отобразит сообщение, что невозможно сгенерировать корректный вывод. Рассмотрим следующий пример:
if ~ defined alpha
alpha:
end if
Если оператор «defined» выдает значение истина, если выражение, следующее за ним, в этом месте может быть вычислено, что в данном случае означает, что метка «alpha» где-то определена. Но блок выше определяет эту метку только, если значение, данное оператором «defined» ложь, что ведет к противоречию и делает невозможным разрешить такой код. Если, обрабатывая директиву «if» ассемблер должен прогнозировать, будет ли где-нибудь определена метка «alpha» (этого делать не приходится только если метка уже определена раньше), то какой бы ни был прогноз, всегда происходит противоположное. Поэтому ассемблирование остановится, если только метка «alpha» не определена где-то в коде перед вышеуказанным блоком — в этом случае, как уже было отмечено прогнозирование не требуется и блок просто будет пропущен.
Предыдущий пример может быть создан как попытка определить метку, только если этого все ещё не сделано. Эти строк неправильны, поскольку оператор «defined» проверяет определена ли метка где-либо вообще, и это включает определение внутри этого условного блока. Однако есть способ обойти эту проблему:
if ~ defined alpha | defined @f
alpha:
@@:
end if
«@f» это всегда та же метка, что ближайший следующий за ним символ «@@», поэтому предыдущий пример значит то же, как если бы вместо анонимной метки было определено любое уникальное имя. Если метка «alpha» ещё не определена, ассемблер спрогнозирует значение «defined alpha» как ложь, это будет однако значить, что будут определены обе метки. Но на следующем проходе ассемблер спрогнозирует, что определены обе метки, что заставит определить их вновь — так прогноз будет совпадать с результатом и процесс ассемблиования придет к правильному решению. Анонимная метка выступает здесь как маркер того, что метка «alpha» определена в этом месте.
Из этого примера вы можете заключить, что прогноз для оператора «defined» очень прямолинейный — метка прогнозируется как определенная только если она была определена в предыдущий проход (а если она была определена в текущий проход, прогноз не требуется). То же самое относится к оператору «used». Однако прогнозы для значений меток не так просты и вам никогда не следует полагать, что ассемблер работает таким способом.
2.3 Директивы препроцессора
Все директивы препроцессора выполняются перед основным ассемблированием, и таким образом директивы управления на них никак не влияют. В это время также удаляются все комментарии.
2.3.1 Включение файлов-исходников
«include» включает указанный файл-исходник туда, где эта директива используется. За ней должно следовать в кавычках имя файла, который должен быть включен, например:
include 'macros.inc'
Весь включенный файл обрабатывается препроцессором перед обработкой строк, следующих за содержащей директиву «include». Нет предела для количества включаемых файлов, пока они умещаются в память.
Путь, заключенный в скобки, может содержать окружающие переменные, заключенные в знаки «%», они будут заменены на их значения внутри пути. Знаки «\» и «/» трактуются как разделители пути. Если не указан абсолютный путь, сначала файл ищется в директории, содержащей файл, в который он включается, и, далее, если его там нет, в директории, содержащей главный файл-исходник (указанный в командной строке). Эти правила так же относятся к путям, которые указываются в директиве «file».
2.3.2 Символьные константы
Символьные константы отличаются от числовых констант тем, что перед процессом ассемблирования они заменяются на их значения во всех строках кода, следующих за их определением, и все может стать их значением.
Определение символьных констант состоит из имени константы, за которой следует директива «equ». Все, что следует за этой директивой, станет значением константы. Если значение символьной константы содержит другие символьные константы, они заменяются на их значения перед присвоением значения новой константе. Например:
d equ dword
NULL equ d 0
d equ edx
После этих трех определений значение «NULL» будет «dword 0», а значение «d» будет «edx». Так, например, «push NULL» будет сассемблировано как «push dword 0», а «push d» как «push edx». А, например, в такой строке:
d equ d,eax
константе «d» будет присвоено новое значение «edx,eax». Таким образом могут определяться растущие списки символов.
«restore» позволяет присвоить назад предыдущее значение переопределенной константы. За ней должно следовать одно или больше имен символьных констант, разделенных запятыми. Так, «restore d» после предыдущего переопределения вернет константе значение «edx», следующее применение этой директивы вернет ей значение «dword», а ещё одно применение восстановит первоначальное значение, как будто такая константа не определялась. Если не константа с заданным именем не определена, то «restore» не вызовет ошибку, а будет просто проигнорирована.
Символьные константы могут использоваться для адаптации синтаксиса ассемблера к персональным предпочтениям. Например, следующие определения создают удобные ярлыки для всех операторов размера:
b equ byte
w equ word
d equ dword
p equ pword
f equ fword
q equ qword
t equ tword
x equ dqword
Так как символьная константа может так же иметь пустое значение, она может использоваться для того, чтобы допустить синтаксис со словом «offset» перед каким-нибудь значением адреса:
offset equ
После такого определения «mov ax, offset char» будет правильной конструкцией, которая будет копировать смещение переменной «char» в регистр «ax», так как «offset» заменяется пустым значением, и поэтому игнорируется.
Символьные константы могут также быть определены директивой «fix», которая имеет такой же синтаксис, как «equ», но определяет константы высшего приоритета — они заменяются их символическим значением даже перед совершением директив препроцессора и макроинструкций. Исключением является сама директива «fix», которая имеет наивысший возможный приоритет, и поэтому допускает переопределение констант, заданных таким путем. Но если такие константы высшего приоритета находятся внутри значения, следующего за директивой «fix», они заменяются их значениями перед присвоением этого значения новой константе.
Директива «fix» может использоваться для адаптирования директив препроцессора, что нельзя сделать директивой «equ». Например:
incl fix include
определяет короткое имя для директивы «include», тогда как такое же определение директивой «equ» не даст такого результата, так как стандартные символьные константы заменяются на из значения после поиска строк с директивами препроцессора.
2.3.3 Макроинструкции
«macro» позволяет вам определить собственный комплекс инструкций, называемых макроинструкциями. Их использование может существенно упростить процесс программирования. В своей простейшей форме директива похожа на описание символьной константы. Например, следующая строка определяет ярлык для инструкции «test al,0xFF»:
macro tst {test al,0xFF}
После директивы «macro» должно идти имя макроинструкции и далее её содержание, заключенное между знаками «{» и «}». Вы можете использовать инструкцию «tst» в любом месте после её определения и она будет ассемблирована как «test al,0xFF». Определение символьной константы с таким значением даст похожий результат, различие лишь в том, что имя макроинструкции будет распознаваться только как мнемоник инструкции. Также, макроинструкции заменяются соответствующим кодом даже перед заменой символьных констант на их значения. То есть, если вы вы определите макроинструкцию и символьную константу под одним и тем же именем и используете это имя как мнемоник инструкции, оно будет заменено на содержание макроинструкции, но если вы используете его внутри операндов, имя будет заменено на значение символьной константы.
Определение макроинструкции может сотоять из нескольких строк, потому что знаки «{» и «}» не обязательно должны находиться на одной строке директивой «macro». Например:
macro stos0
{
xor al,al
stosb
}
Макроинструкция «stos0» будет заменена на эти две инструкции ассемблера, где бы он не использовался.
Как и инструкции, которым требуются несколько операндов, для макроинструкции можно задать требование нескольких аргументов, разделяя их запятыми. Имена этих аргументов должны следовать за именем макроинструкции на строке с директивой «macro». В любом месте в макроинструкции, где эти имена появятся, они будут заменены соостветствующими значениями, указанными там, где макроинструкция используется. Вот пример макроинструкции, которая делает выравнивание данных для двоичного формата вывода:
macro align value { rb (value-1)-($+value-1) mod value }
Когда инструкция «align 4» встречается после этого задания макроинструкции, она заменяется на его содержание, и здесь «value» станет 4, а результат будет «rb (4–1)-($+4–1) mod 4».
Если в определении макроинструкции встречается её же имя, то используется предыдущее значение этого имени. Таким образом могут быть сделаны полезные переопределения макросинструкций, например:
macro mov op1,op2
{
if op1 in
& op2 in
push op2
pop op1
else
mov op1,op2
end if
}
Эта макроинструкция расширяет синтаксис инструкции «mov», позволяя обоим операндам быть сегментными регистрами. Например, «mov ds,es» будет ассемблировано как «push es» и «pop ds». Во всех других случаях будет использована стандартная инструкция «mov». Синтаксис этого «mov» может быть расширен далее определением следующей макроинструкции с таким именем, который будет использовать предыдущий:
macro mov op1,op2,op3
{
if op3 eq
mov op1,op2
else
mov op1,op2
mov op2,op3
end if
}
Это позволяет инструкции «mov» иметь три операнда, но она так же все ещё может иметь два операнда, так как если макроинструкции задается меньше аргументов, чем ему требуется, оставшиеся заполняются пустыми значениями. Если заданы три операнда, то макроинструкция превратится в две ранее определенных, то есть «mov es,ds,dx» будет ассемблировано как «push ds», «pop es» и «mov ds,dx».
Если требуется создать макроинструкцию с аргументом, который содержит запятые, этот аргумент следует заключить между «<» и «>». Если он содержит больше одного знака «<», то для окончания его описания должно быть использовано такое же количество «>».
«purge» позволяет отменить последнее определение указанной макроинструкции. За директивой должно следовать одно или больше имен макроинструкций, разделенных запятыми. Если указанная макроинструкция не определена, это не вызовет ошибку. Например, после расширения синтаксиса «mov» вышеуказанными макроинструкциями вы можете отключить синтаксис с тремя операндами, используя директиву «purge mov». Следующее «purge mov» отключит синтаксис для сегментных регистров, а дальнейшее применение этой директивы не возымеет эффекта.
Если после директивы «macro» вы заключаете некоторую группу аргументов в квадратные скобки, это позволит при использовании макроинструкции задать данной группе аргументов больше значений. Любой следующий аргумент данный после последнего аргумента данной группы начнет новую группу и станет её первым членом. Поэтому после закрытия квадратных скобок не должно быть имен аргументов. Содержание макроинструкции будет обрабатываться для каждой такой группы аргументов отдельно. Простейший пример — это заключение одного имени аргумента в квадратные скобки:
macro stoschar [char]
{
mov al,char
stosb
}
Эта макроинструкция допускает неограниченное число аргументов, и каждый будет обработан этими двумя инструкциями отдельно. Например, «stoschar 1,2,3» будет ассемблирован как следующие инструкции:
mov al,1
stosb
mov al,2
stosb
mov al,3
stosb
Существуют некоторые специальные директивы, возможные только внутри определений макроинструкций. Директива «local» задает локальные имена, которые будут заменены уникальными значениями каждый раз, когда используется макроинструкция. За ней должны следовать имена, разделенные запятыми. Эта директива обычно требуется для внутренних констант или меток макроинструкции. Например:
macro movstr
{
local move
move:
lodsb
stosb
test al,al
jnz move
}
Каждый раз, когда используется эта макроинструкция, «move» заменяется новым уникальным именем. То есть вы не получите ошибку, это обычный случай, когда метка определяется больше, чем один раз.
«forward», «reverse» и «common» делят макроинструкцию на блоки, каждый из которых обрабатывается после окончания обработки предыдущего. Они различаются в поведении только если макроинструкция поддерживает много групп аргументов. Блок инструкций, следующий за «forward» будет обрабатываться для каждой группы аргументов от первой до последней, как блок по умолчанию (без этих директив). Блок, идущий за «reverse» будет обрабатываться для каждой группы аргументов в обратном порядке — от последней до первой. Блок за директивой «common» обрабатывается лишь один раз, просто для всех групп аргументов. Локальное имя, определенное в одном блоке, доступно во всех следующих блоках при обработке той же группы аргументов. Если оно было определено в блоке «common», оно доступно во всех следующих блоках, независимо от обрабатываемой группы.
Вот пример макроинструкции, которая создает таблицу адресов строк и следующих за ними строк.
macro strtbl name,[string]
{
common
label name dword
forward
local label
dd label
forward
label db string,0
}
Первый аргумент, задаваемый этой макроинструкции, станет меткой для таблицы адресов, следующими аргументами должны быть строки. Первый блок обрабатывается однажды и определяет метку, второй блок назначает локальную метку для каждой строки и определяет запись в таблице, содержащий адрес этой строки. Третий блок определяет данные каждой строки с соответствующей меткой.
Первая инструкция, следующая за директивой, начинающей блок в макроинструкции, может идти с ней на той же строке, как на следующем примере:
macro stdcall proc,[arg]
{
reverse push arg
common call proc
}
Это макрос может применяться для вызова процедур, используя соглашение STDCALL, аргументы сохраняются в стеке в обратном порядке. Например, «stdcall foo,1,2,3» будет ассемблировано так:
push 3
push 2
push 1
call foo
Если некоторое имя внутри макроинструкции имеет несколько значений (это либо один из аргументов, заключенных в квадратные скобки, либо локальное имя, определенное в блоке, следующем за директивой «forward» или «reverse») и используется в блоке, следующем за директивой «common», оно будет заменено на все значения, разделенные запятыми. Например, следующий макрос передать все дополнительные аргументы ранее определенной макроинструкции «stdcall»:
macro invoke proc,[arg]
{ common stdcall [proc],arg }
Он может применяться для непрямого вызова (через указатель в памяти) процедуры, используя соглашение STDCALL.
Внутри макроинструкции также может быть использован специальный оператор «#». Этот оператор сцепляет два имени в одно. Это может быть полезно, так как делается после того, как аргументы и локальные имена заменяются на свои значения. Следующая макроинструкция генерирует условный переход в зависимости от аргумента «cond»:
macro jif op1,cond,op2,label
{
cmp op1,op2
j#cond label
}
Например, «jif ax,ae,10h,exit» будет ассемблировано как инструкции «cmp ax,10h» и «jae exit».
Оператор «#» может также использоваться для объединения двух строк, заключенных в кавычки.
Возможно преобразование имени в строку в кавычках с помощью оператора «`», который также может быть использован внутри макроинструкции. Он конвертирует следующее за ним имя в строку, заключенную в скобки, но имейте в виду, что если за ним следует аргумент, который заменяется на значение, содержащее больше, чем один символ, будет преобразован только первый из них, так как оператор «`» конвертирует только символ, идущий непосредственно за ним. Здесь пример использования этих двух свойств:
macro label name
{
label name
if ~ used name
display `name # "is defined but not used.",13,10
end if
}
Если метка, определенная таким макросом, не используется в коде, он известит вас об этом сообщением, указывающим, к какой метке это относится.
Чтобы создать макроинструкцию, ведущую себя по-разному в зависимости от типа аргументов, например если это строки в кавычках, вы можете использовать оператор сравнения «eqtype». Вот пример его использования для отделения строки в кавычках от других типов аргументов:
macro message arg
{
if arg eqtype ""
local str
jmp @f
str db arg,0Dh,0Ah,24h
@@:
mov dx,str
else
mov dx,arg
end if
mov ah,9
int 21h
}
Вышеописанный макрос создан для показа сообщений в программах DOS. Если аргумент этого макроса некоторое число, метка или переменная, показывается строка из этого адреса, но если аргумент — это строка в кавычках, то созданный код покажет её после … и … .
Также возможно объявить макроинструкцию внутри другой макроинструкции, то есть один макрос может определить другой, но с такими определениями есть проблема, вызванная тем, что знак «}» не может появляться внутри макроинструкции, он всегда означает конец его определения. Чтобы обойти эту проблему, можно избавиться от мешающих символов. Это делается путем подстановки одного или больше обратных слэшей перед любыми другими символами (даже специальными знаками). Препроцессор видит эту последовательность как один символ, но каждый раз, когда он видит такой символ во время обработки макроса, он обрезает обратные слэши с его начала. Например, «\{» трактуется как один символ, но во время обработки макроса он станет символом «\{». Это позволит вам определить одну макроинструкцию внутри другой:
macro ext instr
{
macro instr op1,op2,op3
\{
if op3 eq
instr op1,op2
else
instr op1,op2
instr op2,op3
end if
\}
}
ext add
ext sub
Макрос «ext» определен корректно, но когда он используется, символы «\{» и «\}» становятся «{» и «}». То есть когда обрабатывается «ext add», содержание макроса становится действительным определением макроинструкции, и таким образом определяется макрос «add». Так же «ext sub» определяет макрос «sub». Использование символа «\{» не было здесь действительно необходимо, но сделано таким образом для того, чтобы определение было более ясным.
Если некоторые директивы, специфические для макроинструкций, такие как «local» или «common», требуются в некотором макросе, включенном таким образом, то их можно избежать таким же путем. Исключение символа больше чем одним обратным слэшем так же поддерживается, это позволяет допустить множественные уровни вложения определений макросов.
Другая техника определения макроинструкций внутри других состоит в использовании директивы «fix», которая становится полезной, когда некоторый макрос только начинает определение другого, без его закрытия. Например:
macro tmacro params
{
macro params {
}
MACRO fix tmacro
ENDM fix }
определяет альтернативный синтаксис определения макросов, который выглядит как:
MACRO stoschar char
mov al,char
stosb
ENDM
Имейте в виду, что таким образом заданное определение должно быть создано с применением директивы «fix», так как перед тем, как процессор ищет знак «}» во время определения макроса, обрабатываются только символьные константы высшего приоритета! Может возникнуть проблема, если требуется выполнить некоторые дополнительные задания в конце такого определения, но есть еще одно свойство, которое в таких случаях поможет вам. А именно возможно поместить любую директиву, инструкцию или макроинструкцию сразу после символа «}», который заканчивает макроинструкцию и она будет обработана так же, как если бы была на следующей строке.
2.3.4 Структуры
«struc» — это специальный вариант директивы «macro», который используется для определения структур данных. Макроинструкции, определенные директивой «struc», когда используются, должны предваряться меткой (как директивы определения данных). Эта метка будет также присоединена к началу каждого имени, начинающегося с точки, в содержании макроинструкции. Макроинструкция, определенная с использованием директивы «struc», может иметь такое же имя, как макросы, определенные с использованием директивы «macro». Структурная макроинструкция не будет мешать обычному макросу, выполняющемуся без метки перед ним и наоборот. Все правила и свойства, касающиеся стандартных макросов, применимы к структурным макроинструкциям.
Вот пример структуры:
struc point x,y
{
.x dw x
.y dw y
}
Например «my point 7,11» определит структуру, помеченную «my», содержащую две переменные: «my.x» со значением 7 и «my.y» со значением 11.
Еслигде-то в определении структуры находится имя, состоящие из одной лишь точки, оно заменяется на имя метки для данного примера структуры и эта метка таким образом не будет определена автоматически, позволяя полностью задать определение. Следующий пример использует это свойство, чтобы расширить определение директивы «db» с возможностью вычисления размера определяемых данных:
struc db [data]
{
common
. db data
.size = $ —.
}
Таким образом строка «msg db 'Hello!',13,10» определит так же константу «msg.size», равную размеру определяемых данных в байтах.
Определение структур данных, адресованных регистрами или абсолютными значениями может быть сделано структурными макроинструкциями с использованием директивы «virtual» (смотрите 2.2.3).
«restruc» удаляет последнее определение структуры, так же как «purge» делает с макросами и «restore» с символьными константами. Директива имеет тот же синтаксис — за ней должно следовать одно или несколько имен структурных макросов, разделенных запятыми.
2.3.5 Повторение макроинструкций
Директива «rept» — это специальный вид макроинструкций, который делает заданное число дубликатов блока, заключенного в фигурные скобки. Простой синтаксис — число, следующее за «rept» (это не может быть выражение, так как препроцессор не совершает вычисления, если вам нужны повторения, базирующиеся на выражениях, вычисленных ассемблером, используйте одну из директив, обрабатываемых ассемблером, смотрите 2.2.2), и блок кода, заключенный между знаками «{» и «}». Простейший пример:
rept 5 { in al,dx }
создает пять дубликатов строки «in al,dx». Блок инструкций определяется таким же образом, как для стандартных макросов, и допускаются все специальные операторы и директивы, которые могут использоваться только внутри макроинструкций. Если заданное число равно нулю, блок просто пропускается, как если бы вы определили макрос, но не использовали его. За количеством повторений может следовать имя символа-счетчика, который символьно будет заменяться на номер текущего повторения. Таким образом:
rept 3 counter
{
byte#counter db counter
}
Сгенерирует строки:
byte1 db 1
byte2 db 2
byte3 db 3
Механизм повторения, применяемый к блокам «rept» такой же, как тот, что используется для обработки множественных групп аргументов макросов, то есть директивы, такие как «forward», «common» и «reverse» могут использоваться их обычном значении.
Итак, такой макрос:
rept 7 num { reverse display `num }
покажет символы от 7 до 1 как текст. Директива «local» работает так же, как внутри макросов с несколькими группами аргументов, то есть:
rept 21
{
local label
label: loop label
}
сгенерирует уникальную метку для каждого дубликата. Символ-счетчик обычно начинает с 1, но вы можете объявить другое базовое значение, предваренное запятой, сразу же после имени счетчика. Например:
rept 8 n:0 { pxor xmm#n,xmm#n }
Сгенерирует код, очищающий содержимое регистров SSE. Вы можете определить несколько счетчиков, разделенных запятыми, и каждый может иметь свою базу.
«irp» итерирует один аргумент через данный список параметров. Синтаксис такой: за «irp» следует имя аргумента, далее запятая и далее список параметров. Параметры определяются таким же образом, как в вызове стандартного макроса, то есть они должны разделяться запятыми и каждый может быть заключен между знаками «<» и «>». Так же за именем аргумента может следовать «*» для обозначения того, что он не может иметь пустое значение. Такой блок:
irp value, 2,3,5
{ db value }
сгенерирует строки:
db 2
db 3
db 5
«irps» итерирует через данный список символов, за директивой должно следовать имя аргумента, далее запятая и далее последовательность любых символов. Каждый символ в последовательности, независимо от того, символы ли это имен, знаки символов или строки в кавычках, становится значением аргумента на одну итерацию. Если за запятой никаких символов не следует, то итераций не производится вообще. Этот пример:
irps reg, al bx ecx
{ xor reg,reg }
сгенерирует строки:
xor al,al
xor bx,bx
xor ecx,ecx
Блоки, определенные директивами «irp» и «irps», обрабатываются так же, как макросы, то есть операнды и директивы, специфичные для макросов могут в них свободно использоваться.
2.3.6 Условный препроцессинг
При применении директивы «match» некоторый блок кода обрабатывается препроцессором и передаётся ассемблеру, только если заданная последовательность символов совпадает с образцом. Образец идет первым, заканчивается запятой, далее идут символы, которые должны подходить под образец, и далее блок кода, заключенный в фигурные скобки, как макроинструкция.
Есть несколько правил для построения выражения для сравнения, первое — это любые символьные знаки и строки в кавычках должны соответствовать абсолютно точно. В этом примере:
match +,+ { include 'first.inc' }
match +,- { include 'second.inc' }
Первый файл будет включен, так как «+» после запятой соответствует «+» в образце, а второй файл не будет включен, так как совпадения нет.
Чтобы соответствовать любому другому символу буквально, он должен предварятья знаком «=» в образце. Также чтобы привести в соответствие сам знак «=», или запятую должны использоваться конструкции «==» и «=,». Например, образец «=a==» будет соответствовать последовательности «a=».
Если в образце стоит некоторый символ имени, он соответствует любой последовательности, содержащей по крайней мере один символ и его имя заменяется на поставленную в соответствие последовательность везде в следующем блоке, аналогично параметрам в макроинструкции. Например:
match a-b, 0–7
{ dw a,b-a }
сгенерирует инструкцию «dw 0, 7–0». Каждое имя всегда ставится в соответствие как можно меньшему количеству символов, оставляя оставшиеся, то есть:
match a b, 1+2+3 { db a }
имя «a» будет соответствовать символу «1», оставляя последовательность «+2+3» в соответствие с «b». Но, таким образом:
match a b, 1 { db a }
для «b» ничего не остается, и блок вообще не будет обработан.
Блок кода, определенный директивой «match» обрабатывается так же, как любая макроинструкция, поэтому здесь могут использоваться любые операторы, специфичные для макроинструкций.
Что делает директиву «match» очень полезной, так это тот факт, что она заменяет символьные константы на их значения в поставленной в соответствие последовательности символов (то есть везде после запятой до начала блока кода) перед началом сопоставления. Благодаря этому директива может использоваться, например, для обработки некоторого блока кода в зависимости от выполнения условия, что данная символьная константа имеет нужное значение, например:
match =TRUE, DEBUG { include 'debug.inc' }
здесь файл будет включен, только если символьная константа «DEBUG» определена со значением «TRUE».
2.3.7 Порядок обработки
При сочетании разных свойств препроцессора важно знать порядок, в котором они обрабатываются. Кат уже было отмечено, высший приоритет имеет директива «fix» и замены, ею определенные. Это полностью делается перед совершением любого другого препроцессинга, поэтому такой кусок кода:
V fix {
macro empty
V
V fix }
V
делает допустимое определение пустого макроса. Можно сказать, что директива «fix» и приоритетные константы обрабатываются на отдельной стадии, и весь остальной препроцессинг делается на результирующем коде.
Стандартный препроцессинг, который начинается после, на каждой строке начинается с распознавания первого символа. Сначала идет проверка на директивы препроцессора, и если ни одна из них не опознана, препроцессор проверяет, является ли первый символ макроинструкцией. Если макроинструкция не найдена, препроцессор переходит ко второму символу на строке, и снова начинает с проверки на директивы, список которых в этом случае ограничивается лишь «equ», так как только она может оказать вторым символом на строке. Если нет директивы, второй символ проверяется на структурную макроинструкцию, и если ни одна из этих проверок не дала положительного результата, символьные константы заменяются на их значения, и строка передается ассемблеру.
Продемонстрируем это на примере. Пусть «foo» — это макрос, а «bar» — это структура. Эти строки:
foo equ
foo bar
обе будут интерпретированы как вызовы макроса «foo», так как значение первого символа берет верх над значением второго.
Макроинструкции генерируют новые строки от их блоков определения, заменяя параметры на их значения и далее обрабатывая операторы «#» и «`». Оператор конверсии имеет высший приоритет, чем оператор сцепления.
После завершения этого, заново сгенерированная строка проходит через стандартный препроцессинг, как описано выше.
Хотя обычно символьные константы заменяются исключительно в строках, нет ни директив препроцессора, ни макроинструкций, встречается несколько особых ситуаций, где замены проводятся в частях строк, содержащих директивы. Первая — это определение символьной константы, где замены производятся везде после слова «equ» и результирующее значение присваивается новой константе (смотрите 2.3.2). Вторая такая ситуация — это директива «match», где замены производятся в символах, следующих за запятой перед сопоставлением их с образцом. Эти свойства могут использоваться, например, для сохранения списков, как, например, эта совокупность определений:
list equ
macro append item
{
match any, list \{ list equ list,item \}
match, list \{ list equ item \}
}
Здесь константа «list» инициализируется с пустым значением, и макрос «append» может использоваться для добавления новых пунктов к списку, разделяя их запятыми. Первое сопоставление в этом макросе происходит, только если значение списка непусто (смотрите 2.3.6), таким образом новое его значение — это предыдущее с запятой и новым пунктом, добавленным в конец. Второе сопоставление происходит, только если список все еще пуст, и таким образом список определяется как содержащий только лишь новый пункт. Так, начиная с пустого списка, «append 1» определит «list equ 1», а «append 2», следующий за ним, определит «list equ 1,2». Может потребоваться использовать этот список как параметры к некоторому макросу. Но это нельзя сделать прямо — если «foo» это макрос, то в строке «foo list» символ «list» просто прошел бы как параметр к макросу, поскольку символьные константы на этой стадии ещё не развернуты. Для этой цели снова оказывается удобна директива «match»:
match params, list { foo params }
Значение «list», если оно не пустое, соответствует ключевому слову «params», которое далее во время генерации строк, заключенных в фигурные скобки, заменяется на соответственное значение. Так, если «list» имеет значение «1,2», строка, указанная выше, сгенерирует строку, содержащую «foo 1,2», которая далее пройдет стандартный препроцессинг.
Есть ещё один особый случай — когда препроцессор собирается проверить второй символ и натыкается на двоеточие (что далее интерпретируется ассемблером как определение метки), он останавливается в этом месте и заканчивает препроцессинг первого символа (то есть если это символьная константа, она развертывается) и если это все еще выглядит меткой, совершается стандартный препроцессинг, начиная с места после метки. Это позволяет поместить директивы препроцессора и макроинструкции после меток, аналогично инструкциям и директивам, обрабатываемым ассемблером, например:
start: include 'start.inc'
Однако если метка во время препроцессинга разрушается (например, если у символьной константы пустое значение), происходит только замена символьных констант до конца строки.
2.4 Директивы форматирования
«format» со следующим за ним идентификатором формата позволяет выбрать формат вывода. Эта директива должна стоять в начале кода. Формат вывода по умолчанию — это простой двоичный файл, он может быть также выбран директивой «format binary».
«use16» и «use32» указывают ассемблеру генерировать 16-битный или 32-битный код, пренебрегая настройкой по умолчанию для выбранного формата вывода. «use64» включает генерирование кода для длинного режима процессоров x86.
Ниже описаны разные форматы вывода со специфичными для них директивами.
2.4.1 MZ
Чтобы выбрать формат вывода MZ, используйте директиву «format MZ». По умолчанию код для этого формата 16-битный.
«segment» определяет новый сегмент, за ним должна следовать метка, чьим значением будет номер определяемого сегмента. Опционально за этой директивой может следовать «use16» или «use32», чтобы указать битность кода в сегменте. Начало сегмента выровнено по параграфу (16 байт). Все метки, определенные далее, будут иметь значения относительно начала этого сегмента.
«entry» устанавливает точку входа для формата MZ, за ней должен следовать дальний адрес (имя сегмента, двоеточие и смещение в сегменте) желаемой точки входа.
«stack» устанавливает стек для MZ. За директивой может следовать числовое выражение, указывающее размер стека для автоматического создания, либо дальний адрес начального стекового фрейма, если вы хотите установить стек вручную. Если стек не определен, он будет создан с размером по умолчанию в 4096 байт.
«heap» со следующим за ней значением определяет максимальный размер дополнительного места в параграфах (это место в добавление к стеку и для неопределенных данных). Используйте «heap 0», Чтобы всегда отводить только память, которая программе действительно нужна.
2.4.2 PE
Чтобы выбрать формат вывода PE, используйте директиву «format PE», за ней могут следовать дополнительные настройки формата: используйте «console», «GUI» или оператор «native», чтобы выбрать целевую субсистему (далее может следовать значение с плавающей точкой, указывающее версию субсистемы), «DLL» помечает файл вывода как динамическую связывающую библиотеку. Далее может следовать оператор «at» и числовое выражение, указывающее базу образа PE и далее опционально оператор «on» со следующей за ним строкой в кавычках, содержащей имя файла, выбирающей заглушку MZ для PE программы (если указанный файл не в формате MZ, то он трактуется как простой двоичный исполняемый файл и конвертируется в формат MZ). По умолчанию код для этого формата 32-битный. Пример объявления формата PE со всеми свойствами:
format PE GUI 4.0 DLL at 7000000h on 'stub.exe'
«section» определяет новую секцию, за ней должна следовать строка в кавычках, определяющая имя секции, и далее могут следовать один или больше флагов секций. Возможные флаги такие: «code», «data», «readable», «writeable», «executable», «shareable», «discardable», «notpageable». Начало секции выравнивается по странице (4096 байт). Пример объявления секции PE:
section '.text' code readable executable
Вместе с флагами также может быть определен один из специальных идентификаторов данных PE, отмечающий всю секцию как специальные данные, возможные идентификаторы: «export», «import», «resource» и «fixups». Если секция помечена для содержания настроек адресов, они генерируются автоматически, и никаких данных определять больше не требуется. Также данные ресурсов могут быть сгенерированы автоматически из файлов ресурсов, этого можно добиться, написав после идентификатора «resourse» оператор «from» и имя файла в кавычках. Ниже вы можете увидеть примеры секций, содержащих некоторые специальные данные:
section '.reloc' data discardable fixups
section '.rsrc' data readable resource from 'my.res'
«entry» создает точку входа для PE, далее должно следовать значение точки входа.
«stack» устанавливает размер стека для PE, далее должно следовать значение зарезервированного размера стека, опционально может следовать отделенное запятой значение начала стека. Если стек не определен, ему присваивается размер по умолчанию, равный 4096 байт.
«heap» выбирает размер дополнительного места для PE, далее должно следовать значение для зарезервированного для него места, опционально ещё может быть значение его начала, отделенное запятой. Если дополнительное место не определено, оно ставится по умолчанию равным 65536 байт, если не указано его начало, то оно устанавливается равным 0.
«data» начинает определение специальных данных PE, за директивой должен следовать один из идентификаторов данных («export», «import», «resource» или «fixups») или номер записи данных в заголовке PE. Данные должны быть определены на следующих строках и заканчиваться директивой «end data». Если выбрано определение настроек адресов, они генерируются автоматически, и никаких данных определять больше не требуется. То же самое относится к ресурсам, если за идентификатором «resourse» следует оператор «from» и имя файла в кавычках — в этом случае данные берутся из этого файла ресурсов.
2.4.3 COFF
Чтобы выбрать COFF (Common Object File Format), используйте директиву «format COFF» или «format MS COFF», если вы хотите создать классический мелкософтофский файл COFF. По умолчанию код для этого формата 32-битный. Чтобы создать микросфтовский формат COFF для архитектуры x86-64, используйте установку «format MS64 COFF», в этом случае автоматически будет генерироваться код длинного режима.
«section» определяет новую секцию, за директивой должна следовать строка в кавычках, определяющая имя новой секции, и ещё может следовать один или более флагов секций. Возможные флаги такие: «code» и «data» для обоих вариантов COFF, «readable», «writeable», «executable», «shareable», «discardable» и «notpageable» только для микросовтофского варианта COFF. По умолчанию секция выровнена по двойному слову (четыре байта), но микросовтовский вариант COFF можно выровнять еще как-нибудь по-другому с помощью оператора «align» и следующим за ним значением выравнивания (любая степень двойки от двух до 8192) среди фагов секций.
«extrn» определяет внешний символ, за ним должно следовать имя символа и опционально оператор размера, указывающий размер данных, помеченных этим символом. Имя символа также может предваряться строкой в кавычках, содержащей имя внешнего символа и оператор «as». Пара примеров объявления внешних символов:
extrn exit
extrn 'impMessageBoxA@16' as MessageBox: dword
«public» объявляет существующий символ как общедоступный, за ним должно следовать имя символа, и далее опционально оператор «as» и строка в кавычках, содержащая имя, под которым символ будет действителен как общедоступный. Пара примеров объявления общедоступных символов:
public main
public start as '_start'
2.4.4 ELF
Чтобы выбрать формат вывода ELF, используйте директиву «format ELF». По умолчанию код для этого формата 32-битный. Чтобы создать формат ELF для архитектуры x86-64, используйте установку «format ELF», в этом случае автоматически будет генерироваться код длинного режима.
«section» определяет новую секцию, за директивой должна следовать строка в кавычках, определяющая имя новой секции, и ещё может следовать один или оба флага «executable» и «writeable», опционально также может идти оператор «align» со следующим за ним числом, определяющим выравнивание секции (это должна быть степень двойки), если выравнивание не указано, используется значение по умолчанию, которое равно 4 или 8, в зависимости от варианта выбранного формата.
«extrn» и «public» имеют те же значения и синтаксис у них, как в случае формата COFF (описанного в предыдущем параграфе).
Чтобы создать исполняемый файл, придерживайтесь директивы выбора формата со словом «executable». Это позволяет использовать директиву «entry» со следующим за ним значением, чтобы создать точку входа в программу. С другой стороны это делает недоступными директивы «extrn» и «public». За директивой «section» в этом случае может следовать только один или более флагов секций и начало секции будет выровнено по странице (4096 байт). Доступные флаги секций такие: «readable», «writable» и «executable».
Комментарии к книге «Flat Assembler 1.64. Мануал программера», Tomasz Grysztar
Всего 0 комментариев