2. Работа с платформой

2.1. Основные принципы

Система программирования и все сборочные инструменты ориентированы на максимальную совместимость с существующей экосистемой. Во многих случаях программа для сборки и корректной работы вообще не потребует какой-либо адаптации. В случае правок зачастую они сводятся к тому, что поведение архитектуры описывается тем же кодом, что используется для gcc и x86_64. В этом случае достаточно распознать архитектуру.

При нативной сборке можно указывать gcc и g++ в качестве компиляторов C и C++. Это синонимы для компилятора lcc в режимах C и C++.

Этапы сборки open source программ те же, что и для других архитектур:

  • конфигурация (./configure, cmake, mvn, scons …)

  • компиляция (пример - make)

  • установка (пример - make install)

2.1.1. Распознать архитектуру

Если пакет собирается с помощью autotools, вам потребуется вариант файлов config.guess и config.sub, в которых есть поддержка e2k. Вы можете взять их здесь:

  • /usr/share/automake/config.sub

  • /usr/share/automake/config.guess

Замените config.guess и config.sub из ваших исходников на экземпляры выше.

Для платформы Эльбрус предусмотрен макрос языка C:

__e2k__

Компилятор lcc взводит свой макрос (для архитектур Эльбрус и Спарк):

__LCC__

Примеры использования:

#if (defined __LCC__) && (! defined __OPTIMIZE__)
  #define MY_MACROS MY_MACROS_LCC
#endif
#ifdef __e2k__
  #include "implementation_elbrus.h"
#endif

Макрос __LCC__ хранит значение мажорной версии компилятора. Те или иные действия можно задавать только для определённых версий.

#if (defined __LCC__) && (__LCC__ <= 123)
  # error "Not supported for lcc-1.23 or earlier."
#endif

2.1.2. Справочные данные

Платформа поддерживает 64-битную и 32-битную адресацию. Основной режим системного ПО 64 бита.

Организация байтов — little endian.

Локальный стек в процедурах растёт от больших адресов к меньшим.

Размеры данных подробно описаны в разделе Интерфейсные программные соглашения.

2.2. Демонстрация ассемблера

2.2.1. Основные операции

Рассмотрим код с простыми числовыми расчётами, который сможет продемонстрировать основные операции в ассемблере.

Пример 1. арифметические операции

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int f(int a, int b, int c)
{
      int s1, s2, s4;
      double s3;
      s1 = a + b;
      s2 = b - c;
      s3 = s1 / s2;
      s4 = s3 * s1;
      return (int) (s1 * s4);
}

Для просмотра ассемблера подадим компилятору опцию -S:

gcc -O3 -S t.c

Листинг 1. Ассемблер примера 1:

.text
.global f
.type   f, #function
.align  8
f:
{
  setwd wsz = 0x4, nfx = 0x1
  return        %ctpr3; ipd 2
  subs,0        %r1, %r2, %g16
  adds,3        %r0, %r1, %g17
}
{
  nop 2
  istofd,3      %g17, %g18
}
{
  nop 7
  sdivs,5       %g17, %g16, %g16
}
{
  nop 2
}
{
  nop 3
  istofd,3      %g16, %g16
}
{
  nop 3
  fmuld,3       %g16, %g18, %g16
}
{
  nop 3
  fdtoistr,3    %g16, %g16
}
{
  nop 5
  muls,3        %g17, %g16, %g16
}
{
  ct    %ctpr3
  sxt,3 0x2, %g16, %r0
}

Обратим внимание, что каждая широкая команда в ассемблере помещена в пару фигурных скобок.

Широкая команда

— набор операций, которые запускаются процессором параллельно в одном такте.

Большинство операций имеют формат

<мнемоника>,<канал> <аргумент>, <аргумент>, … , <результат>

В качестве аргументов и результатов операций чаще всего выступают регистры. Приведённый пример содержит распространённые виды регистров:

%r0, %r1, %r<N>

рабочие регистры, доступные только в текущей процедуре.

%g0, %g1, %g<N>

глобальные регистры, доступные всей программе. В примере выступают в качестве временных регистров.

%ctpr1, %ctpr2, %ctpr3

особые регистры, используемые для передачи управления. С их помощью устанавливаются адреса переходов.

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

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

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

В приведенном примере встречаются операции:

setwd

задает размер и конфигурацию регистрового окна процедуры; присутствует в первом такте подавляющего большинства процедур.

add

арифметическое сложение.

return

подготовка возврата из процедуры.

sub

арифметическое вычитание.

sdivs

целочисленное деление.

fmuld

вещественное умножение.

istofd

преобразование формата из int в double.

fdtoistr

преобразование формата из double в int c обрубанием точности (truncate).

muls

целочисленное умножение.

ct

передача управления. В данном примере работает совместно с return.

sxt

расширение знаком или нулем 32-битного значения до 64-битного.

Арифметические операции имеют разные мнемоники для типов и разрядностей аргументов. Мнемоника модифицируется префиксами и суффиксами. Рассмотрим их на примере команды сложения add:

adds

32-разрядные целочисленные аргументы.

addd

64-разрядные целочисленные аргументы.

fadds

32-разрядные вещественные аргументы.

faddd

64-разрядные вещественные аргументы.

Более подробное описание ассемблера находится в разделе Команды микропроцессора.

2.2.2. Дизассемблер

Для просмотра дизассемблера объектного или исполняемого файла можно использовать команду ldis.

ldis ./t.o

Возможно включить привязку строк ассемблера к строкам исходного кода. Эту функцию выполняет опция -gline.

gcc -O3 -gline t.c -c
ldis ./t.o

Листинг 2. Использование ldis с -gline, соответствует ассемблеру в Листинге 1

! function 'f', entry = 9, value = 0x000000, size = 0x070, sect = ELF_TEXT num = 1

  0000<000000000000> f:
                      ipd 2
                      subs,0 %r1, %r2, %g16                         ! t.c : 6
                      adds,3 %r0, %r1, %g17                         ! t.c : 5
                      return %ctpr3                                 ! t.c : 9
                      setwd wsz = 0x4, nfx = 0x1
  0001<000000000020> :nop 2
                      istofd,3 %g17, %dg18                          ! t.c : 8
  0004<000000000028> :nop 7
                      sdivs,5 %g17, %g16, %g16                      ! t.c : 7
  0012<000000000030> :nop 2
  0015<000000000038> :nop 3
                      istofd,3 %g16, %dg16                          ! t.c : 7
  0019<000000000040> :nop 3
                      fmuld,3 %dg16, %dg18, %dg16                   ! t.c : 8
  0023<000000000048> :nop 3
                      fdtoistr,3 %dg16, %g16                        ! t.c : 8
  0027<000000000050> :nop 5
                      muls,3 %g17, %g16, %g16                       ! t.c : 9
  0033<000000000060> :
                      ct %ctpr3                                     ! t.c : 9
                      ipd 3                                         ! t.c : 9
                      sxt,3 0x2, %g16, %dr0                         ! t.c : 9

Дизассемблер перед каждой командой показывает дополнительно:

  • номер такта от начала процедуры (десятичное число слева);

  • IP-адрес команды (шестнадцатеричное число в угловых скобках):

    • в случае объектного файла - относительно начала модуля;

    • в случае исполняемого файла - абсолютный адрес;

    • в случае динамической библиотеки - относительно точки связывания библиотеки.

2.2.3. Вызов функций

Пример 2. Использование функций

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
     int func_mul(int, int);

     int main(){
         int a=2,b=11,s=0;
         s=func_mul(a,b);
         return s;
     }

     int func_mul(int x, int y){
         return x*y;
     }

Ниже приведён ассемблер примера 2 для оптимизаций -O0 и -O3.

Листинг 3. Ассемблер примера 2 с оптимизацией -O0:

.file   "t.c"
.ignore ld_st_style
.ignore strict_delay
.text
.global main
.type   main, #function
.align  8
main:
{
  setwd wsz = 0x8, nfx = 0x1, dbl = 0x0
  setbn rsz = 0x3, rbs = 0x4, rcur = 0x0
  getsp,0       _f16s,_lts1hi 0xfff0, %r2
}
{
  adds,0,sm     0x0, 0x2, %r3
  adds,1,sm     0x0, 0xb, %r4
  adds,2,sm     0x0, 0x0, %r5
}
{
  sxt,0,sm      0x2, %r3, %b[0]
  sxt,1,sm      0x2, %r4, %b[1]
}
.LCS.1:
{
  nop 4
  disp  %ctpr1, func_mul
}
{
  call  %ctpr1, wbs = 0x4
}
.LCS.2:
{
  adds,0,sm     0x0, %b[0], %r6
  return        %ctpr3
}
{
  adds,0,sm     0x0, %r6, %r5
}
{
  nop 3
  sxt,0,sm      0x2, %r5, %r0
}
{
  ct    %ctpr3
}
.size   main, .- main
.global func_mul
.type   func_mul, #function
.align  8
func_mul:
{
  setwd wsz = 0x4, nfx = 0x1, dbl = 0x0
}
{
  nop 5
  muls,0        %r0, %r1, %r4
  return        %ctpr3
}
{
  sxt,0,sm      0x2, %r4, %r0
  ct    %ctpr3
}

Листинг 4. Ассемблер примера 2 с оптимизацией -O3:

.file   "t.c"
.ignore ld_st_style
.ignore strict_delay
.text
.global main
.type   main, #function
.align  8
main:
{
  nop 5
  setwd wsz = 0x4, nfx = 0x1, dbl = 0x0
  return        %ctpr3
  addd,0        0x16, 0x0, %r0
}
{
  ct    %ctpr3
}
.size   main, .- main
.global func_mul
.type   func_mul, #function
.align  8
func_mul:
{
  nop 5
  setwd wsz = 0x4, nfx = 0x1, dbl = 0x0
  return        %ctpr3
  muls,0        %r0, %r1, %g16
}
{
  ct    %ctpr3
  sxt,0 0x2, %g16, %r0
}

Листинг 5. Использование ldis. Пример 2, оптимизация -O3.

ldis ./a.out
! function 'main', entry = 56, value = 0x0104e8, size = 0x020, sect = ELF_TEXT num = 12

0000<0000000104e8> main: nop 5
                addd,0 0x16, 0x0, %dr0                    ! t.c : 6
                return %ctpr3                             ! t.c : 6
                setwd wsz = 0x4, nfx = 0x1, dbl = 0x0
0006<000000010500> :
                ct %ctpr3                                 ! t.c : 6
                ipd 3                                     ! t.c : 6

! function 'func_mul', entry = 44, value = 0x010508, size = 0x028, sect = ELF_TEXT num = 12

0000<000000010508> func_mul: nop 5
                muls,0 %r0, %r1, %g16                     ! t.c : 10
                return %ctpr3                             ! t.c : 10
                setwd wsz = 0x4, nfx = 0x1, dbl = 0x0

0006<000000010520> :
                ct %ctpr3                                 ! t.c : 10
                ipd 3                                     ! t.c : 10
                sxt,0 0x2, %g16, %dr0                     ! t.c : 10

В листингах ассемблера примера 2 появились новые команды:

setbn

установить базу вращения числовых регистров. Эта команда дополняет setwd.

Механизм вращаемых регистров описан в разделе
Использование вращаемых регистров в процедурном механизме описано в разделе
getsp

выделить или вернуть область в стеке пользователя локального фрейма.

Пользовательский стек и работа с ним описаны в разделе Локальный стек.
disp

подготовка адреса перехода, в данном случае для операции call.

call

выполнить вызов функции.

Вызов функции легко заметить по подготовке перехода disp:

disp  %ctpr1, func_mul

и последующему вызову функции через call:

call  %ctpr1, wbs = 0x4

Обратите внимание, что в результате оптимизаций в режиме -O3 вызов был заменён на:

addd,0 0x16, 0x0, %r0

Конечный возвращаемый результат определился статически как 0x16, или 22 в десятичной системе.

2.2.4. Чтение и запись в память

Пример 3. Чтение и запись в память

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
long int global_var;
extern void g(int *);

void f(short int *p)
{
    int local_var = 14;

    g(&local_var);
    local_var++;
    (*p)++;
    g(&local_var);
    global_var++;

    return;
}

Покажем на примере, как выглядят обращения в память по глобальным переменным, локальным переменным в стеке и по косвенности.

Для этого примера ассемблер с оптимизацией -O3 и -O0 отличаются несущественно, и будет приведен листинг с оптимизацией -O3.

Листинг 6. Чтение и запись в память.

f:
  {
    nop 1
    setwd     wsz = 0x8, nfx = 0x1
    setbn     rsz = 0x3, rbs = 0x4, rcur = 0x0
    disp      %ctpr1, g; ipd 2
    getsp,0   _f32s,_lts1 0xffffffe0, %r2
    adds,1    0xe, 0x0, %r3
  }
  {
    addd,0    %r2, _f64,_lts0 0x20, %r1
  }
  {
    subd,0    %r1, 0x4, %r4
  }
  {
    addd,0,sm 0x0, %r4, %b[0]
    stw,2     %r1, _f16s,_lts0lo 0xfffc, %r3
  }
.LCS.1:
  {
    call      %ctpr1, wbs = 0x4
  }
  {
    nop 2
    disp      %ctpr1, g; ipd 2
    ldh,0     %r0, 0x0, %r3
    addd,1,sm 0x0, %r4, %b[0]
    ldw,2     %r1, _f16s,_lts0lo 0xfffc, %r5
  }
  {
    adds,0    %r3, 0x1, %r3
    adds,1    %r5, 0x1, %r4
  }
  {
    sth,2     %r0, 0x0, %r3
    stw,5     %r1, _f16s,_lts0lo 0xfffc, %r4
  }
  {
    call      %ctpr1, wbs = 0x4
  }
  {
    nop 2
    return    %ctpr3; ipd 2
    ldd,0     0x0, [ _f64,_lts0 global_var ], %r0
  }
  {
    addd,0    %r0, 0x1, %r0
  }
  {
    nop 1
    std,2     0x0, [ _f64,_lts0 global_var ], %r0
  }
  {
    ct        %ctpr3
  }

В примере значения всех трех ячеек памяти увеличиваются на 1. Для этого они считываются операцией ld, увеличиваются с помощью операции add, и записываются операцией st. Суффикс операций ld и st означает формат данных:

b - 1 байт
h - 2 байта
w - 4 байта
d - 8 байт

Соответствующие операции:

ldb, stb - 1 байт
ldh, sth - 2 байта
ldw, stw - 4 байта
ldd, std - 8 байт

В приведенном примере используются переменные формата short int (2 байта), int (4 байта) и long int (8 байт). Адрес операций ld и st формируется как сумма двух аргументов, как правило, это база адреса и смещение. Третий аргумент операции записи - это записываемое по адресу значение.

Адрес глобальной переменной global_var задается в виде символа:

ldd,0 0x0, [ _f64,_lts0 global_var ], %r0

Кроме имени, в квадратных скобках указаны ключевые слова _f64,_lts0. Они отображают формат и позицию константного значения (напомним, что адрес глобальной переменной становится константой после линковки).

Локальная переменная local_var хранится в стеке. Из-за того, что на нее взят адрес и передан в функцию g(), ее нельзя хранить на регистре. Адрес в стеке задается как сумма регистра, хранящего указатель на стек, и смещения.

ldw,2 %r1, _f16s,_lts0lo 0xfffc, %r5

В начале процедуры видно. как формируется регистр указателя на локальное окно стека:

getsp,0       _f32s,_lts1 0xffffffe0, %r2
...
addd,0        %r2, _f64,_lts0 0x20, %r1

Здесь операция getsp заказывает новую порцию стека размером 0x20 и размещает указатель на новую вершину стека в %r2, а в %r1 заносится «дно» локального окна стека. Подробно механизм описан в разделе Локальный стек.

Работа с указателем, переданным в качестве параметра, ведется по регистру %r0, содержащему параметр p.

ldh,0 %r0, 0x0, %r3

2.2.5. Циклы

Пример 4. Циклы

1
2
3
4
5
6
void f(int *v0, int N)
{
  int i;
  for (i=0; i<N; i++)
    v0[i] += (v0[i] + 3) * v0[i];
}

Листинг 7. Пример 3, оптимизация -O0:

f:
  {
    setwd     wsz = 0x5, nfx = 0x1
  }
  {
    adds,0,sm 0x0, 0x0, %r4
  }
.L4:
  {
    nop 4
    cmplsb,0  %r4, %r1, %pred0
    disp      %ctpr1, .L6; ipd 2
  }
  {
    ct        %ctpr1 ? ~%pred0
  }
.L9:
  {
    sxt,0,sm  0x2, %r4, %r5
    sxt,1,sm  0x2, %r4, %r6
    sxt,2,sm  0x2, %r4, %r7
    adds,3    %r4, 0x1, %r4
    disp      %ctpr1, .L4; ipd 2
  }
  {
    shld,0    %r5, 0x2, %r5
    shld,1    %r6, 0x2, %r6
    shld,2    %r7, 0x2, %r7
  }
  {
    addd,0    %r0, %r5, %r5
    addd,1    %r0, %r6, %r6
    addd,2    %r0, %r7, %r7
  }
  {
    ldw,0     %r6, 0x0, %r6
    ldw,2     %r7, 0x0, %r8
  }
  {
    nop 2
    ldw,0     %r5, 0x0, %r5
  }
  {
    adds,0    %r5, 0x3, %r5
  }
  {
    nop 5
    muls,0    %r5, %r6, %r5
  }
  {
    adds,0    %r8, %r5, %r5
  }
  {
    stw,2,sm  %r7, 0x0, %r5
    ct        %ctpr1
  }
.L6:
  {
    nop 5
    return    %ctpr3; ipd 2
  }
  {
    ct        %ctpr3
  }

В метке L4 можно увидеть две широкие команды. В первой проверяется попадание в цикл и выполняется подготовка перехода. Во второй команде делается переход по отрицанию условия выхода из цикла в метку L6, находящуюся в голове цикла. Содержимое между метками L9 и L6 является циклом.

Листинг 8. Пример 3, оптимизация -O3 в сочетании с опцией -fmax-iter-for-ovlpeel=0

f:
  {
    setwd     wsz = 0x4, nfx = 0x1
    return    %ctpr3; ipd 2
  }
  {
    nop 2
    cmplsb,0  0x0, %r1, %pred0
  }
  {
    ct        %ctpr3 ? ~%pred0
  }
  {
    setwd     wsz = 0x10, nfx = 0x1
    setbn     rsz = 0xb, rbs = 0x4, rcur = 0x0
    return    %ctpr3; ipd 2
    sxt,0,sm  0x6, %r1, %g16
    addd,1    0x0, _f64,_lts1 0x2dff2d00000000, %g17
    addd,2,sm 0x1, 0x0, %g18
    addd,3    0x0, 0x0, %g19
  }
  {
    nop 1
    disp      %ctpr1, .L75; ipd 2
    cmplsb,0,sm       0x0, %r1, %pred0
    addd,1,sm 0x0, 0x0, %b[15]
    aaurwd,2  %r0, %aad0
    aaurwd,5  %g19, %aasti1
  }
  {
    insfd,0,sm        %g17, _f32s,_lts0 0x8800, %g16, %g16 ? %pred0
    insfd,1,sm        %g17, _lit32_ref,_lts0 0x8800, %g18, %g16 ? ~%pred0
  }
  {
    nop 3
    rwd,0     %g16, %lsr
  }
.L75:
  {
    loop_mode
    alc       alcf=1, alct=1
    abn       abnf=1, abnt=1
    ct        %ctpr1 ? %NOT_LOOP_END
    muls,0,sm %g17, %b[10], %b[1]
    addd,1,sm 0x4, %b[15], %b[13]
    adds,2,sm %b[8], 0x3, %g17
    ldw,3,sm  %r0, %b[17], %b[0] ? %pcnt12
    adds,4,sm %b[22], %b[13], %g16
    staaw,5   %g16, %aad0[ %aasti1 ]
    incr,5    %aaincr0
  }
  {
    setwd     wsz = 0x4, nfx = 0x1
    adds,0    0x0, 0x0, %g16
  }
  {
    ct        %ctpr3
    aaurw,2   %g16, %aabf0
  }

При оптимизации -O3 для циклов компилятор строит более компактный код. Дополнительная опция -fmax-iter-for-ovlpeel=0 применяется при компиляции данного примера, чтобы получить более наглядный и читаемый листинг. Значение этой опции описано в разделе Программная конвейеризация.

В листинге появились новые команды и служебные слова:

insf

команда «вставить битовое поле»; формирует значение из двух регистров, заданных в первом и третьем аргументах, управляется значением, заданным во втором аргументе. В данном примере формирует значение, составленное из старших 32 бит одного регистра и младших 32 бит другого.

rwd

операции записи в специальные регистры. В данном случае происходит запись в регистр управления циклом lsr.

aaurwd

операции записи в регистры описания массивов данных.

loop_mode

метка, которая говорит о том, что в пределах цикла работает аппаратная поддержка счетчика цикла. Не является самостоятельной операцией.

alc

продвинуть счетчик цикла (advance loop counter).

abn

продвинуть вращаемые регистры (advance base numeric).

staa incr

записать значение в массив и продвинуть указатель на фиксированную величину. Возможны только в паре.

Специальный регистр %lsr содержит в себе счётчик цикла, который декрементируется на каждой итерации. При исчерпании (обнулении) счетчика условие %NOT_LOOP_END становится ложным, а переход в голову цикла, находящийся под этим условием, не срабатывает.

Тело цикла компактно упаковано в одну широкую команду, и исполняется с темпом 1 итерация за 1 такт. Примененная техника оптимизации называется конвейеризацией и описана в разделе Программная конвейеризация.

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

2.2.6. Условный код

Пример 5. Операции под условием

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void f(int c, int* p)
{

  if (c>0)
  {
    (*p) = (*p) * (*p);
  }
  else
  {
    (*p) = -(*p);
  }

}

Условный код существенно изменяется, если применять оптимизацию -O2 и выше. Рассмотрим код приведенного примера для -O1 и -O3.

Листинг 9. Пример 5, оптимизация -O1

f:
  {
    nop 1
    setwd     wsz = 0x4, nfx = 0x1
    disp      %ctpr1, .L6; ipd 2
  }
  {
    nop 2
    cmplesb,0 %r0, 0x0, %pred0
  }
  {
    ct        %ctpr1 ? ~%pred0
  }
  {
    nop 2
    ldw,0     %r1, 0x0, %g16
  }
  {
    subs,0    0x0, %g16, %g16
  }
  {
    stw,2     %r1, 0x0, %g16
  }
.L25:
  {
    nop 5
    return    %ctpr3; ipd 2
  }
  {
    ct        %ctpr3
  }
.L6:
  {
    nop 2
    disp      %ctpr1, .L25; ipd 2
    ldw,0     %r1, 0x0, %g16
  }
  {
    nop 3
    muls,0    %g16, %g16, %g16
  }
  {
    ct        %ctpr1
    stw,2     %r1, 0x0, %g16
  }

В приведенном коде есть новые операции:

cmpl

операция сравнения двух аргументов. Результат операции сравнения записывается в предикатный регистр %pred<N>, где N может быть от 0 до 31. Это отличает архитектуру Эльбрус от многих других архитектур, в которых сравнение вырабатывает специальный регистр флагов. Предикатные регистры хранят значения 0 или 1, и могут использоваться для:

  • передачи управления,

  • управления исполнением операции,

  • вычисления других предикатных регистров.

disp

операция подготовки перехода. Как мы уже видели, операция может подготовить адрес для вызова процедуры, но в данном примере с ее помощью подготавливается переход по локальным меткам .L6 и .L25.

ct

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

Листинг 10. Пример 5, оптимизация -O3

f:
  {
    nop 2
    setwd     wsz = 0x4, nfx = 0x1
    return    %ctpr3; ipd 2
    ldw,0,sm  %r1, 0x0, %g17
    ldw,2,sm  %r1, 0x0, %g16
  }
  {
    nop 1
    muls,0,sm %g16, %g16, %g16
    subs,1,sm 0x0, %g17, %g17
  }
  {
    nop 1
    cmplesb,0 %r0, 0x0, %pred0
  }
  {
    ct        %ctpr3
    stw,2     %r1, 0x0, %g16 ? ~%pred0
    stw,5     %r1, 0x0, %g17 ? %pred0
  }

Здесь можно увидеть другой способ использования предикатных регистров - управление исполнением операций.

stw,2 %r1, 0x0, %g16 ? ~%pred0

После операции записи указывается символ ?, за которым следует предикатный регистр. Операция записи будет исполнена, если значение регистра равно 1, и не будет исполнена, если значение равно 0. Если перед предикатным регистром указать символ отрицания ~, то значение управляющего предикатного регистра будет использовано инвертированным образом. Управляющий предикат также иногда называют квалифицирующим (Qualifying Predicate).

Отсюда вытекает логика оптимизированного листинга: в зависимости от условия c>0 выполнится операция записи с необходимым значением, сформированном в регистрах %g16 либо %g17 соответственно.

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

2.2.7. Переходы и вызовы по косвенности

Пример 6. Конструкция switch

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
void f( int v, float *arr)
{

  switch(v)  {
  case 10:
  case 20:
          arr[0] *= 2.0;
          break;
  case 11:
  case 21:
          arr[1] *= 3.0;
          break;

  case 12:
  case 22:
          arr[2] *= 4.0;
          break;

  case 13:
  case 23:
          arr[3] *= 5.0;
          break;

  case 14:
  case 24:
          arr[4] *= 6.0;
          break;

  case 15:
  case 25:
          arr[5] /= 7.0;
          break;

  default:
      arr[6] += 1.0;
  }
  return;
}

Для этого примера код на -O1 и -O3 не будет существенно отличаться. Приведем ассемблер для уровня оптимизации -O3.

Листинг 10. Пример 6, оптимизация -O3

f:
  {
    setwd     wsz = 0x4, nfx = 0x1
    disp      %ctpr3, .L72; ipd 2
    subs,3    %r0, 0xa, %g16
  }
  {
    cmpbesb,3 %g16, 0xf, %pred0
    sxt,4,sm  0x2, %g16, %g16
  }
  {
    shld,3,sm %g16, 0x3, %g16
  }
  {
    ldd,3,sm  %g16, [ _f64,_lts0 .T.1 ], %r0
  }
  {
    ct        %ctpr3 ? ~%pred0
  }
  {
    nop 2
  }
  {
    nop 7
    movtd,0,sm        %r0, %ctpr1; ipd 2
  }
  nop
  {
    ct        %ctpr1
  }
.LSC.1:
  {
    nop 2
    return    %ctpr3; ipd 2
    ldw,0     %r1, 0x0, %g16
  }
  {
    nop 3
    fmuls,0   %g16, _f32s,_lts0 0x40000000, %g16
  }
  {
    ct        %ctpr3
    stw,2     %r1, 0x0, %g16
  }
.LSC.2:
  {
    nop 2
    return    %ctpr3; ipd 2
    ldw,0     %r1, 0x4, %g16
  }
  {
    nop 3
    fmuls,0   %g16, _f32s,_lts0 0x40400000, %g16
  }
  {
    ct        %ctpr3
    stw,2     %r1, 0x4, %g16
  }
.LSC.3:
  {
    nop 2
    return    %ctpr3; ipd 2
    ldw,0     %r1, 0x8, %g16
  }
  {
    nop 3
    fmuls,0   %g16, _f32s,_lts0 0x40800000, %g16
  }
  {
    ct        %ctpr3
    stw,2     %r1, 0x8, %g16
  }
.LSC.4:
  {
    nop 2
    return    %ctpr3; ipd 2
    ldw,0     %r1, 0xc, %g16
  }
  {
    nop 3
    fmuls,0   %g16, _f32s,_lts0 0x40a00000, %g16
  }
  {
    ct        %ctpr3
    stw,2     %r1, 0xc, %g16
  }
.LSC.5:
  {
    nop 2
    return    %ctpr3; ipd 2
    ldw,0     0x10, %r1, %g16
  }
  {
    nop 3
    fmuls,0   %g16, _f32s,_lts0 0x40c00000, %g16
  }
  {
    ct        %ctpr3
    stw,2     0x10, %r1, %g16
  }
.LSC.6:
  {
    nop 2
    return    %ctpr3; ipd 2
    ldw,3     0x14, %r1, %g16
  }
  {
    nop 3
    fstofd,3  %g16, %g16
  }
  {
    nop 7
    fdivd,5   %g16, _f64,_lts0 0x401c000000000000, %g16
  }
  {
    nop 5
  }
  {
    nop 3
    fdtofs,3  %g16, %g16
  }
  {
    ct        %ctpr3
    stw,5     0x14, %r1, %g16
  }
.L72:
.LSC.7:
  {
    nop 2
    return    %ctpr3; ipd 2
    ldw,0     0x18, %r1, %g16
  }
  {
    nop 3
    fstofd,0  %g16, %g16
  }
  {
    nop 3
    faddd,0   %g16, _f64,_lts0 0x3ff0000000000000, %g16
  }
  {
    nop 3
    fdtofs,0  %g16, %g16
  }
  {
    ct        %ctpr3
    stw,2     0x18, %r1, %g16
  }
.T.1:
  .dword      .LSC.1
  .dword      .LSC.2
  .dword      .LSC.3
  .dword      .LSC.4
  .dword      .LSC.5
  .dword      .LSC.6
  .dword      .LSC.7
  .dword      .LSC.7
  .dword      .LSC.7
  .dword      .LSC.7
  .dword      .LSC.1
  .dword      .LSC.2
  .dword      .LSC.3
  .dword      .LSC.4
  .dword      .LSC.5
  .dword      .LSC.6

В ассемблере можно видеть следующее:

  • различные альтернативы конструкции switch начинаются с меток .LSC.<N>

  • метки собраны в секции данных в таблицу .T.1

  • в начале процедуры из таблицы производится чтение по смещению:

ldd,3,sm      %g16, [ _f64,_lts0 .T.1 ], %r0
  • результат чтения преобразуется в регистр подготовленного перехода с помощью команды:

movtd,0,sm    %r0, %ctpr1; ipd 2
  • по регистру %ctpr1 выполняется переход:

ct    %ctpr1

Этот переход осуществит передачу управления на метку, соответствующую требуемой альтернативе конструкции switch.

Команда movtd выполняет передачу управления по косвенности: она подготавливает переход по значению регистра. Данная операция может также использоваться для вызовов функций. Продемонстрируем это на примере с вызовом виртуального метода C++.

Пример 7. Вызов виртуального метода

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class base {
protected:
  int val;

public:
  virtual int getval();
};

int f(base *p)
{
  return p->getval();
}

Листинг 11. Пример 7, оптимизация -O3

_Z1fP4base:
  .cfi_startproc
  {
    nop 2
    setwd     wsz = 0x8, nfx = 0x1
    setbn     rsz = 0x3, rbs = 0x4, rcur = 0x0
    getsp,0   _f32s,_lts1 0xfffffff0, %r2
    ldd,3     %r0, 0x0, %r3
  }
  {
    nop 1
    ldd,3     %r3, 0x0, %r3
  }
  {
    nop 1
    addd,0,sm 0x0, %r0, %b[0]
  }
  {
    nop 7
    movtd,0,sm        %r3, %ctpr1; ipd 2
  }
  nop
.LCS.1:
  {
    call      %ctpr1, wbs = 0x4
  }
  {
    nop 5
    return    %ctpr3; ipd 2
    sxt,3     0x2, %b[0], %r0
  }
  {
    ct        %ctpr3
  }

В функции f (манглированное имя _Z1fP4base) в нулевом такте производится чтение с нулевым смещением по указателю p:

ldd,3 %r0, 0x0, %r3

По этому адресу лежит указатель на таблицу виртуальных методов класса base. Далее из полученного указателя на таблицу производится еще одно чтение:

ldd,3 %r3, 0x0, %r3

Полученный адрес является адресом входа в виртуальный метод getval(). Этот адрес записывается в регистр подготовки перехода:

movtd,0,sm    %r3, %ctpr1; ipd 2

И наконец, по полученному регистру %ctpr1 производится вызов:

call  %ctpr1, wbs = 0x4

Необходимо заметить, что процесс подготовки перехода по числовому регистру является весьма длительной операцией: она требует 9 тактов. По этой причине конструкции switch и вызовы по косвенности являются для архитектуры Эльбрус не эффективными с точки зрения производительности. Этот вопрос дополнительно рассматривается в разделе Рекомендации по оптимизации программ под архитектуру Эльбрус.

2.3. Работа в gdb

Рассмотрим особенности отладки для платформы Эльбрус в gdb на примере.

2.3.1. SIGILL как сигнал об ошибках

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
     #include <stdlib.h>

     int *p;
     int *q;
     int g;

     int main(int argc, char* argv[])
     {
       p=&g;
       q=&g;

       q=q+0x100000;

       if (argc>1)
         if (atoi(argv[1])==1)
           *p = *q;
       return 0;
     }

В данном коде происходит попытка чтения по некорректному адресу, хранящемуся в q. Пример скомпилируем с опциями -O0 -g и -O2 -g.

gcc ./t.c -o t_O0 -O0 -g
gcc ./t.c -o t_O2 -O2 -g

При выполнении без параметров падения нет. При запуске с параметром «1» и оптимизации -O0 увидим «Segmentation fault» после некорректного чтения из *q. При запуске с оптимизацией -O2 будет выведена ошибка «Illegal instruction». Она вызвана тем же некорректным чтением, однако приложение получило сигнал SIGILL вместо SIGSEGV.

Это произошло потому, что при оптимизациях чтение из *q было в спекулятивном режиме. Вместо падения исполнение программы продолжилось. При попытке записи некорректного (диагностического) значения в *p происходит слом, который по системе команд вызывает сигнал SIGILL. Данный сигнал говорит о попытке работы с диагностическим значением в неспекулятивной операции, в данном случае в записи в память.

SIGILL необходимо трактовать как сигнал о программной ошибке, наряду с SIGSEGV или SIGBUS.

2.3.2. Отладка оптимизированного кода

Отображение исходного кода при исполнении программы в gdb возможно только при её сборке с опциями -O0 -g. В этом режиме работа отладчика ничем не отличается от поведения на других архитектурах.

В режиме с оптимизациями (-O1 или выше) отображение исходного кода при исполнении не поддержано. Чтобы пошагово выполнять программу, нужно пользоваться командами nexti, stepi вместо их аналогов next, step.

Чтобы видеть пошагово исполняемые широкие команды Эльбруса, можно выполнить:

(gdb) display /i $pc

После этого в каждой точке останова будет печататься следующая широкая команда.

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

Для просмотра данных выполняются команды info registers и печать содержимого памяти x /x 0x<adress>.

(gdb) info registers r0 r1

Выведет содержимое регистров r0, r1.

(gdb) x /4xw 0x1009a

Вывести в 16-ричном формате 4 одинарных слова (32-битных) из локальной памяти по адресу `0x1009a`

2.3.3. Пример сессии gdb

Запустим в gdb программу, приведённую выше в первой секции раздела об отладке.

gcc ./t.c -o t_O2 -O2 -g
gdb ./t_O2

Запуск программы с параметром, чтобы получить ошибку.

(gdb) set args 1
(gdb) r
Starting program: /home/test/t_O2 1

Program received signal SIGILL, Illegal instruction

exc_diag_operand at 0x10738 ALS2

0x0000000000010738 in main ()

В диагностическом выводе указан адрес падения и слог в широкой команде, на котором произошёл слом.

Просмотрим дизассемблер всей функции:

(gdb) disassemble
Dump of assembler code for function main:
   0x0000000000010628 <+0>:
 :
  ipd 2
  getsp,0 _f32s,_lts1 0xffffffe0, %dr3
  addd,1 0x0, _f64,_lts2 0x11c28, %dr4
  disp %ctpr2, M_10428
  setwd wsz = 0x8, nfx = 0x1
  setbn rsz = 0x3, rbs = 0x4, rcur = 0x0

   0x0000000000010658 <+48>:
 :
  ipd 2
  cmplesb,0 %r0, 0x1, %pred0
  addd,1 0x0, _f64,_lts0 0x411c28, %dr5
  std,2 %dr4, [ _f64,_lts2 0x11c20 ]
  return %ctpr3

   0x0000000000010680 <+88>:
 :
  std,2 %dr5, [ _f64,_lts0 0x11c30 ]
  ldd,5,sm [ %dr1 + 0x8 ], %dr1

   0x0000000000010698 <+112>:
 :
  addd,0 0x0, 0x0, %dr0 ? %pred0
  addd,1,sm 0xa, 0x0, %db[2] ? ~ %pred0
  addd,2,sm 0x0, 0x0, %db[1] ? ~ %pred0
  addd,3 0x0, 0x0, %dr0 ? ~ %pred0
  rlp,cd00 %pred0, ~>alc2, ~>alc1, >alc0
  rlp,cd01 %pred0, ~>alc3

   0x00000000000106b0 <+136>:
 :
  ct %ctpr3 ? %pred0
  ipd 3

   0x00000000000106b8 <+144>:
 :
  ldd,0,sm [ _f64,_lts0 0x11c30 ], mas = 0x4, %dr1
  addd,4,sm 0x0, %dr1, %db[0] ? ~ %pred0

  rlp,cd00 %pred0, ~>alc4

   0x00000000000106d8 <+176>:
 :
  ipd 3
  call %ctpr2, wbs = 0x4  ? ~ %pred0

   0x00000000000106e8 <+192>:
 :
        ---Type <return> to continue, or q <return> to quit---
  ipd 2
  ldd,0,sm [ _f64,_lts0 0x11c20 ], %dg17
  addd,1,sm 0x0, %db[0], %dg16 ? ~ %pred0
  return %ctpr3

  rlp,cd00 %pred0, ~>alc1

   0x0000000000010708 <+224>:
 :
  cmpesb,0,sm %g16, 0x1, %pred1

   0x0000000000010710 <+232>:
 :nop 1
  pass %pred0, @p0
  pass %pred1, @p1
  landp ~@p0, @p1, @p4
  pass @p4, %pred1

   0x0000000000010718 <+240>:
 :
  ldd,2 [ _f64,_lts0 0x11c30 ], mas = 0x3, %dr1 ? %pred1

  rlp,cd00 %pred1, >alc2

   0x0000000000010730 <+264>:
 :nop 3
  ldw,0,sm [ %dr1 + 0x0 ], %g16

 => 0x0000000000010738 <+272>:
 :
  ct %ctpr3 ? ~ %pred0
  ipd 3
  stw,2 %g16, [ %dg17 + 0x0 ] ? %pred1
  rlp,cd00 %pred1, >alc2

 End of assembler dump.

В листинге команда с адресом 0x0000000000010738 <+272> отмечена стрелочкой =>. Это говорит о том, что данная команда вызвала ошибку.

Чтобы подробнее разобрать, в чём она заключается, распечатаем значения регистров:

(gdb) info all-registers g16 g17
g16                           <11> 0x4afafafa4afafafa       5402906655891061498
g17                           <00> 0x11c28  72744

g16 содержит странное значение 0x4afafafa4afafafa. Слева от значения регистра расположены теги диагностики, выставленные в две единицы.

Таким образом инициализируется регистр при ошибочной операции, которая произошла в спекулятивном режиме.

g17 при этом содержит корректное значение.

2.4. Прочее

2.4.1. Отладка ядра

Чтобы сбросить стек ядра, можно нажать комбинацию клавиш:

crtl-prtscr-?
crtl-prtscr-'t'
crtl-prtscr-'l'
crtl-prtscr-'r'

Расширенные возможности для отладки ОС выведены в /proc/sys/debug/:

echo 1 > /proc/sys/debug/sigdebug
echo 1 > /proc/sys/debug/datastack
echo 1 > /proc/sys/debug/userstack
echo 1 > /proc/sys/debug/coredump
echo 1 > /proc/sys/debug/pagefault

Чтобы сохранять дампы памяти в пользовательской директории:

mkdir -p /export/mycore
sysctl -w kernel.core_pattern=/export/mycore/core-%e-%s-%u-%g-%p-%t

2.4.2. Модификация запуска задач

В случае, если требуется привязать выполнение задачи к какому-то конкретному ядру или набору ядер, следует использовать команду taskset. Пример:

taskset -c 1,2,3,4 <команда с аргументами>