О специальных макро в ассемблере

    Введение

    Много лет назад американским специалистом Гарри Килдэллом (Gary Kildall) в рамках создания системы программирования для персональных компьютеров был разработан транслятор с языка ассемблера для процессора Intel 8086, который он назвал RASM-86 (Relocating ASseMbler). Этот во многом типичный для своего времени продукт имел особенность: он позволял, не меняя транслятора, добавлять описания новых команд процессора с помощью специальных макросредств.

    Автор статьи, используя и развивая этот транслятор, успешно применял данные средства по мере появления новых поколений процессоров. Конечно, иногда и сам транслятор требовал ряда доработок, например, при переходе на архитектуру IA-32, а затем и на x86-64 (IA-32e). Тем не менее, изначально заложенная идея позволила легко продолжать эволюцию транслятора до настоящего времени. Некоторые итоги этой работы рассматриваются далее.

    Организация генерации команд в трансляторе с ассемблера

    Обычно программистам безразлично, как именно организована генерация кодов команд внутри используемого ими транслятора. Однако, поскольку в данном случае можно влиять на этот процесс, рассмотрим на примере конкретного транслятора ход генерации.

    Транслятор RASM имеет общую внутреннюю таблицу всех возможных команд процессора. Элементы таблицы начинаются с текста мнемоники команды, за которым следует связанный список всех возможных форм данной команды в зависимости от числа, типа и размера ее операндов. Содержимое очередной формы команды состоит из последовательности «микрокодов», каждый из которых представляет отдельно обрабатываемую и уже неделимую часть команды.

    Сначала транслятор ищет заданную команду в таблице по ее мнемонике, а затем, идя по связанному списку, ищет подходящую комбинацию операндов. Если заданной комбинации операндов (включая и случай отсутствия операндов в команде) не найдено – выдается сообщение об ошибке. Иначе транслятор читает последовательность «микрокодов» и выполняет каждый из них, составляя, таким образом, код заданной команды. При этом частные случаи комбинации операндов должны располагаться ранее общих случаев. Например, если команда имеет отдельную форму для операнда-байта, расширяемого со знаком, такой операнд должен встретиться в связанном списке раньше общего случая операнда-константы иначе, естественно, будет находиться только общий случай.

    Идея Килдэлла состояла в том, что программист с помощью специальных макросредств может описать новую команду или новую форму с новой комбинацией операндов для уже существующей команды или даже повторить уже существующую комбинацию операндов, но указать новое содержимое. Команда описывается по тем же правилам, по которым были первоначально описаны все команды при создании самого транслятора.

    Трехпроходный транслятор RASM на первом проходе переведет это описание в последовательность «микрокодов» и вставит новое описание в общую таблицу команд. В этом принципиальное отличие данных макросредств от «обычных», которыми можно изменить исходный текст программы, но нельзя изменить содержимое самого транслятора. Если такая команда уже существовала, новое описание добавляется в начало имеющегося связанного списка. Если уже существовала и такая команда и такая комбинация операндов для нее – новое описание отменит предыдущее, так как будет вставлена «выше». После этого в тексте программы можно свободно использовать новую команду так же, как если бы она изначально была представлена в таблице транслятора.

    Макросредства описания команд

    По виду специальные макросредства RASM похожи на обычные средства макроподстановки: имеются ключевые слова CodeMacro и EndM, между которыми пишется «тело» макроопределения. В первой строке пишется имя макро и, возможно, список его параметров. Например:

    CodeMacro AAA
      DB 37H
    EndM
    
    CodeMacro DIV divisor:Eb
    SEGFIX divisor
      DB 6FH
    EndM
    
    CodeMacro OR dst:Re, src:Ee
      SEGFIX src
      DB 0BH
      MODRM dst,src
    EndM

    Описание формальных параметров

    Каждый формальный параметр в макро имеет имя, задаваемое программистом, и после двоеточия спецификацию, состоящую из типа, размера и при необходимости диапазона допустимых значений в круглых скобках.

    Типы формальных параметров:

    A – сумматор EAX/AX/AL
    C – выражение типа метка 
    D – непосредственный операнд
    E – адресное выражение, записанное в регистре или памяти
    M – адресное выражение, может иметь базовые и индексные регистры
    R – один из общих регистров
    S – сегментный регистр
    X – прямое обращение к памяти при обмене с сумматором

    Размеры формальных параметров

    n - длина не определена
    b – байт
    w – слово
    e – двойное слово
    d – длина при использовании адреса смещение+сегмент
    sb – знаковый байт, расширяемый до слова
    se – знаковый байт, расширяемый до двойного слова

    Примеры описания формальных параметров:

    CodeMacro IN dst:Aw, port:Rw (DX)
    CodeMacro ROR dst:Ee, count:Rb (CL)

    Директивы макроопределений

    Первоначально все описания команд х86 свелись к нескольким директивам, часть из которых используются редко. Перевод транслятора на архитектуру IA-32 потребовал добавления лишь одной новой директивы управления префиксами размера/адреса 66H/67H, причем, чтобы не вводить новых ключевых слов используется уже имевшаяся директива, но с другой формой параметра.

    Директивы DB, DW и DD

    Данные директивы в макро почти эквиваленты обычным операторам ассемблера и используются для задания констант и адресов. Эти директивы характерны и для любых других (не специальных) макросредств.

    Директива DW используется для задания адреса (4 байта в 32-х разрядном режиме), а директива DD – для задания адреса в виде смещение+сегмент. Примеры использования DB, DW и DD:

    CodeMacro CLC
      DB 0F8H
    EndM
    
    CodeMacro XOR dst:Ee,src:De
      SEGFIX dst
      DB 81H
      MODRM 6,dst
      DW src
    EndM
    
    CodeMacro CALLF label:Cd
      DB 9AH
      DD label
    EndM

    Директива адресации MODRM

    Это главная из «специальных» директив, определяющая адресацию архитектуры IA-32. Она определяет и основное отличие данных макросредств от обычных. Именно «микрокод», порождаемый этой директивой, указывает транслятору генерировать адресную часть команды, включая и байт режимов адресации и смещение и SIB-байт, если операнды подразумевают это. Директива имеет два параметра. Это или два имени формальных параметров макро или константа-число и имя. Например:

    CodeMacro RCR dst:Ee, count:Rb(CL)
      SEGFIX dst
      DB 0D3H
      MODRM 3,dst
    EndM
    
    CodeMacro XOR dst:Re,src:Ee
      SEGFIX src
      DB 33H
      MODRM dst,src
    EndM

    Директивы определения относительного адреса RELB, RELW

    Эти директивы используются для описания команд передачи управления по относительному адресу, занимающему или байт или 4 байта для IA-32. Пример:

    CodeMacro LOOP place:Cb
      DB 0E2H
      RELB place
    EndM

    Директива задания кодов DBIT

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

    CodeMacro DEC dst:Re
      DBIT 5(9), 3(dst(0))
    EndM

    Директива формирования префикса сегмента SEGFIX

    Параметром данной директивы является имя формального параметра. Директива указывает транслятору, что если заданный параметр находится не в сегменте, который предполагается по умолчанию, необходимо генерировать соответствующий префикс сегментного регистра.

    Директива контроля сегментов NOSEGFIX

    Директива имеет параметры в виде имени сегментного регистра и имени формального параметра. Она не генерирует кода, а проверяет, что обращение к данному параметру идет с использованием указанного сегментного регистра, иначе сообщает об ошибке. Эта директива требуется лишь в общих формах команд CMPS и MOVS, где один из операндов может адресоваться только через ES.

    Данная директива была расширена для управления префиксами размера и адреса 66H/67H. В этом случае в директиве указывается параметр-число: 0 – нет префиксов, 1- может быть префикс 66H, 2 – может быть префикс 67H, 3 – могут быть оба префикса, 4 – всегда есть оба, 5 – никогда нет 66H, 6 – никогда нет 67H и т.п.

    Такими простыми средствами удается описать все множество команд IA-32, например:

    CodeMacro FLDCW src:Mw
      SEGFIX src
      DB 0D9H
      MODRM 5, src
    EndM
    
    CodeMacro CMOVAE dst:Re, src:Ee
      SEGFIX src
      DB 0FH
      DB 43H
      MODRM dst,src
    EndM

    Некоторое исключение из стройной системы описаний составляют команды FPU, имеющие операнд в памяти. Для простоты в RASM разрядность таких команд указывается прямо в мнемонике, а не определяется по размеру операнда в памяти. Поэтому в RASM есть, например, команды FIST16, FIST32 и FIST64. Однако на практике, с точки зрения ясности текста, указание разрядности операнда прямо в имени команды FPU оказалось вполне приемлемым.

    Создание псевдокоманд с помощью макросредств

    Используя возможность добавления новых комбинаций операндов можно конструировать новые «команды» процессора. Например, команду MOV ECX,10 часто целесообразно заменять двумя командами с более коротким кодом PUSH 10 и POP ECX. А эти две команды можно описать в виде одного макроопределения:

    CodeMacro MOVSX dst:Re, src:Dse
      NOSEGFIX 6
      DB 6AH
      DB src
      DBIT 5(0BH),3(dst(0))
    EndM

    Такую псевдокоманду можно создать и «обычными» макросредствами, однако назвать ее именем уже существующей команды MOVSX другие трансляторы (кроме RASM), скорее всего, не позволят. С точки зрения результата, это именно выполнение MOVSX с параметром-константой. Но такой команды в процессоре нет. А с макросредствами RASM можно считать, что на самом деле есть такая формы команды. Тот факт, что реально выполнение идет за два приема и стек меняется, а затем восстанавливается, в большинстве случаев можно не учитывать.

    За время использования транслятора RASM накопился ряд таких полезных псевдокоманд, например:

    MOV X,Y, где X,Y переменные в памяти;

    MOV DS,0 или MOV DS,ES;

    Команды PUSH и POP для нескольких регистров сразу, т.е. PUSH EAX,EBX,ECX;

    Обращение к портам без указания регистра DX и т.п.

    Добавление новых типов команд

    Но, конечно, главное назначение описываемых средств – это добавление в имеющийся транслятор новых групп команд по мере появления новых поколений процессоров.

    При этом в транслятор иногда приходится добавлять и новую группу имен специальных регистров этих команд (внутри транслятора имена это просто переименованные числа). Так, коды имен регистров CR0-CR7 являются внутри транслятора RASM числами 10H-17H, коды имен регистров MM0-MM7 числами 40H-47H, коды имен регистров XMM0-XMM7 числами 50H-57H, и т.д. Младшая цифра чисел (всегда 0-7) участвует в генерации кода через директиву MODRM, а собственно значения чисел используются для задания допустимого диапазона в формальных параметрах новых макро.

    При поиске подходящих операндов транслятор проверит, что указанный в команде регистр входит в допустимый диапазон и поэтому, например, в командах MMX вместо MM0 нельзя указать «чужой» регистр CR0 или XMM0.

    Часто в новых множествах команд требуется применить директиву NOSEGFIX 5, выключающую обычные правила использования префикса 66H (в зависимости от размера операндов), поскольку в описываемых командах этот префикс используется по-своему.

    Тогда, например, для команд из множества MMX описания выглядят так:

    CodeMacro MOVQ dst:Rn(40H,47H),src:Mn
      NOSEGFIX 5
      SEGFIX src
      DB 0FH
      DB 6FH
      MODRM dst,src
    EndM

    Для команд из множества XMM:

    CodeMacro ADDPS dst:Rn(50H,57H),src:Mn
      NOSEGFIX 5
      SEGFIX src
      DB 0FH
      DB 58H
      MODRM dst,src
    EndM

    Для команд из множества SSE2:

    CodeMacro ADDPD dst:Rn(50H,57H),src:Mn
      NOSEGFIX 5
      DB 66H
      SEGFIX src
      DB 0FH
      DB 58H
      MODRM dst,src
    EndM

    Для команд из множества 3DNow!:

    CodeMacro PFACC dst:Rn(40H,47H),src:Mn
      NOSEGFIX 5
      SEGFIX src
      DB 0FH 
      DB 0FH
      MODRM dst,src
      DB 0AEH
    EndM

    Расширение макросредств для x86-64 (IA-32e), AVX-команд и т.д.

    Разумеется, расширение транслятора для генерации 64-х разрядных команд потребовало очередных доработок макросредств в виде добавления новой длины операнда «Q» (64-битный операнд/регистр) и новой директивы REX, формирующей REX-префикс команд. Потребовалось также ввести новые диапазоны регистров, ну и конечно дополнить таблицу служебных слов названиями требуемых регистров, вроде «SPL» или «R14D» или «YMM15».

    Однако все эти доработки потребовали именно расширения, но не кардинальной переделки транслятора.

    Использование макросредств для генерации команд процессоров другой архитектуры

    При выполнении работ по программированию RISC-процессора микроконтроллера AT90S2313 «штатный» транслятор с ассемблера показался автору после работы с RASM непривычным и поэтому неудобным. Возникла идея использовать специальные макросредства и для того, чтобы генерировать коды команд RISC-архитектуры в соответствии с документацией Atmel, но при этом остаться в привычной среде RASM. Дело упрощалось тем, что RASM имеет режим формирования загрузочного модуля сразу, без использования редактора связей.

    Анализ показал, что имеется лишь три препятствия такого использования RASM для генерирования команд RISC-архитектуры: конфликт мнемоники команды ST с названием регистра FPU, форма записи инкремента указателя типа X+ и другой способ вычисления относительного адреса, делающий директиву RELW неподходящей.

    Первые два препятствия были обойдены с помощью введения новых директив в RASM, позволяющих исключать из лексического анализа заданную лексему (в данном случае ST) и разрешать синтаксические конструкции инкремента типа X+.

    Для вычисления относительного адреса команд RISC-архитектуры были доработаны директивы макро RELW и DBIT. В директиве RELW стало возможно указывать необязательные дополнительные параметры в виде «добавки» и «сдвига вправо», позволяющие не просто вычислить адрес относительно текущего места, но и пересчитать его к нужному виду прибавлением «добавки» и сдвигом на заданную величину. При этом новая форма директивы RELW сама уже не генерирует адрес, а запоминает его для последующего использования в директиве DBIT. Доработка DBIT заключалась в возможности использования адреса, вычисленного выше директивой RELW. Для указания такого адреса используется строка “S” вместо имени параметра.

    Такие несложные доработки транслятора повысили универсальность макросредств. Все RISC-команды были легко описаны с их помощью, например:

    …
    CODEMACRO RJMP  k:Cw
      RELW 2,12,k
      DBIT 8('S'(1))
      DBIT 4(0CH),4('S'(9))
    ENDM
    
    CODEMACRO LDI   R_d:Db(16,31),K:Dn
      DBIT 4(R_d(0)),4(K(0))
      DBIT 4(0EH),4(K(4))
    ENDM
    
    CODEMACRO OUT   P:Dn(0,63),R1_r:Db(0,31)
      DBIT 4(R1_r(0)),4(P(0))
      DBIT 5(17H),2(P(4)),1(R1_r(4))
    ENDM
    и т.д.

    И наконец стало можно программировать микроконтроллер AT90S2313 на RASM:

    …
                     ;---- ПЕРЕХОД ПО RESET (0) ----
    
    0000 02C0  0006  rjmp РЕСТАРТ
    
                     ;---- ПЕРЕХОД ПО INT 0 (1) ----
    
    0002 CBC0  019A  rjmp ПРЕРЫВАНИЕ_ОТ_ГПР
    
    РЕСТАРТ:
                     ;---- ИНИЦИАЛИЗАЦИЯ СТЕКА ----
    
     0006 BFED       ldi  СЧ_ТМ,СТЕК   ;КОНЕЦ РАБОЧЕЙ ПАМЯТИ
     0008 BDBF       out  SPL,СЧ_ТМ    ;УСТАНОВИЛИ СТЕК
    
                     ;---- ИНИЦИАЦИЯ ВЫХОДОВ ПОРТА "B" ----
    
     000A 2FE5       ldi  tmp,РАЗР_B
     000C 27BB       out  DDRB,tmp
    
                     ;---- ИНИЦИАЦИЯ ВЫХОДОВ ПОРТА "D" ----
    
     000E 22E0       ldi  tmp,РАЗР_D
     0010 21BB       out  DDRD,tmp
    
                     ;---- ИНИЦИАЦИЯ RS-232 ----
    
     0012 24E0       ldi  tmp,4           ;115200 БОД
     0014 29B9       out  UBRR,tmp
     0016 28E1       ldi  tmp,(1 SHL RXEN) OR (1 SHL TXEN)
     0018 2AB9       out  UCR,tmp
    …

    Заключение

    Предложенная много лет назад идея, образно говоря, «оставить открытой дверь» в транслятор с ассемблера оказалось конструктивной и легко реализуемой. Почти не меняя транслятора можно оперативно отслеживать постоянно дополняемое множество команд современных процессоров. Легкость реализации обусловлена тем, что все множество команд состоит из небольшого числа базовых элементов.

    Подобные средства целесообразно было бы включать вообще во все трансляторы, поскольку они позволяют начинать использование новых возможностей процессоров, не дожидаясь обновления соответствующих средств разработки. Кроме этого, с помощью данных средств можно совершенствовать систему команд добавлением псевдокоманд типа MOVSX EAX,10, практически ничем не отличающихся от реальных команд процессора.

    Как было показано выше, универсальность описанных макросредств такова, что при незначительных доработках транслятора возможно даже настраивать его на генерацию команд для процессоров совсем другой архитектуры.

    Использование RASM сегодня, разумеется, не требует таскать вместе с ним еще и файлы с описаниями команд, накопившиеся за все это время их эволюции. С помощью примитивной программы такие описания переводятся во внутреннее представление и становятся частью общей таблицы после очередной пересборки транслятора.

    Разумеется также, что очередные изменения архитектуры (например, переход к x86-64) не могут свестись в трансляторе только к использованию подобных макросредств, а объективно требуют и доработок самого транслятора. Тем не менее, как бы в дальнейшем ни продолжилась эволюция команд процессоров, данные средства наверняка окажутся полезными.

    Литература

    1.       «RASM-86 Programmer’s Guide» Digital Research, Сalifornia

    http://bitsavers.org/pdf/digitalResearch/pl1/

    2.       М.Гук, В. Юров «Процессоры Pentium 4, Athlon и Duron». СПб.: Из-во Питер, 2001

    Комментарии 12

      +2

      Может архитектура всё-таки RISC, а не RISK?

        +4
        Отход от практики в асме: 1 мнемоника — 1 команда скрывает ситуацию когда прерывание может вклиниваться между командами и нарушать атомарность операции. Этот подход сильно маскирует такие ситуации. Хотя красиво, и почти всегда не критично, но ИМХО ассемблер обязан такие ситуации отражать явно.
          +1
          В принципе согласен, но из всех неприятностей эта так же вероятна, как смерть от кирпича с крыши.
          Хуже другое: отладчик и дизассемблер этого не понимают и показывают истинные команды))
          Но это беда всех макрорасширений — в конечном коде можно заблудиться при отладке.
            0
            В том то и дело, что в исходнике видно что это макрос и он может скрывать какие-то архитектурные особенности. А вот если это скрыть мнемоникой, и это чья-то библиотека, а вы не причитали аппендикс или еррату на процессор…
            0
            Прерывание должно отрабатывать чисто, незаметно для текущего процесса. Ну, в идеале ;)
              0
              Да как то не очень получается, если оно контекст переключает или в нем читается ECX. Но кажется что MOV ECX, 10 атомарна. И это еще простой случай.
              +2
              Бывают такие команды процессора, которые состоят из нескольких шагов, и между шагами тоже может вклиниться прерывание. Например, это команды блочной пересылки и поиска.

              Во всех случаях программист должен хорошо знать систему команд процессора, на котором работает. Но если он её знает — то ему не составит труда увидеть, что некоторой команды, которая стоит в программе, в системе команд процессора нет, и учесть последствия.

              Код, правильность исполнения которого зависит от количества стоящих в нём машинных команд, даже при программировании на чистом ассемблере встречается довольно редко. А критичные участки, такие, как переключение контекста задач, примитивы синхронизации и т.д. всё равно вылизываются вручную с очень пристальным вниманием, и там обычно не применяются незнакомые макросы.

              Когда программировал на ассемблере Z80, я часто с помощью макросов создавал для себя дополнительные, часто встречающийся, «команды процессора». Никаких проблем не было.
                0
                Ерунда! Есть высокоуровневый ассемблер, а есть низкоуровневый. Отлаживает в отладчике как раз низкоуровневый. Никогда в этом не видел проблему. Да и причём тут прерывания? Непонятно. Я сейчас только высокоуровневый стиль использую.
                  0
                  А понял! Это вы про микроконтроллеры, а я про компы общего назначения!
                  0
                  Надо ещё добавить возможность операторы :=, +=, -=, *= и другие, во время работы парсера переделываются в соответствующие макросы, например eax:=edx. Макрос assign получает соответствующие параметры, может определить тип и другие параметры, например имя класса указателя, и создаст код mov eax,edx.
                  Я тут говорю именно про высокоуровневый ассемблер, промежуточный между С/С++ и низким ассемблером.
                  +1
                  Давно не занимался, но замена MOV ECX,10 на PUSH 10 и POP ECX по моим воспоминаниям приведёт к увеличению времени выполнения (как минимум вдвое). Почему бы тогда не что-то типа xor ECX,ECX move CL,10 (нет стека — нет цикла записи в память)?
                    0
                    Так понимаю, это чтобы исходники на ассемблере с одного процессора на другой таскать.
                    Нет какой-то команды — эмулируем. С 80186 на 8086, например.
                    Нужно быстро с AVR на PIC перетащить — пишем набор макросов, фигак-фигак и старый исходник с минимальными правками — в продакшн.

                    Да и кто из нас не грешил заменой экзотических мнемоник команд на привычные?

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое