Практика программирования на языке ассемблера в RARS

Отвлечёмся пока от собственно архитектуры ЭВМ.

  • Ассемблер — это транслятор исходных текстов в машинный код (возможно, в объектный фал, требующий компоновки; но вся содержательная часть — данные, программа и другие секции — уже находятся в финальном, машинном представлении). Отличие от других компиляторов — в языке ассемблера.

  • Язык ассемблера для определённой архитектуры — низкоуровневый ЯП, однозначно соответствующий вычислительной модели этой архитектуры и принятым в ней конвенциям. Для сравнения, язык Си включает в себя значительную часть аппаратной вычислительной модели (размер целого, адреса, регистровые переменные и др.), но унифицирует многие конвенции (например, по вызову подпрограмм), и, конечно же, берёт на себя планирование регистров при трансляции вычислений. А язык Паскаль, даром что по уровню предлагаемых абстракций мало чем отличается от Си, полностью исключает аппаратную вычислительную модель из синтаксиса (вплоть до стандартизации вычислений с плавающей точкой независимо от возможностей и даже наличия сопроцессора).

Многофайловая сборка

В операционных системах исполняемые программы — это не только код и данные, но и метаинформация относительно правил их загрузки в память, расположении и размере стека, кучи и т. п. (см., например, формат ELF). В RARS этого нет, но задачу многофайловой сборки решать надо.

  • Директива .include — позволяет повторно использовать уже написанный текст

    • Обычно это макросы, реже — подпрограммы
    • …потому что результат — это как бы один файл со встроенными include-ами, и возможен конфликт имён
      • нам уже и так тесно оттого, что в ассемблере RARS нет локальных меток.
  • Несколько отдельных файлов
    • Частичная изоляция пространств имён
    • Директива .globl имя (GLOBal Label; чтобы не путаться, можно писать и .global ☺) задаёт список глобальных имён — такие имена видны при обработке всех файлов (сама метка задаётся только в одном)

    • Директива .extern имя размер_в_байтах — размещает данных в «общей области» (начиная с 0x10000000) и объявляет имена глобальными

    • «Точка входа» (main) — выполнение начинается с адреса main, помеченного как .globl

      • NB: отныне и навсегда включим в настройке RARS «Initialize program counter to global 'main' if defined»

      • соответствует параметру rars sm

В RARS есть три варианта многофайловой сборки:

  • «режим проекта», когда в единый бинарный образ транслируются все файлы из определённого каталога («Settings → Assemble all files in directory»)
  • «ленивый лежим», когда в единый бинарный образ транслируются все открытые для редактирования файлы («Settings → Assemple all files currently open»)
  • «режим командной строки», который используется для трансляции и или запуска программы вообще без GUI (так, напрример, это происходит в EJudge); в этом режиме в единый бинарный образ транслируются все файлы, имена которых были переданы в командной строке

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

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

Псевдонимы

Примитивный макрос — директива .eqv имя строка, которая добавляет имя в список распознаваемых ассемблером лексем, а в результате препроцессинга (обработки текста перед трансляцией) происходит замена этой лексемы на строку:

   1 .eqv    Esize   16
   2 .eqv    Era     12(sp)
   3 .eqv    Es1     8(sp)
   4 .eqv    EA      4(sp)
   5 .eqv    EB      (sp)
   6 
   7 subr:   # некоторая подпрограмма
   8         addi    sp sp -Esize    # выделение памяти на стеке
   9         sw      ra Era          # сохранение ra
  10         sw      s1 Es1          # сохранение s1
  11         sw      zero EA         # первая переменная
  12         sw      zero EB         # вторая переменная
  13         # какой-то код
  14         lw      s1 Es1          # восстановление s1
  15         lw      ra Era          # восстановление ra
  16         addi    sp sp Esize
  17         ret

Макроподстановка

(Строго говоря, «макрос» — это множественное число от «макро», но в современном русском это слово приобрело свойства единственного числа. Множественное число — «макросы», по аналогии с «конверсами», «сникерсами» и т. п.)

Механизм макроподстановки может быть и посложнее:

   1 .macro    exit
   2     li    a7 10
   3     ecall
   4 .end_macro
   5 
   6 .text
   7     nop
   8     exit

Первые 4 строчки — задание макроса exit, оно же макроопределение, последняя — использование этого макроса, оно же макрокоманда. (Не «вызов макроса», потому что на месте макрокоманды не будет никакой инструкции вызова, только то, что составляло тело макроса).

Добрый RARS даже распишет номера строк, в которых находилось макросово тело:

0x00400000  0x00000013  addi x0,x0,0                 7        nop
0x00400004  0x00a00893  addi x17,x0,10               8    <2> li    a7 10
0x00400008  0x00000073  ecall                        8    <3> ecall

Здесь 7 и 8 — номера строк исходного текста, а <2> и <3> — номера строк, на которых располагалось тело макроса.

Сам макрос (в отличие от подпрограммы), конечно, ни во что не странслировался, потому что он — всего лишь задание нового правила для трансляции каждой макрокоманды.

Параметрические макросы

Самое удобное в макроподстановке — параметризация макрокоманд. Общий вид макроопределения:

   1 .macro имя %параметр1 %параметр2
   2        тело макроса, в строках которого
   3        могут встречаться %параметр1, %параметр2 и т. д.
   4 .end_macro

Например:

   1 .macro       print %reg
   2     mv       a0 %reg
   3     li       a7 1
   4     ecall
   5 .end_macro
   6 
   7 .text
   8     li       t0 100
   9     li       t1 -20
  10     print    t0
  11     print    t1

Здесь макрокоманда print дважды раскрывается в три инструкции, причём первая из них (mv) в первом случае подставится в виде mv a0 t0, а во втором — в виде mv a0 t1 (строго говоря add a0 zero …, конечно):

0x00400000  0x06400293  addi x5,x0,0x00000064        8        li       t0 100
0x00400004  0xfec00313  addi x6,x0,0xffffffec        9        li       t1 -20
0x00400008  0x00500533  add x10,x0,x5                10   <2> mv      a0 t0
0x0040000c  0x00100893  addi x17,x0,1                10   <3> li        a7 1
0x00400010  0x00000073  ecall                        10   <4> ecall
0x00400014  0x00600533  add x10,x0,x6                11   <2> mv      a0 t1
0x00400018  0x00100893  addi x17,x0,1                11   <3> li        a7 1
0x0040001c  0x00000073  ecall                        11   <4> ecall

Обратите внимание на то, как отмечает RARS номера строк исходного кода и строк в теле макроса.

Макроподстановка, вообще говоря, может не иметь никакого отношения к синтаксису того текста, в котором встречаются макросы (например, универсальные макропроцессоры m4 или cpp). Однако поскольку в ассемблере RARS подстановка параметров макро приводит к появлению соответствующей подстроки в в виде отдельного слова инструкции или директивы, разрешается передавать только лексемы языка ассемблера. Впрочем, можно передать, например, имя метки:

   1 .macro  input %label %string
   2 .data
   3 %label: .asciz  %string
   4 .text
   5         li      a7 4
   6         la      a0 %label
   7         ecall
   8         li      a7 5
   9         ecall
  10 .end_macro
  11 
  12         input l1 "Enter an integer: "
  13         input l2 "Enter an integer: "

Такая реализация проще (для препроцессора и для последующей трансляции используется один и тот же анализатор), но не такая гибкая. Подставить «--» вместо $t0 в макрокоманде из примера не удастся ещё на этапе макорподстановки (ошибка «riscv1.asm line 10 column 2: forward reference or invalid parameters for macro "print"» в строке с макрокомандой). А вот 100500 вместо $t0 пройдёт макроподстановку (потому что 100500 — это хорошее годное целое число), но полученный текст не пройдёт трансляцию с сообщением «riscv2.asm line 10->2 column 11: "100500": operand is of incorrect type». Ошибка возникнет, с точки зрения ассемблера RARS, всё в той же строке 10, но по вине строки 2 макроопределения.

Кстати, print-ы в примере слились в одну строку, потому что никто не вывел между ними ещё и разделителя. Чтобы исправить это положение, не надо модифицировать основную программу! Достаточно добавить в макроопеделение print такой вывод:

   1 .macro        print %reg
   2     mv        a0 %reg
   3     li        a7 1
   4     ecall
   5     li        a7 11
   6     li        a0 '\n'
   7     ecall
   8 .end_macro

Сама программа при этом разрастётся чуть ли не в два раза:

0x00400000  0x06400293  addi x5,x0,0x00000064        11       li       t0 100
0x00400004  0xfec00313  addi x6,x0,0xffffffec        12       li       t1 -20
0x00400008  0x00500533  add x10,x0,x5                13   <2> mv        a0 t0
0x0040000c  0x00100893  addi x17,x0,1                13   <3> li        a7 1
0x00400010  0x00000073  ecall                        13   <4> ecall
0x00400014  0x00b00893  addi x17,x0,11               13   <5> li        a7 11
0x00400018  0x00a00513  addi x10,x0,10               13   <6> li             a0 '\n'
0x0040001c  0x00000073  ecall                        13   <7> ecall
0x00400020  0x00600533  add x10,x0,x6                14   <2> mv        a0 t1
0x00400024  0x00100893  addi x17,x0,1                14   <3> li        a7 1
0x00400028  0x00000073  ecall                        14   <4> ecall
0x0040002c  0x00b00893  addi x17,x0,11               14   <5> li        a7 11
0x00400030  0x00a00513  addi x10,x0,10               14   <6> li             a0 '\n'
0x00400034  0x00000073  ecall                        14   <7> ecall

Макровзрыв

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

Определим новый макрос printS, который выводит строку, и input, который выводит строку-подсказку (задаётся непосредственным адресом), а затем вводит число. В макрос print тоже добавим подсказку. Ассемблер RARS позволяет определять несколько макросов с одинаковым именем, но разным количеством параметров. Воспользуемся этим.

  •    1 .macro  print   %reg
       2         mv      a0 %reg
       3         li      a7 1
       4         ecall
       5         li      a0 10
       6         li      a7 11
       7         ecall
       8 .end_macro
       9 
      10 .macro  printS  %addr
      11         la      a0 %addr
      12         li      a7 4
      13         ecall
      14 .end_macro
      15 
      16 .macro  print   %msg %reg
      17         printS  %msg
      18         print   %reg
      19 .end_macro
      20 
      21 .macro  input   %msg %reg
      22         printS  %msg
      23         li      a7 5
      24         ecall
      25         mv      %reg a0
      26 .end_macro
      27 
      28 .data
      29 msg1:   .asciz  "First number: "
      30 msg2:   .asciz  "Second number: "
      31 res1:   .asciz  "Result 1: "
      32 res2:   .asciz  "Result 2: "
      33 .text
      34         input   msg1 t0
      35         input   msg2 t1
      36         print   res1 t0
      37         print   res2 t1
    

Второй макрос print (тот, что с двумя параметрами), получился совсем «короткий» — всего две макрокоманды. Но на самом деле он довольно-таки объёмистый, раскрывается в 9 инструкций ассемблера (в 10, с учётом псевдоинструкции la).. Наши четыре строчки кода программы превратились в 34 инструкции!

0x00400000  0x0fc10517  auipc x10,0x0000fc10         34   <11> la      a0 msg1
0x00400004  0x00050513  addi x10,x10,0
0x00400008  0x00400893  addi x17,x0,4                34   <12> li      a7 4
0x0040000c  0x00000073  ecall                        34   <13> ecall
0x00400010  0x00500893  addi x17,x0,5                34   <23> li      a7 5
0x00400014  0x00000073  ecall                        34   <24> ecall
0x00400018  0x00a002b3  add x5,x0,x10                34   <25> mv      t0 a0
0x0040001c  0x0fc10517  auipc x10,0x0000fc10         35   <11> la      a0 msg2
0x00400020  0xfe450513  addi x10,x10,0xffffffe4
0x00400024  0x00400893  addi x17,x0,4                35   <12> li      a7 4
0x00400028  0x00000073  ecall                        35   <13> ecall
0x0040002c  0x00500893  addi x17,x0,5                35   <23> li      a7 5
0x00400030  0x00000073  ecall                        35   <24> ecall
0x00400034  0x00a00333  add x6,x0,x10                35   <25> mv      t1 a0
0x00400038  0x0fc10517  auipc x10,0x0000fc10         36   <11> la      a0 res1
0x0040003c  0xfc850513  addi x10,x10,0xffffffc8
0x00400040  0x00400893  addi x17,x0,4                36   <12> li      a7 4
0x00400044  0x00000073  ecall                        36   <13> ecall
0x00400048  0x00500533  add x10,x0,x5                36   <2> mv      a0 t0
0x0040004c  0x00100893  addi x17,x0,1                36   <3> li      a7 1
0x00400050  0x00000073  ecall                        36   <4> ecall
0x00400054  0x00a00513  addi x10,x0,10               36   <5> li      a0 10
0x00400058  0x00b00893  addi x17,x0,11               36   <6> li      a7 11
0x0040005c  0x00000073  ecall                        36   <7> ecall
0x00400060  0x0fc10517  auipc x10,0x0000fc10         37   <11> la      a0 res2
0x00400064  0xfa050513  addi x10,x10,0xffffffa0
0x00400068  0x00400893  addi x17,x0,4                37   <12> li      a7 4
0x0040006c  0x00000073  ecall                        37   <13> ecall
0x00400070  0x00600533  add x10,x0,x6                37   <2> mv      a0 t1
0x00400074  0x00100893  addi x17,x0,1                37   <3> li      a7 1
0x00400078  0x00000073  ecall                        37   <4> ecall
0x0040007c  0x00a00513  addi x10,x0,10               37   <5> li      a0 10
0x00400080  0x00b00893  addi x17,x0,11               37   <6> li      a7 11
0x00400084  0x00000073  ecall                        37   <7> ecall

Если активно использовать удачно названные и спланированные макросы в своих программах

  • программы становятся хорошо читаемыми
  • код программы разрастается с невероятной скоростью

Вопрос: Если вы использовали в программе 10 макрокоманд, каждая из которых состояла из 10 макрокоманд, каждая из которых состояла из 10 инструкций, сколько инструкций (не считая другого полезного кода) появится в оттранслированной программе?

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

  • Стоит ещё раз напомнить, что макросы вправе рассчитывать на соблюдение конвенций — например, конвенции по сохранению регистров
  • Регистр raне сохраняемый. По конвенции его надо как можно быстрее положить на стек, пока не испортился, но ожидать, что он не испортился, нельзя.

    • Это накладывает ограничение на использование макросов-обёрток в концевых подпрограммах (используете макрос? сохраняйте ra самостоятельно!) и в прологах/эпилогах

       1 .text
       2 _input: # a0 — message / a0 — input value
       3         li      a7 4
       4         ecall
       5         li      a7 5
       6         ecall
       7         ret
       8 
       9 .macro  input   %msg %reg
      10         la      a0 %msg
      11         jal     _input
      12         mv      %reg a0
      13 .end_macro
      14 
      15 _print: # a0 — message, a1 — number
      16         li      a7 4
      17         ecall
      18         mv      a0 a1
      19         li      a7 1
      20         ecall
      21         li      a0 10
      22         li      a7 11
      23         ecall
      24         ret
      25 
      26 .macro  print   %msg %reg
      27         la      a0 %msg
      28         mv      a1 %reg
      29         jal     _print
      30 .end_macro
      31 
      32 .data
      33 msg1:   .asciz  "First number: "
      34 msg2:   .asciz  "Second number: "
      35 res1:   .asciz  "Result 1: "
      36 res2:   .asciz  "Result 2: "
      37 
      38 .text
      39 .globl  main
      40 main:
      41         input   msg1 t0
      42         input   msg2 t1
      43         print   res1 t0
      44         print   res2 t1
    
  • Обратите внимание на директиву .globl main. Видя main в списке глобальных меток, RARS будет загружать в регистр pc не начало секции .text, а адрес main (для этого надо включить соответствующую настройку).

  • Новая программа слегка короче предыдущей, но резуьлтат трансляции тем больше, чем больше в ней макроподстановок

    0x00400000  0x00400893  addi x17,x0,4                3            li      a7 4
    0x00400004  0x00000073  ecall                        4            ecall
    0x00400008  0x00500893  addi x17,x0,5                5            li      a7 5
    0x0040000c  0x00000073  ecall                        6            ecall
    0x00400010  0x00008067  jalr x0,x1,0                 7            ret
    0x00400014  0x00400893  addi x17,x0,4                16           li      a7 4
    0x00400018  0x00000073  ecall                        17           ecall
    0x0040001c  0x00b00533  add x10,x0,x11               18           mv      a0 a1
    0x00400020  0x00100893  addi x17,x0,1                19           li      a7 1
    0x00400024  0x00000073  ecall                        20           ecall
    0x00400028  0x00a00513  addi x10,x0,10               21           li      a0 10
    0x0040002c  0x00b00893  addi x17,x0,11               22           li      a7 11
    0x00400030  0x00000073  ecall                        23           ecall
    0x00400034  0x00008067  jalr x0,x1,0                 24           ret
    0x00400038  0x0fc10517  auipc x10,0x0000fc10         41   <10> la      a0 msg1
    0x0040003c  0xfc850513  addi x10,x10,0xffffffc8
    0x00400040  0xfc1ff0ef  jal x1,0xffffffc0            41   <11> jal     _input
    0x00400044  0x00a002b3  add x5,x0,x10                41   <12> mv      t0 a0
    0x00400048  0x0fc10517  auipc x10,0x0000fc10         42   <10> la      a0 msg2
    0x0040004c  0xfc750513  addi x10,x10,0xffffffc7
    0x00400050  0xfb1ff0ef  jal x1,0xffffffb0            42   <11> jal     _input
    0x00400054  0x00a00333  add x6,x0,x10                42   <12> mv      t1 a0
    0x00400058  0x0fc10517  auipc x10,0x0000fc10         43   <27> la      a0 res1
    0x0040005c  0xfc750513  addi x10,x10,0xffffffc7
    0x00400060  0x005005b3  add x11,x0,x5                43   <28> mv      a1 t0
    0x00400064  0xfb1ff0ef  jal x1,0xffffffb0            43   <29> jal     _print
    0x00400068  0x0fc10517  auipc x10,0x0000fc10         44   <27> la      a0 res2
    0x0040006c  0xfc250513  addi x10,x10,0xffffffc2
    0x00400070  0x006005b3  add x11,x0,x6                44   <28> mv      a1 t1
    0x00400074  0xfa1ff0ef  jal x1,0xffffffa0            44   <29> jal     _print
  • При дальнейшем использовании макросов print и input программа будет прирастать на 4 инструкции, а не на 7 или 10.

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

Мы уже знаем, что процесс макроподстановки достаточно умён, чтобы находить в макроопределении формальные параметры и подставлять вместо них фактические. Не меньше (а может быть, и больше) интеллекта ему требуется, чтобы отслеживать метки.

В самом деле, стоит появиться метке в теле макроопределения, как вторая же макрокоманда раскроется в последовательность инструкций, в которой окажется такая же метка, какая была в первой. По идее это должно привести к ошибке.

Однако ассемблер RARS во время макроподстановки переименовывает все метки, которые встретит в макроопределении — и задание меток, и обращение к ним. Правило такое: метка метка переименовывается в метку метка_M№, где — это порядковый номер текущей операции макроподстановки.

  •    1 .macro  labeled
       2         add     t0 t0 zero
       3 label:  add     t1 t1 zero
       4         add     t2 t2 zero
       5 .end_macro
       6 
       7 .text
       8         labeled
       9         labeled
      10         labeled
    

после макроподстановки будет выглядеть примерно как

  • 0x00400000  0x000282b3  add x5,x5,x0     8    <2> add       t0 t0 zero
    0x00400004  0x00030333  add x6,x6,x0     8    <3> label_M0:  add    t1 t1 zero
    0x00400008  0x000383b3  add x7,x7,x0     8    <4> add       t2 t2 zero
    0x0040000c  0x000282b3  add x5,x5,x0     9    <2> add       t0 t0 zero
    0x00400010  0x00030333  add x6,x6,x0     9    <3> label_M1:  add    t1 t1 zero
    0x00400014  0x000383b3  add x7,x7,x0     9    <4> add       t2 t2 zero
    0x00400018  0x000282b3  add x5,x5,x0     10   <2> add       t0 t0 zero
    0x0040001c  0x00030333  add x6,x6,x0     10   <3> label_M2:  add    t1 t1 zero
    0x00400020  0x000383b3  add x7,x7,x0     10   <4> add       t2 t2 zero

Не слишком красивый приём, с учётом того, что программист может случайно сам завести такую метку в своей программе. Однако действенный: внутри раскрытого макроса метка актуальна, а во всей программе — уникальна.

Генерация меток наводит на мысль о том, что наши макрос-функции print и input можно сделать ещё более удобными, если строку-подсказку передавать макросу прямо в качестве параметра, а превращать в .asciz уже в теле макроса:

  •    1 .globl  main
       2 .text
       3 _input: # a0 — message / a7 — input value
       4         li      a7 4
       5         ecall
       6         li      a7 5
       7         ecall
       8         ret
       9 
      10 .macro  input   %msg %reg
      11 .data
      12 msg:    .ascii  %msg
      13         .asciz  ": "
      14 .text
      15         la      a0 msg
      16         jal     _input
      17         mv      %reg a0
      18 .end_macro
      19 
      20 _print: # a0 — message, a1 — number
      21         li      a7 4
      22         ecall
      23         mv      a0 a1
      24         li      a7 1
      25         ecall
      26         li      a0 10
      27         li      a7 11
      28         ecall
      29         ret
      30 
      31 .macro  print   %msg %reg
      32 .data
      33 msg:    .ascii  %msg
      34         .asciz  ": "
      35 .text
      36         la      a0 msg
      37         mv      a1 %reg
      38         jal     _print
      39 .end_macro
      40 
      41 .text
      42 main:
      43         input   "First input" t0
      44         input   "Second input" t1
      45         print   "First result" t0
      46         print   "Second result" t1
    

Обратите внимание на то, как чередуются .data и .text: на самом деле никакой чересполосицы кода и данных не получится, потому что каждая директива .data просто размещает последующие данные строго после содержимого предыдущей секции .data (если не задавать явно адрес — начиная с 0x10010000); то же самое верно и для .text (начиная с 0x400000).

Кроме того, теперь в параметре задаётся только содержательная подсказка, а ": " «приклеивается» к ней уже в макросе. Полученный код столь же компактен:

0x00400000  0x00400893  addi x17,x0,4                4            li      a7 4
0x00400004  0x00000073  ecall                        5            ecall
0x00400008  0x00500893  addi x17,x0,5                6            li      a7 5
0x0040000c  0x00000073  ecall                        7            ecall
0x00400010  0x00008067  jalr x0,x1,0                 8            ret
0x00400014  0x00400893  addi x17,x0,4                21           li      a7 4
0x00400018  0x00000073  ecall                        22           ecall
0x0040001c  0x00b00533  add x10,x0,x11               23           mv      a0 a1
0x00400020  0x00100893  addi x17,x0,1                24           li      a7 1
0x00400024  0x00000073  ecall                        25           ecall
0x00400028  0x00a00513  addi x10,x0,10               26           li      a0 10
0x0040002c  0x00b00893  addi x17,x0,11               27           li      a7 11
0x00400030  0x00000073  ecall                        28           ecall
0x00400034  0x00008067  jalr x0,x1,0                 29           ret
0x00400038  0x0fc10517  auipc x10,0x0000fc10         43   <15> la      a0 msg_M0
0x0040003c  0xfc850513  addi x10,x10,0xffffffc8
0x00400040  0xfc1ff0ef  jal x1,0xffffffc0            43   <16> jal     _input
0x00400044  0x00a002b3  add x5,x0,x10                43   <17> mv      t0 a0
0x00400048  0x0fc10517  auipc x10,0x0000fc10         44   <15> la      a0 msg_M1
0x0040004c  0xfc650513  addi x10,x10,0xffffffc6
0x00400050  0xfb1ff0ef  jal x1,0xffffffb0            44   <16> jal     _input
0x00400054  0x00a00333  add x6,x0,x10                44   <17> mv      t1 a0
0x00400058  0x0fc10517  auipc x10,0x0000fc10         45   <36> la      a0 msg_M2
0x0040005c  0xfc550513  addi x10,x10,0xffffffc5
0x00400060  0x005005b3  add x11,x0,x5                45   <37> mv      a1 t0
0x00400064  0xfb1ff0ef  jal x1,0xffffffb0            45   <38> jal     _print
0x00400068  0x0fc10517  auipc x10,0x0000fc10         46   <36> la      a0 msg_M3
0x0040006c  0xfc150513  addi x10,x10,0xffffffc1
0x00400070  0x006005b3  add x11,x0,x6                46   <37> mv      a1 t1
0x00400074  0xfa1ff0ef  jal x1,0xffffffa0            46   <38> jal     _print

Конвенции относительно регистров

Конвенции по использованию регистров — такие же, как и для подпрограмм, за исключением того, что параметры макроса нет необходимости раскладывать по регистрам a* (в примере выше этим занимается сам макрос).

Свойства макроассемблера RARS

Замечания авторов RARS относительно их макроассемблера:

  • Макроопределение должно идти в тексте программы до соответствующей макрокоманды (иначе пришлось бы анализировать текст дважды)

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

  • Вложенные макросы не поддерживаются, т. е. внутри макроопределения не может встречаться директива .macro

  • Внутри макроопределения, как и в тексте программы, могут встречаться только ранее определённые макрокоманды, искать их определения далее по тексту никто не будет
  • Все определённые в макросе метки меняются в процессе макроподстановки, превращаясь в метка_M№

    • (метка может сама быть результатом макроподстановки, тогда с ней ничего не происходит, как в примере в начале этой темы)
  • Несколько макросов с одинаковым именем, но разным количеством параметров, считаются различными, и их можно использовать все

  • Повторное определение макроса с тем же именем и тем же количеством параметров игнорируется, макрокоманда раскрывается в первое определение
  • Параметром макроса (в силу ограниченной реализации) может быть только атомарная лексема языка ассемблера. Например, параметром не может быть "4(t0)", потому что это две лексемы, а не одна

  • Макросредства ассемблера не входят ни в какой стандарт и остаются на усмотрение авторов ассемблера

В больших многофайловых проектах принято все макросы складывать в отдельный файл и включать их в код программы с помощью директивы .include файл_с_макросами. Подпрограммы при этом складываются в другой файл (возможно, не один), т. н. «библиотеку», и подключаются посредством многофайловой сборки.

На предыдущем примере:

  1. Файл с программой prog.asm:

       1 .include "macros.inc"
       2 .globl  main
       3 .text
       4 main:
       5         input   "First input" t0
       6         input   "Second input" t1
       7         print   "First result" t0
       8         print   "Second result" t1
       9         exit
    
  2. Файл с подпрограммами lib.asm:

       1 .globl  _input _print
       2 .text
       3 _input: # a0 — message / a7 — input value
       4         li      a7 4
       5         ecall
       6         li      a7 5
       7         ecall
       8         ret
       9 
      10 _print: # a0 — message, a1 — number
      11         li      a7 4
      12         ecall
      13         mv      a0 a1
      14         li      a7 1
      15         ecall
      16         li      a0 10
      17         li      a7 11
      18         ecall
      19         ret
    
    • Не забываем метки всех подпрограмм, которые понадобятся в других файлах, объявлять как .globl

    • Файл с макросами macros.inc (имя файла не заканчивается на .asm в знак того, что его не нужно транслировать отдельно):

       1 .macro  input   %msg %reg
       2 .data
       3 msg:    .ascii  %msg
       4         .asciz  ": "
       5 .text
       6         la      a0 msg
       7         jal     _input
       8         mv      %reg a0
       9 .end_macro
      10 
      11 .macro  print   %msg %reg
      12 .data
      13 msg:    .ascii  %msg
      14         .asciz  ": "
      15 .text
      16         la      a0 msg
      17         mv      a1 %reg
      18         jal     _print
      19 .end_macro
      20 
      21 .macro  exit
      22         li      a7 10
      23         ecall
      24 .end_macro
    

Чего нет в RARS

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

В RARS (и только в RARS) в некоторых случаях (например, в директиве .globl) нельзя использовать имена, по написанию совпадающие с инструкциями — например, нельзя сделать глобальной метку b:!

  • Библиотека макросов и подпрограмм. Чтобы написать большую программу, потребуется множество подпрограмм, реализующих стандартные приёмы работы — ввод-вывод, работа с дисками, управление внешними устройствами и т. п. Для этого в профессиональных инструментариях, типа gas или Gnu Assebmler, имеются заранее подготовленные библиотеки макросов и подпрограмм. А мы пишем их сами :)

  • «Настоящий» макропроцессор. Макропросессор в RARS опирается только на лексемы языка ассемблера и не имеет собственного языка. Это почти не мешает, но временами (как в примере с 4($t0) ) слегка неудобно.

  • Переменные и вычисления периода трансляции. С помощью .eqv мы можем заменить некоторую константу на мнемоническое обозначение, но не более. В действительности макроассемблер RARS вообще не интересуется, что именно кроется за введённым обозначением, и просто выполняет подстановку. В промышленных макропроцессорах возможно вычисление арифметических выражений в момент трансляции, задание констант и даже переменных. Например, ввести переменную SIZE для некоторого базового размера массивов, переменную DOUBLESIZE = SIZE * 2, а в тексте программы писать что-то вроде array: .space DOUBLESIZE + 4. Ещё раз напомним, что всё это происходит до финального этапа трансляции, превращающего текст в машинный код — поэтому транслируется уже результат таких вычислений.

  • Адресная арифметика. Вычисление некоторых значений, смещений и размеров на основании уже известных адресов. Хотелось бы уметь писать что-то вроде Arr+20, что означало бы «отступ в 20 элементов массива» и менялось в зависимости от типа Arr. В некоторых случаях это упрощает разработку, в некоторых — усложняет работу с памятью на низком уровне.

  • Условная трансляция. Наиболее полезное свойство вычислений в период трансляции — это возможность транслировать или не транслировать части текста в зависимости от результата этих вычислений. Например можно вставить исходный текст отладочные сообщения, но транслировать их только если определена некоторая переменная периода трансляции DEBUG. Как-нибудь так:

       1 .if DEBUG
       2         код, транслируемый
       3         только если существует
       4         константа DEBUG
       5 .endif
    
  • Генерация макроопределений. Если разрешить создавать макросы внутри макросов, можно развёртывать целые семейства определений в зависимости от исходного параметра внешнего макроса

  • Конкатенация. Иногда необходимо, чтобы результат постановки нескольких макросов интерпретировался затем как одна лексема языка (например, строка label##suffix##index превращалась бы при наличии констант suffix=_M и index=5 в label_M5). В RARS такого механизма нет

  • Локальные метки. Бывает очень полезно ограничить видимость меток сильнее, чем просто внутри файла. Например, если в файле задано несколько подпрограмм, в каждой из них хотелось бы иметь возможность использовать метки типа start, finish, loop или стандартные имена переменных. Это можно было бы сделать, введя особенный синтаксис временных меток или ограничить видимость меток специальной конструкцией «локальное пространство имён» и т. п.

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

LecturesCMC/ArchitectureAssemblerProject/13_MacroAssembler (последним исправлял пользователь FrBrGeorge 2024-07-15 12:32:59)