allasm.ru |
|
В этом руководстве раскрывается тема создания, использования (а главное – проектирования) макросов и макрофункций в проектах на MASM32. Что не так важно в ЯВУ, то очень важно в программировании на ассемблере. Если выстроить по приоритетам недостатки программирования на ассемблере, то первым недостатком будет не объём строк написанного кода (как нестранно), а отсутствие средств, обеспечивающих хороший стиль написания кода. Что значит стиль? А что значит плохой или хороший? Это можно быстро понять на простом примере. Допустим, у вас есть процедура объёмом на несколько экранов. Вы её написали месяц назад, а теперь вам нужно несколько изменить её поведение. Для того, чтобы сделать это, вам необходимо:
Если код процедуры был прооптимизирован, вероятней всего вам захочется, чтобы после модификации он остался настолько же оптимальным, а поэтому вы должны вспомнить все тонкости кода, или только того участка, который подлежит модификации. А это не так то просто, даже если исходник написан вами, в вашем неповторимом стиле. Если этот стиль будет хорошим, вы потратите меньшее время, если бы стиль был бы плохим.
Конечно же, на ЯВУ легче писать качественно оформленные программы, хотя бы, потому что ЯВУ уже имеет готовые средства выражения, и шаблоны мышления.
Недавно я прочёл следующую мысль на форуме WASM.RU:
Такое заявление говорит, что программист не желает писать проекты более чем на 6 000 строк (или 3 000 :)). Вместо того чтобы извлечь великую выгоду из единообразия кода, мы ругаем его. А ведь это первый звонок к автоматизации программирования.
Ассемблер не определяет шаблонов мышления, и практически не имеет средств выражения каких либо шаблонов (из-за чего автор пользуется им).
Однако я могу ручаться, если вы научитесь писать качественно стилизированные программы на ассемблере, то на ЯВУ… :). Об искусстве стилизации или проектировании архитектуры написано слишком мало, а рассказать хотелось бы слишком много. Только нельзя объять необъятное, и потому цель этого руководства рассказать об использовании макросов в MASM32, а также о том, как их можно либо нужно использовать, чтобы более качественно стилизировать код. I.1 Для тех, кто впервые...Если вы ещё не работали с макросами, или работали, но очень мало, я спешу признаться, что это руководство не предназначалось для начинающих. Но благодаря рекомендациям и советам TheSvin/HI-TECH я решился добавить в него вырезки и упражнения, которые позволят вам быстро войти во вкус макромира MASM32. Если же вы уже имеете дело с макросами, тогда это руководство укрепит ваши знания и представления по данной теме. Для исследования макромира MASM мы воспользуемся директивой echo, которая позволит вывести нам на экран то, что творится в препроцессоре MASM. Очень удобно, а главное наглядно. Я уверен, что вы быстро усвоите этот материал. I.2. Примечания (обо всём понемногу)В данной работе я часто пишу: «Препроцессор ML». Кто-то из умников (или просто жаждущих подловить «на горячем») воскликнет: «Да какой же такой ML.EXE – препроцессор? Наглая ложь». На всякий случай оговорю, что здесь имеется ввиду не утверждение «ML – препроцессор», а именование его подсистемы – препроцессор. Всё, что есть в этом руководстве не взято с потолка, и не является вымышленным. Весь код проверен, и работает именно так как описано, если только автор случайно не ошибся, что так же случается. Многое из того, что написано в этом руководстве недокументированно (или плохо документировано) в официальном. Поэтому вы всегда должны помнить, что если в следующих версиях ML (например, 8.0) что-то не будет работать, никто не виноват. Если вы думаете, что я дизассемблировал ML.EXE – то ошибаетесь. Алгоритмы работы, приведённые здесь, получены логическим путём на основе знаний работы компиляторов, а поэтому их не следует воспринимать как истинные. Важна сама логика работы, понимание которой, поможет вам безболезненно использовать макро, допуская меньшее количество ошибок. На самом деле MASM очень плохо документирован, и видно MS совсем не относится к нему как к продукту (что вполне очевидно). Хотя уже в MSDN 2002 был внесён раздел MASM Reference, и всё равно – вы не найдёте лучше описания чем в MASM32 by Hutch. Когда вы прочтете, то воскликните: «Да, зачем мне такой ML?». Есть NASM и FASM – главная надежда мира ассемблерщиков. Однако и теперь ML всё ещё выигрывает у них по удобству эксплуатации, большей частью видимо благодаря Хатчу, и многим замечательным людям, поддерживающим MASM32. Кто знает, может после этой статьи кто-то воскликнет: «Я знаю, какой должен быть компилятор мечты асмовцев!». И напишет новый компилятор. (Автор шутит ?) Уверен, что программисты из MS вряд ли прочтут эту статью (они плохо знакомы с русским), и оно к лучшему. Возможно, такая статья могла бы их огорчить, а я не люблю портить настроение людям, трудами которых пользуюсь. (Снова шутит, только про что?) И наконец-то мне в свою очередь хочется порадоваться, что многие вопросы по макросам в MASM закрыты на долгое время, во всяком случае, для русскоязычной аудитории. (Шутит, или нет? Гм…) I.3. Особенности терминологииТерминология этой статьи различается от терминологии принятой в MASM. В частности автором было предложено называть: MacroConstant EQU 123 ;; Числовая макроконстанта В MASM: MacroConstant EQU 123 ;; numeric equates Можно было бы попросту выбрать терминологию MASM, однако последняя не позволяет объяснять материал систематически. То есть все четыре вида выражений – по сути, являются переменными или константами. Однако в терминологии MASM два последних определения называются текстовыми макро, подчёркивая их связь с макросами. Если пойти этим путём, то тогда и первые два определения – являются упрощёнными определениями макро. Если разработчики желали подчеркнуть, что сама суть внутренней реализации ML представляет текстовые макросы как макро, то тогда не ясны те все эффекты функциональности, обсуждаемые в этой статье.
Этот спор не решаем, что не так и важно. Поэтому автор отдаёт предпочтение двум терминам для «text macro»: «текстовой макро» и «строковая макропеременная». Понятие: «numeric equates» является общим для первых двух случаев, и разрывает смысловую связь с двумя последними определениями. Поэтому я пользуюсь своим вариантом терминологии, который подчёркивает, что определения: MacroConstant EQU 123 ;; Числовая макроконстанта являются подобными макро. А, кроме того, первое из низ – константа, а второе – переменная. I.4. БлагодарностиНе могу не написать этот пункт, ибо не только автору обязана эта статья. Она обязана замечательной версии Win98 с инсталляцией от 2000, которая отформатировала весь мой винчестер, и унесла в небытие первый вариант настоящей статьи. :) Не малая заслуга в вопросе терминологии MASM, и его разрешении принадлежит Four-F, который как он сам мне признался, съел на макросах собаку, при чём без соли :). Когда я думаю, чтобы было бы без самого Маниакального редактора в Inet, CyberManiacа, то понимаю: без его правок мои статьи приводили бы в ужас, и лишали разума всех морально неустойчивых читателей. CyberManiac: «Только такой замечательный безумец как ты может выдержать ЭТО!!!» :). FatMoon, Rustam, The Svin – вы дали понять мне то, что такая статья действительно нужна, и это, наверное, самое главное. Вряд ли я бы так долго работал над ней, если бы меня никто не подталкивал. Всех кого я забыл поблагодарить здесь, и кого не забыл, жду в условном
месте в условное время для раздачи благодарностей. Когда говорят, что лень – это двигатель прогресса, видимо лицемерят или преувеличивают. Скорее это нежелание выполнять одну и ту же работу очень часто. Первая парадигма к созданию макро звучит так:
Ассемблер, дающий программисту полную свободу в использовании методик программирования, совершенно лишает его средств для выражения этих методик. Например, ООП. В MASM32 нет классов, конструкторов и других механизмов, поддерживающих эту абстракцию. Зато вместо ООП Вы можете придумать множество других методик и абстракций (как, например модель серверов). Та или иная методика программирования обязательно состоит из каких-либо компонентов, которые являются подобными друг другу. Например, следующие макро очень любимы в примерах пакета MASM32: m2m MACRO M1, M2 push M2 pop M1 ENDM return MACRO arg mov eax, arg ret ENDM Предположим, что кому-то так надоело писать: push переменная2 И он решил придумать макро для этого. Эта пара команд осуществляет пересылку данных из одной ячейки памяти в другую. То есть теперь в программе, когда вы захотите написать push/pop, вы можете заменить это некой m2m операнд1, операнд2. Посмотрите на эти два участка кода: mov wc.cbWndExtra, NULL Первый вариант не только занимает меньше строк (что тоже важно), но и намного понятнее, чем push/pop (если вы, знаете что такое m2m). Конечно, если говорить о макро m2m, то он имеет и очень важный недостаток.
Этот недостаток потеря контроля над оптимальностью кода. Например, более быстрыми, по сравнению с парой команд push/pop, являются mov eax,… Употребляя макро m2m, вы получаете худший код, если стремитесь оптимизировать по скорости. И здесь есть две стороны проектирования кода:
Используя макро m2m, вы повышаете уровень стилистики, так как сокращаете время на понимание исходного кода (вами же или другим программистом). Однако с другой стороны вы теряете эффективность.
Другая парадигма использования макро звучит так:
Эта парадигма отличается от предыдущей тем, что создание макроопределения обуславливается только улучшением стилизации кода, и не имеет особой практической ценности. Например, я объявил такие макро для определения кода начала и конца в главном модуле программы: $$$WIN32START macro В этих макро нет по сути никакой пользы, кроме эстетической. Зато, глядя на код, можно сразу понять, что это не что иное, как начало программы нечто вроде main() в C++. И последняя парадигма использования макро:
Наиболее важная часть использования макро. Посмотрите, например, файл Objects.INC из пакета MASM32 в папке oop (NaN & Thomas). Наверное, вы знаете, что EXE приложения всегда могут загружаться по адресу равному: PROGRAM_IMAGE_BASE EQU 400000h Во-первых, это даёт нам право убрать из приложения всю Relock секцию, тем самым, уменьшив объём образа (если эта секция нужна для систем плагинов, её можно держать отдельно). Во-вторых мы можем более не вызывать функцию GetModuleHandle, что так же полезно для нас. Использование константы PROGRAM_IMAGE_BASE очень удобно. Однако, что будет значить это удобство, если всё-таки PROGRAM_IMAGE_BASE не определено? Это будет означать, что мы обязаны переписать весь код. А если этого кода много? Определённо об этом нужно позаботится заранее. Давайте же будем решать эту проблему при помощи макро! Для этого нам станут необходимыми некоторые знания о том, как обрабатывается макро, и что это такое. III. Макромир MASMМакрос представляет собой именованный участок исходного текста программы, который обрабатывается компилятором каждый раз в том месте, где вызывается макрос.
С этого момента вам придётся различать в ассемблере ML две подсистемы: препроцессор и компилятор. Если компилятор переводит код мнемоник в машинный код, вычисляет значения меток и смещений, то препроцессор занимается вычислением выражений этапа компиляции, и что самое важное – процессом раскрытия макросов. Подобно многим объектам мира программирования макро имеет два состояния в исходном тексте: определение, и использование. Таким образом, мы будем иметь дело с определением макроса (макроопределением),
и его вызовом (использованием макроса). MacroName macro paramlist При каждом вызове макро, а именно: … Будет анализироваться и исполнятся текст, заключённый в макро. Именно так это и реализовано в ML. Поскольку текст в макроопределении не компилируется, то естественно, вы не увидите сообщений об ошибке, даже если с точки зрения ассемблера эта ошибка будет в теле макроопределения. Однако ошибка появится при попытке вызова макроопределения, её могут выдать вам, либо сам препроцессор, либо компилятор, если текст, сгенерированный препроцессором является неверным с точки зрения компилятора. Каждый раз, когда препроцессор встречает макроопределение, он помещает его имя в специальную таблицу, и копирует его тело к себе в память (это не обязательно именно так, но вам должна быть понятна суть). Встретив макроопределение, препроцессор не проверяет, а есть ли макро с таким же именем. Это значит, что макро можно переопределять. MyMacro macro echo Это макро 1 endm MyMacro macro echo Это макро 2 endm MyMacro Вы можете самостоятельно удалять макроопределения, из памяти препроцессора используя директиву PURGE: PURGE macroname Эта директива удаляет тело макроопределения, однако не удаляет имя макро из таблицы имён. Таким образом, в данном случае: MyMacro macro mov eax,ebx endm PURGE MyMacro ;; После этой директивы, MyMacro эквивалентен: ;; MyMacro macro ;; endm ;; Определению пустого макро. MyMacro ;; Ничего не произойдёт. Разработчики ML задумывали эту директиву для разрешения конфликтов между файлами с множеством макросов, однако мне совершенно не ясно как ей можно воспользоваться. Если вы хотите получить эффект «удаления» макро, лучше применять следующий метод: <Имя макро, который нужно «удалить»> macro В этом случае при попытке воспользоваться таким макро, компилятор выдаст ошибку, и вы будете проинформированы о его вызове, что намного лучше неведения. Поэтому просто запомните: «Не нужно использовать директиву PURGE». Конечно же, использование макро не было бы столь полезным, если бы макро не имел формальных параметров. При вызове макро, препроцессор заменяет все имена формальных параметров их непосредственными значениями в теле макроопределения. Список формальных параметров разделяется запятой, и может иметь вид: MyMacro macro param0, param1:REQ, param2 := <0>,param3:VARARG Здесь:
Param3:vararg – становится именем параметра, который воспринимает всё остальное как строку. При этом запятые между параметрами так же попадают в строку, а значит число параметров макроса в принципе неограниченно.
Конечно же, после параметра с директивой vararg не возможно объявить другие параметры.
Можно различать два вида макро – макропроцедуры и макрофункции.
Макрофункции в отличие от макропроцедур могут возвращать результат, и получают список формальных параметров в скобках, подобно функциям в С. Например: mov eax,@GetModuleHandle() Заметьте, что к макрофункции невозможно обратится как к макро, вы всегда должны заключать формальные параметры макрофункции между «()», иначе MASM не будет распознавать её как макрофункцию: mov eax,@GetModuleHandle Препроцессор MASM анализирует текст макроопределения на наличие директивы exitm, и помечает макрос как макрофункцию. Ключевое слово exitm <retval>, аналогично оператору return в C++, выполнение макро заканчивается, и возвращается необязательный параметр retval. Этот параметр – строка, которую должен вернуть макрос.
;####################################################### @GetModuleHandle macro Invoke GetModuleHandle,0 exitm
Заметьте, что макропроцедура может быть вызвана только в начале строки: @GetModuleHandle Макрофункция может быть вызвана в любых выражениях: III.1. Функционирование макросов;; Так: Чтобы строить макросы, важно понимать, как они работают, и как их обрабатывает MASM. Давайте рассмотрим типичный макро, и этапы его обработки. MyMacro macro param1,param2,param3:VARARG echo param1 echo param2 echo param3 endm MyMacro Параметр 1, Параметр 2, Параметр 3, Параметр 4 ;; Вывод -=-=-=-=-=-=-=-= Параметр 1 Параметр 2 Параметр 3,Параметр 4 1. Компилятор встречает лексему MyMacro 2. Он проверяет, содержится ли эта лексема в словаре ключевых слов 3. Если нет, то он проверяет, содержится ли эта лексема в списке макросов. 4. Если да, он передаёт текст, содержащийся в макро препроцессору. Препроцессор заменяет все вхождения формальных параметров в этом тексте на их значения. В данном случае мы имеем: echo Параметр 1 5. Препроцессор возвращает компилятору обработанный текст, который после компилируется. Обратите внимание на пункт 4 и 5. Они ключевые. Очень часто при работе с макроопределениями появляются ошибки из-за неверного понимания порядка генерирования макро текста. Например: PROGRAM_IMAGE_BASE EQU 400000h FunMacro macro exitm <Параметр 3,параметр 4> endm MyMacro macro param1,param2,param3:VARARG echo param1 echo param2 echo param3 endm MyMacro PROGRAM_IMAGE_BASE, FunMacro(),Параметр 5 А теперь самостоятельно опишите порядок действий компилятора при вызове этого макро. Запишите его себе куда-нибудь, так чтобы сравнить, и смотрите на вывод: PROGRAM_IMAGE_BASE Прежде чем объяснять действительный порядок, я оговорюсь, что директива echoникогда не обрабатывает определённые константы, такие как PROGRAM_IMAGE_BASE. Это утверждение справедливо даже тогда, когда перед директивой echo стоит оператор %, который может раскрывать только текстовые макроопределения. То есть выражение: echo FunMacro() Даст результат: FunMacro() Теперь, когда мы немного порассуждали можно привести тот текст, который генерируется из макро: echo PROGRAM_IMAGE_BASE Это означает следующее:
Специальный оператор % заставляет ассемблер вычислять текстовую строку, следующую за ним, и только потом подставлять в правое выражение. Например, если мы перепишем макровызов так: MyMacro %PROGRAM_IMAGE_BASE, FunMacro,Параметр 5 То получим вывод: 4194304 ;; Значение PROGRAM_IMAGE_BASE Давайте рассмотрим ещё один пример, который хорошо показывает, как работает макро. Например, вы определили макропроцедуру (именно его, а не макрофункцию). То когда вы пишите такое: @Macro что-то, что придёт вам в голову [символ возврата каретки] Что делает препроцессор ML: 1. Считывает всю строку до символа возврата каретки; 2. Смотрит, как вы определили параметры в макро; 3. Сканирует строку на наличие символа «,» или «<», «>»;
4. Назначает формальным параметрам (любого типа, кроме VARARG) макро участки строк, которые были определены разделителями запятыми (предварительно очистив от хвостовых и начальных пробелов, если только строка не была определена в угловых кавычках <>); 5. Если макро содержит формальный параметр типа VARARG, то ML сперва инициализирует значениями (согласно пункту 4) обычные формальные параметры, и только потом назначает параметру типа VARARG (который может быть только один в конце списка параметров) всю строку до конца.
6. Препроцессор разрешает все вызовы макрофункций, если они есть в лексемах параметра, и присваивает их результат соответствующему параметру. Если лексему в строке параметра предваряет символ %, то он вычисляет её значение до того, как передаст строку внутрь макро.
Теперь вы в состоянии объяснить следующую ситуацию: MyMacro macro … endm MyMacro() Предупреждение при компиляции: : warning A4006: too many arguments in macro call Как нужно было бы изменить этот макро (именно макро, а не макрофункцию), чтобы предупреждение не выдавалось? А почему оно происходит? Если вы с лёгкостью ответили на этот вопрос, значит, материал усвоен, иначе советую ещё раз прочитать его, и ответить на следующий вопрос. Как должен понять компилятор следующий код: MyMacro macro param1 param1 endm MyMacro = 2 Естественно отвечать на этот вопрос вы должны без помощи компилятора (то есть проверить компиляцией). Если вы не можете ответить на этот вопрос, или неуверенны в верности ответа, я поменяю задание: MyMacro macro param1 echo param1 endm MyMacro = 2 Запустите его в ML. Если и теперь вы сомневаетесь – перечитайте этот пункт снова и снова, продолжая экспериментировать. III.2. Определение макро переменных и строкЯ бы назвал следующее: Param = 0 макропеременными (с тем фактом, что переменная может иметь константный тип). В терминологии MASM: WASM EQU <One Wonderful Wonderful ASM> Потому что под термином «переменная» понимается: var dd 123 Переменные являются частью программы, а макропеременные живут только на этапе компиляции. По сути, они есть более простым видом макроопределений, и поэтому их стоит понимать как специальные макро, которые так же раскрываются препроцессором. Макропеременная может иметь только три типа – целочисленная макропеременная INEGER4 (dword), целочисленная макроконстанта или текстовой макро (строковая макропеременная).
При чём, в зависимости от вида определения макропеременной ML считает, что: Param = 0 ;; Param – это целочисленная макропеременная Constant EQU 123 ;; Макроконстанта Как вы уже догадались, каждое макроопределение обладает своими свойствами и возможностями.
А теперь подробнее. Если с целочисленными макропеременными в достаточной степени ясно. То с определениями EQU полный бардак. Как и в случае с вызовами макро, автор попытается построить алгоритм анализа EQU выражений: 1. Анализируем правую часть. В анализе правой части препроцессор выделяет лексемы, которые классифицирует как числа, строки. Так, например, в выражении: qqqq EQU 1234567890 string1 23456789012390 macrofun() «1234567890» – это лексема число, а «string1» – это строка, «macrofun()» – это всё равно строка (а не макрофункция!!!).
2. Если правая часть является верным определением числа в MASM, то есть 123 или 123h или 0101b – выполнить шаг три, иначе шаг четыре.
3. Если полученное число имеет значение, не превышающее диапазон значений для dword – это целочисленная макроконстанта.
4. Иначе – это строковая макропеременная. Теперь попробуйте самостоятельно определить тип макроопределения: qqqq EQU 0x123234 В данном примере только второй и третий вариант – макроконстанта, остальные – текстовые макро. Последний вариант таким не является, так как превышает диапазон значений для dword. Замете, что поскольку препроцессор в правой части выделяет корректные выражения, правая часть не может состоять из недопустимых символов. Но при этом она может состоять из директивы определения литерала: «<>» – угловых кавычек. Директива <текст> – определяет литерал, таким образом, указывая препроцессору ML, что он должен воспринимать нечто как строку символов. При этом сами «<>» – в строку не попадают. Директива <> – является единственной директивой для препроцессора ML, которая определяет литералы.
Кроме директивы, определяющей литерал, препроцессор ML имеет свой ESC-символ (символ отмены). В отличие от С этот символ – «!». Он отменяет действие других символов (<, >, ", ', %, ; , а так же символ запятой), которые могут иметь функциональность в том, или ином выражении. Если вы хотите получить «!», вы должны использовать последовательность «!!». К сожалению, не обходится без проблем и с символом отмены «!». Восстановить точный алгоритм работы мне не удалось. Единственное, что возможно – это привести несколько примеров с непонятными эффектами при его использовании: literal EQU <!> ;; Пустая строка Вывод – не пользуйтесь директивой EQU для определения литералов, для этого есть другая директива – TEXTEQU. Для директивы TEXTEQU алгоритм несколько отличен от алгоритма EQU, так как в TEXTEQU обрабатывается правое выражение на наличие символа %. То есть вы можете определить этот код: literal TEXTEQU %FunMacro() Или literal TEXTEQU %(10-5)*30 ;; literal = “150” На самом деле как вы видите, внутренняя работа TEXTEQU значительно отличается от EQU <>. Видимо по этому разработчики ML решили её ввести.
В заключении к этому пункту, вы должны осознать, что тип определений невозможно изменить. То есть переменная не может стать целочисленной константой: literal EQU string Второе переопределение символа literal, не изменит его тип на тип целочисленной константы.
Свои особенности имеют так же целочисленные выражения с оператором «=». В таких выражениях перед их выполнением осуществляется полная замена всех макроконстант, макропеременных на их значения, и вызов всех макрофункций. Как вы думаете, что будет в следующем примере: literal EQU Something Варианты ответа:
Второй вариант ответа мы должны откинуть сразу, потому что в этом пункте чётко определили, что данное переопределение невозможно. Первый вариант ответа больше похож на правду.… Однако не соответствует истине. Что же произошло? А произошло следующее:
Этот факт может быть легко доказан, следующим тестом: literal EQU Something literal = 1234 %echo @CatStr(%Something) ============================ Вывод: 1234 Если вас сбил с толку этот пример, не отчаивайтесь. Всё дело в том, что препроцессор ML в разных выражениях по-разному заменяет макропеременные. Вот об этом мы и поговорим в следующем пункте. А пока подумайте, что должно случится в этом примере: num EQU number На этом можно было бы закончить данный пункт, если бы не одна особенность использования строк в вызове макро. А точнее приоритет анализа кавычек и директивы определения литерала <>. Не смотря на описанный выше алгоритм поведения макро, оказывается, что препроцессор при вызове макро выполняет определение литерала в кавычках, но что самое интересное, как было отмечено, выше сами кавычки попадают в строку. Если вам нужно передать макро одиночную кавычку вы должны воспользоваться символом отмены «!». Однако самое неприятное таится в том, что символы «<>» и кавычки конкурируют между собой в определениях строк. Например, попробуйте сказать, что должно было бы получиться в этом случае: %echo @CatStr(<Раз">,<"Два>) А можно было бы подумать, что ML должен принять операторы <> и запятую. Данное место – источник многих сложно обнаруживаемых ошибок. Например: FORC char,<str> Если в строке попадается символ кавычки, а макропеременная char заменяется на значение кавычки, имеем: m$__charcode = @InStr (1,<@ABCDEFGHIJKLMNOPQRSTUVWXYZ>,<”>) В этом случае мы получаем ошибку: missing single or double quotation mark in string Так и должно быть, потому что кавычки имеют высший приоритет анализа, чем оператор <>. Более того, угловые кавычки <> имеют самый низкий приоритет по отношению ко всем спец. символам, что согласуется с MASM Reference. Посмотрите на Дополнение к статье: пункт 3.a.i, который подозрительно выделен «жирным». В частности, следующее выражение, которое работает без проблем: TEXT TEXTEQU <"> ;; Это работает? Появляется закономерный вопрос: для чего символ отмены «!»?
Зато благодаря TEXTEQU пример с поиском символа в строке имеет решение: m$__char TEXTEQU <char> Единственно, отчего не может помочь данный код – это от вылавливания в строке символов «> или <». Для этого можно использовать специальную проверку в условных блоках на наличие символа «>», но при этом придётся отказаться от микроблока FORC. III.3. Обработка выражения в MASM MASM обрабатывает выражения в правой и левой части в зависимости от контекста. Там, где вам необходима предварительная обработка выражений, используется оператор «%». Он заставляет препроцессор ML сначала вычислить выражение после оператора % (то есть выражение в правой части относительно %), и только потом продолжить анализ всей строки. Например, если вы хотите, чтобы при вызове макро: num TEXTEQU <123> макропараметр был бы равен не строке «num», а значению текстового макро num, вы должны поставить оператор % перед num. Например: FunMacro %num Но и с оператором % не всё гладко.
Строковые выражения:
Примеры: ;Арифметические выражения Так вот что интересно. В арифметических выражениях происходит полная замена правой части: вызовы макрофункций, значение макроконстант, макропеременных любых типов, как строковых, так и целочисленных. Так же в левой части выражения: замена строковых макропеременных, и вызов макрофункций. То есть:
В строковых выражениях происходит замена только строковых макропеременных (текстовых макро) (замете, что в ML нет строковых макроконстант). Это значит что в случае: %echo PROGRAM_IMAGE_BASE Появится: «PROGRAM_IMAGE_BASE», а не его числовое значение. Однако есть и третий частный случай, когда оператор % относится только к одному литералу: %literal В этом случае происходит полный комплекс подстановок:
Например: FunMacro %literal Значение literal будет подставлено в вызов макро, в независимости от того, какой тип имеет literal.
Следует так же отметить, что в выражениях с exitm оператор % работает точно так же, как с выражениями в TEXTEQU. III.4. Целочисленные выражения MASM Целочисленные, побитовые операции так же необходимы разработчику макроопределений. Они дают возможность скрыть обработку битовых полей, или вычисление сложных выражений. Например, как это сделано в макрофункции $$$MAKELANGID. $$$MAKELANGID macro p:REQ,s:REQ Вы всегда должны помнить, что препроцессор MASM не различается знаковые и беззнаковые числа (подобно тому, как это делает x86), и значение числа не может выходить за диапазон dword. Препроцессор MASM не выдаёт предупреждений при переполнении. Следующий пример демонстрирует такое поведение: myint = 0ffffffffh myint = myint + 1 ;; myint = 0 %echo @CatStr(%myint) ================================= OUT: 0 ;; Ещё один пример с умножением: myint = 0ffffffffh ;; ;; 0ffffffffh * 2 = (dword)1FFFFFFFEh = 4294967294 myint = myint * 2 %echo @CatStr(%myint) ================================= OUT: 4294967294 В следующей статье мы поговорим про то, как работать с 64-bits макропеременными, используя данный факт. Ниже приводится список операций, которые могут участвовать в целочисленных выражениях MASM.
III.5. Вычисление рекурсивных выражений Теперь, когда мы рассмотрели правила анализа и вычисления выражений в MASM, остаётся раскрыть важный вопрос: «Как происходит анализ выражений, если они состоят из других выражений?». Обычно это называется короче: вложенные выражения.
Например, вызов макрофункции при вызове макро – это вложенное выражение: MyMacro FunMacro(Мой парамерт) ;;Или это: %echo FunMacro(Мой параметр) ;;Или это: MyMacro FunMacro(Fun2(Привет)) Вложенность характеризуется параметром количества уровней вложенности. В недавнем примере уровень вложенности был равен двум. При чём вызов Fun2() можно называть выражением низшего уровня вложенности, а вызов макро MyMacro – выражением верхнего уровня. После анализа выражений, и получения их многоуровневой структуры вложенности, препроцессор начинает вычислять результат выражения самого низшего уровня. Потом подставляет его результат в выражение следующего уровня, и так далее. Например, для случая: Fun2 macro param Порядок вычислений такой:
То есть: Вложенные выражения вычисляются последовательно от низшего уровня к верхнему, и результаты вычисления каждого уровня становятся материалом для выражений следующего уровня. Это правило называется рекурсивным вычислением выражений. Оно используется везде, кроме мест вычисления значений макропараметров при вызове макро (как макросов, так и макрофункций). В этом случае действует правило: результат вложенного выражения присваивается макропараметру и не анализируется повторно. Это значит, что в данном примере: myvar EQU <123> MyMacro macro param1,param2,param3 echo param1 endm FunMacro macro param:VARARG exitm <param> endm MyMacro FunMacro(var,@CatStr(<%>,myvar),var4) вывод будет таким: var,myvar,var4 То есть препроцессор не будет снова вычислять выражение для второго макропараметра функции FunMacro(). Если бы он сделал это, то тогда вывод был бы таким, как в этом случае: %echo FunMacro(var,@CatStr(<%>,myvar),var4) Теперь, когда вы знаете все тонкости вычисления выражений в MASM, настало время рассмотреть Встроенные макрофункции и директивы, которые участвуют в этих выражениях. III.6. Встроенные макрофункции и директивы Несмотря на то, что этот пункт не касается самих макросов в MASM, он необходим, для того, чтобы строить макросы, и манипулировать выражениями, возникающими внутри макросов. MASM обладает несколькими встроенными макрофункциями, макропеременными и макроконстантами, которые работают так, как если бы они были макро, определённые вами. Вот список этих предопределений:
Кроме знания макрофункций, нам так же понадобятся знания о блоках ветвлений или просто IF блоках. Эти блоки позволяют исполнять тот или иной участок исходного кода в зависимости от того, выполняется какое-либо условие или нет. Часто это называют «Условным ассемблированием (компиляцией)», однако для MASM это нечто большее, нежели простое управление компилятором, так как, вы уже поняли, мы имеем дело, как с кодом машины, так и с макрокодом, который вычисляется и живёт только во время компиляции. Условный блок в MASM имеет следующий общий вид: [IFDIRECTIVE] условие Если выражение «Условие» равно истине, то выполняется блок кода, идущий после условной директивы, иначе управление передаётся на следующий оператор за блоком. [IFDIRECTIVE]/[ELSEDIRECTIVE] – могут быть той или иной директивой условия. Стандартные директивы IF/ELSEIF/ELSE требуют, чтобы выражение, стоящее при них, было целочисленным. Если вам необходимо проверять другие условия, то для этого в MASM предусмотрены специальные директивы. Список [IFDIRECTIVE]/[ELSEDIRECTIVE]:
На протяжении всей статьи я часто пользовался следующей директивой, которая позволяет выводить текст на консоль во время компиляции. Эта директива echo. Как мы узнаем позже, она оказалось просто незаменимой при проектировании макро. Вы уже, наверное, убедились насколько полезна эта директива, позволяющая заглянуть, а что именно происходит в недрах макроса, или посмотреть значения макропеременных. Кроме этого, есть ещё одна группа директив, без которой мы не сможем обойтись. Не сможем потому, что макрофункции, или макросы, которые мы собираемся создавать должны быть слегка умными, иначе говоря, иметь «защиту от дурака». Если кто-то неправильно использует макрос, то код, полученный таким образом может быть неправильным с точки зрения программиста, но не вызовет подозрений у компилятора. Поэтому макро не просто должен завершится, а и каким-то образом остановить компиляцию программы с выдачей сообщения об ошибке. Именно для этого и существует простой набор директив условной генерации ошибки. Действуют они подобно условным блокам и директиве echo. Пример безусловной генерации ошибки: .ERR <Ошибочка вышла, гражданин начальник> Условная генерация ошибки, имеют ту же форму, что и IFDIRECTIVE в таблице выше, однако последним дополнительным параметром является строка сообщения. Например: III.7. Символ макроподстановки.ERRE выражение,<ошибка, если выражение равно нулю> Ещё раз вернёмся к формальным параметрам макро. Как было сказано, при раскрытии макроопределения препроцессор заменяет в теле макро формальные названия на их величины. В MASM32 предусмотрено ещё одно средство подстановки макропараметров – внутри строкового литерала. Предположим нам нужно, чтобы макро генерировал строку: «label_xx». Где xx – это формальный параметр макро. Это можно сделать двумя способами: @CatStr(label_,xx) ;;Вызов макрофункции конкантенации строк или То есть если во время генерации макро, препроцессор встречает в его теле символ «&», он анализирует строку после него. Если эта строка однозначно определяет один из макропараметров, препроцессор заменяет выражение &макропараметр& на значение макропараметра. Следует отметить, что если макропараметр начинает или заканчивает литерал, то можно использовать только один символ «&»: label_&xx III.8. Макроблоки И, наконец, у читателя должен остаться единственный вопрос: «А как обрабатывать переменные типа VARARG»? Например, рассмотрим возможный макро для вызова функций – STDCALL: stdcall macro funname,params:VARARG endm Этот макро должен генерировать код вызова функции согласно конвенции STDCALL:
Получить видоизмененное имя функции по значению параметра funname можно было бы при помощи символа макроподстановки. call _&funname@(количество параметров * 4) Но непонятно, как распознать параметры функции, которые представляют собой строку, где значения разделены символом «,». Более того, не понятно, как вообще можно получить эти параметры, и посчитать их число, ведь макропараметр params – это одна строка. То есть при вызове макро: stdcall win32fun,1,2,3 Мы должны как-то определить количество параметров, а потом их значения. Именно для решения этой задачи в MASM предусмотрены несколько специальных макроопределений, которые можно назвать макроблоками. Первый из них FOR позволяет получить значения элементов, разделённых в строке символом «,». FOR parameter[:REQ | :=default], string Вспоминая С конструкцию FOR, вы сразу поймёте что это цикл, где значение parameter последовательно принимает значения элементов списка string. Вот вам wonderful пример: FOR parameter, <It’s, wonderful, wonderful, asm> А вот пример макрофункции, который подсчитывает число аргументов VARARG: @ArgCount MACRO parmlist:VARARG Вот в принципе, уже на основе этих знаний можно было бы организовать макрос stdcall: stdcall macro funname,params:VARARG Ещё несколько минут необходимо для того, чтобы понять, что этот макро работает неправильно. Хотя бы потому, что параметры помещаются в стек не так. Нужно было бы помещать их от последнего к первому, а не от первого к последнему. А, кроме того, ведь символ макроподстановки нельзя употреблять к макропеременной count, потому что это не макропараметр, это макропеременная. К сожалению, в MASM нет обратной конструкции FOR. Поэтому самый простой выход, который напрашивается сам собой – это изменить порядок параметров в списке, а потом только генерировать команды push. Вторую проблему можно легко решить, воспользовавшись макрофункцией конкатенации строк: call @CatStr(_,funname,@,%(count*4)) С параметрами в стек будет посложнее. В принципе я бы решил эту задачу, если бы MASM поддерживал бы такой тип макропеременных как массив. Но хотя MASM и не поддерживает этот тип, его можно эмулировать. count = 0 Как вы можете догадаться, в этом примере создаются макропеременные varXX, которым присваиваются значения параметров. Теперь с той же лёгкостью можно работать с этими переменными. Можно снова использовать цикл FOR, однако в данном случае, было бы грамотней воспользоваться значением count, и выполнить цикл столько раз, сколько записано в нашем счётчике параметров. Для этого мы воспользуемся ещё одним макроблоком rept, о котором скажем позже: nparams = count REPT nparams ;; Начало блока push @CatStr(var,%count) count = count - 1 ENDM Блок REPT выполняется столько раз, сколько указано в nparams. Я ввёл эту дополнительную макропеременную, для того, чтобы значение, указанное в REPT осталось неизменным. Однако этого не нужно. Можно было бы написать и так: REPT count ;; Начало блока Значение макропеременной count инициализирует цикл только один раз вначале, после чего, она может, как угодно менять значение. И ещё один макроблок, без которого нам невозможно будет реализовать макрос для определения строк уникода, или макрос, который позволяет писать строки OEM в редакторе использующий кодировку win cp-1251 (например, при создании консольных приложений). Этот макроблок FORC: FORC char, string Блок FORC выполняется столько раз, сколько символов в строке string, при этом
макропараметр char равен текущему символу из строки. count = 0 А вот так, можно было бы посчитать количество пробельных символов. count = 0 Упражнение: TheSvin'у, как и любому программисту, который часто имеет дело с битами, было бы удобно записывать значения бит по группам, через пробел. ;;Вот так неудобно и ненаглядно mov eax,011110111011b ;;Вот так удобно и наглядно, но компилятор выдаст ошибку ;;Вариант1 mov eax, 0111 101 1101 1b ;;А вот так вообще замечательно, только ML неправильно поймёт ;;Вариант2 mov eax, [0111] [101] [1101] [1]b Хорошо бы было написать некую макрофункцию, которая смогла бы позволить записывать эти выражения: mov eax,nf(0111 101 1101 1b) Напишите такую макрофункцию, которая позволила бы это делать. Напишите её для первого и второго вариантов исполнения. III.9. Отладка макроопределений и заключение А напоследок… остаётся маленькая деталь. И эта деталь не самая приятная. Отладка макроопределений и их испытания невозможны под отладчиком. А, кроме того, если при генерации макро возникает ошибка, то ML выдаёт её в жутком виде: .\start.asm(84) : error A2008: syntax error : in directive То есть он выдаёт относительную строку в макро MacroLoop(3), где эта ошибка появилась. А если ещё макровызовы будут вложенными, то вам лучше не видеть этой замечательной картины. Единственной возможностью качественно и относительно легко отлаживать макро – это употребление директивы echo. На протяжении статьи вы не раз наблюдали примеры её использования. Но я снова повторюсь: ;; Для макропараметров Заметьте, чтобы вывести значение целочисленной макропеременной необходимо воспользоваться макрофункцией @CatStr(), и перед аргументом указать оператор %. Почему именно так обсуждалась в пункте III.2. Определение макро переменных и строк. Теперь вы знакомы с теорией использования макроопределений в MASM32, и сможете смело приступать к разработке макро. Именно этим мы и займёмся в следующей практической части нашего руководства, а так же заполним некоторые пробелы, на которые не обратили внимания здесь. III.10. Абстрактный алгоритм анализа строки MASM (Дополнение) 1. Определены таблицы элементов:
2. Начальное состояние анализа строки. 3. Читать поток символов, пока не встретится символ возврата каретки без предыдущего символа «/». Игнорировать часть строки после «;»
4. Перейти к анализу следующей строки. |