Практика программирования на языке ассемблера в 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); в этом режиме в единый бинарный образ транслируются все файлы, имена которых были переданы в командной строке
Позиционно-независимый код
Позиционно-независимый код — способность готовой программы быть загруженной и работать в памяти по произвольному адресу вообще без модификаций или путём начальной установки значений некоторых регистров (как минимум, pc и sp).
В RISC-V инструкции как таковые позиционно независимы «из коробки»: вместо абсолютного адреса повсеместно используется смещение относительно текущего адреса инструкции (auipc).
- →
0x00400000 0x0fc10297 auipc x5,0x0000fc10 5 lw t0 var 0x00400004 0x01c2a283 lw x5,28(x5)
В регистре t0 всё-таки образовался абсолютный адрес var, однако это произошло только в момент исполнения инструкции auipc; машинные коды как таковые содержат только смещения.
Полученный машинный код может быть несовместим с другими конвенциями относительно расположения сегментов .text и .data
Ничто не мешает программисту хранить адреса в ячейках памяти, используя двойную косвенную адресацию — например, так…
- …ничто, кроме того, что этот фрагмент программу позиционно-зависим уже на уровне ассемблера
Использование регистра gp: «полная» косвенная адресация. Регистр gp указывает на «глобальную область данных» (в RARS — 0x10008000); предполагается, что там хранятся какие-то «глобальные переменные». Чем бы они ни были, их использование вообще не предполагает
(не успеем) PLT и GOT
- При загрузке динамических библиотек в память мы не можем предсказать, куда они загрузятся
После загрузки адрес записывается в ячейку некоторой таблицы (не регистр, ибо сколько их, библиотек?); адрес функции в составе библиотеки — это смещение. Это и есть Global Offest Table.
Однако до загрузки адрес всё ещё неизвестен. Поэтому сначала в GOT лежат не адреса функций из библиотеки (мы их не знаем!), а адреса специальных подпрограмм — Procedure Linkage Table, по одной на каждую нужную функцию из билблиотеки. Такая подпрограмма:
- Загружает библиотеку, если та ещё не загружена
- Определяет настоящий адрес нужной функции
- Записывает этот адрес в GOT (вместо своего)
Вызывает функцию (переходит путём j, а не jal)
Макроподстановка и макрокоманды
Макроподстановка — механизм поиска шаблона в тексте и замены его другим текстом. Полученный текст также может содержать шаблоны, так что процесс макроподстановки обычно рекурсивен.
Псевдонимы
Примитивный макрос — директива .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
Макроподстановка
(Строго говоря, «макрос» — это множественное число от «макро», но в современном русском это слово приобрело свойства единственного числа. Множественное число — «макросы», по аналогии с «конверсами», «сникерсами» и т. п.)
Механизм макроподстановки может быть и посложнее:
Первые 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> — номера строк, на которых располагалось тело макроса.
Сам макрос (в отличие от подпрограммы), конечно, ни во что не странслировался, потому что он — всего лишь задание нового правила для трансляции каждой макрокоманды.
Параметрические макросы
Самое удобное в макроподстановке — параметризация макрокоманд. Общий вид макроопределения:
Например:
Здесь макрокоманда 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 подстановка параметров макро приводит к появлению соответствующей подстроки в в виде отдельного слова инструкции или директивы, разрешается передавать только лексемы языка ассемблера. Впрочем, можно передать, например, имя метки:
Такая реализация проще (для препроцессора и для последующей трансляции используется один и тот же анализатор), но не такая гибкая. Подставить «--» вместо $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 такой вывод:
Сама программа при этом разрастётся чуть ли не в два раза:
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№, где № — это порядковый номер текущей операции макроподстановки.
после макроподстановки будет выглядеть примерно как
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 файл_с_макросами. Подпрограммы при этом складываются в другой файл (возможно, не один), т. н. «библиотеку», и подключаются посредством многофайловой сборки.
На предыдущем примере:
Файл с программой prog.asm:
Файл с подпрограммами lib.asm:
Не забываем метки всех подпрограмм, которые понадобятся в других файлах, объявлять как .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. Как-нибудь так:
Генерация макроопределений. Если разрешить создавать макросы внутри макросов, можно развёртывать целые семейства определений в зависимости от исходного параметра внешнего макроса
Конкатенация. Иногда необходимо, чтобы результат постановки нескольких макросов интерпретировался затем как одна лексема языка (например, строка label##suffix##index превращалась бы при наличии констант suffix=_M и index=5 в label_M5). В RARS такого механизма нет
Локальные метки. Бывает очень полезно ограничить видимость меток сильнее, чем просто внутри файла. Например, если в файле задано несколько подпрограмм, в каждой из них хотелось бы иметь возможность использовать метки типа start, finish, loop или стандартные имена переменных. Это можно было бы сделать, введя особенный синтаксис временных меток или ограничить видимость меток специальной конструкцией «локальное пространство имён» и т. п.
В целом макроассемблер RARS достаточен для написания программ среднего объёма, а написание действительно крупных проектов на языке ассемблера выходит за рамки данного курса.
Д/З
TODO (на следующую итерацию, когда на случится — сделать задание «строковые макросы» совместимым с Д/З по следующей лекции)
В этой лекции все задания будут слегка иного типа: потребуется написать что-то вроде библиотеки макросов, состоящей из одного или нескольких определений макроса и, возможно, сопутствующих подпрограмм. В процессе тестирования к вашему решению будет приписываться дополнительный фрагмент (aka footer) с основной программой, а получившийся текст — уходить на проверку.
В footer будет определена метка .globl main, с которой и начнётся запуск программы; в решении этой метки быть не должно
Все остальные метки в footer будут начинаться с символа «_»; в решении таких имён быть не должно (сборка у нас однофайловая). В остальном можно использовать любые метки.
По этой же причине не забывайте чётко расставлять секции .text и .data
В footer, возможно, будет заполняться не только секция .text, но и секция .data, нельзя рассчитывать на то, что данные в решении начинаются с адреса 0x10010000
В footer, возможно, будет проверяться соблюдение конвенций — сохранность регистров sp и s№.
Пока что к каждой задаче по одному тесту. Их будет больше! Когда сделаю, удалю это сообщение и маякну в группе.
Задачи:
- Посмотрите особенности макроассемблера RARS — он реально странненький.
(это фактически упрощённая копипаста из лекции — без вывода дополнительной строки ": " в конце подсказки)
EJudge: InputPrompt 'Ввод с подсказкой'
Написать два макроса:
input строка-подсказка регистр-приёмник, который выводит на экран строку-подсказку, а затем помещает в регистр-приёмник введённое целочисленное значение
print строка-подсказка регистр-источник, который выводит на экран строку-подсказку, а затем — целочисленное значение регистра-источника и перевод строки
- Пример тестирующей программы:
2 3
First input: Second input: First result: 2 Second result: 3
EJudge: StrMacro 'Строковые макросы'
Написать три макроса:
strlen регистр адрес — вычисляет длину asciz-строки по адресу и заносит результат в регистр
strcpy приёмник источник — копирует asciz-строку, находящуюся по адресу источник, в адрес приёмник
Копирование должно происходить «справа налево», чтобы строку с адресом A можно было скопировать, например, на адрес A+1 (если метка A — это адрес, а B — это адрес + 1, то strcpy B A должно приводить к удвоению первого символа A)
strcat приёмник источник — добавляет asciz-строку, находящуюся по адресу источник, в конец строки приёмник
Копирование должно происходить «справа налево», чтобы строку с адресом A можно было добавить к строке с адресом A (strcat STR STR должен приводить к удвоению строки STR)
Для (увы, слегка неэффективного) копирования «справа налево» надо знать длину строки, но всё равно все три операции небезопасны. Пример тестирующей программы.
<пуcтой ввод>
SourceDestination17
EJudge: PolyDouble 'Многочлен'
Написать макрос POLY Массив Регистр D-регистр, который будет вычислять значение многочлена A0+A1x+A2x2+…+Anxn (с двойной точностью).
Массив — это адрес массива A коэффициентов многочлена, начиная с A0
Регистр — это целочисленный регистр, содержащий порядок многочлена n (0 ⩽ n ⩽ 100)
D-регистр — это вещественный регистр двойной точности, содержащий значение переменной x
Результат вычислений помещается в D-регистр. Пример вызывающей программы:
<пустой ввод>
13.450000000000001
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
<пустой ввод>
-101