Лабораторная работа №6. Адресация элементов массива, организация цикла
6.1 Цель и задачи
Целью работы является разработка программы преобразования данных для приобретения практических навыков программирования на языке ассемблера и закрепления знаний по работе с массивами и циклами. Задачи:
- Изучить применение методов адресации для манипуляции массивами.
- Освоить подходы к организации циклов средствами условных и безусловных переходов.
Презентация к блоку "Ассемблер RISC-V"
6.2. Основные теоретические сведения
6.2.1 Хранение массивов в памяти
Архитектура RISC-V обычно использует прямой порядок байтов (от младшего к старшему; little-endian). RISC-V применяет память с побайтовой адресацией. Это значит, что каждый байт памяти имеет уникальный адрес. Поскольку 32-битное слово состоит из четырех 8-битных байтов, то адрес каждого слова (word address) кратен 4. Старший байт (most significant byte, MSB) находится слева, а самый младший байт (least significant byte, LSB) - справа.
Для обращения к элементу одномерного массива в команде используется формула для вычисления его адреса в памяти.
Адрес эл-та = (адрес нач. массива) + (№эл-та) ? (размер эл-та в байтах)
Имя массива однозначно определяет адрес младшего байта его нулевого элемента. Начальный адрес массива и его размер (число элементов) заносятся в регистры (можно использовать любой свободный), и на каждой итерации указатель изменяется на длину элемента в байтах.
Обращение к элементу массива, к оперативной памяти в RISC-V организовано единственно возможным способом - с помощью базовой адресации со смещением:
la rs1, mas # Загрузить адрес массива lw rd, 0(rs1)#Адрес элемента = Смещение (0) + База (rs1)
Пояснение к примеру:
- Смещение - адрес элемента внутри массива
- База - базовый адрес массива в сегменте данных
Другая форма:
li rs1,0 lw rd, mas (rs1) # Адрес элемента = База (mas) +Смещение (rs1)
6.2.2 Организация циклов
В языках высокого уровня циклы предназначены для многократного выполнения участка кода в зависимости от условия. В Ассемблере RISC-V нет специальных инструкций для организации цикла. Для перебора элементов массива в цикле требуется продумать самостоятельно вид цикла, регистр-счетчик количества повторений цикла, условие окончания, регистр для хранения флага окончания и временного индикатора для сравнения с индикатором. Проверка условий выполняется операторами "если-то-иначе", организованными командами условного перехода (beq, bne, blt, bge,bgeu, bltu) или безусловного перехода (jal, jalr и псевдокоманды j, jr ).
Команды условного перехода имеют формат типа B.
В таблице 6.1 представлены все команды условного перехода
Instruction | name | opcode | funct3 | description |
---|---|---|---|---|
beq | Branch == | 1100011 | 0x0 | if (rs1 == rs2) pc+= imm |
bne | Branch != | 1100011 | 0x1 | if (rs1 != rs2) pc+= imm |
blt | Branch < | 1100011 | 0x4 | if (rs1 < rs2) pc+= imm |
bge | Branch >= | 1100011 | 0x5 | if (rs1 >= rs2) pc+= imm |
bltu | Branch < Un | 1100011 | 0x6 | if (rs1 < rs2) pc+= imm |
bgeu | Branch >= Un | 1100011 | 0x7 | if (rs1 >= rs2) pc+= imm |
Команда условного перехода сравнивает значения регистров rs1 и rs2. Если результат сравнения (условие) является истинным, программный счетчик увеличивается на imm. Например, команда blt rs1, rs2, imm проверяет, меньше ли содержимое rs1, чем содержимое rs2. Если это так, то pc = pc + imm. В противном случае работа продолжается со следующей инструкцией в памяти (pc = pc +4).
Инструкции blt и bge используют значения со знаком; а bgeu этого не делает. Использование условного перехода bge и beq
bge s2, s3, else # if (a < b): c = a + 1 addi s4, s2, 1 beq zero, zero, end else: # else: c = b + 2 addi s4, s3, 2 end: # s2=a; s3=b; s4=c;
Использование условного перехода blt
Адрес | Машинный код | Инструкция | Комментарий |
---|---|---|---|
0x00 | 0xffb00093 | addi s2, zero, -5 | s2 = -5 |
0x04 | 0x00500113 | addi s3, zero, 5 | s3 = 5 |
0x08 | 0x0020c463 | blt s2, s3, 0x8 | if (s2 < s3) pc = pc+ 8 |
0x0c | 0x00100193 | addi s4, zero, 1 | skipped if (s2 <s3): s4 = 1 |
0x10 | 0x00200193 | addi s4, zero, 2 | s4 = 2 |
Инструкция по адресу 0x08 проверяет, является ли s2 < s3, то есть -5 < 5. Поскольку это верно, программный счетчик устанавливается на адрес 0x10, а инструкция по адресу 0x0c пропускается.
Если бы команда перехода по адресу 0x08 была bltu, отрицательное значение регистра s2 рассматривалось бы как положительное значение (0xfffffffb как положительное шестнадцатеричное значение для 32-разрядной версии), и результат сравнения был бы ложным.
Ниже приведен более сложный пример программы подсчета количества нулевых элементов в одномерном массиве.
.data mas: .word -1 -2 -3 -4 1 2 0 4 2000 4096 65535 0 size: .word 12 # длина массива head: .asciiz "\n Null element amount: " .text la t0, mas # загрузить адрес массива - начальный индекс [i] lw t5, size # загрузить длину массива в регистр loop: lw t3, 0(t0) # поместить значение mas[i] в регистр $t3 bne 0, t3, met1 # перейти к met1 если $t3 не '0' и элемент массива не 'zero' addi t4, t4, 1 # увеличить счетчик нулевых элементов met1: addi t5, t5, -1 # уменьшить счетчик цикла addi t0, t0, 4 # увеличить индекс текущего элемента bge t5, loop # повторить, пока не закончили la a0, head # загрузить адрес константы заголовка для печати li v0, 4 ecall # напечатать заголовок add a0, t4, zero # загрузить значение счетчика нулевых элементов для печати li v0, 1 ecall # напечатать значение счетчика
Согласно вышеприведенной программе в памяти последовательно расположены 12 четырёхбайтовых элемента массива mas (среди которых есть два нулевых), затем переменная size, содержащая размер массива (12), затем ASCII-коды символов выводимого сообщения head. Далее следуют три инструкции загрузки:
- адреса массива,
- адреса переменной-размера массива,
- значения переменной-размера массива.
Затем начинается цикл просмотра элементов (метка loop), в котором
- загружается очередной элемент массива mas[i],
- сравнивается с нулём и при необходимости увеличивается счетчик нулей,
- уменьшается счетчик цикла - количество оставшихся итераций цикла,
- увеличивается адрес элемента на его длину (4 Байта),
- проверяется условие окончания цикла (счетчик цикла равен нулю).
Далее выполняются два системных вызова
- для вывода строки-сообщения,
- для вывода переменной - счетчика нулей.
В ряде случаев полезными выступают команды безусловного перехода, являющиеся аналогами инструкции GOTO. Команды безусловного перехода имеют формат типа J или I. Для RISC-V есть две инструкции безусловного перехода (Таблица 6.3).
Instruction | name | format | opcode | funct3 | description |
---|---|---|---|---|---|
jal | Jump and Link | 1101111 | - | rd=PC+4; PC+=imm | |
jalr | Jump and Link Register | I | 1101111 | 0x0 | rd=PC+4; PC=rs1+imm |
Команда jal rd, imm добавляет непосредственное значение к программному счетчику pc +=imm и записывает в регистр rd адрес после команды jal. Команда jalr rd, rs, imm устанавливает программный счетчик как результат pc = rs + imm, и записывает в rd обратный адрес. Обратный адрес - это инструкция по адресу, указанному после инструкции перехода.
Адрес | Машинный код | Инструкция | Комментарий |
---|---|---|---|
0x00 | 0x00c000ef | jal s2,0xc | pc = 0x0 + 0xc= 0xc, s2 = pc + 4 = 0x4 |
0x04 | 0x00000013 | addi zero, zero, 0 | no operation |
0x08 | 0x00010093 | addi s2, s3, 0 | s2= s3 |
0x0c | 0x00008167 | jalr s3, s2, 0 | pc = s2, s3= pc + 4 = 0x10 |
0x10 | 0x00100093 | addi s2, zero, 1 | s2= 1 |
Последовательность выполнения программы выглядит следующим образом. Программа начинается с команды перехода jal s2, 0xc по адресу 0х00. Эта команда записывает обратный адрес в регистр2 и переходит к 0х0c. В 0х0c команда перехода jalr s3, s2, 0 записывает свой соответствующий обратный адрес (0х10)в регистр s3 и выполняет переход (возвращает) к адресу, сохраненному в регистре s2, который равен 0х04. Программа продолжает выполнение инструкции в 0х04, которая ничего не делает. Следующая команда в 0х08 перемещает значение 0х10 (обратный адрес jalr) в регистр s2. После этого второй проход jalr в 0xc переходит к 0х10, значение регистра s2. Последовательность счетчика команд 0х00, 0х0c, 0х04, 0х08, 0х0c, и 0х10.
6.3. Задание к лабораторной работе
- Создать программу согласно варианту, которая
- выполняет заданные действия,
- выводит на экран сообщения о результатах/ошибках.
- Откомпилировать программу и запустить на исполнение
- Отладить программу, проследить изменения в регистрах и оперативной памяти.
6.3.1 Описание последовательности выполнения работы
Скомпилируйте исходную программу и убедитесь в ее работоспособности в RISC-V ОС, либо через qemu-riscv64.
6.3.2 Пример выполнения задания на защиту
Напишите программу сложения целых чисел от 0 до 100:
- t0 используйте для текущего индекса quot;iquot; для цикла
- t1 хранит величину для сравнения с текущим индексом (в данном примере 100)
- t2 хранит текущую сумму (по окончанию программы - результат)
Код решения:
andi t0, t0, 0 # Clearing contents of register t0 andi t1, t1, 0 # Clearing contents of register t1 andi t2, t2, 0 # Clearing contents of register t2 # Functions as variable "sum" li t1, 100 # Load t1 with value 100 loop: add t2, t2, t0 # Sum = Sum+i addi t0, t0, 1 # Increment index "i" blt t0, t1, loop # Iterate if t0<t1 End: j End # End of WHILE loop
ВАРИАНТЫ ЗАДАНИЙ
- Найти минимальный и максимальный элемент массива А и запомнить их номера и адреса в переменных в памяти A_min, A_max. Вывести переменные, и предусмотреть случай (вывести сообщение), когда имеются все/несколько минимальных или максимальных элементов.
- Для заданного массива А посчитать и запомнить количество нулевых/отрицательных/положительных элементов. Вывести сообщения с этими значениями и предусмотреть случай (вывести сообщение), когда какие-то типы элементов отсутствуют.
- Найти элементы массива А, содержащие 2 единицы в двоичном представлении и запомнить их номера и адреса в новых одномерных массивах B_nom и B_adr, предусмотреть случай, когда таких элементов нет. Вывести сообщение о количестве таких элементов или об их отсутствии.
- Для заданного массива А составить новый массив В, каждый элемент которого В[i] соответствует № первого слева (старшего) нулевого разряда элемента А[i] . Предусмотреть случай отсутствия нулевых разрядов элемента. Вывести сообщение с числом таких элементов или штатном режиме - весь массив В заполнен.
- Для заданного массива А составить новый массив В, каждый элемент которого В[i] соответствует № первого справа (младшего) единичного разряда элемента А[i]. Предусмотреть случай отсутствия единичных разрядов элемента. Вывести сообщение с числом таких элементов или штатном режиме - весь массив В заполнен.
- Для заданного массива А составить новый массив В, каждый элемент которого В[i] равен сумме единиц в двоичном представлении элемента А[i]. Посчитать общее количество единиц. Предусмотреть случай отсутствия единичных разрядов элементов. Вывести сообщение с общим количеством единиц или об их отсутствии.
- Вычислить сумму всех отрицательных, положительных элементов массива А, число нулевых элементов, число четных и нечетных элементов и запомнить эти 5 значений в указанном порядке в одномерном массиве В. Вывести сообщения о полученных результатах, предусмотреть случай, когда некоторые виды элементов отсутствуют.
- Вычислить сумму всех нулевых и единичных битов в двоичном представлении элементов массива А, запомнить эти значения в памяти и вывести эти значения или сообщение об отсутствии некоторых видов битов.
- Для заданного массива А рассчитать разницу между максимальным и минимальным элементом и вывести это число. Предусмотреть случай их отсутствия (когда все элементы массива равны) и вывести об этом сообщение.
- Вычислить сумму всех элементов массива А, больше заданной переменной. Вывести сообщения о полученных результатах, предусмотреть случай, когда такие элементы отсутствуют.
- Для заданного массива А составить новый одномерный массив символьных строк В. Первым символом строки В[i] будет символ "Р" (positive), если элемент А[i] положительный и "N", если отрицательный. Вторым символом строки В[i] будет "-", третьим символом строки В[i] будет "Z", если элемент А[i] нулевой. Вывести сообщения об отсутствии одного из типов элементов и числе положительных элементов.
6.4. Вопросы для контроля
- У нас есть массив int (32 бита) с именем arr 0. Указатель на первый элемент arr0, хранится в регистре a1. Ответьте на следующие вопросы.
- a. Как поместить четвертый элемент arr 0 в регистр t1?
- b. Как рассчитать t1 + 16? Сохраните результат в регистре t2
- c. Найдите эффективный способ вычисления t2 /16 и t2 %16. Сохраните результаты в t3 и t4 соответственно. Обратите внимание, что / - это целочисленное деление, а % - операция по модулю. (подсказка: использование сдвига и логических операций)
- У нас есть массив int (32 бита) с именем arr1. Указатель на первый элемент arr1 хранится в регистре a2. У нас также есть регистры
t1 = 0xAAAAAAAA, t2 = 0xFEDCBA98
Ответьте на следующие вопросы:
- a. Каково значение t3 для следующей последовательности инструкций?
slli t3, t1, 4 srli t3, t3, 4
- b. Каково значение t3 для следующей последовательности инструкций?
slli t3, t2, 3 srai t3, t3, 3
- c. Напишите фрагмент ассемблерной программы:
- Сохранить результат t1 & t2 в регистре t4;
- Сохранить t4 в первом элементе arr1;
- Сохраните младшие 8 бит из t4 во втором элементе arr1.
- a. Каково значение t3 для следующей последовательности инструкций?
- Рассмотрите следующие инструкции RISC-V
li t1, 0 li t2, 1 li t3, 1 li t4, 10 LOOP: beq t1, t4, DONE add t5, t2, t3 addi t2, t3, 0 addi t3, t5, 0 addi t1, t1, 1 jal x0, LOOP DONE: # end of the program
- a. Сколько раз выполняется цикл (между LOOP и DONE)?
- b. Перечислите значение t2 на каждой итерации цикла.
- c. Перечислите значение t3 на каждой итерации цикла.
- d. Что делает эта программа?