четверг, 30 июня 2016 г.

Управляющие конструкции в ассемблере

Первый урок по ассемблеру состоял из обычного вывода сообщения. Теперь давайте посмотрим на управляющие конструкции.

Содержание:
  1. Введение
  2. amd64
  3. sparc v9
  4. Эльбрус
  5. Заключение
  6. Источники

1. Введение

Итак, сформулируем задание: написать программу, сравнивающую два числа. Если первое число больше, программа выводит "gt", если равно - "eq", если меньше - "le".

На Си программа выглядит следующим образом:

#include <stdio.h>

int a, b;

int main()
{
    scanf("%d%d", &a, &b);

    if( a > b )
        printf("gt\n");
    else if( a == b )
        printf("eq\n");
    else
        printf("le\n");

    return 0;
}

Сборка и запуск:

$ gcc comp.c -o comp
$ ./comp
3 4
le
$ ./comp
3
3
eq
$ ./comp
4 3
gt


Здесь мы уже используем стандартную библиотеку чтобы не возиться с системными вызовами. Использование глобальных переменных сделано специально (чтобы пока не объяснять работу со стеком).

Теперь посмотрим как эту программу писать на ассемблерах.

2. amd64

Программа писалась для процессора Core i5, ОС Gentoo GNU/Linux, синтаксис AT&T.

.section .data
    scanf_str:
        .string "%d%d\0"
    gt_str:
        .string "gt\n
\0"
    eq_str:
        .string "eq\n
\0"
    le_str:
        .string "le\n
\0"

.section .bss
    .lcomm a, 32
    .lcomm b, 32

.section .text
    .globl _start

_start:

    # Считываем два числа
    mov $scanf_str, %rdi # Первый аргумент - форматная строка
    mov $a, %rsi         # Второй аргумент - адрес первого числа
    mov $b, %rdx         # Третий аргумент - адрес второго числа
    call scanf           # Вызов scanf

    # Кладём считанные сравнения на регистры
    mov a, %rax
    mov b, %rbx

    # Сравниваем значения регистров
    cmp %rbx, %rax

    jg .print_gt # Если больше, то идём на участок, печатающий "gt"
    je .print_eq # Если равно, то идём на участок, печатающий "eq"

    # Если переходов не было, то печатаем "le"
    mov $le_str, %rdi
    call printf

    # Теперь безусловно идём на выход
    jmp .exit

.print_gt:
    mov $gt_str, %rdi
    call printf
    jmp .exit

.print_eq:
    mov $eq_str, %rdi
    call printf

# Здесь выходим из программы
.exit:
    mov $60, %rax
    mov $0, %rdi
    syscall


Сборка и запуск:

$ as t.s -o t.o && ld t.o -o a.out -lc --dynamic-linker /lib/ld-2.23.so
$ ./a.out
3 4
le
$ ./a.out
3
3
eq
$ ./a.out
4 3
gt


Видно, что теперь к сборке добавились опции -lc --dynamic-linker /lib/ld-2.23.so. Опция -lc говорит о том, что нам надо линковаться с libc.a (стандартная библиотека), --dynamic-linker задаёт конкретный бинарник динамического линковщика.

Теперь посмотрим на новые элементы в исходном коде. Во-первых мы задействовали секцию .bss. В ней хранятся статические переменные (т.е. локальные для данного модуля).

Операция .lcomm symbol, length является псевдо операцией. Она резервирует length байт для локальной переменной, обозначаемой symbol. Т.о. мы выделили память для двух локальных переменных, в которые будем записывать результаты scanf.

Теперь немного про код, вызывающий scanf. Сейчас мы используем передачу аргументов через регистры. В соответствии с соглашениями [amd64abi] для передачи аргументов используются следующие регистры:
  • rdi - первый аргумент
  • rsi - второй аргумент
  • rdx - третий аргумент
  • rcx - четвёртый аргумент
  • r8 - пятый аргумент
  • r9 - шестой аргумент
Последующие аргументы передаются через стек.

Далее рассмотрим инструкцию cmp. Она вычисляет разницу между двумя целочисленными операндами и в зависимости от результата обновляет один из следующих флагов: OF, SF, ZF, AF, PF, CF.

Немного про данные флаги. В процессоре Intel существует специальный регистр EFLAGS, содержащий группу статусных флагов, флаг управления и группу системных флагов. Графически их можно представить так (взято из [intel1]):
Рассмотрим флаги, на которые влияет cmp:
  • OF - Overflow Flag (флаг переполнения). Выставляется если целочисленный результат - слишком большое положительное или слишком малое отрицательное число.
  • SF - Sign Flag (флаг знака). Выставляется если результат, являющийся знаковым целым отрицателен. Иначе равен нулю.
  • ZF - Zero Flag (флаг нуля). Устанавливается если результат равен нулю.
  • AF - Auxiliary Carry Flag (вспомогательный флаг переноса). Выставляется если произошёл перенос из третьего бита.
  • PF - Parity Flag (флаг чётности). Выставляется если самый младший байт результата содержит чётное количество битов, равных 1.
  • CF - Carry Flag (флаг переноса). Выставляется в случае переполнения unsigned арифметики
Операции jg и je являются операциями условного перехода. Они передают управление на указанный адрес в случае выполнения соответствующего условия. В случае его невыполнения, исполнение продолжается со следующей команды. Бывают следующие операции условного перехода:
ИнструкцияУсловие (Состояния флагов)Описание
Беззнаковые условные переходы
JA/JNBE(CF or ZF) = 0Больше (>)
JAE/JNBCF = 0Больше или равно (>=)
JB/JNAECF = 1Меньше (<)
JBE/JNA(CF or ZF) = 1Меньше или равно (<=)
JCCF = 1Взведён флаг переноса (Carry)
JE/JZZF = 1Равно/ноль (=)
JNCCF = 0Взведён флаг переноса (Carry)
JNE/JNZZF = 0Не равно/не ноль (!=)
JNP/JPOPF = 0Не взведён влаг чётности
JP/JPEPF = 1Взведён флаг чётности
JCXZCX = 0Нулевой регистр CX
JECXZECX = 0Нулевой регистр ECX
Знаковые условные переходы
JG/JNLE((SF xor OF) or ZF) = 0Больше (>)
JGE/JNL(SF xor OF) = 0Больше или равно (>=)
JL/JNGE(SF xor OF) = 1Меньше (<)
JLE/JNG((SF xor OF) or ZF) = 1Меньше или равно (<=)
JNOOF = 0Нет переполнения
JNSSF = 0Не отрицательное
JOOF = 1Переполнение
JSSF = 1Отрицательное

Последней не рассмотренной инструкцией осталась jmp. Это безусловный переход. Он передаёт управление программы по указанному адресу не сохраняя адрес возврата. Адрес перехода может быть как относительным, так и абсолютным. В нашем случае мы прыгали по адресу метки, расположенной прямо перед участком кода, завершающим программу.

С версией для amd64 пожалуй всё, теперь посмотрим как условные переходы выглядят в других системах команд.

3. Sparc v9

Переходим к спарку. Тестовые машины те же - TI UltraSparc III+ (Cheetah+) с ОС Gentoo и Эльбрус R1000 c ОС Эльбрус. Переходим к примеру:

.section .data

  scanf_str:
    .ascii "%d%d\0"
  gt_str:
    .ascii "gt\n\0"
  eq_str:
    .ascii "eq\n\0"
  le_str:
    .ascii "le\n\0"

  .global _start

.section .bss
  .lcomm a, 32
  .lcomm b, 32

.section .text

_start:

  ! Готовим аргументы для scanf
  set scanf_str, %o0 ! Кладём адрес строки на регистр
  set a, %o1         ! Кладём адрес a на регистр
  set b, %o2         ! Кладём адрес b на регистр

  ! Вызываем scanf
  call scanf
  nop

  set a, %g1    ! Кладём адрес a на регистр
  ld [%g1], %g1 ! Загружаем значение, лежащее по адресу в регистре %g1

  set b, %g2    ! Кладём адрес b на регистр
  ld [%g2], %g2 ! Загружаем значение, лежащее по адресу в регистре %g2

  cmp %g1, %g2  ! Сравниваем значения

  bg .print_gt  ! Если больше, то переходим на ветвь с gt
  nop
  be .print_eq  ! Если равно, то переходим на ветвь с eq
  nop

  ! В остальных случаях продолжаем исполнять ветвь с le
  set le_str, %o0
  call printf
  nop

  ba .exit ! Безусловно идём на выход
  nop

.print_gt:
  set gt_str, %o0
  call printf
  nop

  ba .exit ! Безусловно идём на выход
  nop

.print_eq:
  set eq_str, %o0
  call printf
  nop

.exit:
  ! Готовим аргументы для exit
  mov 0, %o0
  mov 1, %g1

  ! Вызываем exit
  ta 0x10


Сборка и запуск:

$ as -Av9 -64 sparc.s -o sparc.o && ld --dynamic-linker /lib64/ld-2.17.so -Av9 -m elf64_sparc sparc.o -lc
$ ./a.out
3 4
le
$ ./a.out
3
3
eq
$ ./a.out
4 3
gt

Тут всё на столько похоже на программу для интела, что не сразу понятно что нуждается в комментариях :) Подготовка аргументов для scanf, думаю, понятна, эти инструкции описывались в предыдущем посте. Перейдём сразу к инструкции вызова.

Итак, мы можем видеть инструкцию call scanf, которая на самом деле является синтетической инструкцией, разворачивамой следующим образом:

call address -> jmpl address, %o7

Но переходы - это тема для отдельного поста, поэтому пока будем считать что это просто вызов процедуры. Отдельно отмечу что после каждого перехода стоит операция nop (т.е. пустышка). Это связано с тем что большинство управляющих инструкций sparc'а работают через delay slot [delay1,delay2]. Если в кратце, то процессор при подаче инструкции перехода безусловно исполнит следующую за переходом инструкцию. Т.к. нам это свойство сейчас не нужно, то мы забиваем эти инструкции nop'ами.

После того как scanf вернёт управление, нам нужно будет загрузить значения, введённые пользователем. Этим занимается инструкция ld, осуществляющая чтение значения из памяти в регистр. В спарке доступ в память осуществляется только через инструкции ld/st. Инструкции ld бывают следующих видов:
  • ldsb [address], regrd - Загрузить знаковый байт (Load Signed Byte)
  • ldsh [address], regrd - Загрузить знаковое полуслово (Load Signed Halfword)
  • ldsw [address], regrd - Загрузить знаковое слово (Load Signed Word)
  • ldub [address], regrd - Загрузить беззнаковый байт (Load Unsigned Byte)
  • lduh [address], regrd - Загрузить беззнаковое полуслово (Load Unsigned Halfword)
  • lduw [address], regrd (синоним: ld) - Загрузить беззнаковое слово (Load Unsigned Word)
  • ldx [address], regrd - Загрузить расширенное (Load Extended Word)
  • ldd [address], regrd - Загрузить двойное (Load Doubleword)
Положив полученные значения на регистр нам нужно их сравнить. И тут мы видим инструкцию cmp, которая... тоже является синтетческой! Она раскрывается следующим образом:

cmp regrs1, reg_or_imm -> subcc regrs1, reg_or_imm, %g0

Да, в спарке нет отдельной инструкции сравнения. Большинство арифметических инструкций имеют два режима работы - с выработкой результатов сравнения в качестве побочного эффекта и без неё. Результат сравнения складывается в CCR (Condition Codes Register) - 8-битный регистр условных кодов. CCR используется для целочисленных операций, причём делится на две части:
Регистр icc используется для 32-х битных операций, а xcc - для 64-х битных. При этом арифметические операции модифицируют обе части CCR. Каждая часть регистра делится на 4 поля по одному биту:
Поля имеют следующие значения:
  • N - показывает что результат вычисления был отрицательным
  • Z - показывает что результат был равен нулю
  • V - показывает что во время последней арифметической операции было переполнение
  • C - флаг переноса (carry flag)
Т.о. оттранслированная операция cmp задаёт нам соответствующие флаги CCR, на основе которых мы совершаем переход:

ИнструкцияУсловие (icc test)Описание
ba1Branch Always
bn0Branch Never
bne (или bnz)not ZBranch on Not Equal
be (или bz)ZBranch on Equal
bgnot (Z or (N xor V))Branch on Greater
bleZ or (N xor V)Branch on Less or Equal
bgenot (N xor V)Branch on Greater or Equal
blN xor VBranch on Less
bgunot (C or Z)Branch on Greater Unsigned
bleuC or ZBranch on Less or Equal Unsigned
bccnot CBranch on Carry Clear (Greater than or Equal, Unsigned)
bcsCBranch on Carry Set (Less than, Unsigned)
bposnot NBranch on Positive
bnegNBranch on Negative
bvcnot VBranch on Overflow Clear
bvsVBranch on Overflow Set

По инструкции ba, как можно догадаться, мы безусловно переходим на участок программы, вызывающий exit. Оставшая часть программы должна быть понятна.

4. Эльбрус

Версия для Эльбруса несколько сложнее, но гораздо интересней. Здесь будет затронуто больше концептов чем хотелось бы рассказывать для данного поста. Машина, на которой всё это проверялось та же - Эльбрус-4С с системой команд v3 под управлением ОС Эльбрус. Собственно, сам код:

.section .data

$scanf_str:
    .ascii  "%d%d\0"
$gt_str:
    .string "gt\n\0"
$eq_str:
    .string "eq\n\0"
$le_str:
    .string "le\n\0"

.section .bss
    .lcomm a, 32
    .lcomm b, 32

.section .text
    .global _start

_start:
    {
!              база       размер     текущий
      setbn    rbs = 0x4, rsz = 0x3, rcur = 0x0
!             размер окна
      setwd    wsz = 0x8, nfx = 0x1
      disp    %ctpr1, $scanf                  ! Подготовка перехода на scanf
      getsp, 0    _f32s,_lts1 0xffffffd0, %r1 ! Получаем адрес стека
    }

    ! Здесь подготовливаем аргументы scanf
    ! ABI Эльбруса говорит что в случае процедур с элипсом следует
    ! резмещать аргументы на стеке, поэтому будут применены операции st
    {
      addd, 0 0x0, [ _f64,_lts0 $scanf_str], %b[0] ! Кладём адрес форматной строки на регистр
      addd, 1 0x0, [ _f64,_lts2 $a],         %b[1] ! Кладём адрес первого глобала на регистр
    }
    {
      addd, 0 0x0, [ _f64,_lts2 $b],         %b[2] ! Кладём адрес второго глобала на регистр
      std,  2 %b[0], 0x0, %r1                      ! Кладём содержимое регистра с адресом строки на стек
    }
    {
      std,  2 %b[1], 0x8, %r1                      ! Кладём содержимое регистра с адресом первого глобала на стек
      std,  5 %b[2], 0x10, %r1                     ! Кладём содержимое регистра с адресом второго глобала на стек
    }

    ! Непосредственно вызов
    call %ctpr1, wbs = 0x4  ! Вызываем подготовленную функцию scanf

    {
      ldw, 2 0x0, [_f64,_lts0 $a], %b[1] ! Кладём значение глобала a на регистр
      ldw, 5 0x0, [_f64,_lts2 $b], %b[2] ! Кладём значение глобала b на регистр
    }

    {
      cmplsb, 1 %b[1], %b[2], %pred0 ! Производим сравнение a < b
      cmplsb, 0 %b[2], %b[1], %pred1 ! Производим сравнение b < a
      disp %ctpr1, $printf           ! Подготавливаем вызов printf
    }

    ! В этой ШК вычисляем третий предикат (т.е. условие == )
    {
      pass %pred0, @p0        ! Записываем результат сравнения a < b в локальный предикат
      pass %pred1, @p1        ! Записываем результат сравнения b < a в локальный предикат
      andp ~@p0, ~@p1, @p4    ! Вычисляем !pred1 & !pred2
      pass @p4, %pred2        ! Записываем результат в глобальный предикат
    }

    ! Готовим аргументы для printf
    {
      addd, 0 0x0, [ _f64,_lts0 $le_str ], %b[0] ? %pred0 ! Если a < b, то в качестве аргумента кладём адрес строки "le" в регистр
      addd, 2 0x0, [ _f64,_lts2 $gt_str ], %b[0] ? %pred1 ! Если a > b, то в качестве аргумента кладём адрес строки "gt" в регистр
    }
    addd, 0 0x0, [ _f64,_lts0 $eq_str ], %b[0] ? %pred2 ! Если a = b, то в качестве аргумента кладём адрес строки "eq" в регистр
    std,  2 %b[0], 0x0, %r1                             ! Кладём содержимое регистра с адресом строки на стек

    ! Вызываем printf
    call %ctpr1, wbs = 4

    ! Готовим аргументы для exit
    {
      sdisp %ctpr2, 0x3
      addd, 0 0x0, 0x0, %b[1]
      addd, 1 0x0, 0x1, %b[0]
    }

    ! Вызываем exit
    call %ctpr2, wbs = 4

Сборка и запуск:

$ las t.s -o t.o && ld t.o -o a.out -lc --dynamic-linker /lib/ld-2.21.so
$ ./a.out 
3 4
le
$ ./a.out
3
3
eq
$ ./a.out
4 3
gt


Начало программы полностью идентично предыдущим вариантам и в пояснениях не нуждается, поэтому перейдём к первой ШК (широкой команде).
    {
      setbn    rbs = 0x4, rsz = 0x3, rcur = 0x0
      setwd    wsz = 0x8, nfx = 0x1
      disp    %ctpr1, $scanf                  ! Подготовка перехода на scanf
      getsp, 0    _f32s,_lts1 0xffffffd0, %r1 ! Получаем адрес стека
    }

Первые две инструкции я подробно описывать не буду, т.к. для этого примера они значения не имеют, но расскажу что они делают. Инструкция setbn устанавливает базу циклических регистров, инструкция setwd изменяет размер окна стека процедур. Эти инструкции являются частью процедурного механизма, о котором я расскажу в других постах. Также в этой ШК присутствует инструкция getsp, которая возвращает свободную область в незащищённом стеке пользователя.

Ну и отдельно рассмотрим инструкцию disp. Инструкция имеет следующий синтаксис:disp ctp_reg, label. Здесь ctp_reg - регистр перехода, а label - адрес перехода. Как мы помним из предыдущего поста, в Эльбрусах есть механизм подготовки переходов, начинающий подкачку кода из указанного адреса. Это позволяет избавиться от накладных расходов при непосредственно переходе. disp подготавливает переход на известный адрес, в нашем случае это адрес функции scanf.

Следующие три ШК подготавливают аргументы для вызова printf. С инструкциями add мы уже знакомы, поэтому рассмотрим только инструкции std. Это инструкция записи в незащищённое пространство памяти. Синтаксис инструкции следующий: st(b/h/w/d) src3, [ address ]. В зависимости от суффикса мы можем записать следующее:
  • stb - запись байта
  • sth - запись полуслова
  • stw - запись одинарного слова
  • std  -запись двойного слова
Рассматривая инструкцию std, 2 %b[0], 0x0, %r1 можно сказать что мы запишем содержимое регистра %b[0] по адресу, хранящемуся в регистре %r1 со смещением 0x0 используя АЛК номер 2.

А теперь зачем это было нужно (и почему не было в примерах для amd64 и sparc). В соответствии с ABI Эльбруса если мы вызываем функцию с эллипсисом (т.е. с переменным количеством аргуменов), то мы должны все аргументы размещать на стеке. Т.о. в рассматриваемых ШК мы положили адреса строки и двух глобалов на стек для вызова scanf.

Далее у нас идёт уже знакомая иструкция call. Здесь особенностью является то что она не обрамлена фигурными скобками. Это означает что наша ШК состоит только из одной инструкции. Это неприятно, но в таком примере ШК особо ничем полезным не набьёшь :)

После выполнения call у нас идёт ШК следующего содержания:
{
  ldd, 3 0x0, [_f64,_lts0 $a], %b[1] ! Кладём значение глобала a на регистр
  ldd, 5 0x0, [_f64,_lts2 $b], %b[2] ! Кладём значение глобала b на регистр
}
В ней использованы инструкции ldd, которые, как можно догадаться, обратны std. Общий синтаксис инструкции таков: ld(b/h/w/d) [ address ], dst. Эта инструкция выполняет чтение из незащищённого пространства. В зависимости от суффикса возможны следующие варианты:
  • ldb - считывание байта
  • ldh - считывание полуслова
  • ldw - считывание одинарного слова
  • ldd - считывание двойного слова
Т.о. инструкцию ldw, 2 0x0, [_f64,_lts0 $a], %b[1] следует читать: прочтём данные по адресу символа a со смещением 0x0 и положим их в регистр %b[1].

Теперь у нас на регистрах есть значения, введённые пользователем, и мы можем приступить к сравнению значений. В Эльбрусах операции сравнения несколько отличаются от интела. Здесь у нас нет отдельного регистра для eflags (хотя мы можем запустить арифметическую операцию с выработкой значения в формате IFL), но есть отдельная проверка под каждый случай:

ИнструкцияОписание
CMP(s/d)b группа из 8 операций сравнения
CMPO(s/d)сравнение 32/64 "переполнение"
CMPB(s/d)bсравнение 32/64 "< без знака"
CMPE(s/d)bсравнение 32/64 "равно"
CMPBE(s/d)bсравнение 32/64 "<= без знака"
CMPS(s/d)bсравнение 32/64 "отрицательный"
CMPP(s/d)bсравнение 32/64 "нечетный"
CMPL(s/d)bсравнение 32/64 "< со знаком"
CMPLE(s/d)bсравнение 32/64 "<= со знаком"
CMPAND(s/d)b группа из 4 операций проверки
CMPANDE(s/d)bпоразрядное "and" и проверка 32/64 "равно 0"
CMPANDS(s/d)bпоразрядное "and" и проверка 32/64 "отрицательный"
CMPANDP(s/d)bпоразрядное "and" и проверка 32/64 "нечетный"
CMPANDLE(s/d)bпоразрядное "and" и проверка 32/64 "<=0 со знаком"

Операции CMP вычитают операнд 2 из операнда 1, определяют флаги результата и проверяют указанное условие. Операции CMPAND выполняют поразрядное логическое "и", а далее по состоянию флагов проверяют заданное условие. Результатом данных операций будет сформированный предикат "true" или "false".

И тут начинается ещё более интересный механизм, применяемый во VLIW процессорах - предикаты. Процессор Эльбрус имеет предикатный файл, содержащий в себе первичные и вторичные предикаты. Первичные предикаты - это битовые значения, вырабатываемые операциями сравнения, вторичные - результат логических операций над первиными предикатами. Всего у нас есть 32 первичных предиката и 7 вторичных. Первичные предикаты записываются как %pred0 - %pred31

Рассмотрим ШК со сравнением:
    {
      cmpldb, 1 %b[1], %b[2], %pred0 ! Производим сравнение a < b
      cmpldb, 0 %b[2], %b[1], %pred1 ! Производим сравнение b < a
      disp %ctpr1, $printf           ! Подготавливаем вызов printf
    }

В ней мы получили результат для двух сравнений a < b и b > a. В первом случае мы записали предикат в регистр %pred0, во втором - в %pred1. В принципе самым простым (и быстрым) способом было бы в этой же ШК выполнить третье сравнение и получить третий предикат, но мне хотелось продемонстрировать вычисление одного предиката на основе других.

Переходим к следующей ШК:
    ! В этой ШК вычисляем третий предикат (т.е. условие == )
    {
      pass %pred0, @p0        ! Записываем результат сравнения a < b в локальный предикат
      pass %pred1, @p1        ! Записываем результат сравнения b < a в локальный предикат
      andp ~@p0, ~@p1, @p4    ! Вычисляем их !a & !b
      pass @p4, %pred2        ! Записываем результат в глобальный предикат
    }

Она довольно необычна. В ней мы выполняем запись значения в реистр, вычисление с ним и запись результата. И всё в одной ШК! Давайте по подробнее рассмотрим что тут происходит. Мы не можем выполнять вычисления с первичными предикатами. Для этих целей мы должны записать их в локальные предикаты командой pass. Всего у нас есть 7 локальных предикатов, обозначаемых @p0 - @p6. При этом предикаты @p0 - @p3 могут быть использованы только для хранения первичных предикатов, а предикаты @p4 - @p6 для хранения результатов вычислений и записи в первичные предикаты.

Для вычислений с предикатами нам доступна только инструкция andp, выполняющая операцию "и". Тильда перед предикатом означает отрицание. Т.о. рассматривая инструкцию andp ~@p0, ~@p1, @p4 можно сказать, что если у нас не выполнилось условие a > b и b > a, то a == b, и мы записываем это в локальный предикат @p4. После этого пересылаем его в первичный предикат %pred2.

И теперь, глядя на следующую ШК, можно понять как используются предикаты. Рассмотрим инструкцию addd, 0 0x0, [ _f64,_lts0 $le_str ], %b[0] ? %pred0. Помимо уже известного синтаксиса сюда добавился хвостик `? %pred0'. Он означает что данная инструкция будет исполнена только если предикат %pred0 имеет значение true. Такой способ называется "условным исполнением", он же "предикатный режим". Под предикат можно поставить почти любую инструкцию. Это обеспечивает возможность исполнять код, содержащий большоее количество ветвей не используя инструкции переходов, при этом плотно забивая ШК.

Остаток программы должен быть более или менее понятен, поэтому описание программ можно заканчивать :)

5. Заключение


Уже на таком простом примере можно видеть довольно сильные различия между системами команд различных процессоров. Так процессоры intel имеют инструкцию mov, способную работать как  с регистрами, так и с памятью, в то время как в процессорах sparc и Эльбрус работа с памятью ведётся через отдельные команды. Случай с Эльбрусом вообще очень показателен, т.к. из него мы можем видеть что программу можно довольно красиво и органично избавить от ветвлений (что рекомендуется делать при любой возможности). Интересно заметить что в sparc'е половина применённых инструкций является "псевдо" и раскрывается в какие-то другие, что, правда, добавляет читабельность коду и делает его красивым. А вот ассемблер Эльбруса довольно сложно читать и писать, хотя для этого есть и объективные причины.

Источники


[0xax] Продолжение серии постов про основы ассемблера, которые меня вдохновили
[intel1] Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 1: Basic Architecture
[amd64abi] System V Application Binary Interface
[sparcv9]The SPARC Architecture Manual Version 9
[sparcasmbook] SPARC Architecture, Assembly Language Programming, and C. Очень хороший учебник по ассемблеру и по спарку
[delay1] Описание delay slot на wiki
[delay2] Хорошее объяснение про delay slot и вообще годный бложик одного ассемблериста
[cs217] Introduction to Programming Systems - учебный курс, включающий в себя описание sparc-машин.
[elbrus] Микропроцессоры и вычислительные комплексы семейства «Эльбрус»
[wasm] Вновь оживший ресурс wasm.ru. Я им не пользовался, но на нём довольно много материала по разным ассемблерам и живой форум.

1 комментарий:

  1. > процессоры intel имеют инструкцию mov, способную работать как с регистрами, так и с памятью, в то время как в процессорах sparc и Эльбрус работа с памятью ведётся через отдельные команды
    В OpenGL то же были всякие ARB_buffer в то время как в Vulkan -е уже прямое управление памятью через отдельные функции. Так что это общемировая глобальная тенденция.

    Спасибо, интересно.

    ОтветитьУдалить

Примечание. Отправлять комментарии могут только участники этого блога.