Цель: Изучение приемов преобразования числовых данных.
В системе команд ARM реализованы две инструкции для умножения чисел: умножение (инструкция MUL) и умножение с накоплением (инструкция MLA).
Формат команд, соответствующих этим инструкциям, приведен на рис. 1.
Как и в случае других команд, поле условия определяет, в каком случае команда должна быть выполнена (он может использоваться, чтобы пропустить команду при определенном стечении обстоятельств, как более простая альтернатива связке команд сравнения и условного перехода).
Обе команды для выполнения целочисленного умножения используют алгоритм, основанный на цепочке операций логического сдвига и сложения 8-битных операндов (Booth’s algorithm).
Формат соответствующих инструкций ассемблера выглядит следующим образом:
MUL{cond}{S} Rd, Rm, Rs
MLA{cond}{S} Rd, Rm, Rs, Rn
Здесь {cond}
– это двухсимвольная мнемоника условия выполнения команды, а {S}
– суффикс, присутствие которого разрешает команде воздействовать на флаги регистра CPSR
, а поля Rd
, Rm
, Rs
и Rn
– выражения, которые определяют номер регистра общего назначения (кроме регистра R15
).
Приведем примеры:
MUL R1,R2,R3 @ R1 = R2*R3_
MLAEQS R1,R2,R3,R4 @ По условию EQ выполнить R1 = R2*R3 + R4
@ и разрешить воздействие на флаги CPSR выполнения (S)
Как видно, действие команды MUL
можно описать формулой Rd = Rm · Rs
. Четвертый аргумент (то есть Rn
) командой игнорируется, а в машинном коде соответствующее поле должно быть равно нулю с целью совместимости (на случай, если когда-нибудь система команд будет дополнена).
Действие команды умножения с накоплением MLA
описывается формулой Rd = Rm · Rs + Rn
. Заметим, что при Rm=1
или Rs=1
эта команда становится эквивалентна выполнению команды ADD
.
На операнды инструкций умножения есть два ограничения:
- Нельзя использовать в качестве операндов регистр-результат
Rd
одновременно в качестве регистра-операнда (Rm
,Rn
илиRs
). - Нельзя использовать регистр R15 в качестве регистра-операнда или регистра-результата.
Все другие комбинации остальных регистров будут давать корректный результат. Если требуется, в роли Rd
, Rn
и Rs
может выступать один и тот же регистр.
Обе команды позволяют выполнять операции только с целочисленными операндами, как без знака так и со знаком (дополнение до двух). Теоретически результат умножения двух 32-битных целых чисел может занимать 64 бита; однако в процессорах ARM размер аргументов должен быть одинаков и равен машинному слову, т. е. 4 байтам. Поэтому команды MUL
и MLA
сохраняют в регистре-приемнике только младшие 32 бита результата.
Старшие 32 бита результата отбрасываются, а поскольку результаты умножения знаковых и беззнаковых 32-битных операндов различаются только своими старшими 32 битами, результат выполнения этих команд будет одинаков как для операндов со знаком, так без знака. Проиллюстрируем сказанное на двух примерах:
Пример интерпретации операндов как знаковых:
Пусть, операнд А = -10
, а операнд B = 20
. Результатом их умножения будет число -200
, которое корректно записывается как 0xFF FF FF 38
.
Пример интерпретации операндов как беззнаковых:
Пусть, операнд А = 4 294 967 286
, а операнд B = 20
. Результатом их умножения будет число 85 899 345 720
, которое корректно записывается как 0x13 FF FF FF 38
. Но, поскольку старшие 32 бита результата отбрасываются, окончательным результатом умножения будет все то же число 0xFF FF FF 38
.
Как было сказано, возможность воздействовать на флаги регистра CPSR
определяется битом S
в соответствующем поле команды. Флаги отрицателььного результата N
и флаг нуля Z
устанавливаются в соответствии с результатом умножения: флаг N
становится равным 31-му биту результата, а флаг Z
устанавливается, если результат оказывается нулевым. Флаг переноса С
устанавливается в неизвестное состояние, а флаг переполнения V
не используется.
Команда MUL
выполняется за 1S + mI
машинных тактов, а команда MLA
– за 1S + I(m + 1)
машинных тактов, где S
и I
зависят от типа машинных тактов, а m
– количество 8-битных множителей, необходимых для выполнения умножения, зависит от содержимого операнда-множителя Rs
. Возможные значения m перечислены ниже:
- m = 1, если биты [31:8] операнда-множителя – либо все нули, либо все единицы;
- m = 2, если биты [31:16] операнда-множителя – либо все нули, либо все единицы;
- m = 3, если биты [31:24] операнда-множителя – либо все нули, либо все единицы;
- m = 4 во всех остальных случаях.
Рассмотрим подробнее, как с помощью скрипта компоновщика разместить секцию .data
программы в ОЗУ. В качестве примера такой программы возьмем программу сложения чисел, которая загружает два значения из оперативной памяти, складывает их и сохраняет результат обратно в ОЗУ. Разместим исходные значения и место для сохранения результата в секции .data
.
.data
val1: .4byte 10 @ Первое число
val2: .4byte 30 @ Второе число
result: .4byte 0 @ Место 4 байта для результата
.text
.align
start:
ldr r0, =val1 @ r0 = &val1
ldr r1, =val2 @ r1 = &val2
ldr r2, [r0] @ r2 = *r0
ldr r3, [r1] @ r3 = *r1
add r4, r2, r3 @ r4 = r2 + r3
ldr r0, =result @ r0 = &result
str r4, [r0] @ *r0 = r4
stop: b stop
Для линковки программы воспользуемся следующим скриптом компоновщика:
SECTIONS {
. = 0×00000000;
.text : { * (.text); }
. = 0xA0000000;
.data : { * (.data); }
}
Дамп таблицы символов ELF-файла, созданного этим скриптом, должен выглядеть так:
$ arm-none-gnueabi-nm -n add-mem.elf
00000000 t start
0000001c t stop
a0000000 d val1
a0000001 d val2
a0000002 d result
Достоинством созданной программы является ее понятность и простота. Однако ее нельзя назвать полностью работоспособной по той причине, что ОЗУ – энергозависимая память, и сразу после включения питания микропроцессорного устройства данным в оперативной памяти просто неоткуда взяться.
Поэтому изначально весь код и все исходные данные должны быть сохранены во флеш-памяти. При включении питания стартовый код должен скопировать данные из флеш-памяти в оперативную память, и только затем приступать к исполнению программы. Таким образом секция .data
нашей программы имеет два адреса: загрузочный адрес из флеш-памяти и адрес в оперативной памяти во время выполнения (т. н. VMA или виртуальный адрес памяти).
Чтобы программа была полностью работоспособной, в нее нужно внести два изменения:
- Скрипт нужно преобразовать так, чтобы он указывал для секции
.data
и загрузочный, и виртуальный адреса памяти. - Необходим фрагмент кода, который скопирует секцию
.data
из флеш-памяти в ОЗУ.
Виртуальный адрес должен использоваться компоновщиком для определения адресов меток. В предыдущем скрипте для .data
был определён именно виртуальный адрес. Если загрузочный адрес явно не указан, по умолчанию он равен виртуальному. Это нормально, когда программа полностью выполняется с флеш-памяти, но если во время выполнения программы данные должны быть расположены в ОЗУ, эти адреса должны различаться.
Задать конкретное значение загрузочного адреса можно ключевым словом AT
:
SECTIONS {
. = 0×00000000;
.text : { * (.text); }
etext = .; ❶
. = 0xA0000000;
.data : AT (etext) { * (.data); } ❷
}
Внесенные в скрипт изменения помечены цифровыми метками. Рассмотрим их подробнее.
etext
присваивается значение счётчика адреса в данной позиции. В нашем случае etext содержит адрес следующего свободного поля после всего кода, находящегося во флеш-памяти. Позже это значение пригодится при определении, куда во флеш-памяти поместить секцию.data
. Отметим, что сам по себе etext не выделяет память, а просто делает запись в таблице символов.- Ключевое слово
AT
(точнее, его аргумент) определяет загрузочный адрес секции.data
. В нашем случае загрузочный адрес секции.data
определяется как место, расположенное сразу после кода во флеш-памяти.
Для копирования данных из флеш-памяти в ОЗУ необходимо знать три числа:
- Адрес данных во флеш-памяти (
flash_sdata
) - Адрес данных в ОЗУ (
ram_sdata
) - Размер секции
.data
(data_size
)
Используя эту информацию, мы можем копировать данные, используя следующий фрагмент кода:
ldr r0, =flash_sdata
ldr r1, =ram_sdata
ldr r2, =data_size
copy:
ldrb r4, [r0], #1
strb r4, [r1], #1
subs r2, r2, #1
bne copy
Задать необходимые значения можно непосредственно через скрипт:
SECTIONS {
. = 0×00000000;
.text : {
* (.text);
}
flash_sdata = .; ❶
. = 0xA0000000;
ram_sdata = .; ❷
.data : AT (flash_sdata) {
* (.data);
}
ram_edata = .; ❸
data_size = ram_edata – ram_sdata; ❹
}
Как видно, содержимое скрипта на этот раз помечено четырьмя метками:
(1) Начало данных во флеш-памяти, следующих сразу за кодом
(2) Начало данных в ОЗУ, т. е. базовый адрес оперативной памяти
(3), (4) Размер данных вычисляется путём вычитания начального и конечного адресов данных в ОЗУ. Как мы видим, в скриптах разрешено использовать простые арифметические выражения.
Итоговая программа должна выглядеть следующим образом:
.data
val1: .4byte 10 @ Первое число
val2: .4byte 30 @ Второе число
result: .space 4 @ 1 байт для результата
.text
start:
ldr r0, =flash_sdata
ldr r1, =ram_sdata
ldr r2, =data_size
copy:
ldrb r4, [r0], #1
strb r4, [r1], #1
subs r2, r2, #1
bne copy
ldr r0, =val1 @ r0 = &val1
ldr r1, =val2 @ r1 = &val2
ldr r2, [r0] @ r2 = *r0
ldr r3, [r1] @ r3 = *r1
add r4, r2, r3 @ r4 = r2 + r3
ldr r0, =result @ r0 = &result
str r4, [r0] @ *r0 = r4
stop: b stop
надо хотя бы одним предложением рассказать, что делает директива .space
– ведь ее раньше не было
Для просмотра ячейки ОЗУ по ее физическому адресу в мониторе QEMU предусмотрена следующая команда:
xp /format address
Параметр format
позволяет указать, в каком формате должны быть показаны данные, и имеет следующую структуру:
/[count][data_format][size]
Назначение полей формата:
count
: число элементов для вывода;data_format
:x
– шестнадцатиричный,d
– десятичный,u
– беззнаковый десятичный,o
– восьмеричный,c
– символьный,i
– дизасемблированная инструкция процессора;size
:b
– 8 бит,h
– 16 бит,w
– 32 бит,g
– 64 бит.
Параметр address
может быть либо значением прямого адреса, например, 0×20000, либо именем регистра, содержащего нужный адрес.
Покажем использование команды xp
на примере программы, скомпилированной с помощью последнего приведенного скрипта. Выполнение программы и общение с монитором QEMU будет выглядеть следующим образом:
$ qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/null
(qemu) xp /4dw 0xA0000000
a0000000: 10 30 40 0
- Написать программу, реализующую функции простейшего калькулятора для работы с двумя целыми положительными числами в диапазоне 0..255:
- Операции: +, -, *, /;
- Программа работает циклически до ввода пустой строки;
- При вводе недопустимого значения оно игнорируется с выдачей диагностического сообщения.