Программирование DSP VisualDSP: Blackfin Run-Time модель и рабочее окружение кода Sat, May 25 2019  

Поделиться

нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.


VisualDSP: Blackfin Run-Time модель и рабочее окружение кода Печать
Добавил(а) microsin   

В этой документации (перевод секции "C/C++ Run-Time Model and Environment" даташита [1]) описывается модель исполняемого кода C/C++ процессора Blackfin и его рабочее окружение (C/C++ run-time model, C/C++ run-time environment). Эта модель, которая применяется в сгенерированном компилятором коде, включает описание структуры стека, доступа к данным и последовательности вызова процедур/функций и входа в них. C/C++ run-time environment включает соглашения, которым должны следовать подпрограммы, функции, ISR и основной код, работающий на процессоре Blackfin. Подпрограммы/функции ассемблера, слинкованные с подпрограммами C/C++, должны следовать этим соглашениям.

Компания Analog Devices рекомендует программистам, пишущим код на ассемблере, соблюдать принятые соглашения по использованию стека. Это облегчит интеграцию кода ассемблера с кодом, написанным на C/C++.

На рис. 1-2 показан обзор вопросов run-time environment, которые должны учитываться при написании подпрограмм на ассемблере, которые должны ликоваться с подпрограммами/функциями C/C++, включая правила, описанные далее в разделе "C/C++ Run-Time Header и Startup Code".

CRT Assembly Interfacing Overview fig1 2

Рис. 1-2. Обзор интерфейса языка ассемблера.

Вопросы, решаемые run-time environment, включают следующее:

• Соглашения по использованию памяти:
   Использование секций памяти [11].
   Использование нескольких куч [12].
   Использование форматов хранения данных [13].
• Соглашения по использованию регистров:
   Выделенные регистры.
   Регистры, сохраняемые при вызовах.
   Временные регистры (Scratch-регистры).
   Регистры стека.
• Соглашения по управлению выполнением программы:
   Управление стеком.
   Передача аргументов в функцию и возврат значения.

[C/C++ Run-Time Header и Startup Code]

C/C++ run-time (CRT) это код, который выполняется после того, как процессор приступает к выполнению кода в момент сброса / включения питания (код, куда переходит безусловный переход при запуске, start address). CRT устанавливает машину в исходное состояние и вызывает функцию _main. Код CRT может быть подключен к проекту одним из следующих способов:

• Может использоваться Мастер создания проекта VisualDSP (окна диалогов Project Wizard), что автоматически сгенерирует настроенный CRT в проекте. Подробнее см. VisualDSP++ Help.
• Во время линковки может быть определен макрос USER_CRT, указывающий на определяемый пользователем объект CRT, подключаемый в сборку проекта.
• Объекты CRT по умолчанию, предоставляемые в run-time библиотеках для всех платформ (т. е. используемых процессоров), и они линкуются со всеми проектами C/C++, если не был определен во время линковки макрос USER_CRT.

Обзор CRT Header. CRT гарантирует, что когда выполнение входит в _main, состояние процессора подчиняется условиям двоичного интерфейса приложений языка C (C application binary interface, сокращенно ABI), и все глобальные данные, декларированные приложением, были инициализированы в соответствии с требованиями стандартов C/C++. CRT организует среду выполнения кода так, что _main выглядит просто как еще одна функция, вызываемая обычной процедурой вызова функции.

Не все приложения требуют для себя одинаковой конфигурации. Например, конструкторы C++ запускаются только для приложений, которые содержат код C++. Список опциональных элементов конфигурации может быть достаточно длинен, что иногда приводит к чрезмерно дорогому по времени запуску программы. По этой причине Project Wizard позволяет генерировать CRT, который включает минимальное количество необходимого кода, добавленного опциями выбора пользователя. Дополнительно предоставляется заранее собранные модули CRT в нескольких различных конфигурациях, которые можно указать во время линковки через макросы файла LDF [3].

CRT header используется для проектов, которые используют C, C++ и VDK [4]. Проекты на языке ассемблера не предоставляют run-time header по умолчанию; Вы сами должны предоставить свой.

Исходный код на языке ассемблера для заранее скомпилированных CRT находится в каталоге инсталляции системы программирования VisualDSP++, в файле basiccrt.s, находящийся в папке Blackfin/lib/src/libc/crt. Каждый из предварительно собранных объектов CRT был скомпилирован из этого исходного кода по умолчанию (default CRT source). Различные конфигурации предоставляются определением различных макросов.

Список операций, выполняемых кодом CRT (startup code) может включать следующее (не обязательно именно в таком порядке):

• Установка регистров и заранее известные / требуемые значения.
• Запрещает аппаратные циклы.
• Запрещает кольцевые буферы.
• Настраивает обработчики событий по умолчанию (default event handlers) и разрешает прерывания.
• Инициализирует указатель стека (регистр SP) и указатель фрейма (регистр FP).
• Разрешает счетчик циклов процессора.
• Конфигурирует порты памяти, используемые двумя генераторами адресов данных (Data Address Generator, DAG).
• Копирует данные из памяти FLASH в RAM.
• Инициализирует драйверы устройств [5].
• Настройка защиты памяти и кешей.
• Изменение приоритетов прерываний процессора.
• Инициализация поддержки профайлинга.
• Запуск конструкторов C++.
• Запуск _main с предоставленными параметрами.
• Запуск _exit при завершении программы (при выходе из _main).

Что не делает CRT. CRT не инициализирует реальную аппаратуру памяти. Инициализация внешней памяти SDRAM оставляется на код загрузчика (boot loader), потому что может быть (и даже желательно), что сам CRT нуждается в переносе во внешнюю память перед своим выполнением.

В этой врезке описаны основные операции, которые может выполнять CRT, в зависимости от выбранных опций в диалогах Project Wizard, или от того, какой подключен при сборке готовый CRT.

[Декларации]

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

• IVBl и IVBh, которые дают адрес таблицы обработки прерываний (event vector table).
• UNASSIGNED_VAL это битовая маска, показывающая, какой регистр / ячейка памяти пока еще не записана приложением. См. далее "Установка значения регистров" и "Terminate Stack Frame Chain".
• INTERRUPT_BITS это маска прерываний по умолчанию. По умолчанию она разрешает самое низкоприоритетное прерывание IVG15. Эта маска по умолчанию может быть изменена runtime Вашей собственной версией __install_default_handlers; подробнее см. далее "Event Vector Table".
• Для некоторых платформ устанавливается SYSCFG_VALUE в качестве значения инициализации для регистра конфигурации системы (system configuration register, SYSCFG).

[Запуск кода и установка регистров]

CRT декларирует первую метку выполняемого кода как start. Это необходимая метка, на которую ссылаются файлы .ldf [3], и которая исключительно преобразуется в адрес сброса процессора.

Сначала CRT запрещает запрещает все функции, которые могли быть случайно разрешены при запуске из-за случайного поведения аппаратуры процессора. Это делается следующим образом:

• SYSCFG устанавливается в значение SYSCFG_VALUE, в соответствии с исправлением anomaly 05-00-0109 для процессоров ADSP-BF531, ADSP-BF532, ADSP-BF533 и ADSP-BF561.
• Запрещаются аппаратные циклы, чтобы предотвратить поведение jump-back-to-loop-start, если регистр дна цикла (loop bottom  register) соответствует инструкции start.
• Длина кольцевых буферов устанавливается в 0. CRT использует регистры Ireg и вызывает функции, которые могут их использовать. Кроме того, C/C++ ABI требует, чтобы кольцевые буферы были запрещены на входе в скомпилированные функции (и на выходе из ник), поэтому кольцевые буферы должны быть запрещены перед запуском _main.

[Event Vector Table]

Вектор сброса (фиксированный) и события эмуляции (их не касаются требования C ABI), не определяются кодом CRT. Событие с самым низким приоритетом IVG15 настраивается для указания на supervisor_mode, это метка, которая появляется позже в коде CRT, и она используется для упрощения входа в режим супервизора процессора. Остальные элементы event vector table загружаются адресами метки кода-заглушки __unknown_exception_occurred (так называемый пустой обработчик прерывания, dummy event handler), который определяет заранее известное поведение с целью отладки (по умолчанию это простое бесконечное зацикливание).

Дополнительно, если разрешена защита памяти memory protection (что выбрано либо через Project Wizard, либо сконфигурировано пользовательским значением переменной ___cplb_ctrl), то необходим обработчик исключения (exception handler) для обработки событий, генерируемых подсистемой памяти. Таким образом, в event vector table устанавливается соответствующий обработчик по умолчанию ___cplb_hdr.

Подробнее про ___cplb_ctrl см. раздел "Caching and Memory Protection" [1] или статью [6].

Вы можете установить дополнительные обработчики; для Вашего удобства CRT для этой цели вызывает функцию __install_default_handlers. Это пустая заглушка, которую Вы можете заменить своей собственной реализацией, которая инсталлирует дополнительные или альтернативные ISR перед тем, как CRT разрешит обработку событий (т. е. разрешит прерывания). Прототип C этой функции:

short _install_default_handlers(short mask);

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

Для приложений, использующих VisualDSP++ Kernel (VDK), см. соответствующую документацию для описания, как конфигурировать ISR [7].

[Stack Pointer и Frame Pointer]

Указатель стека, stack pointer (SP) устанавливается на вершину стека, как это определено в .ldf файле проекта символом ldf_stack_end. По стандарту SP указывает на точку в памяти, соответствующую вершине стека. Поскольку стек при проталкивании в него значений (операция PUSH) уменьшается, то первая инструкция PUSH переместит указатель стека на реальную вершину стека.

Указатель стека пользователя user stack pointer (USP) и указатель фрейма frame pointer (FP) устанавливаются на один и тот же адрес.

Затем из стека берутся 12 байт. Причина в том, что C ABI требует от вызывающего кода выделять пространство под параметры вызываемого кода, и все вызываемые функции требуют как минимум 12 байт стека для регистров R0-R2. Таким образом CRT берет эти 12 байт из стека в качестве входящих параметров для функций, вызываемых перед запуском функции _main.

[Cycle Counter]

CRT разрешает работу аппаратного счетчика тактов процессора (cycle counter), после чего будут обновляться регистры CYCLES и CYCLES2. Это не обязательно необходимо для общего выполнения программы, однако полезно для измерения производительности и отсчета реального времени.

[Выбор порта DAG]

Для процессоров ADSP-BF531, ADSP-BF532, ADSP-BF533, ADSP-BF534, ADSP-BF536, ADSP-BF537, ADSP-BF538, ADSP-BF539 и ADSP-BF561 код CRT конфигурирует DAG-и для использования разных портов доступа к памяти. Это снижает приостановки, когда DAG-и параллельно выдают запросы к памяти.

[Инициализация памяти]

Память инициализируется процессом из двух стадий:

1. Во времени линковки (link-time) утилита Memory Initializer обрабатывает файл .dxe для генерации таблицы областей кода и данных в памяти, которые должны быть инициализированы во время процесса загрузки (booting; подробнее про загрузку см. [8]).

2. Во время работы кода (runtime), когда запускается приложение, функция run-time библиотеки _mi_initialize обрабатывает эту таблицу, чтобы выполнить копирование кода и данных из памяти FLASH в энергозависимую память (SRAM или SDRAM).

Если приложение не было обработано утилитой Memory Initializer, или если Memory Initializer не нашла какой-либо код или данные, который нуждается в таком переносе, то функция _mi_initialize выполнит немедленный возврат в CRT. Если в Project Wizard была выбрана опция "Enable run-time memory initialization", то сгенерированный CRT включает в себя вызов функции _mi_initialize. Исходный код CRT по умолчанию (default CRT source) всегда включает этот вызов.

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

[Инициализация устройств]

Процесс инициализации драйверов устройств, поддерживающих stdio, включает следующее:

1. Инициализация внутренних таблиц файлов.
2. Запуск подпрограммы инициализации для каждого драйвера устройства, зарегистрированного во время сборки проекта (build-time).
3. Связывание потоков stdin, stdout и stderr с драйвером устройства по умолчанию (default device driver).

По умолчанию этот процесс выполняется автоматически, когда происходит первый доступ к устройству. Информация о драйверах устройств, поддерживаемых stdio, см. в разделе "Extending I/O Support to New Devices" документации [1], или см. статью [9].

Если выбрана опция поддержки ввода вывода "C/C++ I/O and I/O device support" на странице Run-Time Initialization диалога Project Wizard (что выбрано по умолчанию), явная инициализация устройства добавляется в генерируемый CRT. Поддержка драйверов устройств для stdio может быть запрещена в Project Wizard путем отмены выбора этой опции.

[Инициализация CPLB]

Когда разрешена функция cacheability protection lookaside buffers (CPLB), код CRT вызывает функцию _cplb_init, передающую значение ___cplb_ctrl в качестве параметра.

Декларация и инициализация глобальной переменной ___cplb_ctrl включена в генерируемый CRT, если в Project Wizard выбрана защита памяти или кеширование. Используется библиотечное определение по умолчанию для этой переменной, если это не отменено декларацией в коде пользователя. См. также раздел "Caching and Memory Protection" документации [1] или статью [6].

[Понижение приоритета выполнения кода]

CRT переводит процессор в режим супервизора и запускает код с самым низким уровнем приоритета (supervisor mode level IVG15). Это заключается в первом вызове (raise) события IVG15, однако это событие не может быть обработано, пока процессор остается в более высокоприоритетном состоянии Reset.

CRT делает возврат инструкцией RETI на метку still_interrupt_in_ipend, на которой есть инструкция RTI, и выполняется следующая инструкция. В результате очищаются все биты более высокоприоритетных прерываний, чем IVG15. В нормальных обстоятельствах это включало бы только прерывание сброса (reset interrupt), однако могут иметь место и другие случаи (например, если программа перезапускается во время работы кода ISR).

Теперь разрешена обработка ожидающего прерывания IVG15, и запустится обработчик IVG15, настроенный ранее кодом CRT (обработчик по метке supervisor_mode). Получается, что поток выполнения вернется с уровня Reset на уровень приоритета кода метки supervisor_mode (т. е. с самым низким приоритетом прерывания IVG15), и самый высокий приоритет выполнения кода режима супервизора (режим супервизора Reset) поменяется на режим выполнения кода супервизора с самым низким приоритетом.

Если разрешены другие события (исключения подсистемы памяти или другие обработчики событий, установленные Вашей собственной версией кода ISR вместо заглушки ISR по умолчанию), то могли бы взять на себя управление между выходом из Reset и входом в IVG15. Таким образом, остальные части CRT могут не выполниться, когда сработает запуск этих обработчиков событий.

Первым действием CRT после входа в IVG15 будет разрешение прерываний, чтобы могли быть выполнены другие более приоритетные прерывания (прерывание IVG15 разрешает вложенные в себя прерывания).

[Установка значения регистров]

Значение UNASSIGNED_FILL записывается в регистры R2-R7 и P0-P5, если в Project Wizard выбрана опция "Initialize data registers to a known value" (или если определен макрос UNASSIGNED_FILL при повторной сборке default CRT source).

[Terminate Stack Frame Chain]

На каждый стековый фрейм, на который указывает FP, содержит предыдущие значения FP и RETS. CRT проталкивает два элемента UNASSIGNED_VAL в стек, показывая тем самым, что больше нет активных фреймов. Библиотека поддержки исключений C++ (C++ exception support library) использует эти маркеры, чтобы определить, нужно ли проходить обратно цепочку всех активных функций, чтобы найти ту, которая захватила (catch) выброшенное исключение (thrown exception).

И снова CRT выделяет 12 байт для исходящих параметров функций, которые будут вызваны из CRT.

[Инициализация профайлера]

Если выбрана оптимизация по профилировщику (с помощью опции Project Wizard "Enable Profiling"), то CRT инициализирует инструментальный код библиотеки профайлинга путем вызова monstartup. Эта подпрограмма обнуляет все счетчики и гарантирует, что нет активных фреймов профилирования. Инструментальный код библиотеки профайлинга использует подпрограммы stdio, чтобы записывать накопленные данные профайлинга в stdout или в файл.

Инструментальный код профайлинга указывается опциями командной строки компилятора -p, -p1 и -p2 (см. описание опции в [10]). При необходимости эти опции компиляции добавляются Project Wizard. Если скомпилированы любые файлы, включающие это профилирование, то предварительная обработка линкера это определяет, и устанавливает макросы времени линковки, которые выбирают предварительно собранный объект CRT с разрешенным профилированием (если не используется Project Wizard).

[Запуск конструкторов C++]

Функция ___ctorloop запустит все конструкторы глобальной области, и это всегда вызывается из CRT, сгенерированном из Project Wizard, и из предварительно собранных CRT, где разрешен код C++ (который выбирается файлом .ldf, если был детектирован объект, скомпилированный в режиме C++).

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

[Многопоточные приложения]

CRT может быть собран для работы в многопоточном окружении (multi-threaded environment). Макрос _ADI_THREADS защищает код, подходящий для многопоточных приложений (этот макрос определен в проекте VDK).

[Парсинг аргументов]

Функция __getargv вызывается для обработки любых предоставленных аргументов в глобальном массиве __Argv (во встраиваемых приложениях обычно это пустой список, т. е. аргументы не предоставляются). Эта функция возвратит количество найденных аргументов, которые вместе с __Argv формируют параметры argc и argv для _main. В default CRT source, если FIOCRT не определен, argc устанавливается в 0, и argv устанавливается на пустой список, статически определенный в CRT.

[Вызов _main и вызов _exit]

Вызывается функция _main с использованием только что определенных аргументов argc и argv. Для встраиваемых программ не подразумевается возврат из _main, однако многие устаревшие программы, предназначенные для не встраиваемых приложений, делают возврат из _main. Таким образом, возвращаемое из _main значение немедленно передается в _exit, чтобы корректно завершить приложение. Из _exit возврат не ожидается.

[Конструкторы и деструкторы глобальных экземпляров класса]

Во время запуска приложения (start-up) C/C++ run-time header запускает конструкторы глобальных экземпляров классов. Это обеспечивают следующие компоненты:

• Соответствующая область данных для каждого глобального экземпляра класса.
• Соответствующий конструктор класса (и деструктор, если он существует).
• Генерируемая компилятором подпрограмма "start".
• Генерируемая компилятором таблица таких подпрограмм "start".
• Конструируемый компилятором связанный список подпрограмм деструктора.
• Сам run-time header.

Эти компоненты взаимодействуют следующим образом.

Компилятор генерирует подпрограмму "start" для каждого модуля, содержащего глобальные инициализации экземпляров классов, которые нуждаются в конструировании или уничтожении. Должна быть как минимум одна подпрограмма "start" на модуль; она обрабатывает все глобальные экземпляры класса в этом модуле:

• Для каждого такого экземпляра класса запускается его конструктор. Это может быть прямой вызов, или может использоваться встраиваемый (inline) код, вставленный оптимизатором компилятора.
• Если экземпляр требует своего уничтожения, то подпрограмма "start" регистрирует этот факт позже путем добавления в связанный список экземпляра и ссылки на его деструктор.

Подпрограмма start именуется после первого попавшегося такого экземпляра, хотя нет гарантии конструирования или уничтожения экземпляров класса в каком-либо определенном порядке (с тем лишь исключением, что деструкторы вызываются в порядке, обратном порядку вызова конструкторов). Такие экземпляры не должны иметь зависимости от порядка вызова конструкторов; опция командной строки -check-init-order [10] полезна для проверки этого в процессе разработки, поскольку она вставляет дополнительный код для детектирования использования еще не сконструированных объектов во время инициализации.

Указатель на подпрограмму "start" помещается в секцию ctor генерируемого объектного файла. Когда происходит линковка приложения, все секции ctor отображаются на одну и ту же выходную секцию ctor, формируя таблицу указателей на подпрограммы "start". В конец этой таблицы добавляется дополнительный объект ctorl, он содержит терминирующий NULL-указатель.

Когда запускается run-time header, он вызывает функцию _ctor_loop(), которая обрабатывает таблицу секций ctor, вызывая по указателям каждую функцию "start", пока не дойдет до указателя NULL из ctorl. Таким способом run-time header вызывает конструктор каждого глобально создаваемого экземпляра класса, косвенно обращаясь к ним через указатели на функции "start".

Когда программа достигает exit(), либо путем прямого вызова, либо при возврате из main(), то подпрограмма exit() следует нормальному процессу обработки связанного списка функций, зарегистрированных через интерфейс atexit(). Это приводит к запуску деструкторов каждого созданного глобального объекта (в порядке, обратном вызову конструкторов).

Эта функция регистрируется с atexit() через _mark_dtors(); компилятор вставляет вызов этой функции в начало каждой функции main(), компилированной в режиме C++.

Функции, зарегистрированные с atexit(), могут не ссылаться на глобальные экземпляры класса, поскольку деструктор для экземпляра может быть вызван перед использованием ссылки.

Конструкторы, деструкторы и размещение в памяти. По умолчанию компилятор помещает код конструкторов и деструкторов в ту же самую секцию, что и любая другая функция кода. Это можно поменять либо указанием секции, специально предназначенной для конструктора или деструктора (см. описание директив "#pragma section/#pragma default_section" и ключевого слова section() в [11]), или путем изменения секции назначения по умолчанию для генерируемого кода (см. описание "#pragma section/#pragma default_section" в [11] описание опции -section в [10]).

Обычно генерируемый компилятором код помещается в область CODE, подпрограмма "start" помещается в область STI. Обе область CODE и STI по умолчанию соответствуют размещению в одной и той же секции, однако это можно раздельно поменять с использованием #pragma default_section или опцией командной строки -section switch (так как функция "start" это внутренняя функция, генерируемая компилятором, на её размещение не может повлиять директива #pragma section).

Указатель на подпрограмму "start" помещается в секцию ctor. Это не конфигурируется, поскольку процесс запуска этих подпрограмм основан на всех указателях "start", находящихся в одной и той же секции в процессе линковки, чтобы они могли сформировать таблицу. Важный момент, что все соответствующие секции ctor отображаются во время линковки; если секция ctor опущена, то связанный конструктор не будет запущен во время start-up, и поведение run-time будет некорректным.

Если нужны деструкторы, то компилятор генерирует структуры данных, указывающие на экземпляр класса и его деструктор. Эти структуры помещаются в секцию для переменных по умолчанию (область DATA).

[Использование секций памяти]

См. статью [11].

[Использование нескольких куч]

Вопросы определения кучи (heap) во время линковки и во время работы программы, советы по работе с кучами, стандартный интерфейс кучи, выделение отдельной кучи для объектов C++ STL, использование альтернативного интерфейса кучи - все это см. в статье [12].

[Выделенные регистры]

C/C++ run-time определяет набор регистров (Dedicated Registers, выделенные регистры), содержимое которых никогда не должно изменяться, кроме специально определенных обстоятельств. Если эти регистры изменяются, то их значения должны быть предварительно сохранены и после модификации восстановлены обратно. Значения выделенных регистров всегда должны быть достоверными для каждого вызова функции (особенно для библиотечных вызовов) и для любого возможного прерывания.

К выделенным регистрам относятся SP, FP и L0-L3.

• Имена/аббревиатуры SP и FP это соответственно указатель стека (stack pointer) и указатель фрейма (frame pointer). Компилятор требует, чтобы оба этих указателя указывали на достоверную ячейку памяти с выровненным на 4 байта адресом, находящуюся в пространстве памяти стека.
• Регистры L0-L3 определяют длины кольцевых буферов генераторов адреса DAG (это регистры Length). Компилятор использует регистры DAG в обоих режимах - линейная буферизация и кольцевая буферизация (linear buffering mode, circular buffering mode). Компилятор подразумевает, что регистры длины (Length) равны 0 как на входе в функции, так и на выходе из функции, и гарантирует это, когда генерирует вызовы функции и возвраты из них. Ваше приложение может изменять регистры Length и использовать кольцевые буферы, однако Вы должны гарантировать, что Length-регистры соответственно сбрасываются, когда вызываются скомпилированные функции, или когда происходит возврат из скомпилированных функций. ISR должны сохранять и восстанавливать регистры Length, если в них используются регистры DAG.

[Регистры, сохраняемые при вызовах]

C/C++ run-time задает набор регистров, содержимое которых должно сохраняться и восстанавливаться (call-preserved registers). Ваша функция на ассемблере должна сохранить эти регистры во время пролога (начала) функции, и восстанавливать во время эпилога (перед непосредственным возвратом) функции. Регистры call-preserved должны быть сохранены и восстановлены, если они модифицируются внутри функции ассемблера; если функция не меняет какой-то определенный регистр, то его соответственно не нужно сохранять и восстанавливать.

К регистрам call-preserved относятся регистры P3-P5, R4-R7.

[Временные регистры]

C/C++ run-time задает набор регистров, содержимое которых не нужно сохранять и восстанавливать (временные, scratch-регистры). Имейте в виду, что содержимое этих регистров не сохраняется между вызовами функций. В таблице 1-42 перечисляются scratch-регистры, и по некоторым из них даны соответствующие примечания.

Таблица 1-42. Временные регистры (Scratch Registers).

Scratch Register Замечания
P0 Используется как агрегатный указатель адреса возврата.
P1–P2  
R0–R3 Первые три слова списка аргументов всегда передаются в регистрах R0, R1 и R2, если эти аргументы имеются (R3 для параметров не используется).
LB0–LB1  
LC0–LC1  
LT0–LT1  
ASTAT Включая CC.
A0–A1  
I0–I3  
B0–B3  
M0–M3  

Счетчики прокруток цикла, оверлеи, и код DMA. Компилятор не гарантирует, что регистры счетчиков прокруток цикла (LC0 и LC1) равны 0 на входе или выходе из функции. Это обычно не приводит к проблемам, потому что точка выхода из аппаратного цикла уникальна в программе, и компилятор гарантирует только один путь выхода - через соответствующую инструкцию установки цикла.

Если используются оверлеи, или если код выполняет перемещение DMA в быструю память с целью ускорения выполнения, то это может быть уже не тот случай. Есть возможность для оверлея или функции DMA установить цикл, который завершается на адресе A, и затем для другого оверлея или функции DMA с другим кодом, занимающим адрес A в более поздний момент времени. Если аппаратный цикл все еще активен - LC0 или LC1 не равны 0 - в точке, когда инструкция достигла адреса A, то в результате может быть непредсказуемое поведение, когда аппаратный цикл перескочит в начало цикла.

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

[Регистры стека]

C/C++ run-time environment резервирует набор регистров, которые управляют стеком времени выполнения (run-time stack). Эти регистры могут быть модифицированы для управления стеком, однако должны быть сохранены и восстановлены. Эти регистры стека включают SP (stack pointer) и FP (frame pointer).

Управление стеком. C/C++ run-time environment использует run-time стек для сохранения автоматических переменных и адресов возврата. Стек управляется регистрами FP и SP, и растет в сторону уменьшения адреса в памяти, т. е. по мере проталкивания сохраняемых значений в стек адрес в регистрах FP (или SP) уменьшается.

Область фрейма стека (stack frame) это секция стека, используемая для хранения информации о текущем контексте программы C/C++. Информация в фрейме включает локальные переменные, временные ячейки компилятора и параметры для следующей функции.

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

На рис. 1-3 показан пример секции run-time стека.

CRT Example Run Time Stack fig1 3

Рис. 1-3. Пример Run-Time Stack.

На рис. 1-3 выполняемая в настоящий момент подпрограмма Current() была вызвана подпрограммой Previous(), и Current() в свою очередь вызывает Next(). Состояние стека показано для момента, когда Current() только что протолкнула (push) аргументы для Next(), и готова вызывать подпрограмму Next().

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

Поскольку Вы пишете код на ассемблере, имейте в виду, что операции по восстановлению указателей SP и FP находятся в зоне ответственности вызываемой функции.

Чтобы войти в функцию и запустить её на выполнение (пролог функции), следуйте последовательности шагов:

• Линковка фреймов стека - адрес возврата и FP вызывающего кода сохраняются в стеке, и FP устанавливается так, что указывает на начало нового (вызываемой функции) stack frame. SP декрементируется так, чтобы выделить пространство для локальных переменных и временных переменных компилятора.
• Сохранение регистров - любые регистры, которые функция должна сохранить, сохраняются в stack frame, и SP устанавливается на вершину stack frame.

По окончании функции (эпилог функции) должны быть выполнены следующие шаги:

• Восстановление регистров - любые регистры, которые были сохранены, восстанавливаются из stack frame, и SP устанавливается на вершину stack frame.
• Освобождение фрейма стека - FP восстанавливается из stack frame, чтобы у него было значение вызывающего кода, RETS восстанавливается из stack frame для адреса возврата, и SP устанавливается на вершину stack frame вызывающего кода.

Типичный пролог функции может быть таким:

      LINK 16;
      [--SP]=(R7:4);
      SP += -16;
      [FP+8]=R0; [FP+12]=R1; [FP+16]=R2;

Здесь LINK 16; это специальная инструкция линковки фрейма, которая сохранит адрес возврата и FP, и обновит SP так, чтобы выделить пространство из 16 байт для локальных переменных. Инструкция [--SP]=(R7:4); выделяет пространство в стеке и сохраняет регистры R7 .. R4 в соответствующей области сохранения. Инструкция SP += –16; выделяет пространство в стеке для исходящих аргументов. Всегда выделяйте как минимум 12 байт в стеке для исходящих аргументов, даже если вызываемая функция требует меньше. Инструкции [FP+8]=R0; [FP+12]=R1; [FP+16]=R2; сохраняют регистры аргументов в область аргументов.

Соответствующий эпилог функции:

      SP += 16;
      P0=[FP+4];
      (R7:4)=[SP++];
      UNLINK;
      JUMP (P0);

Здесь инструкция SP += 16; забирает обратно пространство стека, которое использовалось для исходящих аргументов. Инструкция P0=[FP+4]; загружает адрес возврата в регистр P0. Инструкция (R7:4)=[SP++]; восстанавливает регистры из области сохранения и забирает обратно эту область. UNLINK; это специальная инструкция, которая восстанавливает FP и SP. Инструкция JUMP (P0); возвращает управление в вызвавший функцию код.

C/C++ run-time environment использует набор регистров и run-time стек для передачи параметров в подпрограммы и функции на ассемблере. Ваши функции на языке ассемблера должны следовать соглашениям, которые накладываются при вызове функций C/C++ (или когда функция вызывает другую функцию). В этой врезке описываются тонкости передачи аргументов, передача экземпляра класса C++ и возврат значений из функции.

[Передача аргументов]

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

Реальный список аргументов выглядит так же, как и концептуальный список аргументов в хранилище, за исключением того факта, что содержимое первых трех слов содержит регистры R0, R1 и R2. Обычно это означает, что первые три аргумента (если это тип int или тип указателя) передаются в регистрах R0 .. R2, и если есть какие-либо дополнительные аргументы, то они передаются через стек.

Если любой аргумент имеет размер больше одного слова, то он займет несколько регистров. Вызывающий код отвечает за расширение размеров любых аргументов типа char или short в 32-битные значения.

Когда вызывается C-функция, то в стеке должно быть выделено как минимум 12 байт для аргументов функции, соответствующих регистрам R0 .. R2. Это правило действует даже для функций, у которых аргументов меньше, чем на 12 байт, или когда у функции меньше трех аргументов. Обратите внимание, что вызываемой функции разрешено модифицировать это пространство стека.

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

int varying(char *fmt, ...) { /* ... */ }

Как и с другими, "обычными" функциями, первый аргумент fmt передается в R0, и остальные аргументы будут переданы через R1, затем через R2, и затем через стек - по мере необходимости.

Списки аргументов переменной длины обрабатываются с помощью макросов, определенных в заголовочном файле stdarg.h. Функция va_start() получает указатель на список аргументов, который может быть передан в другие функции, или который может быть обработан макросом va_arg().

Для поддержки этого функционала компилятор в эпилоге функции с переменным количеством аргументов сначала сбрасывает содержимое регистров R0, R1 и R2 в специально для них зарезервированное пространство стека:

_varying:
      [SP+0] = R0;
      [SP+4] = R1;
      [SP+8] = R2;

Функция va_start() затем может получить адрес последнего не переменного аргумента (в примере выше fmt находится по адресу [SP+0]), и с помощью va_arg() может пройтись по полному списку аргументов, находящихся в стеке.

[Передача экземпляра класса C++]

Параметр функции, у которого тип экземпляра класса C++, всегда передается по ссылке, когда для класса C++ определен конструктор копирования (copy constructor). Если конструктор копирования не определен для класса C++, то экземпляр класса C++, присутствующий в параметре функции, передается по значению.

Рассмотрим следующий пример.

class fr
{
public:
   int v;
public:
   fr () {}
   fr (const fr& rc1) : v(rc1.v) {}
};
 
extern int fn(fr x);
 
fr Y;
 
int main()
{
   return fn (Y);
}

Вызов функции fn (Y) в теле main передаст экземпляр класса Y по ссылке, потому что для этого класса определен copy constructor fr (const fr& rc1) : v(rc1.v) {}. Если copy constructor удалить, то Y будет передан по значению.

[Возврат значений]

Если функция возвращает short или char, то вызываемый код отвечает за расширение знака или нулей для возврата значения в 32-битном регистре. Например функция, которая возвращает signed short, должна расширить это значение с учетом знака в регистр R0. Подобным образом функция, которая возвращает unsigned char, должна расширить нулями unsigned в регистр R0.

• Для функций, возвращающих агрегатные значения, занимающие пространство меньше или равное 32 битам, результат будет возвращен в R0.
• Для агрегатных значений, занимающих больше 32 бит, и меньше чем и равным 64 битам, результат возвращается в регистровой паре R0, R1.
• Для функций, возвращающих агрегатные значения, занимающие больше 64 бит, вызывающий код выделяет в стеке объект для возврата значения, и адрес этого объекта передается вызываемой функции как скрытый аргумент в регистре P0.

Таблица 1-43. Примеры передачи параметров и возврата значений.

Прототип функции Как передаются параметры Где находится возвращаемое значение
int test(int a, int b, int c)
a в R0
b в R1
c в R2
R0
char test(int a, char b, char c)
a в R0
b в R1
c в R2
R0
int test(int a)
a в R0 R0
int test(char a, char b,
         char c, char d,
         char e)
a в R0
b в R1
c в R2
d в [FP+20]
e в [FP+24]
R0
int test(struct *a, int b, int c)
a (адрес) в R0
b в R1
c в R2
R0
struct s2a
{
   char ta;
   char ub;
   int vc;
}
 
int test(struct s2a x, int b, int c)
x.ta и x.ub в R0
x.vc в R1
b в R2
c в [FP+20]
R0
struct foo *test(int a, int b, int c)
a в R0
b в R1
c в R2
R0 (адрес)
void qsort(void *base,
           int nel, int width,
           int (*compare)(const void *,
                          const void *))
base (адрес) в R0
nel в R1
width в R2
compare (адрес) в [FP+20]
 
struct s2
{
   char t;
   char u;
   int v;
}
 
struct s2 test(int a, int b, int c)
a в R0
b в R1
c в R2
R0 (s.t и s.u)
R1 (s.v)
struct s3
{
   char t;
   char u;
   int v;
   int w;
}
 
struct s3 test(int a, int b, int c)
a в R0
b в R1
c в R2
*P0 (на основе значения P0 при вызове, не обязательно при возврате)

[Использование форматов хранения данных]

См. статью [13].

[Ссылки]

1. VisualDSP++ 5.0 C/C++ Compiler and Library Manual for Blackfin® Processors site:analog.com.
2. VisualDSP: интерфейс C/C++ и ассемблера Blackfin.
3. Blackfin: руководство по файлам LDF.
4. Специфика использования VDK для процессоров Blackfin.
5. VDK: драйверы устройств и системные службы процессоров Blackfin.
6. Использование кеша в процессорах Blackfin.
7. VDK: менеджер прерываний.
8. Как происходит загрузка ADSP-BF533 Blackfin.
9. VisualDSP: ввод/вывод с использованием файлов (stdio).
10. Опции командной строки компилятора Blackfin.
11. VisualDSP: использование секций памяти.
12. VisualDSP: работа с динамически выделяемой памятью.
13. VisualDSP: использование форматов переменных.

 

Добавить комментарий


Защитный код
Обновить

Top of Page