Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

3222

.pdf
Скачиваний:
5
Добавлен:
15.11.2022
Размер:
3.54 Mб
Скачать

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

void simple() { return;

}

Код на языке ассемблера MIPS

0x00400200 main: jal simple # call function

0x00400204 …

0x00401020 simple: jr $ra # return

Инструкции безусловного перехода с возвратом (jal) и безусловного перехода по регистру (jr $ra) – две необходимые для вызова функций инструкции. Инструкция jal выполняет две операции: сохраняет адрес следующей за ней инструкции в регистре адреса возврата ($ra) и выполняет переход по адресу первой инструкции вызываемой функции.

В предыдущем примере кода функция main вызывает функцию simple, выполняя инструкцию jal. Инструкция jal выполняет переход на метку simple и сохраняет значение 0x00400204 в регистре $ra. Функция simple немедленно завершается, выполняя инструкцию jr $ra, то есть осуществляет переход к инструкции по адресу, находящемуся в регистре $ra. После этого функция main продолжает выполняться с этого ад-

реса (0x00400204).

Входные аргументы и возвращаемые значения

В представленном выше примере кода функция simple не очень-то полезна, потому что она не получает входных значений от вызывающей функции (main) и ничего не возвращает. По соглашениям, принятым в архитектуре MIPS, функции используют регистры $a0–$a3 для входных аргументов и регистры $v0–$v1 для возвращаемого значения. В примере кода представленного ниже функция diffofsums вызывается с четырьмя аргументами и возвращает один результат.

Следуя соглашениям MIPS, вызывающая функция main помещает аргументы функции слева направо в регистры вход-

76

ных значений $a0–$a3. Вызываемая функция diffofsums размещает возвращаемое значение в регистре возвращаемых значе-

ний $v0.

Функция, возвращающая 64-битное значение, например, число с плавающей точкой двойной точности, использует оба регистра $v0 и $v1. Если функции нужно передать более четырех аргументов, то дополнительные аргументы размещаются в стеке, который мы обсудим далее.

Пример. Вызов функции с аргументами и возвращаемым значением.

Код на языке высокого уровня int main()

{

int y;

y = diffofsums(2, 3, 4, 5);

}

int diffofsums(int f, int g, int h, int i)

{

int result;

result = (f + g) − (h + i); return result;

}

Код на языке ассемблера MIPS

# $s0 = y

 

main:

 

 

 

addi $a0, $0, 2

# argument 0 = 2

addi $a1, $0, 3

# argument 1 = 3

addi $a2, $0, 4

# argument 2 = 4

addi $a3, $0, 5

# argument 3 = 5

jal

diffofsums

# call function

add

$s0, $v0, $0

# y = returned value

 

 

 

 

77

# $s0 = result

 

 

 

 

diffofsums:

 

 

 

 

add

$t0, $a0, $a1

# $t0

= f + g

add

$t1, $a2, $a3

# $t1

= h + i

sub $s0, $t0, $t1

# result = (f + g) − (h + i)

add

$v0, $s0, $0

 

# put return value in $v0

jr $ra

# return to caller

Стек

Стек (англ.: stack) – это участок памяти для хранения локальных переменных функции. Стек расширяется (занимает больше памяти), если процессору нужно больше места, и сужается (занимает меньше памяти), если процессору больше не нужны сохранённые там переменные. Перед тем, как объяснить, как функции используют стек для хранения временных переменных, мы объясним, как стек работает.

Стек является очередью, работающей в режиме «последним пришёл – первым ушёл» (LIFO, от англ. last-in-first- out). Как и в стопке тарелок, последний элемент, помещенный (англ.: push) на стек (верхняя тарелка), будет первым элементом, который с него снимут (извлекут, англ.: pop). Каждая функция может выделить память на стеке для хранения локальных переменных, и она же должна освободить её перед возвратом. Верхушка стека (англ.: top of the stack) – это память, которая была выделена последней. Так же, как стопка тарелок растёт вверх в пространстве, стек в архитектуре MIPS увеличивается в памяти.

Стек расширяется в сторону младших адресов по мере выделения нового места в памяти для программы (функции).

На рис. 2.14 изображен стек. Регистр указателя стека ($sp, от англ. stack pointer) – это специальный регистр, который указывает на верхушку стека. Указатель (англ.: pointer) – причудливое имя для обычного адреса памяти. Он указывает на

78

данные, то есть предоставляет их адрес. Например, на рис. 2.14 (a) указатель стека $sp содержит адрес 0x7FFFFFFC и указывает на значение 0x12345678.

Регистр $sp указывает на верхушку стека – наименьший адрес памяти, доступной на стеке. Таким образом, на Рис. 2.14

(a)стек не может использовать память ниже слова с адресом

0x7FFFFFFC.

Указатель стека ($sp) изначально равен большему адресу памяти, после чего его значение по необходимости уменьшается для увеличения доступного программе места. На Рис. 2.14

(b)изображен стек, расширяющийся для того, чтобы выделить два дополнительных слова данных для хранения временных

переменных. Для этого значение регистра $sp уменьшается на 8 и становится равным 0x7FFFFFF4. Два дополнительных слова данных 0xAABBCCDD и 0x11223344 временно размещаются на стеке.

Рис. 2.14. Стек

Одно из важных применений стека – сохранение и восстановление значений регистров, используемых внутри функции. Вспомним, что функция должна производить вычисления и возвращать значения, но не должна приводить к неожиданным побочным эффектам. В частности, она не должна менять значения никаких регистров кроме регистра $v0, содержащего

79

возвращаемое значение. В предыдущем примере функция diffofsums нарушает это правило, потому что она изменяет регистры $t0, $t1 и $s0. Если бы функция main использовала регистры $t0, $t1 или $s0 до вызова diffofsums, то содержимое этих регистров было бы повреждено вызовом этой функции.

Чтобы решить эту проблему, функция сохраняет значения регистров на стеке перед тем, как изменить их, и восстанавливает их из стека перед тем, как завершиться. А именно, совершает следующие шаги:

1.Выделяет пространство на стеке для сохранения значений одного или нескольких регистров.

2.Сохраняет значения регистров на стек.

3.Выполняет функцию, используя регистры.

4.Восстанавливает исходные значения регистров из

стека.

5.Освобождает пространство на стеке.

В следующем примере кода приведена улучшенная версия функции diffofsums, которая сохраняет и восстанавливает регистры $t0, $t1 и $s0.

Пример. Функция, сохраняющая регистры на стеке Код на языке ассемблера MIPS

# $s0 = result diffofsums:

addi $sp, $sp, −12 # make space on stack to store three

registers

sw

$s0, 8($sp)

# save $s0 on stack

sw

$t0, 4($sp)

# save $t0 on stack

sw

$t1, 0($sp)

# save $t1 on stack

add

$t0, $a0, $a1

# $t0 = f + g

add

$t1, $a2, $a3

# $t1 = h + i

sub

$s0, $t0, $t1

# result = (f + g) − (h + i)

add

$v0, $s0, $0

# put return value in $v0

lw

$t1, 0($sp)

# restore $t1 from stack

lw

$t0, 4($sp)

# restore $t0 from stack

 

 

80

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]