#1、前言 写这篇博客的目的是因为最近在做一个STM32的离线编程器,离线下载需要用到FLM文件的下载算法,所以实现了一下提取FLM文件中下载算法的C程序。

有关ELF格式的详细说明可查看这个文件:http://flint.cs.yale.edu/cs422/doc/ELF_Format.pdf

推荐一个elf分析软件: http://www.elfparser.com/index.html

软件如下图:

ELFParser

2、快速扫盲

ELF 全称 “Executable and Linkable Format”,即可执行可链接文件格式,目前常见的Linux、 Android可执行文件、共享库(.so)、目标文件( .o)以及Core 文件(吐核)均为此格式。

常见的ELF文件大致结构如下:

如果是LINUX系统,使用GCC编译出来的程序就是该格式,性质等同于windows系统下的.exe格式运行程序; 在keil中,编译完成之后有一个 .axf 文件,这个文件也是elf格式; FLM格式文件是KEIL里的FLASH下载算法文件,他其实就是.axf文件的拷贝,换了个后缀名称而已。

3、运行效果

我提供了两种可执行程序,一是带UI界面的,一个是控制台使用的,效果如下所示:

控制台应用程序:

可执行应用程序:

4、源码实现

由于elf.h这个头文件太大了,就不在博客里贴代码了,只贴一下flm文件的解析:

/*
 * flmparse.c
 *
 *  Created on: 2021年4月10日
 *      Author: hello
 */

#include <stdio.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <ctype.h>
#include "elf.h"

#define FILENAME "STM32F4xx_2048.FLM"

static int ReadDataFromFile(const char* FName, uint32_t offset, void* buf, uint32_t size);
static int FLM_Prase(const void* FName, void* pBuffer, uint32_t* Size, uint32_t* Init, uint32_t* UnInit, uint32_t* EraseChip, uint32_t* EraseSector, uint32_t* ProgramPage);

int main(int argc, char const *argv[])
{
	int i = 0;
	uint32_t RAM[256] = {0};
	uint32_t Addr[5] = {0};
	uint32_t Size = 0;

	if(argc != 2)
	{
		printf("\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n");
		printf("Usage:\n");
		printf("        flmparse.exe [filename]\n");
		printf("\n\n");
		goto __exit;
	}

	/* 这8个数是中断halt程序,让函数执行完后返回到这里来执行从而让CPU自动halt住 */
	RAM[0] = 0xE00ABE00;
	RAM[1] = 0x062D780D;
	RAM[2] = 0x24084068;
	RAM[3] = 0xD3000040;
	RAM[4] = 0x1E644058;
	RAM[5] = 0x1C49D1FA;
	RAM[6] = 0x2A001E52;
	RAM[7] = 0x4770D1F2;

	if(FLM_Prase(argv[1], &RAM[8], &Size, &Addr[0],&Addr[1],&Addr[2],&Addr[3],&Addr[4]) < 0)
	{
		printf("错误:解析FLM格式文件失败,请检查FLM文件是否存在或格式正确性!\r\n");
		goto __exit;
	}

	fprintf(stdout, "\r\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\r\n");

	Size += 32;

	fprintf(stdout, "\r\nstatic const uint32_t flash_code[] = \n{");
	for(i = 0; i < (Size >> 2); i++)
	{
		if(i % 8 == 0)
		{
			fprintf(stdout, "\n    ");
		}
		fprintf(stdout, "0X%08X,", RAM[i]);
	}
	fprintf(stdout, "\n};\n");

	fprintf(stdout, "\r\nconst program_target_t flash_algo =\n{\n");
	fprintf(stdout, "    0X%08X,  // Init\n",        Addr[0] + 0X20000020);
	fprintf(stdout, "    0X%08X,  // UnInit\n",      Addr[1] + 0X20000020);
	fprintf(stdout, "    0X%08X,  // EraseChip\n",   Addr[2] + 0X20000020);
	fprintf(stdout, "    0X%08X,  // EraseSector\n", Addr[3] + 0X20000020);
	fprintf(stdout, "    0X%08X,  // ProgramPage\n", Addr[4] + 0X20000020);
	fprintf(stdout, "\n");
	fprintf(stdout, "    // BKPT : start of blob + 1\n");
	fprintf(stdout, "    // RSB  : address to access global/static data\n");
	fprintf(stdout, "    // RSP  : stack pointer\n");
	fprintf(stdout, "    {\n");
	fprintf(stdout, "        0X20000001,\n");
	fprintf(stdout, "        0X20000C00,\n");
	fprintf(stdout, "        0X20001000,\n");
	fprintf(stdout, "    },\n");
	fprintf(stdout, "\n");
	fprintf(stdout, "    0x20000400,                      // mem buffer location\n");
	fprintf(stdout, "    0x20000000,                      // location to write prog_blob in target RAM\n");
	fprintf(stdout, "    sizeof(flash_code),              // prog_blob size\n");
	fprintf(stdout, "    flash_code,                      // address of prog_blob\n");
	fprintf(stdout, "    0x00000400,                      // ram_to_flash_bytes_to_be_written\n");
	fprintf(stdout, "};\n");
	fprintf(stdout, "\n");
	fprintf(stdout, "\n");
	fprintf(stdout, "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n");

__exit:
	return 0;
}


static int ReadDataFromFile(const char* FName, uint32_t offset, void* buf, uint32_t size)
{
	int ret = 0;
	int fd = 0;

	if ((fd = open(FName, O_RDONLY | O_BINARY)) < 0)
	{
		ret = -1;
		goto __exit;
	}

	if (lseek(fd, offset, SEEK_SET) < 0)
	{
		ret = -2;
		goto __exit;
	}

	if (read(fd, buf, size) != size)
	{
		ret = -3;
		goto __exit;
	}

__exit: 
	close(fd);
	return ret;
}

int FLM_Prase(const void* FName, void* pBuffer, uint32_t* Size, uint32_t* Init, uint32_t* UnInit, uint32_t* EraseChip, uint32_t* EraseSector, uint32_t* ProgramPage)
{
#define LOAD_FUN_NUM 5

	uint8_t buffer[1024] = {0};
	int i = 0, k = 0;
	int found = 0;
	const Elf32_Phdr* pPhdr = (const Elf32_Phdr *) buffer;
	const Elf32_Shdr* pShdr = (const Elf32_Shdr *) buffer;
	const Elf32_Sym* pSymbol = (const Elf32_Sym *) buffer;
	Elf32_Ehdr ehdr = {0};      // ELF文件信息头
	Elf32_Shdr ShdrSym = {0};   // 符号表头
	Elf32_Shdr ShdrStr = {0};   // 字符串表头
	const char* StrFunNameTable[LOAD_FUN_NUM] = { "Init", "UnInit", "EraseChip", "EraseSector", "ProgramPage"};
	int StrFunIndexTable[LOAD_FUN_NUM] = {-1, -1, -1, -1, -1};

	//
	// 读取ELF文件头信息(ELF Header)
	//
	ReadDataFromFile(FName, 0, &ehdr, sizeof(Elf32_Ehdr));

	// 不是ELF格式文件
	if (strstr((const char *)ehdr.e_ident, "ELF") == NULL)
	{
		return -1;
	}

	//
	// 读取程序头信息(Program Header)
	//
	ReadDataFromFile(FName, ehdr.e_phoff, buffer, sizeof(Elf32_Phdr) * ehdr.e_phnum);
	for (i = 0; i < ehdr.e_phnum; i++)
	{
		if (pPhdr[i].p_type == PT_LOAD && (pPhdr[i].p_flags & (PF_X | PF_W | PF_R)) == (PF_X | PF_W | PF_R))
		{
			if (pPhdr[i].p_filesz > sizeof(buffer))  // RAM代码过大
			{
				return -2;
			}
			if(ReadDataFromFile(FName, pPhdr[i].p_offset, pBuffer, pPhdr[i].p_filesz) < 0)  // 提取需要下载到RAM的程序代码
			{
				return -3;
			}
		printf("====:%d\r\n", pPhdr[i].p_filesz);
			*Size = pPhdr[i].p_filesz;
		}
	}

	//
	// 读取节区头部(Sections Header)
	//
	ReadDataFromFile(FName, ehdr.e_shoff, buffer, sizeof(Elf32_Shdr) * ehdr.e_shnum);

	// 查找符号表头并拷贝出来备用
	for (i = 0; i < ehdr.e_shnum; i++)
	{
		if (pShdr[i].sh_type == SHT_SYMTAB)
		{
			memcpy(&ShdrSym, &pShdr[i], sizeof(Elf32_Shdr));

			// 查找字符串表头并拷贝出来备用
			if (pShdr[ShdrSym.sh_link].sh_type == SHT_STRTAB)
			{
				memcpy(&ShdrStr, &pShdr[ShdrSym.sh_link], sizeof(Elf32_Shdr));
				found = 1;
				break;
			}
		}
	}

	if(!found)
	{
		return -4;
	}

	//
	// 根据字符串表头读取所有字符串表
	//
	ReadDataFromFile(FName, ShdrStr.sh_offset, buffer, ShdrStr.sh_size);
	for (i = 0; i < ShdrStr.sh_size; i++)    if (buffer[i] == '\0')    buffer[i] = '\n';
	buffer[ShdrStr.sh_size] = 0;
	for (i = 0; i < LOAD_FUN_NUM; i++)
	{
		char* p = NULL;
		if (StrFunNameTable[i] != NULL && (p = strstr((const char *) buffer, StrFunNameTable[i])) != NULL)
		{
			StrFunIndexTable[i] = (uint32_t) p - (uint32_t) buffer;
		}
	}

	//
	// 读取符号表
	//
	ReadDataFromFile(FName, ShdrSym.sh_offset, buffer, ShdrSym.sh_size);

	// 遍历查询我们用到的函数符号
	for (i = 0; i < ShdrSym.sh_size / sizeof(Elf32_Sym); i++, pSymbol++)
	{
		for (k = 0; k < LOAD_FUN_NUM; k++)
		{
			if (StrFunIndexTable[k] >= 0 && StrFunIndexTable[k] == pSymbol->st_name)  // symbol.st_name的值就是偏移地址
			{
				switch (k)
				{
				case 0:
					*Init = pSymbol->st_value;
					break;
				case 1:
					*UnInit = pSymbol->st_value;
					break;
				case 2:
					*EraseChip = pSymbol->st_value;
					break;
				case 3:
					*EraseSector = pSymbol->st_value;
					break;
				case 4:
					*ProgramPage = pSymbol->st_value;
					break;
				default:
					break;
				}
			}
		}
	}

	return 0;
}

关于Thumb-2指令集与ARM指令集

Thumb 指令集

Thumb 指令可以看作是ARM 指令压缩形式的子集,是针对代码密度的问题而提出的,它具有16 位的代码密度。 Thumb 不是一个完整的体系结构,不能指望处理只执行Thumb 指令而不支持ARM 指令集。因此,Thumb 指令只需要支持通用功能,必要时可以借助于完善的ARM 指令集,比如,所有异常自动进入ARM 状态。

在编写Thumb 指令时,先要使用伪指令CODE16 声明,而且在ARM 指令中要使用BX指令跳转到Thumb 指令,以切换处理器状态。

编写ARM 指令时,则可使用伪指令CODE32声明。

	/* 这8个数是中断halt程序,让函数执行完后返回到这里来执行从而让CPU自动halt住 */
	RAM[0] = 0xE00ABE00;
	RAM[1] = 0x062D780D;
	RAM[2] = 0x24084068;
	RAM[3] = 0xD3000040;
	RAM[4] = 0x1E644058;
	RAM[5] = 0x1C49D1FA;
	RAM[6] = 0x2A001E52;
	RAM[7] = 0x4770D1F2;

上面这段代码要找到对应的机器码指令手册才能翻译得出来具体意思,有兴趣的可以详细去看看。

00000000 <.data>:
   0:   be00            bkpt    0x0000
   2:   e00a            b.n     0x1a
   4:   780d            ldrb    r5, [r1, #0]
   6:   062d            lsls    r5, r5, #24
   8:   4068            eors    r0, r5
   a:   2408            movs    r4, #8
   c:   0040            lsls    r0, r0, #1
   e:   d300            bcc.n   0x12
  10:   4058            eors    r0, r3
  12:   1e64            subs    r4, r4, #1
  14:   d1fa            bne.n   0xc
  16:   1c49            adds    r1, r1, #1
  18:   1e52            subs    r2, r2, #1
  1a:   2a00            cmp     r2, #0
  1c:   d1f2            bne.n   0x4
  1e:   4770            bx      lr

上面那段代码转成C代码如下:

// r0 = initial value
// r1 = ptr
// r2 = count
// r3 = modifier xor'd into r0
uint32_t foo(uint32_t r0, uint32_t r1, uint32_t r2, uint32_t r3)
{
    while (r2 != 0)
    {
        uint32_t r5 = *(uint8_t *)r1;
        r5 <<= 24;
        r0 ^= r5;

        uint32_t r4 = 8;
        do {
            uint32_t b = r0 & (1 << 31);
            r0 <<= 1;
            if (b)
            {
                r0 ^= r3;
            }
            r4 -= 1;
        } while (r4 != 0);

        r1 += 1;
        r2 -= 1;
    }
 return r0;
}

这段汇编代码什么意思呢?

对于汇编不了解的同学我就先简单说一下,不展开说ARM汇编指令了。

bkpt指令是断点中断指令,执行该指令可以暂停程序的运行,以上反汇编代码中除去bkpt指令会操作硬件,其他指令都是软件层面的,例如b指令是跳转指令,ldrb(Load Register Byte)字节数据加载指令,cmp比较指令等。所以除去bkpt指令,其他汇编代码其实就是在做类似于CRC校验算法的一个计算过程。

那么现在这段代码的功能结合C代码也就明确了:

首先使用bkpt指令暂停了程序的运行,然后使用b指令跳转到了0X1A这个地址处运行,

由于内存地址是从0X20000000开始的,也就是跳转到了0X2000001A地址处运行。

执行完这个类似于CRC检验算法的这个过程后,就去执行0X20000021开始的地方, 也就是真正的FLM中提取出来的下载算法代码了,接下来就是通过DAP对目标程序进行调试了。

参考资料

  1. ARM内核STM32学习笔记——Thumb-2指令集与ARM指令集

  2. GitHub上DAPLink关于下载器的提问

  3. 关于自制CMSIS_DAP离线下载器下载算法的代码说明

  4. C语言解析FLM(ELF)格式文件

  5. ELF分析软件ELFParser

  6. ELF格式说明文档:ELF_Format.pdf