龙空技术网

详解可执行文件的结构

一轮世间 2559

前言:

今天咱们对“静态变量通常存储在进程中哪一区”可能比较关注,各位老铁们都想要了解一些“静态变量通常存储在进程中哪一区”的相关文章。那么小编在网上收集了一些对于“静态变量通常存储在进程中哪一区””的相关知识,希望看官们能喜欢,大家快快来学习一下吧!

程序员编写的程序需要翻译成一个可执行文件才可以被操作系统执行,对于采用C语言编写的程序来说,这个翻译过程包括预编译,编译,汇编,链接。

在PC平台上,不同的操作系统,可执行文件的格式可能不同。

对于Windows操作系统:常见的可执行文件格式为PE格式,例如exe文件就是PE格式的。

对于Linux和Unix操作系统:可执行文件格式为ELF格式,例如常见的/bin/bash就是ELF格式的。

PE格式和ELF格式比较相似,因为它们都是COFF文件格式的变种,很多其它的可执行文件格式都是COFF文件格式的变种。

本篇文章主要阐述ELF文件的结构,其它可执行文件格式概念比较类似,细节有些不同。

以下为C语言代码,它的文件名为SimpleSection.c,本篇文章围绕这个代码片段来阐述ELF文件的结构。

int printf( const char* format,...);int global_init_var = 84;//全局变量int global_uninit_var;//全局未初始化变量void func1( int i) {	printf( "%d\n" ,i );}int main(void) {	static int static_var = 85;//静态变量	static int static_var2;//静态    int a = 1;    int b;    func1(static_var + static_var2 + a + b);    return 1;	}
ELF文件类型

不光可执行文件的格式为ELF格式,可重定位文件和共享库文件也是ELF格式,因此ELF文件类型可以分为以下三种类型:

可重定位目标文件:

上文所述的SimpleSection.c文件经过编译,汇编后生成一个SimpleSection.o文件,这个文件就是可重定位目标文件,它包含代码和数据,这个目标文件尚未被链接,因此它可以与其它目标文件或者共享库链接成一个可执行文件,链接过程会进行重定位。

可以执行 【file SimpleSection.o】命令来印证一下这个文件的类型,命令结果如下:

SimpleSection.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

可执行文件:

该类文件可以直接被操作系统执行,如/bin/bash

可以执行 【file /bin/bash】命令来印证一下这个文件的类型,命令结果如下:

/bin/bash: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=8bd6b05295658d71a9ff4eed7cae55609a703623, stripped。

共享目标文件:

包含了代码和数据,有两种使用场景。

1.链接器将该类文件与其它的可重定位目标文件和共享目标文件即共享库链接,产生新的目标文件。

2.动态链接器将该类文件与可执行文件结合,实现运行时链接。

执行 【file /usr/lib64/ld-2.17.so】命令来印证一下这个文件的类型,命令结果如下:

/usr/lib64/ld-2.17.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=a9980cf253c79740e69f70dcb8fea7b8c2f641b5, not stripped

ELF文件类结构

如下图为ELF文件结构

ELF文件结构

由上图得知,ELF文件由文件头(ELF Header),节表(Section header table),以及多个节构成。

ELF文件头(ELF Header):包含了整个文件的基本属性,例如节表的开始位置,节表的大小,ELF文件版本,目标机器型号,程序入口地址等。

节表(Section header table):包括了所有节的属性信息,描述了每个节的名称,节的长度,节在文件中的开始位置,读写权限等。

节(Section):ELF文件的核心内容,一个ELF文件包括很多个节,不同类型的节包括的内容不同,例如.text表示代码节,它包括代码。

不同的ELF文件类型(可重新定位目标文件,共享库目标文件,可执行文件)包含的节不同,不过至少包括代码和数据节,在编译时,可以增加编译选项来控制生成或者去掉某些节,通常这些节都是辅助作用,可有可无。

ELF文件头(ELF Header):

可以通过readelf -h 来查看文件头信息,如下图所示

ELF文件头信息

如上图所示,文件头包括了很多属性,下面逐个阐述各个属性。

Magic:ELF文件的魔幻数。

通过Magic不光可以指示这个文件是不是ELF文件,还能确定其它的信息。

Magic大小为16个字节,前7个字节有意义,后面的9个字节都是0,从前7个字节中可以解析出Class,Data,Version,OS/ABI,ABI Version这个5个属性,其中前4个字节:7f 45 4c 46是固定的,所有的ELF文件类型都一样,第5个字节表示32位还是64位,01表示32位,02表示64位,第6个字节表示内存存储方式是大端还是小端的,第7个字节 表示ELF的主版本号,一般是1 。

Type:ELF文件类型

包括以下几种文件类型:

REL:可重定位目标文件。

EXEC:可执行文件。

DYN:共享目标文件即共享库。

Machine:机器类型

包括ELF文件适用的CPU类型,有以下几种:

M32(AT&T WE32100)

SPARC

Inter x86

Motorola 68000

Motorola 88000

Inter 80860

Entry Point address:程序的入口虚拟地址

操作系统加载可执行文件后,从这个地址开始执行指令,可重定位目标文件一般没有入口地址,因此为0。

Start Of program headers:程序头表在文件中的偏移量即开始位置。

Start Of Section headers:节表在文件中的偏移量即开始位置。

Flags:用来标识与平台相关的属性。

Size Of this header:ELF 文件头(ELF Header)的大小。

Size Of program headers:程序头表的大小。

Number Of program headers:程序头表中程序头的个数。

Size Of Section Headers:节表的大小。

Number Of Section Headers:节表中节的个数

Section header string table index:节名字符串表(shstrtab)在节表中的索引。

总体来说,通过ELF 文件头(ELF HEADER)可以确定ELF文件类型,适用的CPU类型,文件版本,程序头表的起始位置,程序头表中程序头的个数,程序头表的大小,节表的起始位置,节表中节的个数,节表的大小,第一条执行的指令的起始位置等。

节表(Section header table):

通过文件头(ELF Header)可以确定节表的开始位置和大小,节表由多个固定大小的节表项组成,每个节表项包括了节的属性信息,可以通过readelf -S查看ELF文件的节表,如下图所示:

节表

如上图所示,节表里总共有13个选项,第0项是空的,因此节表中总共12个有效的节表项,每一项长度都是固定的,包括了10个属性:Name,Type,Address,Offset,Size,EntSize,Flags,Link,Info,Align,下面来逐个阐述节的各个属性。

Name:节名

节名存储在【节名字符串表】(.shstrtab表)中,这里显示的名称就来自于.shstrtab表。

Type:节的类型

1.NULL表示无效节。

2.PROGBITS表示该节为程序节,例如代码节,数据节都是这种类型。

3.SYMTAB表示该节为符号表,程序中的变量和函数就属于符号,存储在符号表。

4.STRTAB表示该节为字符串表,用于存储除了节名以外的各类字符串如变量名,函数名。

5.RELA重定位表,表示该节包含重定位信息。

7.HASH表示该节为符号表的哈希表,主要用来用于加快符号的查找速度。

8.DYNAMIC 动态链接信息节。

9.NOTE 提示性信息节。

10.NOBITS 表示该节在文件中的没有内容,不占用存储空间,比如.bss段。

11.REL 该节包含了重定位信息。

12.SHLIB 保留。

13.DNYSYM 动态链接的符号表。

Address:节的虚拟地址。

如果该节可以被加载到内存,该地址就是节被加载到进程虚拟地址空间的虚拟地址,否则地址为0。

Offset:节在文件中的开始位置。

如果该节在文件中存储,这个值表示该节在文件中的开始位置,如果该节不在文件中存储例如.bss节,这个值就没有任何意义。

Size:节的大小,即使节不在文件中存储也可以有大小,例如.bss节不在文件中存储,但是有大小,这个大小主要是指明加载到内存时,分配的内存大小。

EntSize:节的每一项的大小。

有些节由固定大小的项构成,对于这些节来说,EntSize表示项的大小,如果为0表示该节包含的项的大小不固定。

Flags:节在进程虚拟地址空间的属性。

比如该节是否可写,是否可执行等,当然只有节能够被加载到内存时,这个Flags才有效,例如.text,.data,有以下几个值:

1.WRITE: 该节在虚拟地址空间可写,一般指数据节。

2.ALLOC: 该节在虚拟地址空间需要分配空间,只有节能够被加载到内存时,才有这个属性。

3.EXECINSTR: 该节在虚拟地址空间可以被执行,一般指代码节。

Link:不同类型的节含义不同。

当节类型(Type)为DYNAMIC时:用于表示该节使用的字符串表在节表中的索引。

当节类型(Type)为HASH时:用于表示该节使用的符号表在节表中的索引。

当节类型为REL,RELA时:用于表示该节使用符号表在节表中的索引。

当节类型为SYMTAB,DYNSYM时:操作系统相关的。

对于其它节,值为UNDEF。

Info:不同类型的节含义不同。

当节类型为DYNAMIC时,值为0。

当节类型为HASH时,值为0

当节类型为REL,RELA时,该重定位表所作用的节在节表中的下标。

当节类型为SYMTAB,DYNSYM时,操作系统相关的。

对于其它节,值为0。

Align:每个节的对齐大小。

有些节要求节的起始位置,必须是Align的整数倍,例如Align=8表示节的起始位置必须是8个整数倍,通常Align是2的整数倍,如果Align为0或者1表示节没有对齐要求。

总体来说,通过节表可以知道每个节在文件中的起始位置,节的大小,节是不是可以被加载到内存,节加载到内存后的权限,节加载到内存后的虚拟地址,节如果用到其它节,则可以知道其它节在节表中的索引,这样就可以在节表中通过索引找到其它节。

代码节,数据节(.text,.data,.bss):

代码节通常的名字为.code或者.text,数据节分为.data和.bss两部分,如下图

程序对应的节

.text节用于存储机器指令,如上图黄色字体部分,包括函数等。

.data节用于存储已经初始化的全局变量和静态变量,如上图绿色字体部分,函数的局部变量存储在栈中,不在.data中存储。

.bss节用于存储未初始化的全局变量和静态变量,如上图蓝色字体部分,bss节在文件中不占用存储空间,只是一个名义上占个位置而已,当程序被加载到内存时,会在内存分配bss段,将未初始化的全局变量和静态变量存储在那里。

重定位表(.rela):

在代码节和数据节中,有些函数或者变量定义在其它的目标文件中,因此在链接时,需要将这些函数和变量进行重定位,重定位时依据的信息就在重定位表中。

对于代码节,它的重定位信息在.rela.text中,对于数据节,它的重定位信息在.rela.data中。

对于重定位表,因为它用到了符号表,它的Link属性表示符号表在节表中的索引,Info表示这个重定位表作用于哪个节,如下图所示,.rela.text中Link为11,从节表中查找第11项就可以定位到符号表,Info为1,从节表中查找第1项就可以定位到代码节,表示重定位表作用于索引为1的代码节。

重定位表定位符号表和代码节

字符串表(.strtab和.shstrtab):

一个程序中会出现很多字符串,例如变量名,函数名,这些名称的长度通常是不固定的,因此一个节中如果要存储这些信息的话,节的大小就需要是动态的,一种常见的做法是将所有不固定的字符串单独用字符串表进行存储,其它的节可以用一个偏移量来表示字符串,这样除了字符串表外,其它的节的长度都是固定的了,便于文件的处理。

其它的节知道了字符串的偏移量后,就可以到字符串表中进行查找,由于字符串的最后一个字符是\0,就可以从偏移量开始截取字符串到\0,一个完整的字符串就可以得到了。

在ELF文件中,字符串表有.strtab和.shstrtab两种,.strtab用于保存普通的字符串,比如变量,函数的名称,.shstrtab通常用于保存节表中用到的字符串,比如节的名称。

符号表(.symtab):

链接过程本质上就是将多个目标文件粘合在一起,就像搭建积木一样,每个积木就是一个目标文件,每个积木都有凹凸部分,这样不同的积木之间才能粘合在一起,目标文件的凹凸部分就是函数或者变量地址,重定位的过程就是替换地址的过程。

一个目标文件A定义了函数func1,另外一个目标文件B引用了函数func1,func1是函数的名字,对于变量也是类似,函数和变量叫做符号,函数名和变量名就叫做符号名。

每个目标文件都有一个符号表,每个符号表中记录了目标文件用到的所有符号,每个符号都对应一个符号值,对于变量和函数来说,符号值就是变量和函数的地址。

符号表中除了函数和变量外,还有其它不常用到的符号,经过统一整理后,可以对所有的符号进行如下分类:

强符号:定义在本目标文件中的全局符号并且已经初始化,这些符号可以被其它目标文件引用,例如SimpleSection.c中的func1,main,global_init_var。

弱符号:定义在本目标文件中的全局符号但是没有初始化,这些符号可以被其它目标文件引用,例如SimpleSection.c中的global_uninit_var。

强引用:声明在本目标文件中的全局符号,但并未定义在当前目标文件,这个符号引用了其他目标文件中的符号,例如SimpleSeciton.c的printf函数,对于变量来说,可以通过extern关键字进行声明。

对于强引用,如果链接过程中,在其它目标文件中没有找到该引用,则报找不到符号的错误。

弱引用:通过__attribute__((weakref))方式声明弱引用,这个符号引用了其他目标文件中的符号,例如__attribute__((weakref)) void foo();就声明了foo函数就是一个弱引用。

对于弱引用,如果静态链接或者加载链接时,在其它目标文件中没有找到该引用,则不会报错,因此弱引用可以用于动态链接。

局部符号:在目标文件中可见,其它目标文件不可见,比如SimpleSection.c中的static_var和static_var2。

链接过程中比较关心的符号就是强符号,弱符号,强引用,弱引用,局部符号则是次要的,它们对其它目标文件是不可见的。

可以通过readelf -s 查看符号表信息

符号表信息

由上图得知,SimpleSection.o总共有16个符号,每个符号都有几个属性,下面对属性介绍如下:

Size:符号大小。

对于包含数据的符号,这个值表示数据类型的大小,例如一个double类型占用8个字节。

Type:符号类型。

有以下几种符号类型:

NOTYPE 表示未知类型符号,一般指的是符号定义在其他目标文件中,例如强引用,弱引用。

OBJECT 表示数据对象,比如变量,数组等。

FUNC 表示函数或者其它可执行代码。

SECTION 表示一个节,符号的Bind属性必须是Local,即它必须是一个局部符号。

FILE 表示一个文件名,一般为该目标文件对应的源文件名。

Bind:符号绑定信息。

Local 表示局部符号,只在当前目标文件可见,其它目标文件不可见。

GLOBAL 表示全局符号可以是强符号,也可是是强引用。

WEAK 表示弱引用。

Ndx:符号所在节的信息。

ABS 表示该符号包含一个绝对的值,比如表示文件名的符号就属于这种类型。

COM 表示该符号是一个”COMMON”块类型的符号,一般来说,未初始化的全局符号就是这种类型。

UND 表示该符号未定义,该符号在当前目标文件中被引用了,但是没有在目标文件中定义。

其它,表示符号所在节在节表中的下标。

Name:符号名。

通常存储于字符串表(.strtab)。

Value:符号值。

当Ndx的值是节表索引时,可以知道这个符号在哪个节中,例如SimpleSection.o中的func1函数,func1这个符号它的Ndx就是1,通过1可以在节表中找到代码节.text,此时符号值就是func1在.text节中偏移量。

当Ndx是COM时,符号值就是该符号的对齐属性。

当文件是可执行文件,符号值就是符号的虚拟地址。

除了在符号表中定位的符号外,还有些特殊符号。

特殊符号不在符号表中定义,但是程序中可以直接引用它,这些特殊符号通常被定义在链接器的链接脚本中,因此这些符号类似与内置符号,不需要在符号表中定义,由如下几类特殊符号:

__executable_start:表示程序的起始地址,是程序的最开始地址。

__etext或_etext或etext:表示代码段的结束地址。

_edata或edata:表示数据段的结束地址。

_end或end:程序的结束地址。

以上地址都是程序被装载时的虚拟地址。

其它节:

rodata1:

只读数据节,比如字符串常量,全局const变量都存储在这里。

comment :

存储编译器版本信息。

.debug:

存储调试类信息。

.hash:

符号哈希表,用于加速符号的查找过程。

.line:

调试时的行号表,即源代码和编译后的指令的对应表。

.note:

额外的编译器信息,比如程序的公司名,发布版本号等。

.plt :

动态链接的跳转表。

.got:

全局入口表,用于动态链接跳转到变量和函数。

番外篇

其它常用的查看ELF文件信息命令

通过objdump -h 命令可以查看ELF文件的各个常用的节的信息

可以通过objdump -s -d 查看ELF文件头信息,符号表,以及反汇编指令。

标签: #静态变量通常存储在进程中哪一区 #全局变量都是静态存储类型