目录
作为迪捷软件研发人员,有时与客户交流过程中,客户会问到:「我们的程序如何在你们的SkyEye(全数字实时仿真平台)上运行?你们支持运行什么格式的可执行文件?」,下面就来简单介绍下嵌入式可执行程序相关的内容。
1. 什么是可执行程序?
嵌入式系统由硬件系统和软件系统组成,因此为了使整个嵌入式系统运行起来,必须有相应的程序,我们一般所说的程序,实际上就是存储在硬件设备中的一些可执行代码。可执行代码包括嵌入式操作系统和应用软件。
可执行程序也可叫目标二进制程序,相同的源程序在不同的处理器架构编译工具链下编译链接后会生成对应架构平台下的可执行程序。因而不同架构平台下的可执行程序只能在对应的架构平台下运行,不能直接移植到其他平台下运行。例如Windows(X86指令集)下编译生成的exe程序不能在ARM(ARM指令集)目标平台下运行。
1.1 可执行文件格式
由于不同的开发环境与不同的硬件架构,存储于嵌入式存储设备中的可执行文件格式也不尽相同,但它们基本上包含以下一些典型的特征:
1. 可执行文件的一般信息,如:文件大小、创建时间,文件名,文件权限等。
2. 与硬件处理器架构相关的二进制代码和数据。
3. 符号表与符号重定位表。
4. 调试器需要的调试信息与一些下载时需要的一些信息。
不同的嵌入式环境中,其生成的可执行文件的格式也不相同,主要有以下几种:
ELF文件格式
Executable and linking format(ELF)文件是一种常用、可移植目标文件(object file)格式,它有三种主要类型:
1) 可重定位文件(Relocatable File):包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。
2) 可执行文件(Executable File):包含适合于执行的一个程序,此文件规定了exec() 如何创建一个程序的进程映像。
3) 共享目标文件(Shared Object File):包含可在两种上下文中链接的代码和数据。首先链接编辑器可以将它和其它可重定位文件和共享目标文件一起处理,生成另外一个目标文件。其次,动态链接器(Dynamic Linker)可能将它与某个可执行文件以及其它共享目标一起组合,创建进程映像。
HEX文件格式
Intel HEX文件是记录文本行的ASCII文本文件,在Intel HEX文件中,每一行是一个HEX记录,由十六进制数组成的机器码或者数据常量。记录类型包括:记录数据域,文件结束域,扩展线性地址的记录,扩展段地址的记录。在上面的后2种记录,都是用来提供地址信息的。每次碰到这2个记录的时候,都可以根据记录计算出一个「基」地址。对于后面的数据记录,计算地址的时候,都是以这些「基」地址为基础的。HEX文件是包括地址信息的,在烧写或下载HEX文件的时候,一般都不需要用户指定地址。
COFF文件格式
通用对象文件格式(Common Object File Format),是一种很流行的对象文件格式。
COFF文件一共有8种数据,如下所示:
1. 文件头(File Header)
2. 可选头(Optional Header)
3. 段落头(Section Header)
4. 段落数据(Section Data)
5. 重定位表(Relocation Directives)
6. 行号表(Line Numbers)
7. 符号表(Symbol Table)
8. 字符串表(String Table)
BIN文件格式
BIN文件格式对二进制文件而言,其实没有「格式」。文件只是包括了纯粹的二进制数据。其内部没有地址标记,只包括了数据本身。一般用编程器烧写时,是需要指定地址信息的 。
一般来说,可以由elf文件转化为其它两种文件,hex也可以直接转换为bin文件,但是bin要转化为hex文件必须要给定一个基地址。而hex和bin不能转化为elf文件,因为elf的信息量很大。
elf格式文件转换bin文件的方法:
Objcopy -O binary xxx.elf xxx.bin
2. 可执行程序如何执行?
程序执行时所需要的指令和数据必须在内存中才可以正常运行,最简单的方法就是采用静态装载方法,即将程序运行所需要的指令和数据全都装入内存,这样程序就可以正常运行。
通过SkyEye搭建的虚拟目标系统其中包括存储设备(RAM、FLASH等)的仿真,SkyEye执行可执行程序的流程如下:
1. 解析可执行文件,把程序中的指令和数据信息写入到相应的内存中;
2. 解析可执行文件获取程序第一条运行PC地址,并把运行PC地址给到处理器PC寄存器;
3. 处理器根据PC寄存器中的地址从内存中读取指令,开始执行指令。
3. ELF格式解析
3.1 ELF文件格式视图
ELF文件格式提供了两种视图,分别是链接视图和执行视图。
链接视图是以节(section)为单位,执行视图是以段(segment)为单位。链接视图就是在链接时用到的视图,而执行视图则是在执行时用到的视图。上图左侧的视角是从链接来看的,右侧的视角是执行来看的。整个文件可以分为四个部分:
- ELF header: 描述整个文件的组织。
- Program Header Table: 描述文件中的各种segments,用来告诉系统如何创建进程映像的。
- sections 或者 segments:segments是从运行的角度来描述elf文件,sections是从链接的角度来描述elf文件,也就是说,在链接阶段,我们可以忽略program header table来处理此文件,在运行阶段可以忽略section header table来处理此程序(所以很多加固手段删除了section header table)。从图中我们也可以看出,segments与sections是包含的关系,一个segment包含若干个section。
- Section Header Table: 包含了文件各个section的属性信息。
程序头部表(Program Header Table)如果存在的话,告诉系统如何创建进程映像。
节区头部表(Section Header Table)包含了描述文件节区的信息,比如大小、偏移等。
下面以PowerPC架构的可执行文件为例进行说明:
通过GNU binutils的readelf工具来查看可执行文件相关信息。
1. 可以通过执行命令「readelf -S vxWorks」来查看该可执行文件中有哪些section。
2.通过执行命令readelf –segments vxWorks,可以查看该文件的执行视图。
由上面两图可以看出segment是section的一个集合,sections按照一定规则映射到segment。
3.2 为什么需要区分两种不同视图?
ELF文件被加载到内存中后,系统会将多个具有相同权限(flg值)section合并一个segment。操作系统往往以页为基本单位来管理内存分配,一般页的大小为4096B,即4KB的大小。
同时,内存的权限管理的粒度也是以页为单位,页内的内存是具有同样的权限等属性,并且操作系统对内存的管理往往追求高效和高利用率这样的目标。
ELF文件在被映射时,是以系统的页长度为单位的,那么每个section在映射时的长度都是系统页长度的整数倍,如果section的长度不是其整数倍,则导致多余部分也将占用一个页。而我们从上面的例子中知道,一个ELF文件具有很多的section,那么会导致内存浪费严重。这样可以减少页面内部的碎片,节省了空间,显著提高内存利用率。
3.3 ELF Header结构
ELF Header数据结构定义如下:
#define EI_NIDENT 16
typedef struct {
unsigned char e_ident[EI_NIDENT];
ELF32_Half e_type;
ELF32_Half e_machine;
ELF32_Word e_version;
ELF32__Addr e_entry;
ELF32_Off e_phoff;
ELF32_Off e_shoff;
ELF32_Word e_flags;
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;
通过执行命令readelf -h vxWorks命令,可以看到ELF Header结构的内容。
45:代表字符E
4C:代表字符L
46:代表字符F
01:代表ELF32, 02代表ELF64
02:代表大端,01代表小端
01:版本
在ELF Header中我们需要重点关注以下几个字段:
- e_entry:程序入口地址
- 所谓程序进入点是指当程序真正执行起来的时候,其第一条要运行的指令的运行时地址。如上图vxWorks可执行文件的e_entry指向0x100000,即程序第一条执行的指令地址是0x100000。
- e_ehsize:ELF Header结构大小
- e_phoff、e_phentsize、e_phnum:描述Program Header Table的偏移、大小、结构。
- e_shoff、e_shentsize、e_shnum:描述Section Header Table的偏移、大小、结构。
- e_shstrndx:这一项描述的是字符串表在Section Header Table中的索引,值15表示的是Section Header Table中第15项是字符串表(String Table)。
3.4 Section Header Table表
一个ELF文件中到底有哪些具体的 sections,由包含在这个ELF文件中的 section head table(SHT)决定。在SHT中,针对每一个section,都设置有一个条目(entry),用来描述对应的这个section,其内容主要包括该 section 的名称、类型、大小以及在整个ELF文件中的字节偏移位置等等。
section head table数据结构定义如下:
/*
* Section header
*/
struct Elf32_Shdr {
uint32_t sh_name;
uint32_t sh_type;
uint32_t sh_flags;
uint32_t sh_addr;
uint32_t sh_offset;
uint32_t sh_size;
uint32_t sh_link;
uint32_t sh_info;
uint32_t sh_addralign;
uint32_t sh_entsize;
};
由上图可以看到vxWorks可执行文件Section Header Table中有18个条目,且索引为15确实为section header section string table,与上述ELF Header结构中显示的e_shnum和e_shstrndx的值是一致的。
3.5 Program Header Table表
程序头部(Program Header)描述与程序执行直接相关的目标文件结构信息。用来在文件中定位各个段的映像。同时包含其他一些用来为程序创建映像所必须的信息。
可执行文件或者共享目标文件的程序头部是一个结构数组,每个结构描述了一个段或者系统准备程序执行所必须的其他信息。目标文件的「段」包含一个或者多个「节区」,也就是「段内容(Segment Contents)」。程序头部仅对可执行文件和共享目标文件有意义。
程序头部的数据结构如下:
/*
* Program header
*/
typedef struct
{
Elf32_Word p_type; //此数组元素描述的段的类型,或者如何解释此数组元素的信息。
Elf32_Off p_offset; //此成员给出从文件头到该段第一个字节的偏移
Elf32_Addr p_vaddr; //此成员给出段的第一个字节将被放到内存中的虚拟地址
Elf32_Addr p_paddr; //此成员仅用于与物理地址相关的系统中。
Elf32_Word p_filesz; //此成员给出段在文件映像中所占的字节数。可以为0。
Elf32_Word p_memsz; //此成员给出段在内存映像中占用的字节数。可以为0。
Elf32_Word p_flags; //此成员给出与段相关的标志。
Elf32_Word p_align; //此成员给出段在文件中和内存中如何对齐。
} Elf32_phdr;
4. SkyEye支持运行哪些格式的可执行文件
SkyEye支持上述ELF、COFF、HEX、BIN文件格式的加载和运行。
SkyEye常用的几种加载命令包括如下:
load-binary命令
命令说明:用于加载ELF、COFF和HEX文件格式的二进制文件。
load-binary <cpuname><filepath>
参数:
描述:对于包含符号信息、调试信息等内容的二进制文件直接使用load-binary加载。
load-file命令
命令说明:用于加载BIN文件格式的二进制文件到指定的内存中。
load-file <cpuname ><filepath><address>
参数:
描述:对于一些可执行文件需要从指定地址开始加载,使用load-file指定其加载地址。
load-bin-binary命令
命令说明:用于加载BIN文件格式二进制到指定内存并指定第一条运行PC地址
load-bin-binary <cpuname><filepath><address><length><start_pc>
参数:
描述:对于一些bin文件需要指定其加载的加载地址、文件大小和PC起始地址,使用load-bin-binary进行加载。