2.5. Массивы
Массивы удобно использовать для доступа к большому количеству однородных или сходных данных. Массив располагается в ячейках памяти со строго последовательными адресами и занимает непрерывный участок памяти. Каждый массив состоит из последовательности элементов одинакового размера, и каждый элемент массива имеет порядковый номер, называемый индексом. В этом разделе показано, как получать доступ к элементам массива в памяти.
Индексация массивов
На рис. 2.12 показан массив из пяти элементов – целых чисел. Элементы имеют индексы от 0 до 4. Массив хранится в оперативной памяти, начиная с базового адреса 0x10007000. Базовый адрес определяет адрес самого первого элемента массива, array[0].
В следующем примере кода первые два элемента массива array умножаются на 8 и помещаются обратно в массив.
Пример кода Доступ к массиву Код на языке высокого уровня
int array[5];
array[0] = array[0] * 8; array[1] = array[1] * 8;
Код на языке ассемблера MIPS
# $s0 = base address of array
lui $s0, 0x1000 |
# $s0 = 0x10000000 |
|
ori $s0, $s0, 0x7000 |
# $s0 = 0x10007000 |
|
lw |
$t1, 0($s0) |
# $t1 = array[0] |
sll $t1, $t1, 3 |
# $t1 = $t1 << 3 = $t1 * 8 |
|
sw |
$t1, 0($s0) |
# array[0] = $t1 |
lw |
$t1, 4($s0) |
# $t1 = array[1] |
sll $t1, $t1, 3 |
# $t1 = $t1 << 3 = $t1 * 8 |
|
sw |
$t1, 4($s0) |
# array[1] = $t1 |
|
|
71 |
Рис. 2.12. Массив из пяти элементов
Первым шагом при доступе к элементам массива является загрузка базового адреса массива в регистр. В примере кода 6.21 базовый адрес загружается в $s0. Вспомним, что инструкции загрузки константы в старшие 16 бит (lui) и побитовое логическое «ИЛИ» с константой (ori) можно использовать для загрузки 32-битной константы в регистр.
Из предыдущего примера кода также становится понятно, почему инструкция lw вычисляет эффективный адрес путем сложения базового адреса и смещения. Базовый адрес указывает на начало массива, а смещение можно использовать для доступа к последующим его элементам. Так, например, элемент array [1] хранится в памяти по адресу 0x10007004, т.е. на одно слово дальше, чем array [0], поэтому доступ к нему осуществляется со смещением 4 от базового адреса.
Вы могли заметить, что для манипуляций с двумя элементами массива в предыдущем примере кода по существу используется один и тот же код, разница состоит лишь в значениях индексов этих элементов. Дублирование кода не представляет проблемы, когда мы работаем с двумя элементами массива, но такой подход становится крайне неэффективным, когда необходим доступ ко многим элементам массива.
В следующем примере кода использован цикл for для умножения на 8 всех элементов массива, состоящего из 1000 элементов и находящегося в памяти по базовому адресу
0x23B8F000.
72
Пример. Доступ к массиву с помощью цикла for Код на языке высокого уровня
int i;
int array[1000];
for (i = 0; i < 1000; i = i + 1) array[i] = array[i] * 8;
Код на языке ассемблера MIPS
#$s0 = array base address, $s1 = i
#initialization code
lui $s0, 0x23B8 |
|
# $s0 = 0x23B80000 |
|
ori $s0, $s0, 0xF000 # $s0 = 0x23B8F000 |
|||
addi $s1, $0, 0 |
|
# i = 0 |
|
addi $t2, $0, 1000 |
# $t2 = 1000 |
||
loop: |
|
|
|
slt |
$t0, $s1, $t2 |
|
# i < 1000? |
beq |
$t0, $0, done |
# if not, then done |
|
sll |
$t0, $s1, 2 |
|
# $t0 = i*4 (byte offset) |
add |
$t0, $t0, $s0 |
|
# address of array[i] |
lw |
$t1, 0($t0) |
|
# $t1 = array[i] |
sll |
$t1, $t1, 3 |
|
# $t1 = array[i] * 8 |
sw |
$t1, 0($t0) |
|
# array[i] = array[i] * 8 |
addi $s1, $s1, 1 |
|
# i = i + 1 |
|
j |
loop |
# repeat |
|
done: |
|
|
|
На рис. 2.13 показан массив, состоящий из 1000 элементов. В качестве индекса массива теперь используются не константы, а переменная i, поэтому мы не можем использовать непосредственные операнды в качестве смещения для инструкции lw. Вместо этого мы вычисляем адрес i-го элемента. Вспомним, что каждый элемент нашего массива – это четырёхбайтовое слово. Поскольку память адресуется побайтно, то смещение i-го элемента относительно базового адреса массива будет равно i*4. Сумма этого смещения и базового адреса массива и даст нам адрес i-го элемента в памяти. Для вычисления
73
i*4 можно воспользоваться инструкцией сдвига влево на 2 бита.
Рис. 2.13. Размещение array[1000] в памяти
2.6. Вызовы функций и работа со стеком
В языках высокого уровня обычно используют функции, или процедуры, для повторного использования часто выполняемого кода и для того, чтобы сделать программу модульной и читаемой. У функций есть входные параметры, называемые аргументами, и выходной результат, называемый возвращаемым значением. Функции должны вычислять возвращаемое значение, не вызывая неожиданных побочных эффектов. Когда одна функция вызывает другую, вызывающая функция и вызываемая функция должны прийти к соглашению о том, где размещать аргументы и возвращаемое значение. Следуя соглашениям, принятым в архитектуре MIPS, вызывающая функция размещает до четырёх аргументов в регистры $a0–$a3 перед тем, как произвести вызов, а вызываемая функция помещает возвращаемое значение в регистры $v0–$v1 перед тем, как завершить работу. Следуя этому соглашению, обе функции знают, где искать аргументы и куда возвращать значение, даже если вызывающая и вызываемая функции были написаны разными людьми. Вызываемая функция не должна вмешиваться в работу
74
вызывающей функции. Вкратце, это означает, что вызываемая функция должна знать, куда передать управление после завершения работы, и она не должна портить значения любых регистров или памяти, которые нужны вызывающей функции. Вызывающая функция сохраняет адрес возврата (англ.: return address) в регистре адреса возврата ($ra) в тот момент, когда она передаёт управление вызываемой функции путем выполнения инструкции безусловного перехода с возвратом (jal).
Вызываемая функция не должна изменять архитектурное состояние и содержимое памяти, от которых зависит вызывающая функция.
В частности, вызываемая функция должна оставить неизменным содержимое сохраняемых регистров $s0–$s7, регистра $ra и стека – участка памяти, используемого для хранения временных переменных.
Вызовы и возвраты из функций
Архитектура MIPS использует инструкцию безусловного перехода с возвратом (jal) для вызова функции и инструкцию безусловного перехода по регистру (jr) для возврата из функции. Представленный ниже пример кода демонстрирует главную функцию main, вызывающую функцию simple. Здесь функция main является вызывающей, а simple – вызываемой.
Функция simple не получает входных аргументов и ничего не возвращает, она просто передаёт управление обратно вызывающей функции. В примере кода слева от каждой инструкции MIPS приведены их адреса в шестнадцатеричном формате.
Код на языке высокого уровня int main() {
simple();
…
}
// void means the function returns no value
75