嵌入式常见可执行文件格式介绍
在嵌入式系统中,常见的可执行文件大概有Hex、Axf、Bin 及 ELF等几种格式,其中ELF是需要运行在操作系统下,其它几种主要用于非操作系统下的可执行文件的格式。
一、Hex
Hex文件,一般是指Intel标准的十六进制文件。Intel Hex 文件常用来保存单片机或其他处理器的目标程序代码。它保存物理程序存储区中的目标代码映象。一般编程器均支持生成此种格式文件。如,Keil下可通过配置相关选项来生成Hex目标文件。
Hex文件由记录(RECORD)组成。在Hex文件里面,每一行代表一个记录。
形如 :BBAAAATTHHHH...HHHHCC
BB : 字节个数。
AAAA: 数据记录的开始地址,高位在前,低位在后。
TT : Type
00 数据记录,用来记录数据。
01 记录结束,放在文件末尾,用来标识文件结束。
02 用来标识扩展段地址的记录
04 扩展地址记录(表示32位地址的前缀)
HHHH:一个字(Word)的数据记录,高字节在前,低字节在后。TT之后共有BB/2 个字的数据 。
CC : 占据一个Byte的CheckSum
举例分析:
:020000040000FA:10000400FF00A0E314209FE5001092E5011092E5A3:00000001FF
第1条记录:
长度为0x02,Load offset为0000,Type为04,说明该记录为扩展段地址记录。数据为0000,校验和为FA。
第2条记录:
长度为0x10(16),Load offset为0004,Type为00,说明该记录为数据记录。数据为FF00A0E314209FE5001092E5011092E5,共16个字节,记录的校验和为A3。
第3条记录:
长度为00,Load offset为0000,Type= 01,校验和为FF。类型为01,说明这个是一个END OF FILE RECORD,标识文件的结尾。Hex结束符一般以:00000001FF结尾。
从这个记录的长度和数据,我们可以计算出基地址为0x0000。后面的数据记录都以此地址为基地址。第2条记录此时的基地址为0x0000,加上OFFSET,这个记录里的16BYTE的数据的起始地址就是0x0000 + 0x0004 = 0x0004. 其实际的数据只有16个BYTE:FF00A0E314209FE5001092E5011092E5。
二、Bin
Bin文件是最纯粹的二进制机器代码, 或者说是"顺序格式"。按照assembly code顺序翻译成binary machine code,内部没有地址标记。Bin是直接的内存映象表示,二进制文件大小即为文件所包含的数据的实际大小。
三、Axf
Axf文件由ARM编译器产生,除了包含bin的内容之外,还附加其他调试信息,这些调试信息加在可执行的二进制数据之前。调试时这些调试信息不会下载到RAM中,真正下载到RAM中的信息仅仅是可执行代码。
因此,如果ram小于axf文件的大小,程序是完全有可能在ram中调试的,只要axf除去调试信息后文件小于ram的大小即可。 调试信息有以下功用: 1、 可将源代码包括注释夹在反汇编代码中,这样我们可随时切换到源代码中进行调试。 2、 我们还可以对程序中的函数调用情况进行跟踪(通过Watch & Call Stack Window查看)。 3、对变量进行跟踪(利用Watch & Call Stack Window)。 调试信息虽然有用,但程序功能实现后,在目标文件和库中减少调试信息却是非常有益的。减少调试信息可减少目标文件和库大小、加快链接速度、减小最终镜象代码。
以下几种方法可用来减少每个源文件产生的调试信息: 1、避免在头文件中条件性使用#define,链接器不能移除共用的调试部分,除非这些部分是完全一样的。 2、更改C/C++源文件,使#included包含的所有头文件有相同顺序。 3、尽量使用数量较多的小头文件而不是较大的单一头文件,这有利于链接器获取更多的通用块。 4、程序中最好只包含必须用到的头文件。避免重复包含头文件,可使用编译器选项--remarks来产生警告信息; 5、使用编译命令行选项--no_debug_macros,从调试表中丢弃预处理宏定义。
四、ELF文件格式
ELF ( Executable and Linkable Forma ) 是一种可执行文件、目标文件和库使用的文件的格式,类似于Windows下的PE文件格式。ELF格式是UNIX系统实验室作为ABI(Application Binary Interface)而开发和发布的,早已经是Linux下的标准格式了。 下面以简单测试程序来具体讲述ELF文件格式。
#include <stdio.h> int add(int a,int b) { printf("Number are added together\n"); return a + b; } int main() { int a,b; a = 3; b = 4; int ret = add(a,b); printf("Result:%u\n",ret); exit(0); } //gcc test.c -o test //gcc test.c -c -o test.o
一. ELF概述
ELF文件是x86 Linux系统下的一种常用目标文件(objectfile)格式,有三种主要类型:
ELF主要包括三种类型文件:
(1)适于连接的可重定位文件(relocatablefile),。
(2)适于执行的可执行文件(executable file),用于提供程序的进程映像,加载到内存执行。
(3)共享目标文件(shared object file),连接器可将它与其它可重定位文件和共享目标文件连接成其它的目标文件,动态连接器又可将它与可执行文件和其它共享目标文件结合起来创建一个进程映像。 ELF文件格式比较复杂。
-
可重定位文件(relocatable):编译器和汇编器产生的.o文件,可与其它目标文件一起创建可执行文件和共享目标文件,被Linker所处理
-
可执行文件(executable):Linker对.o文件进行处理输出的文件,进程映像
-
共享对象文件(shared object):动态库文件.so
下面是三种类型的示例:
新的gcc编译出来的可执行文件是shared object,而不是executable:
查看gcc版本信息,发现新的gcc默认开启了PIE, --enable-default-pie
这里要谈到PIE(Position-Independent-Executable),这是Linux程序的一种保护机制,是gcc的一个功能选项,目的是为了让程序能在任意地址装载,减少了系统攻击的风险。
如需要编译后的可执行文件是executable类型,需要在编译的时候加上-no-pie选项:
32bit系统及老版本gcc信息如下,老版本是没有pie的选项的,编译出来的可执行文件类型就是executable:
ELF的布局如下:
由图可以知道,ELF文件从概念上来说包括了5个部分:
-
ELF header,描述体系结构和操作系统等基本信息,指出section header table和program header table在文件的位置
-
program header table,这个是从运行的角度来看ELF文件的,主要给出了各个segment的信息,在汇编和链接过程中没用
-
section header table,这个保存了所有的section的信息,这是从编译和链接的角度来看ELF文件的
-
sections,就是各个节区
-
segments,就是在运行时的各个段
注意,经过上面解释我们可以看到,其实sections和segments占的一样的地方。这是从链接和加载的角度来讲的。左边是链接视图,右边是加载视图,sections是程序员可见的,是给链接器使用的概念,而segments是程序员不可见的,是给加载器使用的概念。一般是一个segment包含多个section。Windows的PE就没有这个program header table和section header table区分都统一为section,只是在加载时会进行处理。所以program header table和section header table都是可选的。
二. ELF的组成结构
在介绍这部分之前,前把定义中的各个类型数据结构的大小放在这里。
(1) ELF header
ELF Header描述了体系结构和操作系统等基本信息,并指出Section Header Table和Program Header Table在文件中的什么位置,每个成员的解释参见注释。
#define EI_NIDENT 16 typedef struct{ /*ELF的一些标识信息,固定值*/ unsigned char e_ident[EI_NIDENT]; /*目标文件类型:1-可重定位文件,2-可执行文件,3-共享目标文件等*/ Elf32_Half e_type; /*文件的目标体系结构类型:3-intel 80386*/ Elf32_Half e_machine; /*目标文件版本:1-当前版本*/ Elf32_Word e_version; /*程序入口的虚拟地址,如果没有入口,可为0*/ Elf32_Addr e_entry; /*程序头表(segment header table)的偏移量,如果没有,可为0*/ Elf32_Off e_phoff; /*节区头表(section header table)的偏移量,没有可为0*/ Elf32_Off e_shoff; /*与文件相关的,特定于处理器的标志*/ Elf32_Word e_flags; /*ELF头部的大小,单位字节*/ Elf32_Half e_ehsize; /*程序头表每个表项的大小,单位字节*/ Elf32_Half e_phentsize; /*程序头表表项的个数*/ Elf32_Half e_phnum; /*节区头表每个表项的大小,单位字节*/ Elf32_Half e_shentsize; /*节区头表表项的数目*/ Elf32_Half e_shnum; /*某些节区中包含固定大小的项目,如符号表。对于这类节区,此成员给出每个表项的长度字节数。*/ Elf32_Half e_shstrndx; }Elf32_Ehdr; typedef struct { /*ELF的一些标识信息,固定值*/ unsigned char e_ident[EI_NIDENT]; /*目标文件类型:1-可重定位文件,2-可执行文件,3-共享目标文件等*/ Elf64_Half e_type; /*文件的目标体系结构类型:3-intel 80386*/ Elf64_Half e_machine; /*目标文件版本:1-当前版本*/ Elf64_Word e_version; /*程序入口的虚拟地址,如果没有入口,可为0*/ Elf64_Addr e_entry; /*程序头表(segment header table)的偏移量,如果没有,可为0*/ Elf64_Off e_phoff; /*节区头表(section header table)的偏移量,没有可为0*/ Elf64_Off e_shoff; /*与文件相关的,特定于处理器的标志*/ Elf64_Word e_flags; /*ELF头部的大小,单位字节*/ Elf64_Half e_ehsize; /*程序头表每个表项的大小,单位字节*/ Elf64_Half e_phentsize; /*程序头表表项的个数*/ Elf64_Half e_phnum; /*节区头表每个表项的大小,单位字节*/ Elf64_Half e_shentsize; /*节区头表表项的数目*/ Elf64_Half e_shnum; /*某些节区中包含固定大小的项目,如符号表。对于这类节区,此成员给出每个表项的长度字节数。*/ Elf64_Half e_shstrndx; } Elf64_Ehdr;
这里简单解释一下最后一个字段e_shstrndx的含义,“e_shstrndx”是Elf32_Ehdr的最后一个成员,它是“Section header string table index”的缩写。我们知道段表字符串表本身也是ELF文件中的一个普通的段,知道它的名字往往叫做“.shstrtab”。那么这个“e_shstrndx”就表示“.shstrtab”在段表中的下标,即段表字符串表在段表中的下标。
下面是test的ELF header结构各个数据成员对应的值:
可以看到这个ELF的基本信息,比如,体系结构和操作系统,Section header table中有30个section,从4420开始,每个40个字节,Program header table中有9个segment,每个32字节。下面再从字节码上面看看具体的。标出了某些结构,可以对照上面的结构看。
(2) program header table与grogram header entry
程序头表是从加载的角度来看ELF文件的,目标文件没有该表,每一个表项提供了各段在虚拟地址空间和物理地址空间的大小、位置、标志、访问权限和对其方面的信息。从上面知道,test中有9个segment,如下图:
下面对其中的一些进行简单的介绍。
-
PHDR保存程序头表
-
INTERP指定在程序已经从可执行文件映射到内存之后,必须调用的解释器。在这里,解释器并不意味着二进制文件的内容必须由另一个程序解释。它指的是这样一个程序:通过链接其他库,来满足未解决的引用。通常/lib/ld-linux.so.2、/lib/ld-linux-ia-64.so.2等库,用于在虚拟地址空间中插入程序运行所需要的动态库。对几乎所有的程序来说,可能C标准库都是必须映射的。还需要添加的各种库包括,GTK、数学库、libjpeg等等
-
LOAD表示一个需要从二进制文件映射到虚拟地址空间的段。其中保存了常量数据(如字符串),程序的目标代码等。
-
DYNAMIC段保存了由动态链接器(即,INTERP中指定的解释器)使用的信息。
-
NOTE保存了专有信息
一个entry对应一个segment,由如下的数据结构表示
typedef struct { /*segment的类型:PT_LOAD= 1 可加载的段*/ Elf32_Word p_type; /*从文件头到该段第一个字节的偏移*/ Elf32_Off p_offset; /*该段第一个字节被放到内存中的虚拟地址*/ Elf32_Addr p_vaddr; /*在linux中这个成员没有任何意义,值与p_vaddr相同*/ Elf32_Addr p_paddr; /*该段在文件映像中所占的字节数*/ Elf32_Word p_filesz; /*该段在内存映像中占用的字节数*/ Elf32_Word p_memsz; /*段标志*/ Elf32_Word p_flags; /*p_vaddr是否对齐*/ Elf32_Word p_align; } Elf32_phdr;
(3) section header table与section header entry
节表头包含了文件中的各个节,每个节都指定了一个类型,定义了节数据的语义。各节都指定了大小和在二进制文件内部的偏移。从上面知道,test中有30个section,如下图:
下面对其中的一些进行简单的介绍:
-
.interp保存了解释器的文件名,这是一个ASCII字符串
-
.data保存初始化的数据,这是普通程序数据一部分,可以再程序运行时修改
-
.rodata保存了只读数据,可以读取但不能修改。例如,编译器将出现在printf语句中的所有静态字符串封装到该节
-
.init和.fini保存了进程初始化和结束所用的代码,这两个节通常都是由编译器自动添加
-
.gnu.hash是一个散列表,允许在不对全表元素进行线性搜索的情况下,快速访问所有的符号表项
section的结构定义如下:
typedef struct{ /*节区名称*/ Elf32_Word sh_name; /*节区类型:PROGBITS-程序定义的信息,NOBITS-不占用文件空间(bss),REL-重定位表项*/ Elf32_Word sh_type; /*每一bit位代表一种信息,表示节区内的内容是否可以修改,是否可执行等信息*/ Elf32_Word sh_flags; /*如果节区将出现在进程的内存影响中,此成员给出节区的第一个字节应处的位置*/ Elf32_Addr sh_addr; /*节区的第一个字节与文件头之间的偏移*/ Elf32_Off sh_offset; /*节区的长度,单位字节,NOBITS虽然这个值非0但不占文件中的空间*/ Elf32_Word sh_size; /*节区头部表索引链接*/ Elf32_Word sh_link; /*节区附加信息*/ Elf32_Word sh_info; /*节区带有地址对齐的约束*/ Elf32_Word sh_addralign; /*某些节区中包含固定大小的项目,如符号表,那么这个成员给出其固定大小*/ Elf32_Word sh_entsize; }Elf32_Shdr;
这就是ELF的大致结构了。
总结
1、Hex文件包含地址信息而Bin文件只包含数据本身,烧写或下载Hex文件时,一般不需要用户指定地址,因为Hex文件内部已经包含了地址信息。烧写Bin文件时则需要用户指定烧录的地址信息。
2、Hex文件是用ASCII码来表示二进制的数值。例如8-BIT的二进制数值0x7F,用ASCII来表示就需要分别表示字符‘7’和字符‘F’,每个字符均需要一个字节,因此Hex文件至少需要2倍Bin文件的空间。
3、可由elf文件转化为Hex和Bin两种文件,Hex也可以直接转换成Bin文件,但Bin要转化为Hex文件必须要给定一个基地址。
4、Hex和Bin文件不能转化为elf文件,因为elf头需要的信息量比较多。
5、Axf文件可以转化为Bin文件,Keil下用命令fromelf-nodebug xx.axf -bin xx.bin即可。