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

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

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

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

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

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

Ничто не мешает программисту хранить адреса в ячейках памяти, используя двойную косвенную адресацию — например, так…

   1 .data
   2 addr:   .word   var
   3 var:    .word   0xf00d
   4 .text
   5         lw      t0 addr
   6         lw      t0 (t0)

В RISC-V инструкции как таковые позиционно независимы «из коробки»: вместо абсолютного адреса повсеместно используется смещение относительно текущего адреса инструкции (auipc).

   1 .data
   2         .space  28
   3 var:    .word   100
   4 .text
   5         lw      t0 var

Использование регистра gp: «полная» косвенная адресация. Регистр gp указывает на «глобальную область данных» (в RARS — 0x10008000); по соглашениям там хранятся какие-то «глобальные переменные». Чем бы они ни были, их использование

  1. предполагает предварительную инициализацию регистра gp во время загрузки программы в память;

  2. предполагает загрузку глобальных данных именно в это заранее не определённое место памяти;
  3. приводит к тому, что вся адресация данных в программе оказывается не только относительной, но и не зависящей от регистра pc — то есть от взаимного расположения секций данных и текста;

  4. позволяет более эффективно обращаться и собственно к секции .data (которая начинается со следующего килобайта) — а ведь это вторая по частоте использования операция работы с памятью (после стека, конечно).

(подробнее не успеем) PLT и GOT

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

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

Псевдонимы

Примитивный макрос — директива .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: "

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

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

FrBrGeorge/MyDict/speech_balloon_question.png Если вы использовали в программе 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

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

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

Д/З

<!> В этой лекции все задания будут слегка иного типа: потребуется написать что-то вроде библиотеки макросов, состоящей из одного или нескольких определений макроса и, возможно, сопутствующих подпрограмм. В процессе тестирования к вашему решению будет приписываться дополнительный фрагмент (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 'Строковые макросы'

    Написать комплект макросов для работы со строками. Параметры макросов — адреса (метки) ASCIZ-строк в памяти. Результаты работы сохраняются в регистрах a0 и a1.

    Макрокоманда

    Параметр 1

    Параметр 2

    Описание

    a0

    a1

    strlen

    Адрес

    Определение длины строки

    Длина строки

    strcpy

    Приёмник

    Источник

    Копирует строку с адреса «источник» по адресу «приёмник»

    Приёмник

    Длина строки-приёмника

    strcat

    Приёмник

    Источник

    Копирует строку с адреса «источник» в конец строки по адресу «приёмник»

    Приёмник

    Длина строки-приёмника

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

    • Аналогично, strcat STR STR должен приводить к удвоению строки STR)

    • Разрешается модифицировать регистры типа t*

    • Все три подпрограммы — «небезопасные»: strcpy A B из предыдущих примеров может привести к бесконечному циклу или к заполнению строки последним символом, а отсутствие нулевого байта — к выходу за пределы памяти.

    Пример тестирующей программы:

    •    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  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: VectorOp 'Векторная операция'

    Написать макрос VectorOp Массив_А Массив_Б длина операция, который поэлементно заполняет Массив_А результатами выражения операция(Массив_А[i], Массив_Б[i]).

    • Массив_А и Массив_Б — адреса начала массивов; размер ячейки массива — 32 бита;

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

    • операция — адрес функции, которая в a0 возвращает результат некоторой операции над a0 и a1

    Пример тестирующей программы:

    •    1 .data
         2 A:      .space  4000
         3 B:      .space  4000
         4 .text
         5         VectorOp        A A 5 input
         6         VectorOp        B B 5 input
         7         VectorOp        A B 5 sum
         8         VectorOp        A A 5 print
         9         li      a7 10
        10         ecall
        11 sum:    add     a0 a0 a1
        12         ret
        13 input:  li      a7 5
        14         ecall
        15         ret
        16 print:  li      a7 1
        17         ecall
        18         ret
      
    Input:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Output:

    79111315

LecturesCMC/ArchitectureAssembler2025/05_Assembler (последним исправлял пользователь FrBrGeorge 2025-03-22 20:10:01)