VisualDSP: интерфейс C/C++ и ассемблера Blackfin Печать
Добавил(а) microsin   

Система программирования VisualDSP++ предусматривает возможность вызывать подпрограммы на ассемблере из программ на C/C++, и наоборот, как из программ на ассемблере вызывать функции C/C++ [1]. Перед тем, как попытаться выполнить любую из этих операций, следует хорошо разобраться с моделью выполнения кода C/C++ (C/C++ run-time model), включая информацию о том, как организован стек, какие используются типы данных, и как обрабатываются аргументы (см. [4]). В конце будут рассмотрены примеры кода, показывающие смешивание в проекте кода C/C++ и ассемблера.

[Вызов подпрограмма ассемблера из программ C/C++]

Перед вызовом подпрограммы на языке ассемблера из программы на языке C/C++ создайте прототип, чтобы определить аргументы для подпрограммы на ассемблере, и интерфейс из программы C/C++ для подпрограммы на ассемблере. Хотя даже допустимо использовать функцию в C/C++ без её прототипа, создание хорошо описанных прототипов настоятельно рекомендуется практикой разработки ПО. Когда прототип опущен, компилятор не может выполнить проверку типов аргументов, а также подразумевает, что возвращаемое значение имеет тип int, и использует правила формирования кода (promotion rules) Кернигана & Ричи (K&R) вместо правил ANSI [6].

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

Модель run-time определяет некоторые регистры в качестве временных (scratch registers), и другие регистры резервируются, или они выделяются для специальных целей (dedicated registers). Scratch-регистры можно использовать в программе на языке ассемблера, не беспокоясь об их предыдущем содержании, т. е. их не надо сохранять на входе в подпрограмму и восстанавливать на выходе из подпрограммы. Если нужно больше места (или используется существующий код), и Вы хотите использовать зарезервированные регистры, то необходимо сохранять и восстанавливать их содержимое соответственно на входе и выходе подпрограммы.

Используйте выделенные регистры или регистры стека только для их непосредственного предназначения; компилятор, библиотеки, отладчик и обработчики прерываний (ISR) зависят от наличия доступного стека, как определено этими регистрами.

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

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

Компилятор всегда выравнивает массивы в памяти по 32-разрядной границе слова, и компилятор нормально использует это знание при оптимизации доступа к содержимому памяти. Поэтому необходимо гарантировать, что массивы, определенные в коде ассемблера, к которым осуществляется доступ из кода C/C++, также выровнены подобным образом. Это обычно достигается вставкой перед определением массива в ассемблере директивы .align 4.

Если аргументы находятся в стеке, то они адресуются через смещение относительно указателя стека (SP) или указателя фрейма (FP) [2]. Хороший способ разобраться, как передавать аргументы через стек между программой C/C++ кодом на ассемблере - написать пустую заглушку подпрограммы на C/C++ и скомпилировать её в язык ассемблера (ниже приведен пример, как это делается). Скомпилировать код C/C++ в код ассемблера можно в среде VisualDSP IDDE, если поставить галочку Save temporary files в свойствах проекта (или использовать дополнительный ключ -save-temps командной строки компилятора [3]).

Ниже показан пример, включающий присваивание глобальной volatile-переменной [5], чтобы показать, где могут быть найдены аргументы на входе в функцию asmfunc на ассемблере.

// Код примера, показывающий интерфейс компилятора с ассемблером.
// В нем глобальным переменным global_a, global_b, global_pglobal
// просто присваиваются значения из аргументов функции. Это сделано
// для демонстрации, как на ассемблере можно получить доступ
// к аргументам функции, находящимся в стеке (тип каждой глобальной
// переменной соответствует типу одного из аргументов):
 
int global_a;
float global_b;
int * global_p;
 
// Пустая демонстрационная  функция, код которой компилируется
// в код ассемблера:
int asmfunc(int a, float b, int * p)
{
   // Присваивание значений из аргументов, которые будут потом
   // найдены в коде ассемблера:
   global_a = a;
   global_b = b;
   global_p = p;
 
   // Значение, которое будет загружено в регистр возврата:
   return 12345;
}

Когда этот пример кода C/C++ скомпилирован с опциями командной строки -save-temps и -no-annotate -O, получится следующий код на ассемблере:

   .section program;
   .align 2;
_asmfunc:
      P0.L = .epcbss;
      P0.H = .epcbss;
      [P0+ 0] = R0;
      R0 = 0x1234 (X);
      [P0+ 4] = R1;
      [P0+ 8] = R2;
      RTS;
 
._asmfunc.end:
   .global _asmfunc;
   .type _asmfunc,STT_FUNC;
   
   .section data1;
 
   .align 4;
.epcbss:
      .byte _global_a[4];
      .global _global_a;
      .type _global_a,STT_OBJECT;
      .byte _global_b[4];
      .global _global_b;
      .type _global_b,STT_OBJECT;
      .byte _global_p[4];
      .global _global_p;
      .type _global_p,STT_OBJECT;
.epcbss.end:

[Вызов функций C/C++ из программ на ассемблере]

В некоторых случаях может потребоваться вызвать из кода на ассемблере функции библиотеки, написанной на C/C++, и другие функции C/C++. Как обсуждалось в предыдущей секции "Вызов подпрограмма ассемблера из программ C/C++", Вы можете захотеть создать тест-функцию, чтобы сделать такой же вызов на C/C++, после чего использовать код ассемблера, сгенерированный компилятором, как справочное руководство при создании аналогичного вызова в программе на ассемблере. Использование глобальных переменных с атрибутом volatile поможет разобраться, как в коде на ассемблере подготавливаются аргументы для вызова функции, написанной на C/C++.

Модель run-time определяет некоторые регистры в качестве временных (scratch registers), и другие регистры резервируются, или они выделяются для специальных целей (dedicated registers). Scratch-регистры можно использовать в программе на языке ассемблера, не беспокоясь об их предыдущем содержании, т. е. их не надо сохранять на входе в подпрограмму и восстанавливать на выходе из подпрограммы. Если нужно больше места (или используется существующий код), и Вы хотите использовать зарезервированные регистры, то необходимо сохранять и восстанавливать их содержимое соответственно на входе и выходе подпрограммы.

Используйте выделенные регистры или регистры стека только для их непосредственного предназначения; компилятор, библиотеки, отладчик и обработчики прерываний (ISR) зависят от наличия доступного стека, как определено этими регистрами.

Могут использоваться и зарезервированные регистры; их содержимое не меняется вызовом функции C/C++. Функция всегда сохраняет эти регистры на входе и восстанавливает на выходе.

Если аргументы находятся в стеке, то они адресуются через смещение относительно указателя стека (SP) или указателя фрейма (FP) [2]. Хороший способ разобраться, как передавать аргументы через стек между программой C/C++ кодом на ассемблере - написать пустую заглушку подпрограммы на C/C++ и скомпилировать её в язык ассемблера (см. выше пример, как это делается). Скомпилировать код C/C++ в код ассемблера можно в среде VisualDSP IDDE, если поставить галочку Save temporary files в свойствах проекта (или использовать дополнительный ключ -save-temps командной строки компилятора [3]). Путем анализа содержимого volatile-переменных в файле *.s Вы можете определить, как функция C/C++ передает переменные, после чего задублировать процесс подготовки переменных у себя в рабочей программе на языке ассемблера.

Перед вызовом функции C/C++ должен быть корректно установлен стек. Если Вы вызываете другие функции, то следование базовой модели использования стека также упрощает использование отладчика. Самый простой способ разобраться, как правильно инициализировать стек - написать простейшую программу на C/C++ с функцией main, где инициализируется среда выполнения кода (run-time system); поддерживайте состояние стека до момента, когда нужно будет вызвать функцию C/C++ из программы на языке ассемблера. Во время выполнения кода гарантируйте, что содержимое выделенных регистров корректное. Вам не нужно устанавливать FP перед вызовом функции; состояние FP вызывающего кода никогда не используется кодом вызываемой подпрограммы.

Интерфейс ассемблера требует, чтобы все вызывающие функции резервировали в стеке пространство из первых 12 байт (регистры R0-R2) для пространства параметров вызываемого кода, даже когда вызываемый код не нуждается в этом пространстве. В VisualDSP++ 5.0 компилятор увеличивает это выделяемое пространство в стеке для сохранения временных переменных если не находит, что это пространство необходимо для других целей (таких как сохранение регистров, используемых для передачи параметров). Таким образом, все функции ассемблера, которые вызывают скомпилированные функции C/C++, должны следовать корректной процедуре передачи параметров и использования стека. В VisualDSP++ 5.0 компилятор более эффективно использует стек, однако есть соответствующий риск что функции, которые нарушают правила ABI (Application Binary Interface), могут обнаружить поврежденные необходимые значения переменных.

[Соглашение об именовании при смешивании кода C/C++ и ассемблера]

Вы можете использовать символы C/C++ (имена функций или переменный) в подпрограммах на ассемблере, и наоборот, использовать символы ассемблера в коде C/C++. Эта секция описывает, как назначать имена и использовать символы C/C++ и ассемблера.

Чтобы дать имя символу ассемблера, который соответствует символу языка C, добавьте к имени префикс одиночного нижнего подчеркивания в коде на языке ассемблера; соответствующий символ C будет иметь то же самое имя, но без этого префикса. Например, символ main в коде C станет символом ассемблера _main. Глобальные символы языка C++ обычно используют дополнительное декорирование имен (mangling), чтобы закодировать дополнительную информацию типов (например, перегружаемые функции, поля классов). Декларируйте глобальные символы C++ с использованием директивы extern "C", чтобы запретить дополнительное декорирование имен. Это обеспечит беспроблемное использование кода функций C++ в коде C и ассемблера, а также функций C в коде C++.

Чтобы использовать функцию или переменную C/C++ в коде на ассемблере, декларируйте их как глобальную функцию или переменную в программе C. Импортируйте символ в код ассемблера путем декларирования символа директивой .EXTERN ассемблера.

Для использования функции или переменной ассемблера в программе C/C++, декларируйте этот символ директивой .GLOBAL ассемблера, и импортируйте этот символ декларацией его как extern в программе на C.

Таблица 1-46 показывает несколько примеров кода C/C++ и ассемблера, и соглашения именования их интерфейса.

Таблица 1-46. C/C++ Naming Conventions для символов в коде.

В программе C/C++ В коде ассемблера
int c_var; /* глобально декларированная переменная */
.extern _c_var;
.type _c_var,STT_OBJECT;
void c_func(void);
.global _c_func;
.type _c_func,STT_FUNC;
extern int asm_var;
.global _asm_var;
.type _asm_var,STT_OBJECT;
.byte = 0x00,0x00,0x00,0x00
extern void asm_func(void);
.global _asm_func;
.type _asm_func,STT_FUNC;
_asm_func:

[Таблицы исключений в коде ассемблера]

Подпрограммы ассемблера, которые вызывают функции C++, и которые сами могут быть вызваны функциями C++, и требующие выбрасывания исключений, которые могут быть на стороне вызывающего кода, должны быть предоставлены вместе с таблицей исключений "function exceptions table", чтобы библиотека run-time могла восстановить регистры значениями, которые были при входе в подпрограмму.

Подпрограмма на ассемблере должна выделить окно в стеке (stack frame), используя регистры FP и SP, как это описано в разделе "Managing the Stack" документации [1]. На входе в подпрограмму на ассемблере модифицируемые внутри подпрограмме регистры должны быть сохранены в непрерывной области окна стека, так называемой области сохранения (save area). Регистры сохраняются в save area с возрастанием адреса в порядке, показанном в таблице 1-48 (см. далее).

Слово в секции .gdt должно быть инициализировано адресом таблицы исключений функции (Function Exceptions Table). Это слово должно быть помечено директивой .RETAIN_NAME, чтобы оно не было удалено фичей удаления не используемых объектов (linker data elimination). Сама таблица исключений функции должна быть инициализирована, как иллюстрируется в таблице 1-47.

Таблица 1-47. Function Exceptions Table.

Смещение Размер в байтах Что означает
0 4 Начальный адрес подпрограммы.
4 4 Первый адрес после завершения подпрограммы.
8 4 Смещение со знаком относительно FP для области сохранения регистров.
12 8 Набор бит, показывающий, какие регистры сохранены.
20 4 Всегда 0. Показывает, что это не код C++.

Битовое поле таблицы исключений функции содержит бит для каждого регистра. Биты, соответствующие регистрам, сохраненными в save area, должны быть установлены в 1, и другие биты должны быть сброшены в 0. Нумерация бит, соответствующая каждому регистру, дана в таблице 1-48, где бит 0 это самый младший бит самого младшего по адресу слова, бит 31 это самый значащий бит этого слова, бит 32 это младший бит второго по адресу слова, и так далее.

Нумерация бит может быть лучше пояснена кодом C для проверки номера бита.

int wrd = r/32;
int bit = lu << (r%32);
if (bitset[wrd] & bit)
   /* регистр r был сохранен */

Таблица 1-48. Номера регистров для Function Exception Table. Каждый регистр, будучи сохраненным, занимает в стеке 4 байта.

Регистр № бита Регистр № бита
LB1 0 A1X 22
LB0 1 A1W 23
LT1 2 A0X 24
LT0 3 A0W 25
LC1 4 P5 26
LC0 5 P4 27
M3 6 P3 28
M2 7 P2 29
M1 8 P1 30
M0 9 P0 31
B3 10 R7 32
B2 11 R6 33
B1 12 R5 34
B0 13 R4 35
I3 14 R3 36
I2 15 R2 37
I1 16 R1 38
I0 17 R0 39
L3 18 ASTAT 40
L2 19    
L1 20    
L0 21    

Пример ниже показывает подпрограмму на ассемблере с таблицей исключений функции:

      .section program;
_asmfunc:
.LN._asmfunc:
      LINK 0;                 /* Настройка FP */
      [--SP] = (R7:5, P5:4);  /* Сохранение R5,R6,R7,P4,P5 по адресу FP-20 */
         /* R5,R6,R7,P4,P5 используются вызовом функции C++ */
      (R7:5, P5:4) = [SP++];  /* Восстановление регистров */
      UNLINK;
      RTS;
.LN._asmfunc.end:
._asmfunc.end:
 
.global _asmfunc;
.type _asmfunc, STT_FUNC;
 
.section .edt; /* Традиционно таблицы function exceptions
                  входят в .edt */
.align 4;
.byte4 .function_exceptions_table[6] =
   .LN._asmfunc,     /* Первый адрес _asmfunc */
   .LN._asmfunc.end, /* Первый адрес после _asmfunc */
   -20,              /* Смещение save area относительно FP */
    0x0c000000, 0x00000007, /* Набор установленных бит, 26=P5,
                               27=P4,32=R7,33=R6,34=R5 */
   0;                /* Всегда 0 для не C++ кода */
.section .gdt;
.align 4;
.fet_index:
.byte4 = .function_exceptions_table;
                     /* Адрес таблицы в .gdt */
.retain_name .fet_index;

[Ссылки]

1. VisualDSP++ 5.0 C/C++ Compiler and Library Manual for Blackfin® Processors site:analog.com.
2. Blackfin: система команд (ассемблер) - часть 1.
3. Опции командной строки компилятора Blackfin.
4. VisualDSP: Blackfin Run-Time модель и рабочее окружение кода.
5. Как использовать ключевое слово volatile на языке C.
6. Отличия кода ANSI C и кода K&R C.