| Версия для печати
Нажмите сюда для просмотра этой темы в оригинальном формате |
| Форум на Исходниках.RU > Assembler FAQ > Некоторые возможности макроязыка TASM |
| Автор: Eretic 23.11.19, 14:15 |
| Некоторые возможности макроязыка TASM. Введение. Данная статья не является учебником по макроязыку Turbo Assembler, скорее это забавный пример его практического использования, предполагающий знания основ макроязыка. Когда-то мне понадобилось защитить часть кода загрузчика CD-ROM. Вариант шифрования кода и данных отбросил сразу, поскольку аналогичная защита на диске «Reanimator 2K» была снята мной менее чем за день. И тогда вспомнил про старый добрый Форт, с его прямым шитым кодом, который и без шифрования создает трудный для понимания код. Вот его мы и создадим, используя макроязык Turbo Assembler. Нам понадобится TASM последней версии, то есть 5.3. Есть еще версия 5.4, но она отличается от предыдущей только копирайтом. Имена. Основная проблема, которую нужно решить, заключается в том, что в Форте для имён допустимы любые символы, включая те, что TASM запрещает использовать в именах меток и идентификаторов. Вдобавок, имена в Форте могут начинаться с цифры. Поэтому, строку вида: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> ассемблер не воспримет. Единственным выходом является создание макроса, которому будет передаваться строка с командами Форта, а он разберет её на составляющие и скомпилирует в код, примерно так:>R R@ R> ; <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> _ >R R@ R> EXIT Первым делом необходимо изъять из строки некоторые спецсимволы, которые, которые макроязык ассемблера интерпретирует особым образом. Это символы ‘>’, ‘<’ и ‘!’. Для начала определим макрос подмены одного символа в строке: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Поиск производится в строке ??NAME, штатной функцией INSTR, а строка, в случае успешного поиска символа old, разбивается функцией SUBSTR на две части – до символа (??prev) и после (??next). Затем строки склеиваются функцией CATSTR, а между ними, вместо символа old, вставляется строка new. macro $SWAP new, old: rest ??ch equ &old& ??pos instr ??NAME, ??ch :while if ??pos ??prev substr ??NAME, 1, ??pos-1 ??next substr ??NAME, ??pos+1 ??NAME catstr ??prev, <new>, ??next ??pos instr ??NAME, ??ch goto while endif endm Теперь можно написать макрос предварительной обработки строки: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Макрос подменяет символы ‘>’, ‘<’ и ‘!’ на строки ‘more’, ‘less’ и ‘save’ соответственно. Это поможет корректно разбить строку на отдельные слова, но не избавит от проблем с именами подпрограмм, поскольку остаются другие недопустимые символы. Поэтому нам понадобится еще один макрос, завершающий подмену остальных символов:macro $PRENAME name: rest ??NAME equ &name& $SWAP less, << $SWAP more, > $SWAP save, ! endm <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Он будет использоваться на завершающем этапе формирования имен, когда уже будут отсеяны числа и символы. Почему не сразу? Проще это понять на примере. Допустим, имеется строка:macro $POSTNAME name ??NAME equ &name& $SWAP plus, + $SWAP minus, - $SWAP mul, * $SWAP eq, = $SWAP div, / $SWAP dot, . $SWAP tld, ~ endm <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Если бы мы подменяли все символы сразу, то получили бы строку:54 -2 * ‘*’ ! <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> То есть, вместо числа (-2) идентификатор (minus2), а вместо символа (‘*’) целую строку (‘mul’), что очевидно неправильно. Если же использовать сначала $PRENAME, то получим для разбора строку:54 minus2 mul ‘mul’ save <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> В этой строке мы легко выберем числа и символы, а оставшиеся слова пропустим через $POSTNAME и получим допустимые идентификаторы:54 -2 * ‘*’ save <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> mul save Поскольку числа-константы, символы и подпрограммы компилируются разными способами, нужен макрос для их распознавания. В макроязыке нет предусмотренных средств для определения типа идентификатора, а что есть – не работает. Придется сделать макрос идентификации чисел и символов самостоятельно: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Вначале пропускаем возможные символы знака числа. Затем, убираем символ основания числа в конце строки и задаем набор допустимых цифр для данного числа в идентификатор @numstr. И в заключение, проверяем оставшуюся строку на предмет соответствия цифрам из строки @numstr. Результат возвращаем в глобальном идентификаторе ??ISNUM. Все просто. macro $ISNUM n ??ISNUM = 0 ??NUM equ &n& @len sizestr ??NUM ;; проверяем первый символ @cfirst SubStr ??NUM, 1, 1 @IsFirst InStr <''>, @cfirst if (@IsFirst ne 0) ;; это символ, обрабатывается как число ??ISNUM = 1 exitm endif ;; проверяем первый символ на наличие знака числа @IsFirst InStr <-+>, @cfirst if (@IsFirst ne 0) ;; первым идет знак числа, убираем его ??NUM SubStr ??NUM, 2, @len-1 @cfirst SubStr ??NUM, 1, 1 @len sizestr ??NUM endif if (@len eq 0) ;; это не число exitm endif ;; проверяем основание числа по последнему символу @numstr CatStr <0123456789> @clast SubStr ??NUM, @len, 1 @IsLast InStr <bBhH>, @clast if (@IsLast ne 0) ;; это основание числа, убираем его if (@IsLast gt 2) ;; это шестнадцатеричное число @numstr CatStr @numstr,<ABCDEFabcdef> else ;; это двоичное число, допустимы только 0 и 1 @numstr CatStr <01> endif ??NUM SubStr ??NUM, 1, @len-1 @len sizestr ??NUM endif ;; первым символом, после знака, должна быть цифра @IsFirst InStr <0123456789>, @cfirst if (@len eq 0) or (@IsFirst eq 0) exitm endif ;; сканируем число, без учета символов знака и основания rept @len @nchr SubStr ??NUM, 1, 1 ??NUM SubStr ??NUM, 2, @len @isDigit InStr @numstr, @nchr if (@isDigit eq 0) exitm endif endm if (@isDigit ne 0) ??ISNUM = 1 endif endm Компиляция кода Теперь можно приступать к макросу компиляции Форт-строки. Цель макроса – разбить строку на отдельные слова, идентифицировать их и скомпилировать в шитый код. Сам макрос выглядит проще некуда: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> То есть, подменяем в строке недопустимые символы и результат передаем в макрос $parsewords, который и произведет разбивку строки на слова. Обращаем внимание, что параметр макроса определен как REST, чтобы строка передавалась в него в неизменном виде, без предварительной обработки транслятором ассемблера. Однако есть нюанс. Если первым символом идет ‘<’, то его нужно продублировать ‘<<’, иначе ассемблер воспримет его, как начало текстовой строки и естественно отбросит. В других местах стоки прокатит и одиночный символ ‘<’.macro _ listcmd: rest $PRENAME listcmd % $parsewords ??NAME endm Макрос разбивки строки на слова вполне стандартен: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Параметр задан через VARARG, поэтому строку на слова разобьет уже сам транслятор ассемблера и макрос получит готовый массив слов, который он поочередно, с помощью IRP, скормит макросу $COMPILE:macro $parsewords name: vararg irp @i, <name> $COMPILE @i endm endm <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> А вот это и есть компилятор слов Форта. Если слово распознано как число или символ, то оно компилируется в связке с подпрограммой LIT, которая, во время выполнения, извлекает его из кода и помещает на арифметический стек. В противном случае считаем слово идентификатором и, завершив подмену спецсимволов макросом $POSTNAME, компилируем его в код: macro $COMPILE name $ISNUM <&name> if ??ISNUM dw _LIT dw &name else $POSTNAME name % $COMPILENAME ??NAME endif endm <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Тут две тонкости. Во первых, к имени добавляется префикс ‘_’, для решения проблем с именами, начинающимися с цифры. Во вторых, есть два типа подпрограмм – немедленного исполнения (immediate) и простые. С простыми все ясно, их адрес просто добавляется в код, т.е. компилируется. А вот с immediate сложнее. Они не компилируются, а сразу же выполняются, то есть это некий аналог макросов. Такие подпрограммы используются, в основном, в командах ветвления, или циклах, где необходимо сразу же расставить метки для переходов. Поэтому, реализуем их в виде макросов, а для отличия от простых слов, параллельно им будем определять идентификатор, с тем же именем, но с постпрефиксом ‘_F’. Подробнее о подпрограммах немедленного исполнения будет ниже.macro $COMPILENAME name ifdef _&name&_F _&name ;; немедленно выполняем слово else dw _&name ;; просто компилируем слово в код endif endm Подпрограммы Настало время определиться со стеками и регистрами. Стеков будет два, арифметический и возвратов. На верхушку первого будет указывать регистр SP, а второй реализуем через регистр BP. Регистр SI задействуем в качестве некоего аналога IP, он будет указывать на адрес следующей исполняемой подпрограммы. Регистры CS, DS и SS должны быть настроены на один сегмент. Остальные регистры свободны. Подпрограммы бывают двух видов – высокого и низкого уровня. Начнем с простейших. Низкоуровневые подпрограммы – это почти обычные ассемблерные вставки. Для их объявления достаточно преобразовать имя в допустимый вид и объявить метку: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Для завершения ассемблерных вставок нельзя использовать команду RET, вместо этого нужно передать управление на следующую подпрограмму. Адрес следующей подпрограммы хранится в слове, на которое указывает регистр SI. Для удобства оформим макрос:macro $CODE cname: rest $PRENAME cname % $POSTNAME ??NAME $LABEL <%??NAME> endm macro $LABEL name _&name&: endm <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Таким образом, низкоуровневая подпрограмма будет выглядеть следующим образом:macro $NEXT lodsw jmp ax endm <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> $CODE >R ; ( x -- )(R: -- x ) переносит x с ариф. стека на стек возвратов pop ax xchg bp, sp push ax xchg bp, sp $NEXT Подпрограммы высокого уровня реализуются прямым шитым кодом. Это значит, что для их выполнения нужна специальная функция. Назовем её DOLST. В качестве аргумента ей передается адрес подпрограммы высокого уровня, которую она и запустит на выполнение: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Функция сохраняет в стек возвратов текущее значение регистра SI и загружает в регистр новое, с арифметического стека. Этим значением будет указатель на массив адресов подпрограмм, что составляют тело исполняемой подпрограммы. С помощью макроса $NEXT запускаем выполнение первой подпрограммы из массива, а она передаст управление следующей и т.д., пока не будет достигнут конец массива, то есть особая функция завершения подпрограммы. В Форте имя этой подпрограммы – точка с запятой. Ассемблер не даст нам использовать такое имя, поэтому выберем другое:$CODE DOLST ; ( addr -- ) xchg bp, sp push si xchg bp, sp pop si $NEXT <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Подпрограмма восстанавливает значение регистра SI из стека возвратов и таким образом возвращает управление туда, откуда и была вызвана подпрограмма, точнее на следующий адрес.$CODE EXIT mov si, [bp] add bp, 2 $NEXT Теперь можно написать макрос для объявления подпрограмм высокого уровня: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Для примера:macro $COLN cname: rest $CODE cname call _DOLST endm <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> сгенерирует код:$COLN NEG ; ( n -- -n ) n = -n _ NOT 1+ EXIT <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Инструкция CALL кладет на арифметический стек адрес первого элемента массива подпрограмм, что составляют тело текущей подпрограммы (dw _NOT, _1plus и т.д.), а DOLST запускает выполнение.03C5 _NEG: 03C5 E8 FD93 call _DOLST 03C8 0539r dw _NOT 03CA 03F2r dw _1plus 03CC 011Dr dw _EXIT Подпрограммы немедленного исполнения Как уже упоминалось ранее, для реализации ветвления кода и циклов необходимы подпрограммы немедленного исполнения, так называемые immediate. Все дело в том, что еще на этапе генерации кода необходимо расставить команды переходов на код, который еще даже не объявлен. Возьмем для примера простейшую конструкцию: IF – ELSE - THEN (так в Форте называется ENDIF). <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> При выполнении макроса IF придется сгенерировать код проверки условия и возможный переход либо на конец условия (THEN), либо на возможную ветку ELSE, на данном этапе просто невозможно определить, куда именно будет переход. Поэтому, мы сгенерируем переход на некую метку и занесем ее имя в список. Последующий макрос, THEN или ELSE, извлечет это имя из списка, и сгенерируют метку в коде. IF if (param == FALSE) then GOTO (ELSE or THEN) <code> jmp THEN [ELSE] <code> THEN Список меток организуем по принципу стека, последним пришел – первым ушел. Всего нам понадобится три макроса, $PUSH – для генерации новой метки и помещению ее в список, $POP – соответственно для извлечения метки из списка, и $TOP – для получения метки со списка, без ее извлечения из этого списка. Примерно так: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> В параметре макросам передается имя идентификатора, в который будет сохранено имя метки. Имена меток нам генерирует сам ассемблер, посредством своего макроса LOCAL. Это гарантирует уникальность имени. В остальном, думаю все понятно, новое значение добавляется в начало строки, чтобы при извлечении снова быть первым.macro $PUSH variable local @@lab ??STACK CatStr <@@lab>, <,>, ??STACK ifnb <variable> variable CatStr <@@lab> endif endm macro $POP variable: req variable equ <> @pos InStr ??STACK, <,> if @pos @pos = @pos-1 variable SubStr ??STACK, 1, @pos @pos = @pos+2 ??STACK SubStr ??STACK, @pos endif endm macro $TOP variable: req variable equ <> @pos InStr ??STACK, <,> if @pos @pos = @pos-1 variable SubStr ??STACK, 1, @pos endif endm Теперь непосредственно сами макрокоманды условия. Первым делом объявляем их IMMEDIATE: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Сам макрос условия:IMMEDIATE = 1 ; флаг слов немедленного исполнения _IF_F = IMMEDIATE _THEN_F = IMMEDIATE _ELSE_F = IMMEDIATE <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Генерирует метку и условный переход на нее, если на верхушке арифметического стека находится значение FALSE (подпрограмма ?BRANCH). macro _IF ; ( bool -- ) $PUSH <??LABEL> dw _?BRANCH % dw ??LABEL endm Макрос завершения тела условия: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Просто извлекает из списка метку, что сгенерировал макрос IF или ELSE, и ставит в конец условия. macro _THEN $POP <??LABEL> % ??LABEL: endm Макрос альтернативного выбора: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Вначале генерирует код безусловного перехода на конец условия (BRANCH), то есть на метку, что сгенерирует THEN. Этим самым завершается выполнение ветки IF. После этого объявляем метку, что сгенерировал макрос IF. Теперь именно сюда и перейдет управление, в случае если не сработает условие оператора IF.macro _ELSE $POP <??LABEL> $PUSH <??NEWLAB> dw _BRANCH % dw ??NEWLAB % ??LABEL: endm Похожим образом строятся конструкции CASE – OF – ENDCASE, а также конструкции циклов BEGIN – UNTIL, BEGIN – WHILE – REPEAT, DO – LOOP. Приводить их реализацию не буду, все есть во вложении. Данные В Форте нет переменных и констант, в том виде, что есть в других языках программирования. Любая переменная или константа - это подпрограмма. При вызове переменной, она оставляет на стеке адрес связанной с ней ячейки памяти, куда можно что-то записать, или считать из нее. В отличие от переменной, константа оставляет на стеке не адрес ячейки, а её значение. При желании, значение константы можно менять по ходу выполнения программы, хотя вряд ли в этом есть смысл. В большинстве реализаций Форта для создания переменных и констант предусмотрены слова VARIABLE и CONSTANT. Не будем и мы отбиваться от большинства. Начнем с констант: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Макрос создает подпрограмму, которая кладет на арифметический стек значение константы. macro CONSTANT name, imm:req $CODE <&name> push &imm $NEXT endm При объявлении переменной нужно учесть возможность зарезервировать место под массив, а так же возможность объявления инициированных данных: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Макрос создает подпрограмму, которая кладет на арифметический стек адрес зарезервированной памяти, из LEN ячеек. Если LEN не задана, то резервируется одна ячейка. Если задано значение параметра IMM, то создаются инициализированные данные.macro VARIABLE name: req, len, imm $CODE <&name> @name CatStr <$$_>,??NAME % push offset @name $NEXT ifnb <imm> align 2 ifnb <len> % @name dw len dup(imm) else % @name dw imm endif else udataseg align 2 ifnb <len> % @name dw len dup(?) else % @name dw ? endif codeseg endif endm Что касается строк, то в Форте предусмотрены средства для работы с паскаль-строками, но я не стал реализовывать определяющие строки слова. Вместо этого создал отдельный макрос для объявления строк: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Это удобнее чем в Форте, так можно задавать строки с любым набором символов. Например:macro STRING name, imm:rest local @endstr $CODE <&name> @name CatStr <$$_>,??NAME % push offset @name $NEXT align 2 % label @name byte db @endstr - @name - 1 ;; байт длины строки db &imm& label @endstr byte endm <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> В стандартном Форте пришлось бы выводить это сообщение в три приема.STRING sAnyKey, "Press any key",13,10,"to continue",'$' Практика Макросы готовы, ядро написано и заняло всего 2 Кб. Осталось проверить. Я не сторонник искусственных тестов, поэтому поискал нечто более комплексное. Не помню, откуда, но завалялись у меня старые исходники тетриса на Форте. Добавил цвет и получил годный тетрис под DosBox. Полностью приводить код не буду, он есть в аттаче, разве что небольшой кусочек для наглядности: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Если бы не символ подчеркивания, ничем бы не отличалось от кода на «натуральном» Форте. Что собственно и требовалось.$COLN PLAY-GAME ; ( -- ) _ BEGIN _ NEW-BRICK _ -1 3 INSERT-BRICK _ NEXT-DRAW _ WHILE _ BEGIN _ 4 0 DO _ HELP.X HELP.Y GOTO-XY _ KEY? _ IF _ BEGIN _ KEY KEY? _ WHILE _ DROP _ REPEAT _ ON-KEY 0= ; обрабатываем нажатия клавиш _ IF _ UNLOOP EXIT ; прерываем игру _ THEN _ THEN _ GAME.SPEED @ DELAY _ LOOP ; перемещаем фигуру вниз _ 1 0 MOVE-BRICK 0= _ UNTIL ; фигура достигла "дна", проверяем и удаляем зап. линии _ PIT-CHECKFULL _ SHOW-SCORES _ CALCK-SPEED _ REPEAT ; запрашиваем новую игру, или выходим _ sNew GET-INPUT NOT GAME.CONTINUE ! _ EXIT Заключение Понятно, что я далеко не первый и не последний, кто занимался «исследованиями» на тему возможностей макроязыка. Уже на заре увлечения ассемблером имел удовольствие ознакомиться с макробиблиотекой “Macro Language Version 2.0”. Просто из примера: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Если кто-нибудь вам скажет, что это не ассемблер, плюньте ему в глаза © «Эл Стоу». Include "system.h" Program ArrayDemo; Var array ARR1[10*20] of shortint; array ARR2 of integer = <10,9,8,7,6,5,4,3,2,1>; words i, j; Begin lea si,[ARR2]; for i = 1 to 10 do asm lodsw \ j = ax; WriteLn<'ARR2['>,i,<'] = '>,j; next i; End_ Пожалуй, всё. К статье, помимо «Форта», прикладываю вышеупомянутую макробиблиотеку «Macro Language Version 2.0». Заодно выложу инклюды к Turbo Assembler. Начинал их еще AndNot, а я доработал и опробовал в нескольких небольших проектах, пока окончательно не перешел на Си. В них тоже есть немножко макросов, облегчающих жизнь при работе со строками – text, invoke и move. В качестве примера их использования приложу вывод фрактала папоротника, написанный по мотивам книги Никулина «Компьютерная геометрия и алгоритмы машинной графики». Пример в папке EXAMPLE, для компиляции нужно в батнике настроить пути к TASM’у. В папке TOOLS конвертеры инклюдов, детали в файле ‘READ.ME’. FORTH.ZIP (, : 342)
INCLUDE.rar (, : 340)
LIB.rar (, : 338)
TASM_Macro_Language.part1.rar (, : 347)
TASM_Macro_Language.part2.rar (, : 344)
TASM_Macro_Language.part3.rar (, : 339)
|
| Автор: Kopa 02.10.21, 04:38 |
| Дополню тему такой статьёй в применении ассемблера. (Fasm) Разместить FORTH в 512 байтах P.S. Проект MiniForth на Github |