Обработка файлов LDR Печать
Добавил(а) microsin   

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

LDF-файл имеет простой двоичный формат, он состоит из идущих друг за другом блоков разного размера. Ниже на рисунке показана структура файла LDF из n блоков, в котором находится один загружаемый код DXE.

LDR format01

Каждый блок содержит либо часть загружаемого кода, либо несет в себе какую-то служебную информацию (подробнее о формате LDR-файла и формате блока см. [1]). Все блоки имеют одинаковый формат - каждый снабжен заголовком фиксированного размера (размер и структура заголовков всех блоков одинаковы), и за блоком может идти или не идти секция данных. Особое значение имеет только блок 0, он должен быть всегда в начале файла, и он всегда имеет размер 14 байт. Также немного отличается от остальных блоков последний блок - у него в заголовке взведен флаг Last Block ("последний блок"). Остальные блоки могут находится в любом месте файла, их можно тасовать и переставлять по файлу в любом порядке, на функциональность загрузчика это никак не повлияет (см. Пример 1). Кроме того, в LDR-файл можно добавлять и свои блоки с нужной служебной информацией, необходимой приложению, если в заголовке у этого блока взвести флаг Ignore Block (пропускаемый блок). Это может понадобиться, например, если нужно оставить место в памяти FLASH под энергонезависимые настройки приложения (см. Пример 2).

[Пример 1 - перестановка блока в другое место]

Задача: нужно найти блок в LDF-файле, в котором находится определенный известный маркер, и переместить этот блок в начало LDF-файла. В этом блоке находится информация о версии приложения. На скриншоте показан этот блок LDR-файла, который нужно программно найти и переместить в начало (LDR-файл открыт программой Loader File Viewer [2]):

LDR recode input

///////////////////////////////////////////////////////////////////////
// Утилита перекодировки блоков 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 0x0001

static u8* ldrbuffer;      // Буфер, куда считывается *.ldf
static size_t ldrbufsize;  // Размер буфера ldf.
char inputfilename[256];   // Имя входного файла.
char outputfilename[256];  // Имя выходного файла.
char marker[] = "[DSP] ";  // Искомый маркер.

///////////////////////////////////////////////////////////////////////
// TBlockHeader: структура заголовка блока LDF-файла.
#ifdef _MSC_BUILD
//Упаковка структуры для компилятора Visual Studio C++:
#pragma pack(1)
typedef struct _TBlockHeader
#else
//Упаковка структуры для компилятора GCC:
typedef struct __attribute__((__packed__)) _TBlockHeader
#endif
{
   u32 address;
   u32 count;
   u16 flag;
}TBlockHeader;

///////////////////////////////////////////////////////////////////////
// Функция вернет размер блока в байтах вместе с заголовком
// (размер заголовка + размер данных блока).
// ВХОД:
//    ldrbuffer   буфер с данными LDF-файла
//    blockbegin  абсолютное смещение заголовка блока
static int BlockSize (int blockbegin)
{
   TBlockHeader *bh = (TBlockHeader*)(ldrbuffer + blockbegin);
   // Если в поле флагов установлен бит ZEROFILL,
   // то этот блок состоит только из заголовка.
   // Иначе размер равен зазмеру заголовка + размер данных.
   if (bh->flag & ZEROFILL)
      return sizeof(TBlockHeader);
   else
      return sizeof(TBlockHeader) + bh->count;
}

///////////////////////////////////////////////////////////////////////
// Функция вернет true, если параметр указывает на заголовок блока,
// у которого нет полезных данных (он состоит из только из заголовка).
static bool ZeroFillBlock (TBlockHeader *bh)
{
   return (bh->flag & ZEROFILL)?true:false;
}

static int NextBlock (int blockbegin)
{
   return blockbegin + BlockSize(blockbegin);
}

///////////////////////////////////////////////////////////////////////
// Функция ищет маркер findstr в блоках LDF буфера ldrbuffer,
// и вернет абсолютное смещение найденного блока в файле.//
// Если блок не найден, то функция вернет -1.
// 
// Если блок найден, то переменной *blocknum будет возвращен
// порядковый номер найденного блока (нумерация начинается с 0,
// нулевой номер имеет блок заголовка DXE), и в переменной
// *blocksize будет возвращен размер найденного блока.
//
// ВХОД:
//    ldrbuffer   буфер с данными LDF-файла
//    ldrbufsize  количество байт в буфере
// ВЫХОД:
//    *blocknum   номер найденного блока
//    *blocksize  размер найденного блока в байтах
static int FindBlock (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;
}

int main(int argc, char *argv[])
{
   if (argc==2)
   {
      // Если указан только параметр входного файла,
      // то у входного и выходного файла будут одинаковые
      // имена, т. е. входной файл будет перезаписан.
      strcpy(inputfilename, argv[1]);
      strcpy(outputfilename, inputfilename);
   }
   else if (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);
   return 0;
}

После перекодировки блок с маркером перемещается в начало файла, так его легче найти и обработать программно (например, когда LDR-файл считывается и анализируется загрузчиком).

LDR recode output

[Пример 2 - добавление Ignore-блока]

FLASH-память процессора ADSP-BF538F реализована на основе чипа S29AL008D, которая организована в виде набора секторов определенного размера. Ниже на картинке показана организация секторов этой микросхемы. Слева показаны абсолютные адреса начала и конца каждого сектора в адресном пространстве процессора.

S29AL008D sectors

FLASH-память S29AL008D обычно используется для двух целей - как загрузочная (т. е. начиная с сектора 0 туда записывается либо LDR-файл загрузчика, либо LDR-файл рабочего приложения) и как место для хранения энергонезависимых настроек приложения. 

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

Для хранения настроек удобнее всего применять сектор 1 или 2, потому что они самые маленькие. Для загрузчика нужно обязательно использовать сектор 0, потому что именно с него начинает обработку загрузки код BootROM процессора. Если загрузчик простой, и умещается в 16 килобайт сектора 0, то проблем нет - в секторе 0 записан загрузчик, в секторе 1 записаны настройки, и начиная с сектора 2 можно разместить код приложения.

LDR map example1

Но так бывает не всегда. Предположим, что код загрузчика больше 64 килобайт, он не помещается в секторе 0 и залезает на секторы 1, 2 и 3. Тогда под настройки придется отвести сектор 4, а он слишком большой, и будет использоваться не экономно.

LDR map example2

Проблему не эффективного использования секторов памяти FLASH можно решить "в лоб", просто разместив в друг за другом данные загрузчика, настроек и приложения. Но тогда эти разные части данных будут перемешиваться в разных секторах, что очень неудобно при перепрограммировании секторов FLASH. Например, если разместить настройки в третьем секторе, который также частично занимает загрузчик, то при неудачной операции записи настроек есть опасность повредить данные загрузчика. Т. е. есть риск повреждения целостности загрузочных данных прибора, когда он станет мертвым "кирпичом".

Есть более корректный способ - можно обработать LDR-файл загрузчика таким образом, чтобы в нем появилось пустое место, точно попадающее на сектор 1. Тогда данные настроек можно беспрепятственно поместить в секторе 1, а остальные секторы использовать под код загрузчика. Это можно реализовать с помощью блока с атрибутом Ignore, который будет пропущен кодом BootROM в процессе обработки LDR-данных загрузчика.

LDR map example3

При таком размещении данных экономится целый сектор FLASH размером 64 килобайта, и все данные размещены строго в своих секторах. Поэтому перезапись настроек не повредит ни загрузчик, ни приложение. Загрузчик также имеет возможность при обновлении программы перезаписывать сектора приложения без опасности повредить данные других секторов.

///////////////////////////////////////////////////////////////////////
// Утилита перекодировки блоков LDF-файла. Вставляет Ignore Block
// размером в 8 килобайт в позицию сектора 1 памяти FLASH S29AL008D,
// которая стоит в корпусе процессора ADSP-BF538F.
//
// [Как использовать, примеры]
//
// 1. Вывод подсказки:
//       bl-plus-nv.exe
// 2. Перекодировать файл input.ldr (файл input.ldr будет перезаписан):
//       bl-plus-nv.exe input.ldr
// 3. Перекодировать файл input.ldr в файл output.ldr. Файл input.ldr
// останется нетронутым:
//       bl-plus-nv.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

//Параметры сектора 1 памяти FLASH S29AL008D:
#define SECTOR1BEGIN  0x00004000
#define SECTOR1SIZE   0x00002000

// Флаг заголовка, показывающий блок пустыми полезными данными:
#define ZEROFILL        0x0001
// Флаг заголовка, показывающий последний блок:
#define LASTBLOCK       0x2000
// Флаг заголовка, показывающий игнорируемый блок:
#define IGNOREBLOCK     0x0010
// Флаг заголовка, показывающий тип процессора:
#define PROCESSORTYPE   0x0002

static u8* ldrbuffer;      // Буфер, куда считывается *.ldf
static size_t ldrbufsize;  // Размер буфера ldf.
static size_t ldrdatasize; // Размер данных ldf.
char inputfilename[256];   // Имя входного файла.
char outputfilename[256];  // Имя выходного файла.
char marker[] = "[DSP] ";  // Искомый маркер.

///////////////////////////////////////////////////////////////////////
// TBlockHeader: структура заголовка блока LDF-файла.
#ifdef _MSC_BUILD
//Упаковка структуры для компилятора Visual Studio C++:
#pragma pack(1)
typedef struct _TBlockHeader
#else
//Упаковка структуры для компилятора GCC:
typedef struct __attribute__((__packed__)) _TBlockHeader
#endif
{
   u32 address;
   u32 count;
   u16 flag;
}TBlockHeader;

//Макросы для проверки флагов заголовка блоков:
#define BLOCK_LAST(x) (x->flag & LASTBLOCK)?true:false
#define BLOCK_ZERO(x) (x->flag & ZEROFILL)?true:false

///////////////////////////////////////////////////////////////////////
// Функция вернет размер блока в байтах вместе с заголовком
// (размер заголовка + размер данных блока).
// ВХОД:
//    ldrbuffer   буфер с данными LDF-файла
//    blockbegin  абсолютное смещение заголовка блока
static int BlockSize (int blockbegin)
{
   TBlockHeader *bh = (TBlockHeader*)(ldrbuffer + blockbegin);
   // Если в поле флагов установлен бит ZEROFILL,
   // то этот блок состоит только из заголовка.
   // Иначе размер равен зазмеру заголовка + размер данных.
   if (BLOCK_ZERO(bh))
      return sizeof(TBlockHeader);
   else
      return sizeof(TBlockHeader) + bh->count;
}

int main(int argc, char *argv[])
{
   if (argc==2)
   {
      // Если указан только параметр входного файла,
      // то у входного и выходного файла будут одинаковые
      // имена, т. е. входной файл будет перезаписан.
      strcpy(inputfilename, argv[1]);
      strcpy(outputfilename, inputfilename);
   }
   else if (argc==3)
   {
      strcpy(inputfilename, argv[1]);
      strcpy(outputfilename, argv[2]);
   }
   else
   {
      // Был запуск без опций, вывод подсказки и выход:
      printf ("Insert LDF-block 8k with flag 'Ignore' to byte pos 0x4000\n");
      printf ("(sector 1 FLASH S29AL008D). 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);
   
   //Открыть выходной файл на запись:
   FILE *pfout;
   pfout = fopen (outputfilename, "wb");
   if (NULL == pfout)
   {
      printf("Error open file %s", outputfilename);
      exit(6);
   }

   int totalwrited = 0;    //Сколько записано байт в выходной файл
   int blockcnt = 0;       //Счетчик блоков
   int ldrbufidx = 0;      //Индекс данных входного буфера

   // Буфер ldrbuffer заполнен данными входного файла,
   // входной файл закрыт. Начало формирования выходного
   // файла. Сначала запись заголовка.
   fwrite(ldrbuffer, 1,
          14, pfout);
   ldrbufidx   = 14;
   totalwrited = 14;
   blockcnt++;
   
   //Цикл записи в выходной файл, пока не будет достигнуто
   // начало сектора 1, или пока не будет достигнут
   // конец данных буфера:
   while ((totalwrited + BlockSize(ldrbufidx) < SECTOR1BEGIN)
       && (ldrbufidx < ldrbufsize))
   {
      //Записать в выходной файл данные блока:
      fwrite(ldrbuffer+ldrbufidx, 1,
             BlockSize(ldrbufidx), pfout);
      totalwrited += BlockSize(ldrbufidx);
      ldrbufidx   += BlockSize(ldrbufidx);
      blockcnt++;      
   }
   
   //Генерация данных Ignore Block и запись их в выходной файл.
   int IgnoreBlockSize = (SECTOR1BEGIN - totalwrited) + SECTOR1SIZE;
   TBlockHeader *ignoreblock = (TBlockHeader*)malloc(IgnoreBlockSize);
   //Это игнорируемый блок, поэтому адрес может быть любой.
   //Я решил записать сюда адрес начала сектора 1 памяти FLASH
   // S29AL008D в адресном пространстве ADSP-BF538F:
   ignoreblock->address = 0x20000000 + SECTOR1BEGIN;
   ignoreblock->count   = IgnoreBlockSize - sizeof(TBlockHeader);
   ignoreblock->flag    = IGNOREBLOCK + PROCESSORTYPE;
   for(int i=0; i<ignoreblock->count; i++)
      ((u8*)ignoreblock)[sizeof(TBlockHeader)+i] = 0xFF;
   //Запись Ignore Block.
   fwrite(ignoreblock, 1,
          IgnoreBlockSize, pfout);
   totalwrited += IgnoreBlockSize;
   free(ignoreblock);
   blockcnt++;      

   //Запись остальных данных LDF-файла.
   while (ldrbufidx < ldrbufsize)
   {
      //Записать в выходной файл данные блока:
      fwrite(ldrbuffer+ldrbufidx, 1,
             BlockSize(ldrbufidx), pfout);
      totalwrited += BlockSize(ldrbufidx);
      ldrbufidx   += BlockSize(ldrbufidx);
      blockcnt++;      
   }
   
   fclose(pfout);
   free(ldrbuffer);
   return 0;
}

[Ссылки]

1. Blackfin: утилита elfloader.exe.
2. Утилита просмотра файла загрузки Blackfin.
3. ADSP-BF538F: драйвер встроенной FLASH-памяти S29AL008D.