1.说明
2.elf文件的基本格式
3.elf文件的头部信息
4.elf文件的节区(Section)
4.1 节区的作用
4.2 节区的组成
5.elf文件的段(Segment)
6.用python解析elf文件
7.总结
1.说明ELF的英文全称是The Executable and Linking Format,最初是由UNIX系统实验室开发、发布的ABI(Application Binary Interface)接口的一部分,也是Linux的主要可执行文件格式。
从使用上来说,主要的ELF文件的种类主要有三类:
可执行文件(.out):Executable File,包含代码和数据,是可以直接运行的程序。其代码和数据都有固定的地址 (或相对于基地址的偏移 ),系统可根据这些地址信息把程序加载到内存执行。可重定位文件(.o文件):Relocatable File,包含基础代码和数据,但它的代码及数据都没有指定绝对地址,因此它适合于与其他目标文件链接来创建可执行文件或者共享目标文件。共享目标文件(.so):Shared Object File,也称动态库文件,包含了代码和数据,这些数据是在链接时被链接器(ld)和运行时动态链接器(ld.so.l、libc.so.l、ld-linux.so.l)使用的。本文主要从elf文件的组成构造的角度来进行分析,将elf文件的解析通过一步一步的分析得到里面的信息,同时通过python脚本解析,可以直观的看到文件的信息,通过本文的阅读,将对elf文件格式有着更加深刻的理解。
2.elf文件的基本格式elf文件是有一定的格式的,从文件的格式上来说,分为汇编器的链接视角与程序的执行视角两种去分析ELF文件。
从程序执行视角来说,这就是Linux加载器加载的各种Segment的集合。比如只读代码段、数据的读写段、符号段等等。而从链接的视角上来看,elf又分为各种的sections。
注意Section Header Table和Program Header Table并不是一定要位于文件开头和结尾的,其位置由ELF Header指出,上图这么画只是为了清晰。
为了彻底的弄清楚elf文件的内容,可以先从ELF文件的头部开始分析。
3.elf文件的头部信息对于elf头部文件信息,首先可以可以查看一下内存的布局情况:
根据readelf可以得到该文件的头部信息的情况。
根据定义,elf32的结构体定义,在Linux上可以在/usr/include/elf.h中找到
typedefstruct
{
unsignedchare_ident[EI_NIDENT];/*Magicnumberandotherinfo*/
Elf32_Halfe_type;/*Objectfiletype*/
Elf32_Halfe_machine;/*Architecture*/
Elf32_Worde_version;/*Objectfileversion*/
Elf32_Addre_entry;/*Entrypointvirtualaddress*/
Elf32_Offe_phoff;/*Programheadertablefileoffset*/
Elf32_Offe_shoff;/*Sectionheadertablefileoffset*/
Elf32_Worde_flags;/*Processor-specificflags*/
Elf32_Halfe_ehsize;/*ELFheadersizeinbytes*/
Elf32_Halfe_phentsize;/*Programheadertableentrysize*/
Elf32_Halfe_phnum;/*Programheadertableentrycount*/
Elf32_Halfe_shentsize;/*Sectionheadertableentrysize*/
Elf32_Halfe_shnum;/*Sectionheadertableentrycount*/
Elf32_Halfe_shstrndx;/*Sectionheaderstringtableindex*/
}Elf32_Ehdr;
上述的Elf32_Half定义
/*Typefora16-bitquantity.*/
typedefuint16_tElf32_Half;
其中Elf32_Word的定义
/*Typesforsignedandunsigned32-bitquantities.*/typedefuint32_tElf32_Word;
然后Elf32_Addr与Elf32_Off定义
/*Typeofaddresses.*/
typedefuint32_tElf32_Addr;
/*Typeoffileoffsets.*/
typedefuint32_tElf32_Off;
有了这些数据结构的信息,然后对应具体的数据细节如下:
e_ident[EI_NIDENT]文件的标识以及标识描述了elf如何编码等信息。
Magic: 7f 45 4c 46 01 01 01 000000000000000000关于该结构体的索引可以看下面的表格:
名称取值目的EI_MAG00文件标识(0x7f)EI_MAG11文件标识(E)EI_MAG22文件标识(L)EI_MAG33文件标识(F)EI_CLASS4文件类EI_DATA5数据编码EI_VERSION6文件版本EI_PAD7补齐字节开始处EI_NIDENT16e_ident[]大小EI_CLASS的内容,当取值为0时,是非法类别,1是32位的目标,2是64位的目标。这里是1所以程序是32位的目标。
EI_DATA表示数据的编码,当为0时,表示非法数据编码,1表示高位在前,2表示低位在前。
EL_VERSION表示了elf的头部版本号码。
前面四个基本上确定的,内容第一个字符为7f,后面用ELF字符串表示该文件为ELF格式。
e_type该数据类型是uint16_t数据类型的,占两个字节。通过字段查看,可以看到这个值为00 02。表格定义如下:
名称取值含义ET_NONE0x0000未知目标文件格式ET_ERL0x0001可重定位文件ET_EXEC0x0002可执行文件ET_DYN0x0003共享目标文件ET_CORE0x0004Core文件(转储格式)ET_LOPROC0xff00特定处理器文件ET_HIPROC0xffff特定处理器文件对应表格内容,可以看到类型为EXEC即可执行文件类型。
e_machine由字段可以看到为00 28,关于这个字段的解析,基本上就是表示该elf文件是针对哪个处理器架构的。
下面只列出几个常见的架构的序号
名称取值含义EM_NONE0No machineEM_SPARC2SPARCEM_3863Intel 80386EM_MIPS8MIPS I ArchitectureEM_PPC0x14PowerPCEM_ARM0x28Advanced RISC Machines ARM通过上述的表格,可以看到该架构是ARM处理器上运行的程序。
e_version该字段占四个字节,表示当前文件版本的信息。现在取值为00 00 00 01。从取值上来看
名称取值含义EV_NONE0非法版本EV_CURRENT1当前版本e_entry这里表示程序的入口地址,目前为四字节,所以通过字段解析到的内容为00 00 80 00。得到可执行程序的入口地址为0x8000。
e_phoff该字段表示程序表头偏移。占四个字节,根据字段解析,可以查看当前的偏移量为00 00 00 34。也就是实际的偏移量为52个字节。这52个字节其实就是头部的信息数据结构体的大小。
e_shoff该区域比较重要,记录了section的偏移地址。为四字节,解析出来的字段为0x00 04 24 5c。所以得到地址为0x4245c。
根据这个偏移得到section的内容:
通过readelf -t也可以得到类似的结果。
关于节区如何解析。后面再进行描述。
e_flags特定处理器格式的标志,这里的字段解析为05 00 02 00。与特定的处理器相关。
e_ehsizeelf文件的头部大小。该取值与头文件结构体的大小相关,目前为52字节,即00 34。
e_phentsize程序头部表项大小,当前取值为00 20,为32个字节,这里表示
关于程序表项的解析,后面再进行具体分析。
e_phnum目前取值为00 01,这里表示程序头的个数当前只有一个程序头,如果有多个程序头表,那么会在elf头文件之后,也就是52个字节之后,依次向下排列。因为这里是1,所以只有1个程序头。
e_shentsize表示节区头部表格大小,解析字段为00 28,也就是第一个节区的大小为40个字节的偏移处。根据e_shoff可以知道。
将从e_shoff的区域向后面偏移40个字节,得到第一个节区的内容。
e_shnum节区的数量,由字段解析得到数据为00 11。此时得到节区的数量为17个。通过readelf -t也可以解析到节区的数量为17个。
bigmagic@bigmagic:~/work/python_elf/elf$readelf-trtthread.elfe_shstrndx
Thereare17sectionheaders,startingatoffset0x4245c:
标记字符串节区的索引。当前的解析为00 0e。也就是14个节区为字符节区。
到这里,头部信息的相关字段就解析完成了。
4.elf文件的节区(Section)elf文件中的节是从编译器链接角度来看文件的组成的。从链接器的角度上来看,包括指令、数据、符号以及重定位表等等。
4.1 节区的作用在可从定位的可执行文件中,节区描述了文件的组成,节的位置等信息。通过readelf -s可以查看信息。
这些节信息通过特定的地址偏移组成了一个elf文件的整体。
4.2 节区的组成关于理解ELF中的Section。首先需要知道程序的链接视图,在编译器将一个一个.o文件链接成一个可以执行的elf文件的过程中,同时也生成了一个表。这个表记录了各个Section所处的区域。在程序中,程序的section header有多个,但是大小是一样。拿elf32文件来说
typedefstruct{
Elf32_Wordsh_name;/*Sectionname(stringtblindex)*/
Elf32_Wordsh_type;/*Sectiontype*/
Elf32_Wordsh_flags;/*Sectionflags*/
Elf32_Addrsh_addr;/*Sectionvirtualaddratexecution*/
Elf32_Offsh_offset;/*Sectionfileoffset*/
Elf32_Wordsh_size;/*Sectionsizeinbytes*/
Elf32_Wordsh_link;/*Linktoanothersection*/
Elf32_Wordsh_info;/*Additionalsectioninformation*/
Elf32_Wordsh_addralign;/*Sectionalignment*/
Elf32_Wordsh_entsize;/*Entrysizeifsectionholdstable*/
}Elf32_Shdr;
根据e_shoff可以找到section的地址,根据e_shentsize可以找到具体的第一个section的内容。
如果要找到每个段的具体细节,首先可以根据e_shstrndx找到节的字段。由于e_shstrndx=14。而且每个为40字节。那么一共是560字节的偏移。从e_shoff的地址0x4245c开始,首先偏移了e_shentsize也就是40个字节。然后向下得到40x14个Section表项。最后可以得到e_shstrndx对应的节区。
为什么首先需要得到这个字符串节区,通过这个就可以得到节区的名字了。然后通过计算,节区字符串存在的区域:
每个字符串以\\0结尾。大小为0000ab也就是171个字节。接下来我们来举个具体的例子来解析Section。比如要读取.text的段。那么首先看一下细节。
首先从字段结构体上进行分析:
sh_name表示从e_shstrndx的偏移地址开始,得到的字符字符串信息为该段的名字。目前解析到的为0x1b。最后算出得到实际的名称为.text。
sh_type字段的类型为01,关于sh_type的类型,解析如下:
/*Legalvaluesforsh_type(sectiontype).*/
#defineSHT_NULL0/*Sectionheadertableentryunused*/
#defineSHT_PROGBITS1/*Programdata*/
#defineSHT_SYMTAB2/*Symboltable*/
#defineSHT_STRTAB3/*Stringtable*/
#defineSHT_RELA4/*Relocationentrieswithaddends*/
#defineSHT_HASH5/*Symbolhashtable*/
#defineSHT_DYNAMIC6/*Dynamiclinkinginformation*/
#defineSHT_NOTE7/*Notes*/
#defineSHT_NOBITS8/*Programspacewithnodata(bss)*/
#defineSHT_REL9/*Relocationentries,noaddends*/
#defineSHT_SHLIB10/*Reserved*/
#defineSHT_DYNSYM11/*Dynamiclinkersymboltable*/
#defineSHT_INIT_ARRAY14/*Arrayofconstructors*/
#defineSHT_FINI_ARRAY15/*Arrayofdestructors*/
#defineSHT_PREINIT_ARRAY16/*Arrayofpre-constructors*/
#defineSHT_GROUP17/*Sectiongroup*/
#defineSHT_SYMTAB_SHNDX18/*Extendedsectionindeces*/
#defineSHT_NUM19/*Numberofdefinedtypes.*/
#defineSHT_LOOS0x60000000/*StartOS-specific.*/
#defineSHT_GNU_ATTRIBUTES0x6ffffff5/*Objectattributes.*/
#defineSHT_GNU_HASH0x6ffffff6/*GNU-stylehashtable.*/
#defineSHT_GNU_LIBLIST0x6ffffff7/*Prelinklibrarylist*/
#defineSHT_CHECKSUM0x6ffffff8/*ChecksumforDSOcontent.*/
#defineSHT_LOSUNW0x6ffffffa/*Sun-specificlowbound.*/
#defineSHT_SUNW_move0x6ffffffa
#defineSHT_SUNW_COMDAT0x6ffffffb
#defineSHT_SUNW_syminfo0x6ffffffc
#defineSHT_GNU_verdef0x6ffffffd/*Versiondefinitionsection.*/
#defineSHT_GNU_verneed0x6ffffffe/*Versionneedssection.*/
#defineSHT_GNU_versym0x6fffffff/*Versionsymboltable.*/
#defineSHT_HISUNW0x6fffffff/*Sun-specifichighbound.*/
#defineSHT_HIOS0x6fffffff/*EndOS-specifictype*/
#defineSHT_LOPROC0x70000000/*Startofprocessor-specific*/
#defineSHT_HIPROC0x7fffffff/*Endofprocessor-specific*/
#defineSHT_LOUSER0x80000000/*Startofapplication-specific*/
#defineSHT_HIUSER0x8fffffff/*Endofapplication-specific*/
当前为1,所以得到数据为程序数据。比如.text .data .rodata等等。
sh_flags表示段的标志,A表示分配的内存、AX表示分配可执行、WA表示分配内存并且可以修改。
sh_addr加载后程序段的虚拟地址
sh_offset表示段在文件中的偏移。
sh_size段的长度
sh_addralign段对齐
sh_entsize每项固定的大小
5.elf文件的段(Segment)关于Linking View与Execution View的具体含义,可以查看
http://www.skyfree.org/linux/references/ELF_Format.pdf这里有一张图值得研究一下:
对于链接视图,也就是我们前面分析的Section,可以理解目标代码文件的内容布局。而右边的ELF的执行视图,则可以理解为可执行的文件内容布局。链接视图由sections组成,而可执行的文件的内容由segment组成。
两者是有一些区别的,我们平时在进行程序构建的时候理解的.text、.bss、.data段,这些都是section,也就节区的概念。这些段通过section header table进行组织与重定位。
但是对于segment来说,程序代码段、数据段是Segment。代码段又可以分为.text,数据段又分为.data、.bss等。
通过readelf -l可以查看具体的可执行文件的细节。
这里的信息和程序的加载直接相关。具体的elf文件加载过程这篇文章不会多说,后面会写文章专门叙述。本文的目的是elf文件格式的解析过程。
6.用python解析elf文件为了验证上述的分析过程是否合理,可以通过python脚本来解析elf文件。得到elf文件相关的信息。目前采用的是python3进行解析。
第一步:程序组织
当前组织的程序分为三步:
1.校验elf文件
2.显示elf头部信息
3.解析段信息
if__name__==\'__main__\':
file=sys.argv[1]
verify_elf(file)
display_elfhdr(file)
display_sections(file)
首先需要导入sys模块import sys。
当程序执行的时候输入python elf_parse.py rtthread.elf就可以向下执行了。
第二步:校验elf
该函数的作用主要是校验elf文件,并且将相关的信息存到字典里面。
elfhdr={}defverify_elf(filename):
f=open(filename,\'rb\')
elfident=f.read(16)
magic=[iforiinelfident]
if(magic[0]!=127ormagic[1]!=ord(\'E\')ormagic[2]!=ord(\'L\')ormagic[3]!=ord(\'F\')):
print(\'yourinputfile%snotaelffile\'%filename)
return
else:
temp=f.read(struct.calcsize(\'2H5I6H\'))
temp=struct.unpack(\'2H5I6H\',temp)
globalelfhdr
elfhdr[\'magic\']=magic
elfhdr[\'e_type\']=temp[0]
elfhdr[\'e_machine\']=temp[1]
elfhdr[\'e_version\']=temp[2]
elfhdr[\'e_entry\']=temp[3]
elfhdr[\'e_phoff\']=temp[4]
elfhdr[\'e_shoff\']=temp[5]
elfhdr[\'e_flags\']=temp[6]
elfhdr[\'e_ehsize\']=temp[7]
elfhdr[\'e_phentsize\']=temp[8]
elfhdr[\'e_phnum\']=temp[9]
elfhdr[\'e_shentsize\']=temp[10]
elfhdr[\'e_shnum\']=temp[11]
elfhdr[\'e_shstrndx\']=temp[12]
f.close()
校验的方法是读取前面的四个字节,是否第一个字节为7E,后面为ELF字符,如果满足,则表示为ELF文件。
后面是将数组进行了一个填充。
第三步:展示elf文件的头部信息
defdisplay_elfhdr(elffile):
globalelfhdr
print(\'ELFHeader\')
magic=elfhdr[\'magic\']
print(\'Magic:%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d\'%(magic[0],magic[1],magic[2],magic[3],magic[4],magic[5],magic[6],magic[7],magic[8],magic[9],magic[10],magic[11],magic[12],magic[13],magic[14],magic[15]))
ifmagic[4]==1:
print(\'Class:ELF32\')
else:
print(\'Class:ELF64\')
ifmagic[5]==1:
print(\'Data:2\'scomplement,littleendian\')
else:
print(\'Data:2\'scomplement,bigendian\')
print(\'Version:%d(current)\'%magic[6])
ifmagic[7]==0:
os_abi=\'SystemVABI\'
elifmagic[7]==1:
os_abi=\'HP-Uxoperatingsystem\'
elifmagic[7]==255:
os_abi=\'Standalone(embedded)application\'
print(\'OS/ABI:%s\'%os_abi)
print(\'ABIVersion:%d\'%magic[8])
ifelfhdr[\'e_type\']==0:
type=\'Nofiletype\'
elifelfhdr[\'e_type\']==1:
type=\'Relocatableobjectfile\'
elifelfhdr[\'e_type\']==2:
type=\'Executablefile\'
elifelfhdr[\'e_type\']==3:
type=\'Corefile\'
print(\'Type:%s\'%type)
print(\'Machine:%d\'%elfhdr[\'e_machine\'])
print(\'Version:0x%x\'%elfhdr[\'e_version\'])
print(\'Entrypointaddress:0x%x\'%elfhdr[\'e_entry\'])
print(\'Startofprogramheaders:%d(bytesintofile)\'%elfhdr[\'e_phoff\'])
print(\'Startofsectionheaders:%d(bytesintofile)\'%elfhdr[\'e_shoff\'])
print(\'Flags:0x%x\'%elfhdr[\'e_flags\'])
print(\'Sizeofthisheader:%d(bytes)\'%elfhdr[\'e_ehsize\'])
print(\'Sizeofprogramheaders:%d(bytes)\'%elfhdr[\'e_phentsize\'])
print(\'Numberofprogramheaders:%d\'%elfhdr[\'e_phnum\'])
print(\'Sizeofsectionheaders:%d(bytes)\'%elfhdr[\'e_shentsize\'])
print(\'Numberofsectionheaders:%d\'%elfhdr[\'e_shnum\'])
print(\'Sectionheaderstringtableindex:%d\'%elfhdr[\'e_shstrndx\'])
该函数主要是解析了elf头部信息中对应的相关字节,并且做了解析过程。
第四步:解析sections
在解析具体的段的时候,主要是利用地址偏移找到相关的符号表名称,然后根据偏移算出细节。
defdisplay_sections(elffile):verify_elf(elffile)
sections=[]
globalelfhdr
sec_start=elfhdr[\'e_shoff\']
sec_size=elfhdr[\'e_shentsize\']
f=open(elffile,\'rb\')
f.seek(sec_start)
foriinrange(0,elfhdr[\'e_shnum\']):
temp=f.read(sec_size)
temp=struct.unpack(\'10I\',temp)
sec={}
sec[\'sh_name\']=temp[0]
sec[\'sh_type\']=temp[1]
sec[\'sh_flags\']=temp[2]
sec[\'sh_addr\']=temp[3]
sec[\'sh_offset\']=temp[4]
sec[\'sh_size\']=temp[5]
sec[\'sh_link\']=temp[6]
sec[\'sh_info\']=temp[7]
sec[\'sh_addralign\']=temp[8]
sec[\'sh_entsize\']=temp[9]
sections.append(sec)
print(\'Thereare%dsectionheaders,startingatoffset0x%x:\\n\'%(elfhdr[\'e_shnum\'],sec_start))
print(\'SectionHeaders:\')
print(\'[Nr]NameTypeAddressOffset\')
print(\'SizeEntsizeFlagsLinkInfoAlign\')
start=sections[elfhdr[\'e_shstrndx\']][\'sh_offset\']
foriinrange(0,elfhdr[\'e_shnum\']):
offset=start+sections[i][\'sh_name\']
name=get_name(f,offset)
type2str=[\'NULL\',\'PROGBITS\',\'SYMTAB\',\'STRTAB\',\'RELA\',\'HASH\',\'DYNAMIC\',\'NOTE\',\'NOBITS\',\'REL\',\'SHLIB\',\'DYNSYM\']
flags=sections[i][\'sh_flags\']
if(flags==1):
flagsstr=\'W\'
elif(flags==2):
flagsstr=\'A\'
elif(flags==4):
flagsstr=\'X\'
elif(flags==3):
flagsstr=\'W\'+\'A\'
elif(flags==6):
flagsstr=\'A\'+\'X\'
elif(flags==0x0f000000orflags==0xf0000000):
flagsstr=\'MS\'
else:
flagsstr=\'\'
print(\'[%d]%s%s%x%x\'%(i,str(name,encoding=\'utf-8\'),type2str[sections[i][\'sh_type\'] 0x7],sections[i][\'sh_addr\'],sections[i][\'sh_addralign\']))
print(\'%x%x%s%d%d%x\'%(sections[i][\'sh_size\'],sections[i][\'sh_entsize\'],flagsstr,sections[i][\'sh_link\'],sections[i][\'sh_info\'],sections[i][\'sh_addralign\']))
f.close()
defget_name(f,offset):
name=b\'\'
f.seek(offset)
while1:
c=f.read(1)
ifc==b\'\\x00\':
break
else:
name+=c
returnname
第五步:结果展示
上述过程如果无误,那么可以看到解析出来的elf文件信息了。
这样就完成了一个elf文件的解析过程。
7.总结ELF文件经常的见到,但是要具体的分析ELF文件中所对应的具体的含义却需要费一番功夫。本文主要通过对elf文件的构造、具体的含义以及如何去分析elf文件的角度,全面的进行elf文件格式的剖析。在程序链接、程序加载执行上会有更多不一样的理解。
本文链接: http://enelf.immuno-online.com/view-735006.html