Компиляция загружаемого кода
Для компиляции загружаемого кода используются компилятор и линкер GCC, стандартная библиотека С для встраиваемых систем newlib, утилита make и несколько сервисных утилит.
Существует два варианта использования GCC:
Для работы с ключами Guardant Code рекомендуется использовать свободно распространяемый инструментарий YAGARTO. Все примеры из комплекта разработчика, тестировались именно на нем.
Чтобы приступить к работе с YAGARTO требуется выполнить несколько простых шагов:
После выполнения этих шагов компилятор полностью готов к работе. Дальнейшая работа с ним будет выполняться посредством вызова утилиты make на заранее созданных и настроенных makefile.
Все инструкции для компилятора и линкера, а также команды для обработки скомпилированного кода, содержатся в конфигурационном файле утилиты make (имя этого файла по умолчанию - makefile).
Соответственно, для компиляции приложения необходимо:
Важная информация
В makefile нельзя использовать символ «\» и пути с пробелами. В качестве разделителя необходимо указывать «/». Чтобы избежать пробелов, можно задавать относительные пути, либо копировать необходимые для компиляции файлы в отдельную директорию.
Приложение собирается при помощи GNU-утилиты make, которая использует конфигурационный файл с именем makefile.
Для утилиты make доступны следующие команды:
Если конфигурационный файл имеет имя по умолчанию (makefile):
make |
Если конфигурационный файл имеет имя, отличное от имени по умолчанию:
make –f confname |
make clean |
или
make -f confname clean |
make template |
или
make -f confname template |
Важная информация
Если в сгенерированные при создании шаблона проекта файлы Startup.S и rom.ld были внесены изменения, они будут потеряны при повторном создании шаблона!
make clean |
или
make -f confname clean |
Пересборка может требоваться при изменении уровня оптимизации и при добавлении новых файлов (см. следующий раздел).
Важная информация
Если в системе одновременно с GCC установлены другие компиляторы, использующие собственные утилиты make (например, Borland C), при вызове make следует указывать полный путь, поскольку пути к другим утилитам make могут быть прописаны в переменной среды PATH. Можно придумать и иные способы дифференциации.
Универсальный makefile содержит секции настроек с параметрами:
При внесении изменений в первую секцию требуется перегенерация шаблона проекта (см. соответствующий раздел). При внесении изменений во вторую секцию требуется пересборка проекта путем подачи команды make clean и затем make all.
Настройки секции генерации шаблона:
Имя параметра | Значение |
---|---|
CFG_ENTRYPOINT_NAME | Имя точки входа (по умолчанию функция main) |
CFG_PROGRAM_ADDR *) | Адрес Flash-памяти, по которому располагается приложение |
CFG_PROGRAM_SIZE *) | Размер приложения во Flash-памяти |
CFG_RAM_ADDR *) | Адрес начала RAM, резервированной для загружаемого кода |
CFG_RAM_SIZE *) | Размер RAM, зарезервированной для загружаемого кода |
CFG_INPUT_BUFFER_NAME | Имя буфера ввода, через который данные передаются в загружаемый код |
CFG_INPUT_BUFFER_SIZE | Размер буфера ввода |
CFG_OUTPUT_BUFFER_NAME | Имя буфера вывода, данные из которого возвращаются вызывающему приложению |
CFG_OUTPUT_BUFFER_SIZE | Размер буфера вывода |
CFG_STACK_SIZE | Размер программного стека |
CFG_INCLUDE_DIR | Путь до директории, содержащей заголовочные файлы GcaAPI.h и GrdAPI.h |
CFG_SYS_DIR | Путь до директории, содержащей служебные утилиты |
CFG_TARGET_NAME | Имя двоичного bin-файла, получаемого при компиляции |
*) Поскольку в ключе под загружаемый код по умолчанию резервируется вся Flash-память и вся RAM, значения в этих пунктах изменять не нужно.
В makefile доступны следующие настройки сборки проекта:
Имя параметра | Значение |
OPT | Уровень оптимизации. Рекомендуемые значения 2 или s (так же допустимые значения 0 и 1, значение 3 крайне не рекомендуется) |
SRC | Набор С-файлов, используемых в проекте |
ASRC | Набор ASM-файлов, используемых в проекте |
Важно заметить, что задаваемые имена файлов зависят от регистра. К примеру, при несовпадении регистра в имени файла main.c, при сборке может возникнуть следующая ошибка:
|
Несовпадение имен как таковых проверяется отдельно и вызывает более внятную ошибку:
|
При старте приложения, в самом начале начинает исполняться код, находящийся в файле Startup.S. Он инициализирует стек и C-окружение (предварительно инициализированные переменные) и обнуляет неинициализированные переменные и область стека, при необходимости вызывает конструкторы глобальных объектов C. После этого он передает управление в приложение на C. Этот файл генерируется автоматически из универсального makefile.
По умолчанию точка входа в C-приложении на GCC имеет стандартное имя main. Прототип, однако, отличается от стандартного ANSI C и имеет следующий вид:
int main(DWORD dwInDataLng, DWORD dwOutDataLng, DWORD dwP1); |
Где:
dwInDataLng – размер данных поступивших из PC,
dwOutDataLng – размер данных, который PC запрашивает назад,
dwP1 – параметр dwP1, переданный функции GrdCodeRun().
Если требуется изменить адрес точки входа, то в файле Startup.S требуется исправить строчки:
.global main |
В микроконтроллерах на основе ядра CORTEX-M3, на которых построен ключ Guardant Code, имеется единое адресное пространство в 4Гб. Для загружаемого кода доступны следующие диапазоны адресов:
Адреса | Назначение |
00020000h-0003FFFFh | Flash-память для размещения загружаемого кода и ROM-секции микропрограммы (для варианта с 128 кб Flash-памяти) |
00020000h-00077FFFh | Flash-память для размещения загружаемого кода и ROM-секции микропрограммы (для варианта с 352 кб Flash-памяти) |
40003000h-40007FDFh | RAM (ОЗУ), доступная загружаемому коду. Тут размещаются: стек, буфер ввода-вывода, переменные загружаемого кода |
Диапазон используемых адресов указывается в makefile (параметры CFG_PROGRAM_ADDR, CFG_PROGRAM_SIZE, CFG_ RAM, CFG_RAM_SIZE). Задаваемые адреса должны быть кратны 0x8000 байт, и быть выровнены по границе 32768 байт.
Диапазон адресов, доступных загружаемому коду, описывается в соответствующем дескрипторе аппаратного алгоритма. GrdUtil автоматически заполняет соответствующие поля дескриптора информацией из файла *.bmap.
Поскольку по умолчанию под загружаемый код резервируется вся Flash-память и вся RAM, значения этих настроек без насущной необходимости изменять не нужно.
Имена буферов ввода и вывода в makefile могут быть разными, а могут и совпадать.
В случае, когда они совпадают, выделяется один буфер, который работает одновременно и на ввод, и на вывод. В C-коде буфер ввода-вывода может быть объявлен так:
extern BYTE iodata[]; |
При этом в параметрах CFG_INPUT_BUFFER_NAME и CFG_OUTPUT_BUFFER_NAME указывается значение iodata.
Если же используются раздельные буферы, то каждый из них объявляется в C-коде отдельно, и имеет собственное имя и размер.
При объявлении буферов допустимо использование любых типов данных, однако в случае структур рекомендуется добавлять в определение макрос ALIGNED, например:
extern struct |
Это указывает компилятору, что структура выровнена в памяти, и позволяет генерировать более эффективный код для доступа к полям структуры.
По умолчанию размер буфера ввода-вывода установлен равным 1024 байта. Для ввода-вывода в примерах используется единый буфер. Перед запуском загружаемого приложения данные в него помещаются, а после окончания работы возвращаются обратно в PC.
Максимальный суммарный размер буферов для ввода-вывода составляет 0x3F00 байт (16128 байт). Так же в объявлении переменной желательно указание макроса ALIGNED, который говорит компилятору, что буфер выровнен в памяти, и, в некоторых случаях, оптимизировать доступ к данной переменной.
Размер программного стека для GCC указывается в makefile. За это отвечает параметр:
CFG_STACK_SIZE = 0x800; |
Т. е., размер стека по умолчанию равен 2кБ (0x800 байтам).
Поскольку размер RAM достаточно сильно ограничен, рекомендуется небольшие и простые, но часто используемые функции оформлять как inline. Можно использовать макрос INLINE из syscalls_public.h. Например:
INLINE void add(int a, int b) |
За счет этого происходит экономия памяти стека и увеличивается быстродействие кода.
Прямой перенос кода из исходного приложения может быть сопряжен с определенными трудностями. В общем случае, код, перенесенный в том же виде, как он существует в приложении, будет неработоспособен в электронном ключе. Поэтому код должен быть модифицирован и оптимизирован для выполнения на платформе CORTEX-M3. Желательно, чтобы этот код был написан заново и реализовывал функции, которых в ранних версиях приложения не было, либо эти функции должны быть видоизменены.
Прототип функции main() объявляется следующим образом:
DORD main( |
Параметры dwInDataLng и dwOutDataLng устанавливают количество данных, считываемых из буфера ввода и возвращаемых в буфер вывода. Параметр dwP1 используется для передачи кода подфункции загружаемого кода.
Параметр dwP1 передается функции GrdCodeRun() и его можно получить в загруженном коде в виде третьего параметра функции main (функции, которая первой получает управление в С-коде):
DWORD func1(dwInDataLng, dwOutDataLng) |
По возможности, переменные лучше объявлять глобально (хотя это и противоречит принципам функционального программирования), а не в теле функции, чтобы не передавать данные через стек. Этим экономится память стека и увеличивается быстродействие.
В загружаемом коде можно создать глобальные переменные, содержимое которых не будет обнуляться между вызовами. Такие переменные требуется объявлять с макросом NO_INIT:
DWORD buffer[100] NO_INIT |
Эти переменные являются аналогами статических переменных.
Польза от них может заключаться в возможности запоминания некоторых состояний загружаемого кода. Это делает анализ «черного ящика» гораздо более сложным.
Выход из приложения можно осуществлять следующим образом:
Кроме того, принудительное завершение приложения происходит в следующих случаях:
В примерах в качестве кода возврата с ошибкой используется значение -1. Для упрощения отладки можно возвращать значения макроса _LINE или пользоваться вызовом GcaExit(0, __LINE_). Этот способ поможет определить строку, на которой произошел выход из приложения. Оставлять в конечных версиях возвраты в данном виде нежелательно, так как это может дать дополнительную информацию для злоумышленника.
Для отладки можно, к примеру, использовать следующий макрос:
#define ASSERT(cond){if(cond)GcaExit(0,_LINE_);} |
ASSERT(x != 0); // Если x!=0, осуществит возврат из программы с указанием номера строки, в которой вставлен ASSERT.
За вычисления с плавающей точкой отвечает библиотека libm из комплекта GCC. Полное описание математических функций, доступных в ней, можно найти в документации к данной библиотеке. Для каждой функции имеется 2 варианта: обычный, для вычислений с двойной точностью (тип double), а также с приставкой «f», для вычислений с половинной точностью (тип float).
Разработка и первоначальная отладка загружаемого кода производится на компьютере. Для этого можно использовать любую IDE и отладчик языка С.
Основная проблема состоит в том, что отлаживать уже загруженный код затруднительно, поскольку нет возможности «залезть» отладчиком в контроллер ключа. Поэтому первоначально отлаживают сам алгоритм загружаемого кода.
Загруженный в ключ код имеет ограниченные возможности для трассировки. Например, нельзя вывести трассу на консоль или записать в файл. Однако некоторые средства все же есть. Для этой цели можно использовать функции управления светодиодом. Сигналы, подаваемые с его помощью, можно применять в качестве признаков прохождения тех или иных веток кода.
Как вариант, можно использовать принудительный возврат из загружаемого кода с соответствующим кодом возврата и передачей необходимых для отладки данных через буфер вывода.
Методы отладки кода такие же, как и при разработке на PC. Если в загружаемом коде используются вызовы Guardant Code API, то для отладки не нужно загружать этот код в ключ: можно использовать входящую в комплект разработчика отладочную библиотеку.
Отладочная библиотека представлена двумя частями:
Порядок работы с отладочной библиотекой таков:
Следует принимать во внимание, что ни сама отладочная библиотека, ни отладочный модуль не содержат логики работы функций. Они являются всего лишь своеобразным «туннелем», через который параметры вызова функций передаются в электронный ключ и возвращаются обратно.
Пример использования макроса DEBUGDLL_INIT:
main() |
Также стоит отметить, что функции GcaExit() и GcaLedOn()/ GcaLedOff() не могут работать в отладочном режиме. Первая – из-за того, что результат ее работы просто нельзя зафиксировать, а функции управления светодиодом – из-за того, что сразу после их вызова работа кода будет завершаться, при этом индикатор просто зажигается вновь.
Использование отладочной библиотеки демонстрируется в примере №19 (см. Краткая характеристика примеров).
Для загрузки кода в электронный ключ первоначально используется GrdUtil. При помощи этой утилиты создается дескриптор аппаратного алгоритма типа Загружаемый код.
В свойствах алгоритма указывается бинарный файл, который содержит скомпилированный загружаемый код. Этому файлу должен сопутствовать файл bmap, содержащий настройки адресов памяти.
Бинарный файл перед загрузкой должен быть преобразован в файл типа GCEXE (Guardant Code executable). Преобразование осуществляется в автоматическом режиме утилитой программирования ключей GrdUtil.
При выполнении преобразования GrdUtil генерирует ключевыепары:
Зашифрование производится на открытом ключе, который хранится в маске и не записывается в электронный ключ.
Расшифрование – на закрытом ключе, который хранитьсяи в файле маски, и в дескрипторе алгоритма, записанногов электронный ключ
Перед загрузкой бинарный файл зашифровывается на сеансовом ключе и подписывается ЭЦП. Это гарантирует возможность загрузки кода только разработчиком. При необходимости файл GCEXE можно сгенерировать таким образом, чтобы он мог быть загружен только в ключ с указанным ID. Эта возможность полезна для создания адресных обновлений, например – платных.
При записи данных в ключ первоначально записывается дескриптор алгоритма, а уже затем – файл GCEXE.
Однажды сгенерированный файл GCEXE может быть в дальнейшем записан и в другие ключи, содержащие соответствующие ключи шифрования и подписи. Для этого используется функция GrdCodeLoad().
Отладке приложений, использующих загружаемый код, следует уделить особое внимание, поскольку поиск ошибок при работе с «черным ящиком» является непростым делом.
Очень важным является итоговое быстродействие загруженного кода. Если оно получается неудовлетворительным, требуется принять меры по приведению кода к обозначенным в начале этой главы требованиям.
Проблема обновления информации в ключах, уже находящихся у пользователей приложения, актуальна и для загружаемого кода. Рано или поздно в этот код может потребоваться внести изменения или исправления.
Для успешного обновления загружаемого кода необходимо выполнение следующих условий:
Для обновления загружаемого кода необходимо сгенерировать новый GCEXE-файл с обновленным кодом, зашифрованным и подписанным на соответствующих ключах.
Само обновление может производиться как при помощи технологии TRU, так и прямой загрузкой GCEXE-файла из защищаемого приложения функцией GrdCodeLoad().
При желании можно сделать процедуру обновления загружаемого кода «прозрачной» для пользователя. Тогда от него потребуется только получить обновление, поместить его рядом с исполняемым файлом приложения (или в специально для этого предназначенную директорию) и запустить приложение.