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

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

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

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

TODO написать полный текст

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

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

Псевдоннимы

Примитивный макрос — директива .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 в макрокоманде из примера не удастся ещё на этапе макорподстановки (ошибка «mips2.asm line 10 column 2: forward reference or invalid parameters for macro "print"» в строке с макрокомандой). А вот 100500 вместо $t0 пройдёт макроподстановку (потому что 100500 — это хорошее годное целое число), но полученный текст не пройдёт трансляцию с сообщением «mips2.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 позволяет определять несколько макросов с одинаковым именем, но разным количеством параметров. Воспользуемся этим.

Второй макрос 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 инструкций, сколько инструкций (не считая другого полезного кода) появится в оттранслированной программе?

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

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

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

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

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

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

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

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

Обратите внимание на то, как чередуются .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

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

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

В больших многофайловых проектах принято все макросы складывать в отдельный файл и включать их в код программы с помощью директивы .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 достаточен для написания программ среднего объёма, а написание действительно крупных проектов на языке ассемблера выходит за рамки данного курса.

LecturesCMC/ArchitectureAssembler2022/04~_Assembler (последним исправлял пользователь FrBrGeorge 2022-03-11 11:47:13)