目标文件里的秘密

目标文件里的秘密

摘要:目标文件内包含指令代码、数据和链接环节所需要的一些信息,如符号表、调试信息、字符串表等。一般目标文件将这些信息按不同的属性分类,以“段”的形式存储。本文将Linux环境下,用bjdump、readelf工具查看目标文件存储的具体形式,可得目标文件中由C语言编译后执行语句放在.text段,已经初始化的全局变量和和局部静态变量放在.data段,未初始化的全局变量和局部静态变量放在.bss段,ELF文件中各个段的基本属性放在.symtab段,段表中很重要的还有重定位表以及ELF文件头。

关键词数据段,代码段,.bss段,符号表,文件头

The secret of the target document

ABSTRACT: A target file contains instruction codes, data and some information needed in the link process, such as symbol tables, debugging information, string tables etc. Generally the mentioned information is classified based on its different properties and stored in the form of the “block”. This paper will examine the concrete storage form of the target file with the help of bjdump and readelf in the Linux environment, thus reaching the following conclusion: in the target file the executive statement compiled with C language compiler is executed at the .text segment, the global variable and static local variable which have initialized are at the .data segment, the global variable and static local variable which haven't uninitialized are at the .bss segment, the basic properties of each segment in the ELF file at the symtab segment. It is of great importance to relocate table and ELF file header in the segment table.

KEYWORDS: Data segment , Code segment, .bss segment, Symbol table, File header

1、引言

编译器编译源代码后生成的文件叫做目标文件[1],那么目标文件里面到底存放的是什么呢?或者我们的源代码在经过编译以后是怎么存储的?

了解程序中各种变量在编译和运行阶段的状态和属性,对我们开发程序而言是非常有益的,清楚目标文件中都包含了哪些信息,能让我们明白C语言中各种存储类对程序的影响,也让我们在编写一个程序的时候胸有成竹。真正的程序员,必须对自己程序的每一个字节了如指掌。

目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。其实它本身就是按照可执行文件格式存储的,只是跟真正的可执行文件在结构上稍有不同。

2、目标文件的格式

各个系统之间,目标文件都不相同。从贝尔实验室诞生的第一个Unix系统使用的是a.out格式,System V Unix 的早期版本使用的是一般文件目标文件格式,现在PC平台流行的可执行文件格式主要是Windows下的PE(Portable Executable)LinuxELF(Executable Linkable Format)[2],它们都COFFCommon File Format)格式的变种。目标文件就是源代码编译后但未进行链接的那些中间文件,它跟可执行文件的内容与结构很相似,所以一般跟可执行文件格式一起采用一种格式存储。

不光是可执行文件(Windows.exeLinux下的ELF可执行文件)按照可执行文件格式存储。动态链接库(Dynamic Linking Library)(Windows.dllLinux.so)和静态链接库(Static Linking Library)(Windows.libLinux.a)文件都按照可执行文件格式存储。它们在Window是下偶读按照PE-COFF格式存储,Linux下按照ELF格式存储。

ELF文件标准里面把系统中采用ELF格式的文件归为如表1所列举的3类:

1 ELF文件

ELF文件类型

说明

实例

 

可重定位目标文件

包含二进制代码和数据,其形式可以在编译时与其他可重定向目标文件合并起来,创建一个可执行目标文件。

Linux.o文件

Window下的.obj文件

可执行文件

包含二进制代码和数据,其形式可直接拷贝到存储器并执行。

Window下的.exe文件

共享目标文件

一类特殊类型的可重定向目标文件,可以在加载或者在运行时被动态加载到存储器并链接。

Linux.so文件

 

我们可以在Linux环境下使用file命令来查看相应的文件格式。

3、目标文件的内容

目标文件中的内容至少有编译后的机器指令代码、数据。没错,除了这些内容外,目标文件还包括链接时所需要的一些信息,比如符号表、调试信息、字符串等。一般目标文件将这些信息按不同的属性,以“段(Segment)”的形式存储。

程序源代码编译后的机器指令经常被放在代码段(Code Section)里,代码段常见的名字有“.code”或“.text”;全局变量和局部静态变量数据经常放在数据段(Data Section),数据段的一般名字都叫".data"。ELF文件的开头是一个“文件头”,它描述了整个文件的文件属性,包括文件是否可执行、是静态链接还是冬天链接以及入口地址(如果是可执行文件)、目标硬件、目标操作系统等信息,文件头还包括一个段表(Section Table),段表其实是一个描述文件中各个段的数组。段表描述了文件中各个段在文件中的偏移位置以及段的属性等。从段表中可以得到每个段的所有信息。文件头后面就是各个段的内容,比如代码段保存的就是程序的指令等等。

一般C语言的编译后执行语句都编译成机器代码,保存在.text段;已初始化的全局变量和局部静态变量都保存在.data段;未初始化的全局变量和局部静态变量一般放在一个叫.bss的段中,我们知道未初始化的全局变量和局部静态变量默认值都为0,本来它们也可以放在.data段的,但是因为它们都是0,所以为它们在.data段分配空间并且存放数据0是没有必要的。程序运行的时候它们的确是要占内存空间的,并且可执行文件必须记录所有未初始化的全局变量和局部静态变量的大小总和,即为.bss段。所以.bss段只是为未初始化的全局变量和局部静态变量预留位置而已,它并木有内容,所以它在文件中也不占据空间。

总体来说,程序源代码经过编译之后主要分为两种段:程序指令和程序数据。代码段属于程序指令,而数据段和.bss段属于程序数据。

3.1程序指令和程序数据的秘密

为何把程序指令和数据分开来放[3]?主要有如下几个方面原因:

(1)一方面是程序被装载后,数据和指令分别被映射到两个虚存区域。由于数据区域对于进程来说是可读写的,而指令区域对于进程来说是只读的,所以这两个虚存区域的权限可以被分别设置成可读写和只读。这样可以防止程序的指令被有意或无意的改写。

(2)另一方面是对于现代的CPU来说,它们有着极为强大的缓存体系。程序必须尽量提高缓存的命中率,指令区和数据区的分离有利于提高程序的局部性。

(3)第三个原因,其实也是很重要的是原因,就是当系统中运行着多个许多个该程序的副本时,它们的指令都是一样的,所以内存中只需要保存一份该程序的指令部分。对于指令这种只读的区域来说是这样,对于其他的只读数据也是这么。当然,每个副本进程的数据区域是不一样的,它们是进程私有的。

3.2目标文件里的各种段

接下来我们看一个简单程序被编译成目标文件后的结构,更形象的了解目标文件里有什么,用选项 -c获取一个可重定位目标文件simple_section.o,用objdump工具查看它,如1所示

1目标文件simple_section.o

从图中我们可以看到,没有经过链接的可重定位文件包含我们熟悉的.text段、.data段等,size字段表明了各个段的大小,file off表示各个段相对于文件头的偏移量,.text段的偏移量是0x000034,即52个字节,这52个字节存放了ELF文件的文件头,也就是通常说的ELF头。从第53个字节开始就是.text段,这个段存放了源程序中编译出来的所有机器指令。

紧跟着的第0x85个字节开始就是.data段,这个段存放了源程序中所有已经初始化了的静态局部变量和初始化了的全局变量。从图中看到这个段此时共有8个字节,分别存放了程序中的全局变量global_init_var 和静态局部变量static_init_var.rodata段存放了程序中的只读数据,所谓的只读数据包括字符串常量、整数常量、浮点数常量。单独设立.rodata段有很多好处,不光是在语义上支持了C++的const关键字,而且操作系统在加载的时候可以将.rodata段的属性映射成只读,这样对于这个段的任何修改操作都会作为非常操作处理,保证了程序的安全性。另外值得一提的是,有时候编译器会把字符串常量放到.data段,而不会单独放在.rodata段。有兴趣的读者可以试着把SimpleSection.c的文件名改为SimpleSection.cpp,然后用各种MSVC编译器编译下看看字符串常量的存放情况。

.bss段的大小是4个字节,但是我们知道.bss段是不占用存储空间的,这里的4个字节大小只是一个说明而已,并没有占用磁盘空间。bss三个字母的意思是block started by symbol,我们也可以记成better save space,也就是说它是用来节省空间的,我们可以我们在各个字段的属性信息里也可以看到,除了.bss段之外的每个段都有CONTENTS 属性,CONTENTS表示这个段在磁盘文件中是真实内容。而.bss只有ALLOC 属性,并没有在文件中占有空间,这样做的原因是没有必要为它们(所有未初始化的全局变量和所有未初始化的静态局部变量)分配磁盘空间,由于它们没有初始化,系统在运行程序的时候会为他们全部默认初始化为0,既然都是0,就没有必要在磁盘里放一堆0了,只要记住它们在运行的时候需要多少内存字节就行了。

另外注意到,.bss段中的size4个字节,而我们的程序中有一个未初始化的全局变量global_uninit_var和一个未初始化的静态局部变量static_uninit_var,按道理需要8个字节才对。少了4个字节到哪儿去了?

为了搞清楚这个问题,我们需要更多的信息,用readelf可以查看更详细的信息,我们用-S 选项来预览一下文件的所有段的情况,如图2所示:

文件的段

输出的结果就是ELF文件段表的内容,可以看到,除了前面用objdump工具看到的几个基本的段之外还有其他的辅助字段,其中看到一个叫做符号表的.symtab字段,这个字段保存了ELF文件中各个段的基本属性,比如每个段的段名、长度、在文件中的偏移量等。

这个段表是以一个结构体数组为基础的,这个结构体就在/usr/include/elf.h中定义,叫做Elf32_Shdr,编译程序的时候就是用这个结构体来对应目标文件的各个段的,每个Elf32_Shdr结构体对应一个段,所以Elf32_Shdr又被称为段描述符。

段表中很重要的还有重定位表,比如上面的.rel.text,这个叫做代码段的重定位表,其他的段也可以有重定位表,只不过这个例子比较简单,只出现了代码段的重定位表,如果数据段.data也需要重定位,那么在ELF文件中就会出现.rel.data段。重定位表的作用当然是重定位,需要重定位的原因是,当几个目标文件合并在一起成为一个真正可以被OS加载执行的时候,它们之间的各个段需要重新组合,比如把各个文件的代码段合并在一起,各个文件的数据段合并在一起,这个时候就会发生数据的移动,比如一个原来在代码段某个位置的函数被重定位到另一个地方了,这时候如果程序的其他部分对此一无所知,那么调用该函数的代码将来肯定找不到北,因此我们有必要在链接之前把各个目标文件里面每个将来需要移动的段的移动信息放在一个地方保存起来,以便链接的时候有根有据,不至于出乱子。这个地方就是重定位段。

3.3 ELF文件头

ELF文件还有52个字节的ELF文件头,具体如图3:

3  ELF文件头

这个ELF文件头存放了很多信息,对照上面的图,从上到下分别是ELF幻数,文件及其字节长度,数据存储方式,版本,运行平台,ABI版本,ELF重定位类型,硬件平台,硬件平台版本,入口地址,程序头入口和长度,段表的位置和长度及段的数量。

幻数中的前面4个字节是所有ELF文件都必须相同的标识符。分别是0x7f、0x45、0x4c、0x46。OS在加载可执行文件的时候检查这个幻数是否正确,不正确则拒绝加载。后面的一个字节用来标识ELF文件的文件类,0x01代表32位的,0x02代表64位的。接下了的那个字节用来表示这个文件的字节序,1为小端,2为大端。接下来是ELF文件的主版本号,一般就是1了,ELF标准自从1.2版本以来就没有升级过。后面的都是保留的。后面的9个字节ELF标准没有定义,一般填0,有些平台会使用这9个字节作为扩展标志。

 

参考文献

[1]冯博琴,吴宁.微型计算机原理与接口技术(第3版)[M].清华大学出版社,2011.8.p145-p148

[2](美)Randal E.Bryant/David O'Hallaron龚奕利/雷迎春.深入理解计算机系统[M],机械工业出版社,2011.1.p448-p473

[3]俞甲子,石凡,潘爱民.程序员自我修养[M].电子工业出版社,2009.4.p56-p95

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值