У меня сейчас завелось вычислительное устройство с таким процессором, -- о нем еще будет отдельный пост, -- и я, наконец, решил поинтересоваться, как устроена тамошняя система команд. И мне понравилось.
Во-первых, это вполне честный 32-битный RISC, с 16-ю целочисленными регистрами (есть еще какие-то дополнительные для привилегированных режимов). Указатель стека, счетчик команд, а также link register (как по-русски?) и указатель кадра (frame pointer) входят в число этих шестнадцати. Некоторые команды, вроде переходов и вызова подпрограмм, работают с ними особым образом, но с тем же успехом к ним можно обращаться и обыкновенными командами для пересылки значений и арифметики. Например,
b .L5 -- команда перехода на метку .L5, а
ldr pc,.L10
...
.L10: .word .L5 -- загрузка слова в регистр, которая делает то же самое.
Как полагается, в отдельном регистре хранятся флаги состояния. И, опять же, как полагается, условные переходы могут производиться или не производиться в зависимости от значения этих флагов. Разница с привычными мне другими системами команд в том, что, во-первых, при каждой арифметической команде можно указать, будет она выставлять флаги или нет (видимо, в формате инструкции просто заготовлен для этого битик), а во-вторых, выполнять или не выполнять в зависимости от флагов тоже можно любую команду, а не только переходы -- тоже, наверное, есть просто особое поле в формате команды. В результате можно делать, например, так: сишный код
int
qq (int a, int b, int c, int d)
{
if (a+b < 0) {
return a+b+c+d;
} else {
return a+b-c+d;
}
} команда
gcc -S -Os tt.c переводит в
qq:
@ Параметры передаются в регистрах r0 (a), r1 (b), r2 (c)
@ и r3 (d).
@ Результат ожидается в r0.
adds r1, r1, r0 @ Получаем a+b в регистре r1.
@ При этом устанавливаем флаги
@ (за это отвечает буква s).
addmi r1, r2, r1 @ Получаем в r1 a+b+с. Но делаем это
@ только в том случае, когда флаги,
@ полученные предыдущей командой,
@ показывают минус (mi).
@ Эта команда флаги не ставит.
rsbpl r0, r2, r3 @ А это команда с ветки else,
@ поскольку имеет в имени pl,
@ то есть, ждет неотрицательных флагов.
@ (не знаю, почему gcc именно так их
@ упорядочил). Она кладет в r0
@ разницу c и d.
addmi r0, r1, r3 @ Вторая команда ветки then. Флаги
@ по-прежнему действуют те, которые
@ утстановлены первым сложением.
addpl r0, r0, r1 @ Вторая команда ветки else.
bx lr @ Возврат. Как видим, в теле функции нет никаких переходов.
Обращение к элементу целочисленного массива может выглядеть так:
ldr r0, [r2, r3, asl #2] Здесь r2 -- адрес начала массива, r3 -- индекс в нем, но поскольку целые занимают по четыре байта, прежде, чем добавить его к базовому адресу, надо его сдвинуть влево на два (asl #2). Опять-таки, такой сдвиг можно вставить в любую команду -- может, мы, например хотим получить адрес начала восьмибайтовой структуры в регистре:
add r1, r0, r1, asl #3 А вот как выглядит
k = *(--pp) -- вот так:
ldr r2, [r1, #-4]! То есть сначала r1 уменьшается на 4, а потом по получившемуся адресу идет обращение. Кроме такого преинкремента есть, разумеется, и постинкремент -- было бы желание.
В заголовке функции бывает нужно сохранить регистры на стеке:
stmfd sp!, {r4, lr} Какие именно регистры сохранять, указывается битовой маской, а восклицательный знак говорит, что указатель стека надо в результате подвинуть. Соответственно, стек восстанавливается так:
ldmfd sp!, {r4, lr} Lr (link register) при вызове подпрограммы содержит адрес возврата. Соответственно, говорит учебник, при желании вместо команд
ldmfd sp!, {r4, lr}
bx lr можно вернуться просто через
ldmfd sp!, {r4, pc} Но почему-то компилятор такого не порождает. (Мешает предсказанию ветвлений?)
В общем, вроде все простенько, ничего особенного, но все к месту. И, кстати, очень похоже на мою любимую PDP-11. Я почти наверняка ничего на этом ассемблере писать никогда не буду, но само ощущение, что в моем компьютере живет такой зверь, а не уебище x86, прибавляет радости при общении с ним.
|