You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 3 Next »

Компиляция загружаемого кода

Инсталляция и настройка компилятора GCC

Для компиляции загружаемого кода используются компилятор и линкер GCC, стандартная библиотека С для встраиваемых систем newlib, утилита make и несколько сервисных утилит.
Существует два варианта использования GCC:

  1. Загрузить исходный код GCC, make, newlib из соответствующих репозиториев и самостоятельно скомпилировать.
  2. Воспользоваться готовым комплектом инструментов для разработки под ARM.

Для работы с ключами Guardant Code рекомендуется использовать свободно распространяемый инструментарий YAGARTO. Все примеры из комплекта разработчика, тестировались именно на нем.
Чтобы приступить к работе с YAGARTO требуется выполнить несколько простых шагов:


После выполнения этих шагов компилятор полностью готов к работе. Дальнейшая работа с ним будет выполняться посредством вызова утилиты make на заранее созданных и настроенных makefile.

Общие сведения о компиляции и сборке

Все инструкции для компилятора и линкера, а также команды для обработки скомпилированного кода, содержатся в конфигурационном файле утилиты make (имя этого файла по умолчанию - makefile).
Соответственно, для компиляции приложения необходимо:

  1. Отредактировать настройки в секции Main Configuration внутри makefile:
  • Задать желаемое имя точки входа, адреса RAM, ROM
  • Задать требуемый размер стека
  • Задать имена и размеры буферов ввода-вывода
  • Задать путь к системным утилитам hex2bin.exe и map_parse.exe
  • Задать путь к папке с заголовочными файлами GrdAPI.h и GcaAPI.h
  • Задать путь к папке с файлом libgcaapi.lib
  1. Сгенерировать шаблон проекта: make template (для использования компиляторов, отличных от GCC, требуется ручная настройка проекта). Проект должен иметь определенный набор файлов и быть настроен для работы в среде Guardant Code
  2. Скопировать в папку проекта файлы с исходными текстами с модулями на C/C++ и добавить имена этих файлов в переменные makefile SRC и CPPSRC соответственно
  3. В скопированных модулях должна присутствовать функция с именем, совпадающим с именем заданной точки входа (и соответствующим прототипом)
  4. После этого можно выполнить команду: make и приложение будет собрано
  5. В папке проекта будет создана папка .out с двумя файлами, имеющими расширения bin и bmap. Эти файлы требуются для генерации GCEXE-файла и загрузки его в электронный ключ при помощи утилиты GrdUtil.exe

Важная информация
В makefile нельзя использовать символ «\» и пути с пробелами. В качестве разделителя необходимо указывать «/». Чтобы избежать пробелов, можно задавать относительные пути, либо копировать необходимые для компиляции файлы в отдельную директорию.

Команды утилиты make

Приложение собирается при помощи GNU-утилиты make, которая использует конфигурационный файл с именем makefile.
Для утилиты make доступны следующие команды:

1. Сборка проекта

Если конфигурационный файл имеет имя по умолчанию (makefile):

make

Если конфигурационный файл имеет имя, отличное от имени по умолчанию:

make –f confname

 

2. Удаление всех файлов, создаваемых при сборке(файлы, создаваемые при make template не удаляются)

make clean

или

make -f confname clean

 

3. Создание шаблона приложения

make template

или

make -f confname template


Важная информация
Если в сгенерированные при создании шаблона проекта файлы Startup.S и rom.ld были внесены изменения, они будут потеряны при повторном создании шаблона!

Полная пересборка приложения

make clean
make all

или

make -f confname clean
make -f confname all


Пересборка может требоваться при изменении уровня оптимизации и при добавлении новых файлов (см. следующий раздел).
Важная информация
Если в системе одновременно с GCC установлены другие компиляторы, использующие собственные утилиты make (например, Borland C), при вызове make следует указывать полный путь, поскольку пути к другим утилитам make могут быть прописаны в переменной среды PATH. Можно придумать и иные способы дифференциации.

Настройка универсального makefile

Универсальный makefile содержит секции настроек с параметрами:

  • Генерации шаблона проекта командой make template
  • Сборки приложения по команде make/make all

При внесении изменений в первую секцию требуется перегенерация шаблона проекта (см. соответствующий раздел). При внесении изменений во вторую секцию требуется пересборка проекта путем подачи команды 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, при сборке может возникнуть следующая ошибка:

                  • begin --------*
                    make: *** No rule to make target `main.o', needed by `elf'. Stop.


Несовпадение имен как таковых проверяется отдельно и вызывает более внятную ошибку:

                  • begin --------*
                    File main.c not found! Please check makefile (SRC, ASRC and CPPSRC values).

 

Точка входа в приложение

При старте приложения, в самом начале начинает исполняться код, находящийся в файле 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

LDR R4, =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
{
double x;

} iodata ALIGNED;


Это указывает компилятору, что структура выровнена в памяти, и позволяет генерировать более эффективный код для доступа к полям структуры.
По умолчанию размер буфера ввода-вывода установлен равным 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)
{
return a+b;
}

За счет этого происходит экономия памяти стека и увеличивается быстродействие кода.

Устройство загружаемого кода

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

Параметры функции main()

Прототип функции main() объявляется следующим образом:

DORD main(
DWORD dwInDataLng, DWORD dwOutDataLng, DWORD dwP1)


Параметры dwInDataLng и dwOutDataLng устанавливают количество данных, считываемых из буфера ввода и возвращаемых в буфер вывода. Параметр dwP1 используется для передачи кода подфункции загружаемого кода.
Параметр dwP1 передается функции GrdCodeRun() и его можно получить в загруженном коде в виде третьего параметра функции main (функции, которая первой получает управление в С-коде):

DWORD func1(dwInDataLng, dwOutDataLng)
{
// Логика работы 1:
return 101;
}
DWORD func2(dwInDataLng, dwOutDataLng)
{
// Логика работы 2:
return 102;
}
DWORD func3(dwInDataLng, dwOutDataLng)
{
// Логика работы 3:
return 103;
}

DWORD main(DWORD dwInDataLng, DWORD dwOutDataLng, DWORD dwP1) {
switch (dwP1)
{
case 0x01:
return func1(dwInDataLng, dwOutDataLng);
case 0x02:
return func2(dwInDataLng, dwOutDataLng);
case 0x03:
return func3(dwInDataLng, dwOutDataLng);
case 0x04:
// ...
default:
return -1;
}
}

 

Статические и глобальные переменные

По возможности, переменные лучше объявлять глобально (хотя это и противоречит принципам функционального программирования), а не в теле функции, чтобы не передавать данные через стек. Этим экономится память стека и увеличивается быстродействие.
В загружаемом коде можно создать глобальные переменные, содержимое которых не будет обнуляться между вызовами. Такие переменные требуется объявлять с макросом NO_INIT:

DWORD buffer[100] NO_INIT

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

Возврат из загружаемого кода

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

  • Возврат из main при помощи return. Код возврата будет помещен в параметр dwRetCode функции GrdCodeRun()
  • Вызов функции GcaExit(). Ей также передается код возврата

Кроме того, принудительное завершение приложения происходит в следующих случаях:

  • Наступление таймаута времени выполнения загружаемого кода (3 секунды)
  • Попытка выполнения приложением недопустимого действия (обращение к недопустимым адресам памяти и т.д.)

В примерах в качестве кода возврата с ошибкой используется значение -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, то для отладки не нужно загружать этот код в ключ: можно использовать входящую в комплект разработчика отладочную библиотеку.

Описание отладочной библиотеки

Отладочная библиотека представлена двумя частями:

  • Модуль для загрузки в электронный ключ,
  • Динамическая библиотека, содержащая функции, прототипы которых аналогичны тем, что доступны для загружаемого кода внутри электронного ключа (GcaXXX и GccaXXX)

Порядок работы с отладочной библиотекой таков:

  1. При помощи GrdUtil.exe создается файл маски, в котором один из алгоритмов представляет собой отладочный модуль загружаемого кода – DebugModule.bin. При этом нужно убедиться, что соответствующий модулю bmap-файл находится в той же директории. Можно взять готовый файл маски DebugMask.nsd из примера и изменить его для использования в собственном приложении. От файла маски зависит, какой номер будет у алгоритма, содержащего загружаемый код.
  2. Полученный файл маски с отладочным модулем прошивается в электронный ключ при помощи GrdUtil.
  3. К проекту загружаемого кода на PC подключается отладочная библиотека gcaapidll.dll. Для этого используется библиотека экспортов gcaapidll.lib. Файл GcaAPIdll.h содержит описание прототипов функций GcaXXX/GccaXXX.
  4. Перед вызовами функций GcaXXX/GccaXXX из gcaapidll.dll в исходном коде следует разместить вызов макроса DEBUGDLL_ INIT(hHandle, dwAlgoNum), который настраивает библиотеку для работы с текущим контекстом Guardant API. Макрос осуществляет привязку библиотеки к используемому контексту Guardant API и открытому ключу, также ему передается номер аппаратного алгоритма, в который был загружен отладочный модуль.
  5. Если код, предполагаемый для размещения в электронном ключе, использует вызовы внешнего Guardant API, то их требуется заменить на соответствующие вызовы Guardant Code API. В данном случае функции импортируются из отладочной библиотеки. Если же код содержит функции Guardant API, не имеющие прямых аналогов в Guardant Code API, требуется создать эквивалентные им конструкции из доступных функций.


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





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

main()
{
// Хэндл ключа, в котором запускается загруженный пользователем код:
HADNLE hGrd;

// Инициализация API и подключение к электронному ключу
...

#ifdef DEBUG
// Инициализация DLL
DEBUGDLL_INIT(hGrd, 1);
// hGrd – хендл открытого электронного ключа.
// 1 – номер аппаратного алгоритма, в котором находится
// загруженный отладочный модуль
#endif

// Передача параметра hGrd необязательна.
GcaGetRandom(0, &iodata[i]);

return 0;
}


Также стоит отметить, что функции GcaExit() и GcaLedOn()/ GcaLedOff() не могут работать в отладочном режиме. Первая – из-за того, что результат ее работы просто нельзя зафиксировать, а функции управления светодиодом – из-за того, что сразу после их вызова работа кода будет завершаться, при этом индикатор просто зажигается вновь.
Использование отладочной библиотеки демонстрируется в примере №19 (см. Краткая характеристика примеров).

Загрузка кода в электронный ключ

Для загрузки кода в электронный ключ первоначально используется GrdUtil. При помощи этой утилиты создается дескриптор аппаратного алгоритма типа Загружаемый код.
В свойствах алгоритма указывается бинарный файл, который содержит скомпилированный загружаемый код. Этому файлу должен сопутствовать файл bmap, содержащий настройки адресов памяти.
Бинарный файл перед загрузкой должен быть преобразован в файл типа GCEXE (Guardant Code executable). Преобразование осуществляется в автоматическом режиме утилитой программирования ключей GrdUtil.
При выполнении преобразования GrdUtil генерирует ключевыепары:

  • Для зашифрования и расшифрования загружаемого кода

Зашифрование производится на открытом ключе, который хранится в маске и не записывается в электронный ключ.
Расшифрование – на закрытом ключе, который хранитьсяи в файле маски, и в дескрипторе алгоритма, записанногов электронный ключ

  • Для электронной цифровой подписи загружаемого кода Подписывание производится – на закрытом ключе, который хранится только в маске и не записывается в сам ключ. Проверка – на открытом, который будет храниться и в маске, и в дескрипторе алгоритма, который будет записан в ключ


Перед загрузкой бинарный файл зашифровывается на сеансовом ключе и подписывается ЭЦП. Это гарантирует возможность загрузки кода только разработчиком. При необходимости файл GCEXE можно сгенерировать таким образом, чтобы он мог быть загружен только в ключ с указанным ID. Эта возможность полезна для создания адресных обновлений, например – платных.
При записи данных в ключ первоначально записывается дескриптор алгоритма, а уже затем – файл GCEXE.
Однажды сгенерированный файл GCEXE может быть в дальнейшем записан и в другие ключи, содержащие соответствующие ключи шифрования и подписи. Для этого используется функция GrdCodeLoad().

Отладка защищенного приложения

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

Дистанционное обновление загружаемого кода

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

  • У разработчика должна храниться прошивка (файл маски), содержащая ключевые пары для шифрования и подписи загружаемого кода,
  • У конечного пользователя должен находиться электронный ключ Guardant Code, содержащий дескриптор алгоритма с загружаемым кодом, а также закрытый ключ для расшифрования кода и открытый ключ для проверки ЭЦП
  • Ключевые пары в маске и ключе должны быть идентичны.


Для обновления загружаемого кода необходимо сгенерировать новый GCEXE-файл с обновленным кодом, зашифрованным и подписанным на соответствующих ключах.
Само обновление может производиться как при помощи технологии TRU, так и прямой загрузкой GCEXE-файла из защищаемого приложения функцией GrdCodeLoad().
При желании можно сделать процедуру обновления загружаемого кода «прозрачной» для пользователя. Тогда от него потребуется только получить обновление, поместить его рядом с исполняемым файлом приложения (или в специально для этого предназначенную директорию) и запустить приложение.

  • No labels