Файл LDR имеет довольно простую структуру, удобную для программной обработки. Такая обработка может понадобиться для переноса нужной информации в определенное место LDR-файла - например, чтобы этот файл можно было проще обрабатывать загрузчиком. В этой статье приведен пример такой обработки LDR-файла.
LDF-файл имеет простой двоичный формат, он состоит из идущих друг за другом блоков разного размера. Ниже на рисунке показана структура файла LDF из n блоков, в котором находится один загружаемый код DXE.
Каждый блок содержит либо часть загружаемого кода, либо несет в себе какую-то служебную информацию (подробнее о формате LDR-файла и формате блока см. [1]). Все блоки имеют одинаковый формат - каждый снабжен заголовком фиксированного размера (размер и структура заголовков всех блоков одинаковы), и за блоком может идти или не идти секция данных. Особое значение имеет только блок 0, он должен быть всегда в начале файла, и он всегда имеет размер 14 байт. Также немного отличается от остальных блоков последний блок - у него в заголовке взведен флаг Last Block ("последний блок"). Остальные блоки могут находится в любом месте файла, их можно тасовать и переставлять по файлу в любом порядке, на функциональность загрузчика это никак не повлияет (см. Пример 1). Кроме того, в LDR-файл можно добавлять и свои блоки с нужной служебной информацией, необходимой приложению, если в заголовке у этого блока взвести флаг Ignore Block (пропускаемый блок). Это может понадобиться, например, если нужно оставить место в памяти FLASH под энергонезависимые настройки приложения (см. Пример 2).
[Пример 1 - перестановка блока в другое место]
Задача: нужно найти блок в LDF-файле, в котором находится определенный известный маркер, и переместить этот блок в начало LDF-файла. В этом блоке находится информация о версии приложения. На скриншоте показан этот блок LDR-файла, который нужно программно найти и переместить в начало (LDR-файл открыт программой Loader File Viewer [2]):
///////////////////////////////////////////////////////////////////////// Утилита перекодировки блоков LDF-файла. Ищет блок, данные которого// содержат marker, и перемещает этот блок в начало LDF-файла, сразу// за заголовком DXE.//// [Как использовать, примеры]//// 1. Вывод подсказки:// ldfrv.exe// 2. Перекодировать файл input.ldr (файл input.ldr будет перезаписан):// ldfrv.exe input.ldr// 3. Перекодировать файл input.ldr в файл output.ldr. Файл input.ldr// останется нетронутым:// ldfrv.exe input.ldr output.ldr#include <stdlib.h>#include <string.h>#include <fstream>#define u8 unsigned char#define u16 unsigned short#define u32 unsigned int// Флаг, показывающий блок пустыми полезными данными:#define ZEROFILL 0x0001static u8* ldrbuffer; // Буфер, куда считывается *.ldfstaticsize_t ldrbufsize; // Размер буфера ldf.char inputfilename[256]; // Имя входного файла.char outputfilename[256]; // Имя выходного файла.char marker[] ="[DSP] "; // Искомый маркер.///////////////////////////////////////////////////////////////////////// TBlockHeader: структура заголовка блока LDF-файла.#ifdef _MSC_BUILD//Упаковка структуры для компилятора Visual Studio C++:#pragma pack(1)typedefstruct _TBlockHeader
#else//Упаковка структуры для компилятора GCC:typedefstruct __attribute__((__packed__)) _TBlockHeader
#endif
{
u32 address;
u32 count;
u16 flag;
}TBlockHeader;
///////////////////////////////////////////////////////////////////////// Функция вернет размер блока в байтах вместе с заголовком// (размер заголовка + размер данных блока).// ВХОД:// ldrbuffer буфер с данными LDF-файла// blockbegin абсолютное смещение заголовка блокаstaticintBlockSize (int blockbegin)
{
TBlockHeader *bh = (TBlockHeader*)(ldrbuffer + blockbegin);
// Если в поле флагов установлен бит ZEROFILL,// то этот блок состоит только из заголовка.// Иначе размер равен зазмеру заголовка + размер данных.if (bh->flag & ZEROFILL)
returnsizeof(TBlockHeader);
elsereturnsizeof(TBlockHeader) + bh->count;
}
///////////////////////////////////////////////////////////////////////// Функция вернет true, если параметр указывает на заголовок блока,// у которого нет полезных данных (он состоит из только из заголовка).staticboolZeroFillBlock (TBlockHeader *bh)
{
return (bh->flag & ZEROFILL)?true:false;
}
staticintNextBlock (int blockbegin)
{
return blockbegin + BlockSize(blockbegin);
}
///////////////////////////////////////////////////////////////////////// Функция ищет маркер findstr в блоках LDF буфера ldrbuffer,// и вернет абсолютное смещение найденного блока в файле.//// Если блок не найден, то функция вернет -1.// // Если блок найден, то переменной *blocknum будет возвращен// порядковый номер найденного блока (нумерация начинается с 0,// нулевой номер имеет блок заголовка DXE), и в переменной// *blocksize будет возвращен размер найденного блока.//// ВХОД:// ldrbuffer буфер с данными LDF-файла// ldrbufsize количество байт в буфере// ВЫХОД:// *blocknum номер найденного блока// *blocksize размер найденного блока в байтахstaticintFindBlock (char* findstr, int*blocknum, int*blocksize)
{
int result =-1;
// Данные блоков начинаются со смещения 14, потому что// перед этим идет заголовок DXE размером 14 байт:int currpos =14;
// Подсчет блоков начинается с 1, потому что в блоке// номер 0 идет заголовок DXE:int currblocknum =1;
TBlockHeader *bh;
int markerlength = strlen(marker);
do
{
// Цикл итерации по блокам:
bh = (TBlockHeader*)(ldrbuffer+currpos);
// Если блок для заполнения нулями, то его просматривать// не нужно, потому что в нем нет данных.if (!ZeroFillBlock(bh))
{
// Указатель data показывает на полезные данные блока,// в которых нужно делать поиск:
u8* data = ldrbuffer + currpos +sizeof(TBlockHeader);
// Переменная memsize хранит размер данных блока// в байтах:int memsize = bh->count;
// Цикл поиска маркера в данных блока:while (memsize >= markerlength)
{
if (0== memcmp(data, marker, markerlength))
{
// Маркер найден, будет возвращено абсолютное// смещение найденного блока:
result = currpos;
// Также будут возвращены номер блока и его размер:*blocknum = currblocknum;
*blocksize = BlockSize(currpos);
break;
}
memsize--;
data++;
}
}
// Переход к следующему блоку данных LDF:
currpos = NextBlock(currpos);
currblocknum++;
} while (currpos<ldrbufsize &&-1==result);
return result;
}
intmain(int argc, char*argv[])
{
if (argc==2)
{
// Если указан только параметр входного файла,// то у входного и выходного файла будут одинаковые// имена, т. е. входной файл будет перезаписан.
strcpy(inputfilename, argv[1]);
strcpy(outputfilename, inputfilename);
}
elseif (argc==3)
{
strcpy(inputfilename, argv[1]);
strcpy(outputfilename, argv[2]);
}
else
{
// Был запуск без опций, вывод подсказки и выход:
printf ("Move LDF-block with tag '%s' to begin LDF file. Usage:\n", marker);
printf ("%s <LDF_input_file> [LDF_output_file]", argv[0]);
exit(1);
}
// Открыть входной LDF-файл на чтение: FILE* pfin;
pfin = fopen (inputfilename, "rb");
if (NULL== pfin)
{
printf("Error open file %s", inputfilename);
exit(2);
}
// Узнать размер входного файла:
fseek(pfin, 0L, SEEK_END);
ldrbufsize = ftell(pfin);
// Выделение памяти под содержимое файла:
ldrbuffer = (u8*)malloc(ldrbufsize);
if (NULL== ldrbuffer)
{
printf("Error malloc %i bytes", ldrbufsize);
fclose(pfin);
exit(3);
}
// Чтение входного файла в буфер:
fseek(pfin, 0L, SEEK_SET);
size_t readed;
readed = fread(ldrbuffer, 1, ldrbufsize, pfin);
if (readed != ldrbufsize)
{
printf("Error read %s", inputfilename);
fclose(pfin);
free(ldrbuffer);
exit(4);
}
// Закрыть входной файл:
fclose(pfin);
// Буфер ldrbuffer заполнен данными входного файла,// входной файл закрыт. Поиск в буфере блока,// в котором находится нужный маркер:int blockoffset; //Смещение блока в файлеint blocknum; //Номер блока в файлеint blocksize; //Размер блока
blockoffset = FindBlock(marker, &blocknum, &blocksize);
if (-1== blockoffset)
{
printf("Marker '%s' not found", marker);
free(ldrbuffer);
exit(5);
}
else
{
printf("Block:%i Offset:%i Size:%i\n", blocknum, blockoffset, blocksize);
}
// Исхомый блок найден. Теперь запишем данные буфера так,// чтобы сначала находился найденный блок, а потом// остальные данные.//Открыть выходной файл на запись:FILE*pfout;
pfout = fopen (outputfilename, "wb");
if (NULL== pfout)
{
printf("Error open file %s", outputfilename);
exit(6);
}
//Записать в выходной файл данные заголовка:
fwrite(ldrbuffer, 1,
14, pfout);
//Записать в выходной файл данные найденного блока:
fwrite(ldrbuffer+blockoffset, 1,
blocksize, pfout);
//Записать в выходной файл данные перед найденным блоком:
fwrite(ldrbuffer+14, 1,
blockoffset-14, pfout);
//Записать в выходной файл данные после найденного блока:
fwrite(ldrbuffer+blockoffset+blocksize, 1,
ldrbufsize-(blockoffset+blocksize), pfout);
fclose(pfout);
// Выходной файл успешно записан и закрыт,// он имеет тот же размер, что и входной.// Освобождение буфера и успешный возврат:
free(ldrbuffer);
return0;
}
После перекодировки блок с маркером перемещается в начало файла, так его легче найти и обработать программно (например, когда LDR-файл считывается и анализируется загрузчиком).
[Пример 2 - добавление Ignore-блока]
FLASH-память процессора ADSP-BF538F реализована на основе чипа S29AL008D, которая организована в виде набора секторов определенного размера. Ниже на картинке показана организация секторов этой микросхемы. Слева показаны абсолютные адреса начала и конца каждого сектора в адресном пространстве процессора.
FLASH-память S29AL008D обычно используется для двух целей - как загрузочная (т. е. начиная с сектора 0 туда записывается либо LDR-файл загрузчика, либо LDR-файл рабочего приложения) и как место для хранения энергонезависимых настроек приложения.
Перед разработчиком стоит задача эффективного использования секторов FLASH-памяти. Необходимо учитывать размеры секторов и то обстоятельство, что записывать во FLASH-память можно только сектор целиком, даже если нужно поменять в секторе только один байт. Сначала нужно прочитать в буфер все данные изменяемого сектора, потом стереть сектор целиком, поменять в буфере записываемые данные, и только потом записать сектор. Для обеспечения сохранности данных нужно распределить данные загрузчика, настроек и приложения по разным секторам, чтобы они не пересекались - например, чтобы запись настроек случайно не повредила ни загрузчик, ни приложение.
Для хранения настроек удобнее всего применять сектор 1 или 2, потому что они самые маленькие. Для загрузчика нужно обязательно использовать сектор 0, потому что именно с него начинает обработку загрузки код BootROM процессора. Если загрузчик простой, и умещается в 16 килобайт сектора 0, то проблем нет - в секторе 0 записан загрузчик, в секторе 1 записаны настройки, и начиная с сектора 2 можно разместить код приложения.
Но так бывает не всегда. Предположим, что код загрузчика больше 64 килобайт, он не помещается в секторе 0 и залезает на секторы 1, 2 и 3. Тогда под настройки придется отвести сектор 4, а он слишком большой, и будет использоваться не экономно.
Проблему не эффективного использования секторов памяти FLASH можно решить "в лоб", просто разместив в друг за другом данные загрузчика, настроек и приложения. Но тогда эти разные части данных будут перемешиваться в разных секторах, что очень неудобно при перепрограммировании секторов FLASH. Например, если разместить настройки в третьем секторе, который также частично занимает загрузчик, то при неудачной операции записи настроек есть опасность повредить данные загрузчика. Т. е. есть риск повреждения целостности загрузочных данных прибора, когда он станет мертвым "кирпичом".
Есть более корректный способ - можно обработать LDR-файл загрузчика таким образом, чтобы в нем появилось пустое место, точно попадающее на сектор 1. Тогда данные настроек можно беспрепятственно поместить в секторе 1, а остальные секторы использовать под код загрузчика. Это можно реализовать с помощью блока с атрибутом Ignore, который будет пропущен кодом BootROM в процессе обработки LDR-данных загрузчика.
При таком размещении данных экономится целый сектор FLASH размером 64 килобайта, и все данные размещены строго в своих секторах. Поэтому перезапись настроек не повредит ни загрузчик, ни приложение. Загрузчик также имеет возможность при обновлении программы перезаписывать сектора приложения без опасности повредить данные других секторов.