基于本文的一个实践《使用Python分析ELF文件优化Flash和Sram空间的案例》。
1.背景
ELF是Executable and Linkable Format缩写,其官方规范在《Tools Interface Standard Executable and Linkable Format Specification version 1.2》分为三部分:Executable and Linking Format;Processor Specific(Intel Architecture);Operating System Specific(UNIX System V Release 4)。重点关注第一部分通用标准:Object Files和Program Loading and Dynamic Linking。前者可以说是静态,后者是动态,程序加载和动态链接。
ELF文件是二进制格式并不能直接读取,可以通过readelf工具来进行分析。所以在分析ELF文件的过程中会穿插使用readelf。
最后介绍可执行文件在运行时不同部分的加载状态和动态链接过程。
ELF即Executable and Linkable Format,可执行链接格式,ELF格式的文件用于存储Linux程序。
ELF文件(目标文件)格式主要三种:
- 可重定向文件:文件保存着代码和适当的数据,用来和其他的目标文件一起来创建一个可执行文件或者是一个共享目标文件。(目标文件或者静态库文件,即linux通常后缀为.a和.o的文件)
- 可执行文件:文件保存着一个用来执行的程序。(例如bash,gcc等)
- 共享目标文件:共享库。文件保存着代码和合适的数据,用来被下连接编辑器和动态链接器链接。(linux下后缀为.so的文件。)
目标文件既要参与程序链接又要参与程序执行:
一般的 ELF 文件包括三个索引表:ELF header,Program header table,Section header table。
- ELF header:在文件的开始,保存了路线图,描述了该文件的组织情况。
- Program header table:告诉系统如何创建进程映像。用来构造进程映像的目标文件必须具有程序头部表,可重定位文件不需要这个表。
- Section header table:包含了描述文件节区的信息,每个节区在表中都有一项,每一项给出诸如节区名称、节区大小这类信息。用于链接的目标文件必须包含节区头部表,其他目标文件可以有,也可以没有这个表。
参考文档:《linux第三次实践:ELF文件格式分析》
ELF文件相关的结构体定义在/usr/include/elf.h中,下面借助工具读取信息,和结构体对比分析。
2. readelf使用介绍
使用readelf -h可以得到使用方法:
Usage: readelf <option(s)> elf-file(s) Display information about the contents of ELF format files Options are: -a --all Equivalent to: -h -l -S -s -r -d -V -A -I -h --file-header Display the ELF file header -l --program-headers Display the program headers --segments An alias for --program-headers -S --section-headers Display the sections' header --sections An alias for --section-headers -g --section-groups Display the section groups -t --section-details Display the section details -e --headers Equivalent to: -h -l -S -s --syms Display the symbol table --symbols An alias for --syms --dyn-syms Display the dynamic symbol table -n --notes Display the core notes (if present) -r --relocs Display the relocations (if present) -u --unwind Display the unwind info (if present) -d --dynamic Display the dynamic section (if present) -V --version-info Display the version sections (if present) -A --arch-specific Display architecture specific information (if any) -c --archive-index Display the symbol/file index in an archive -D --use-dynamic Use the dynamic section info when displaying symbols -x --hex-dump=<number|name> Dump the contents of section <number|name> as bytes -p --string-dump=<number|name> Dump the contents of section <number|name> as strings -R --relocated-dump=<number|name> Dump the contents of section <number|name> as relocated bytes -w[lLiaprmfFsoRt] or --debug-dump[=rawline,=decodedline,=info,=abbrev,=pubnames,=aranges,=macro,=frames, =frames-interp,=str,=loc,=Ranges,=pubtypes, =gdb_index,=trace_info,=trace_abbrev,=trace_aranges, =addr,=cu_index] Display the contents of DWARF2 debug sections --dwarf-depth=N Do not display DIEs at depth N or greater --dwarf-start=N Display DIEs starting with N, at the same depth or deeper -I --histogram Display histogram of bucket list lengths -W --wide Allow output width to exceed 80 characters @<file> Read options from <file> -H --help Display this information -v --version Display the version number of readelf
3. ELF文件解释(Linking View)
这里主要通过readelf工具静态分析ELF文件,从(Figure 1-1. Object File Format)可知它的组成大概有如下部分。
总共有三种类型的ELF文件:可重定位文件、共享文件和可执行文件。
可重定位文件(Relocatable file):这是由汇编器汇编生成的 .o 文件。后面的链接器(link editor)拿一个或一些 Relocatable object files 作为输入,经链接处理后,生成一个可执行的对象文件 (Executable file) 或者一个可被共享的对象文件(Shared object file)。我们可以使用 ar 工具将众多的 .o Relocatable object files 归档(archive)成 .a 静态库文件。如何产生 Relocatable file,你应该很熟悉了,请参见我们相关的基本概念文章和JulWiki。另外,可以预先告诉大家的是我们的内核可加载模块 .ko 文件也是 Relocatable object file。
共享文件(Shares Object file):这些就是所谓的动态库文件,也即 .so 文件。如果拿前面的静态库来生成可执行程序,那每个生成的可执行程序中都会有一份库代码的拷贝。如果在磁盘中存储这些可执行程序,那就会占用额外的磁盘空间;另外如果拿它们放到Linux系统上一起运行,也会浪费掉宝贵的物理内存。如果将静态库换成动态库,那么这些问题都不会出现。动态库在发挥作用的过程中,必须经过两个步骤:
a) 链接编辑器(link editor)拿它和其他Relocatable object file以及其他shared object file作为输入,经链接处理后,生存另外的 shared object file 或者 executable file。
b) 在运行时,动态链接器(dynamic linker)拿它和一个Executable file以及另外一些 Shared object file 来一起处理,在Linux系统里面创建一个进程映像。
可执行文件(Executable file):这我们见的多了。文本编辑器vi、调式用的工具gdb、播放mp3歌曲的软件mplayer等等都是Executable object file。你应该已经知道,在我们的 Linux 系统里面,存在两种可执行的东西。除了这里说的 Executable object file,另外一种就是可执行的脚本(如shell脚本)。注意这些脚本不是 Executable object file,它们只是文本文件,但是执行这些脚本所用的解释器就是 Executable object file,比如 bash shell 程序。
使用readelf -a xxxx可以看个全貌,实际的显示的顺序和Linking View稍有不同。首先是ELF Header;然后是Section Headers和Program Headers,再然后是Section to Segment mapping的映射表;最后是一系列Section的详细内容。
ELF Header: Section Headers: Program Headers: Section to Segment mapping: Dynamic section at offset 0xe28 contains 24 entries: Relocation section '.rela.dyn' at offset 0x3e8 contains 1 entries: Relocation section '.rela.plt' at offset 0x400 contains 4 entries: Symbol table '.dynsym' contains 6 entries: Symbol table '.symtab' contains 81 entries: …… |
ELF header放在文件开头显示了整个ELF文件的概况,Sections包含了一系列从Linking View角度来看的对象文件信息,包含指令、数据、符号表、重定位信息等等。
Program headers是提供给系统创建进程的依据,可执行文件必须包含Program headers用以创建进程。
3.1 ELF Header
ELF头的结构体在include/uapi/linux/elf.h中定义,包含32位和64位两种。结构体成员名一致,只是成员数据类型不尽相同。所以就取elf64_hdr。
typedef struct elf64_hdr { |
使用hexdump xxx –s 0 –n 64结果如下:
0000000 457f 464c 0102 0001 0000 0000 0000 0000 |
通过readelf –h xxx,所以ELF Header主要内容是定义了ELF Header大小,Program Headers偏移、数目和大小、Section Headers偏移、数目和大小。据此就可以分析整个ELF文件。
ELF Header: . |
将结构体成员名和ELF Header对照一看,就能知道大概。就是e_ident需要再分解一下:
#define EI_MAG0 0 /* e_ident[] indexes */ |
3.2 Section Headers
同样的结合elf64_shdr和readelf读取信息对照:
typedef struct elf64_shdr { |
readelf –S xxx,下面每段的解释可以参照《ELF-64 Object File Format》的Table12-Table13:
There are 31 section headers, starting at offset 0x2250:----------------------从ELF Header的e_shnum和e_shoff可知偏移为31和8784。 Section Headers: |
.interp
.note.ABI-tag
.note.gnu.build-i
.gnu.hash
.dynsym
通过readelf --dyn-syms test读取。
动态符号表(.dynsym)用来保存与动态连接相关的导入导出符号,不包括模块内部的符号
.dynstr
动态符号表(.dynsym)中所包含的符号的符号名保存在动态符号字符串表 .dynstr 中。
.gnu.version
.gnu.version_r
.rela.dyn
重定位的地方在.got段内。主要是针对外部数据变量符号。例如全局数据。重定位在程序运行时定位,一般是在.init段内。定位过程:获得符号对应value后,根据rel.dyn表中对应的offset,修改.got表对应位置的value。另外,.rel.dyn 含义是指和dyn有关,一般是指在程序运行时候,动态加载。区别于rel.plt,rel.plt是指和plt相关,具体是指在某个函数被调用时候加载。我个人理解这个Section的作用是,在重定位过程中,动态链接器根据r_offset找到.got对应表项,来完成对.got表项值的修改。
rel.dyn和.rel.plt是动态定位辅助段。由连接器产生,存在于可执行文件或者动态库文件内。借助这两个辅助段可以动态修改对应.got和.got.plt段,从而实现运行时重定位。
.rela.plt
重定位的地方在.got.plt段内(注意也是.got内,具体区分而已)。 主要是针对外部函数符号。一般是函数首次被调用时候重定位。首次调用时会重定位函数地址,把最终函数地址放到.got内,以后读取该.got就直接得到最终函数地址。我个人理解这个Section的作用是,在重定位过程中,动态链接器根据r_offset找到.got对应表项,来完成对.got表项值的修改。
.init
.plt
段(过程链接表):所有外部函数调用都是经过一个对应桩函数,这些桩函数都在.plt段内。具体调用外部函数过程是:
调用对应桩函数—>桩函数取出.got表表内地址—>然后跳转到这个地址.如果是第一次,这个跳转地址默认是桩函数本身跳转处地址的下一个指令地址(目的是通过桩函数统一集中取地址和加载地址),后续接着把对应函数的真实地址加载进来放到.got表对应处,同时跳转执行该地址指令.以后桩函数从.got取得地址都是真实函数地址了。
下图是.plt某表项,它包含了取.got表地址和跳转执行两条指令。
.text
text section是可执行指令的集合,.data和.text都是属于PROGBITS类型的section,这是将来要运行的程序与代码。查询段表可知.text section的位偏移为0x0000320,size为0x0000192。
.fini
.rodata
rodata section,ro代表read only。
.eh_frame_hdr
.eh_frame
.init_array
.fini_array
.jcr
.dynamic
.got
.got.plt
.data
.bss
.comment
.shstrtab
.symtab
通过readelf -s读取。
symtab section存放所有section中定义的符号名字,比如“data_items”,“start_loop”。 .symtab section是属于SYMTAB类型的section,它描述了.strtab中的符号在“内存”中对应的“内存地址”。
.strtab
strtab section是属于STRTAB类型的section,可以在文件中看到,它存着字符串,储存着符号的名字。
.dynsym和.symtab区别
需要先了解allocable/non-allocable ELF section, ELF文件包含一些sections(如code和data)是在运行时需要的, 这些sections被称为allocable; 而其他一些sections仅仅是linker,debugger等工具需要, 在运行时并不需要, 这些sections被称为non-allocable的. 当linker构建ELF文件时, 它把allocable的数据放到一个地方, 将non-allocable的数据放到其他地方. 当OS加载ELF文件时, 仅仅allocable的数据被映射到内存, non-allocable的数据仍静静地呆在文件里不被处理. strip就是用来移除某些non-allocable sections的.
动态符号表(.dynsym)用来保存与动态连接相关的导入导出符号,不包括模块内部的符号。而.symtab则保存所有符号,包括.dynsym中的符号。
.symtab包含大量linker,debugger需要的数据, 但并不为runtime必需, 它是non-allocable的;
.dynsym包含.symtab的一个子集, 比如共享库所需要在runtime加载的函数对应的symbols, 它是allocable的。
3.3 Program Headers
Program Headers在内核中对应的结构体为:
typedef struct elf64_phdr { |
readelf –l xxx结果如下:
Elf file type is EXEC (Executable file) Program Headers: Section to Segment mapping:-------------------------------------------这里将Section映射到Segment |
3.4 Symbol Table(Dynamic Symbol Table)
Symbol Table包括Dynamic Symbol Table,在内核中定义如下:
typedef struct elf64_sym { |
st_info包括Type和Bind两部分:
Type:
#define STT_NOTYPE 0------No type specified
#define STT_OBJECT 1------Data object
#define STT_FUNC 2-------Function entry point
#define STT_SECTION 3-----Symbol is associcated with a section
#define STT_FILE 4--------Source file associated with the object file
#define STT_COMMON 5
#define STT_TLS 6
Bind:
#define STB_LOCAL 0------Not visible outside the object file
#define STB_GLOBAL 1-----Global symbol, visible to all object files
#define STB_WEAK 2------Global scope, but with lower precedence than global symbols
readelf –s xxx结果如下:
Symbol table '.dynsym' contains 6 entries: Symbol table '.symtab' contains 81 entries:-----------------------------------可以看到.dynsym中有的符号在.symtab中都可以找到。 |
那么不同类型的变量,是否占用空间呢?
变量类型 | 是否占用空间 | |
全局变量 | 不论是否使用,都占用空间。 | 因为全局变量作用域跨文件,所以即使此文件没有使用,也不能被优化。 |
全局静态变量 | 如果没被使用,会被编译器优化。 如果被使用,则占用空间。 | 全局静态变量的作用域为文件,编译器可以判定在此文件是否使用。没有使用,则别处也不会使用。没有存在意义。 |
局部变量 | 局部变量不占用空间。 | 局部变量只在函数内使用,分配在栈中。 |
局部静态变量 | 如果没被使用,会被编译器优化。 如果被使用,则占用空间。 | 局部静态的作用域是函数,虽然存在静态存储区,但是如果函数内没有使用。在别处再不会被使用,所以可以优化掉。 存在静态存储区。 |
malloc/free | 堆中分配和释放,所以是动态的。 |
4. 通过readelf分析符号表用于空间优化
通过readelf -s xxx获取elf文件的符号信息,然后解析每个符号的大小、地址、类型和名称。根据解析的数据列出所有符号大小降序排列,和FUNC/OBJECT的降序排列。
4.1 解析elf符号信息
所有符号信息都保存到elf_lists中:
elf_file = 'iot_ap.elf' elf_summary = elf_file.split('.')[0] elf_lists = [] top_counts = 20 if not os.path.exists(elf_summary): os.mkdir(elf_summary) #elf_summary_object = open(elf_summary, 'wb') tmp = os.popen('readelf -s %s' % elf_file).readlines() elf_symbol_fmt = ' *(?P<num>[0-9]*): (?P<value>[0-9abcdef]*) *(?P<size>[0-9]*).*' for line in tmp: m = re.match(elf_symbol_fmt, line) if not m: continue #num = m.group('num') elf_line_list = re.split(r'\s+', line) if elf_line_list[3][0:2] == '0x': elf_line_list[3] = int(elf_line_list[3][2:], 16) # size address tyoe name elf_lists.append([elf_line_list[3], elf_line_list[2], elf_line_list[4], elf_line_list[8]]) #elf_summary_object.writelines(tmp) #elf_summary_object.close()
4.2 分析符号列表
将elf_lists转成pandas.DataFrame,然后分别进行排序。
elf_data = pd.DataFrame(np.asarray(elf_lists), columns=['size', 'address', 'type', 'name']) elf_data['size'] = elf_data['size'].astype(int) #elf_data.sort_values(by=['size', 'type'], ascending=[False, False]).head(top_counts).to_csv(elf_summary, mode='w') top_all_head = pd.DataFrame(elf_data.sort_values(by=['size', 'type'], ascending=[False, False])) top_func_head = elf_data[elf_data['type'] == 'FUNC'].sort_values(by=['size'], ascending=False) top_object_head = elf_data[elf_data['type'] == 'OBJECT'].sort_values(by=['size'], ascending=False)
4.3 查看结果
4.3.1 所有符号总占用空间:
elf_types = elf_data['type'].unique() totalsize_of_types = 0 print '\nSize of %s:' % (elf_types) for i in elf_types: size_of_type = np.asarray(elf_data[elf_data['type'] == i].sort_values(by=['size'], ascending=False)['size']).sum() print i, ':', size_of_type, 'Bytes' totalsize_of_types += size_of_type print 'Total : ', totalsize_of_types/1024, 'KB'
如下:
Size of ['NOTYPE' 'SECTION' 'FILE' 'FUNC' 'OBJECT']:
NOTYPE : 0 Bytes
SECTION : 0 Bytes
FILE : 0 Bytes
FUNC : 141478 Bytes
OBJECT : 77770 Bytes
Total : 214 KB
4.3.2 所有符号降序Top 20
top_all_head.to_csv('%s/top_all.csv' % elf_summary) print '\nThe top %d of %s:' % (top_counts, elf_file) top_all_head.head(top_counts)
4.3.3 所有OBJECT类型符号Top 20
top_object_head.to_csv('%s/top_object.csv' % elf_summary) print '\nThe top %d of %s:' % (top_counts, 'OBJECT') top_object_head.head(top_counts)
4.3.4 所有FUNC符号Top 20
top_func_head.to_csv('%s/top_func.csv' % elf_summary) print '\nThe top %d of %s:' % (top_counts, 'FUNC') top_func_head.head(top_counts)
参考文档:
1.《Tool Interface Standard (TIS) Executable and Linking Format (ELF) Specification Version 1.2》,中文翻译版《可执行文件(ELF)格式的理解》
4.《深入Linux内核架构》附录E ELF二进制格式
5.《C语言的变量的作用域和生存期》
6.《C/C++堆、栈及静态数据区详解》
7.《elf文件格式和运行时内存布局》
8.《ELF格式文件符号表全解析及readelf命令使用方法》
9. objdump