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

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

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

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

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

Позиционно-независимый код

Позиционно-независимый код — способность готовой программы быть загруженной и работать в памяти по произвольному адресу вообще без модификаций или путём начальной установки значений некоторых регистров (как минимум, pc и sp).

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

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

Псевдонимы

Примитивный макрос — директива .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 позволяет определять несколько макросов с одинаковым именем, но разным количеством параметров. Воспользуемся этим.

Второй макрос 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

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

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

Свойства макроассемблера 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 (и только в RARS) в некоторых случаях (например, в директиве .globl) нельзя использовать имена, по написанию совпадающие с инструкциями — например, нельзя сделать глобальной метку b:!

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

Д/З

TODO (на следующую итерацию, когда на случится — сделать задание «строковые макросы» совместимым с Д/З по следующей лекции)

<!> В этой лекции все задания будут слегка иного типа: потребуется написать что-то вроде библиотеки макросов, состоящей из одного или нескольких определений макроса и, возможно, сопутствующих подпрограмм. В процессе тестирования к вашему решению будет приписываться дополнительный фрагмент (aka footer) с основной программой, а получившийся текст — уходить на проверку.

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

Задачи:

  1. Посмотрите особенности макроассемблера RARS — он реально странненький.
  2. (это фактически упрощённая копипаста из лекции — без вывода дополнительной строки ": " в конце подсказки)

    EJudge: InputPrompt 'Ввод с подсказкой'

    Написать два макроса:

    • input строка-подсказка регистр-приёмник, который выводит на экран строку-подсказку, а затем помещает в регистр-приёмник введённое целочисленное значение

    • print строка-подсказка регистр-источник, который выводит на экран строку-подсказку, а затем — целочисленное значение регистра-источника и перевод строки

    • Пример тестирующей программы:
         1 .text
         2         input   "First input: " s0
         3         input   "Second input: " s1
         4         print   "First result: " s0
         5         print   "Second result: " s1
         6         li      a7 10
         7         ecall
      
    Input:

    2
    3
    Output:

    First input: Second input: First result: 2
    Second result: 3
  3. EJudge: StrMacro 'Строковые макросы'

    Написать три макроса:

    1. strlen регистр адрес — вычисляет длину asciz-строки по адресу и заносит результат в регистр

    2. strcpy приёмник источник — копирует asciz-строку, находящуюся по адресу источник, в адрес приёмник

      • Копирование должно происходить «справа налево», чтобы строку с адресом A можно было скопировать, например, на адрес A+1 (если метка A — это адрес, а B — это адрес + 1, то strcpy B A должно приводить к удвоению первого символа A)

    3. strcat приёмник источник — добавляет asciz-строку, находящуюся по адресу источник, в конец строки приёмник

      • Копирование должно происходить «справа налево», чтобы строку с адресом A можно было добавить к строке с адресом A (strcat STR STR должен приводить к удвоению строки STR)

    Для (увы, слегка неэффективного) копирования «справа налево» надо знать длину строки, но всё равно все три операции небезопасны. Пример тестирующей программы.

    •    1 .globl  main
         2 .data
         3 src:    .asciz  "Source"
         4 dst:    .asciz  "Destination"
         5 fin:    .asciz  "Destination+Source"
         6 .text
         7 main:
         8         strcpy  fin src
         9         strcat  fin dst
        10         la      a0 fin
        11         li      a7 4
        12         ecall
        13         strlen  a0 fin
        14         li      a7 1
        15         ecall   
      
    Input:

    <пуcтой ввод>
    Output:

    SourceDestination17
  4. EJudge: PolyDouble 'Многочлен'

    Написать макрос POLY Массив Регистр D-регистр, который будет вычислять значение многочлена A0+A1x+A2x2+…+Anxn (с двойной точностью).

    • Массив — это адрес массива A коэффициентов многочлена, начиная с A0

    • Регистр — это целочисленный регистр, содержащий порядок многочлена n (0 ⩽ n ⩽ 100)

    • D-регистр — это вещественный регистр двойной точности, содержащий значение переменной x

    Результат вычислений помещается в D-регистр. Пример вызывающей программы:

    •    1 .data
         2 _x:     .double 1.1
         3 _p345:  .double 3, 4, 5
         4 .text
         5 .globl  main
         6 main:
         7         fld     ft0 _x t0
         8         li      t0 2
         9         POLY    _p345 t0 ft0
        10         fmv.d   fa0 ft0
        11         li      a7 3
        12         ecall
      
    Input:

    <пустой ввод>
    Output:

    13.450000000000001
  5. EJudge: UniCompare 'Сравнение векторов'

    Напишите макрос COMPARE Массив_А Массив_Б длина компаратор, который лексикографически (поэлементно слева направо) сравнивает два массива одинаковой длины. Размер ячейки массива — 32 бита.

    • Массив_А и Массив_Б — адреса начала массивов

    • длина — натуральное число ⩽ 1000

    • компаратор — адрес функции, которая в регистре a0 возвращает -1, если a0 < a1; 1, если a0 > a1 и 0, если a0 == a1

    В результате работы макроса в регистр a0 заносится результат сравнения (фактически, это первый же ненулевой результат работы компаратора, либо 0, если все элементы массивов равны). Пример тестирующей программы:

    •    1 .data
         2 VecA:   .word   1, 2, 3, 4, 5
         3 VecB:   .word   1, 2, 3, 4, 6
         4 .text
         5 .globl   main
         6 main:
         7         COMPARE VecA VecB 5 _cmpint
         8         li      a7 1
         9         ecall
        10         COMPARE VecA VecA 5 _cmpint
        11         li      a7 1
        12         ecall
        13         COMPARE VecB VecA 5 _cmpint
        14         li      a7 1
        15         ecall
        16         li      a7 10
        17         ecall
        18 _cmpint:
        19         slt     t0 a0 a1
        20         sgt     t1 a0 a1
        21         sub     a0 t1 t0
        22         ret
      
    Input:

    <пустой ввод>
    Output:

    -101

LecturesCMC/ArchitectureAssembler2024/05_Assembler (последним исправлял пользователь FrBrGeorge 2024-07-19 18:27:17)