栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 系统运维 > 运维 > Linux

《程序员的自我修养-链接-装载与库》第三章 目标文件里有什么

Linux 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

《程序员的自我修养-链接-装载与库》第三章 目标文件里有什么

0.引言
   编译器编译源代码后生成的文件叫做目标文件,那么目标文件里面到底存放的是什么呢﹖
或者我们的源代码在经过编译以后是怎么存储的?我们将在这一节剥开目标文件的层层外壳,
去探索它最本质的内容。
 
    目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,
其中可能有些符号或有些地址还没有被调整。其实它本身就是按照可执行文件格式存储的,
只是跟真正的可执行文件在结构上稍有不同。

    可执行文件格式涵益了程序的编译、链接、装载和执行的各个方面。了解它的结构并深
入剖析它对于认识系统、了解背后的机理大有好处。
1.目标文件的格式 1.1 目标文件的格式及ELF文件格式的文件的分类
   现在PC平台流行的可执行文件格式(Executable)主要是Windows下的PE(PortableExecutable)
和Linux的ELF(Executable linkable Format),它们都是COFF(Common fileformat)格式的变
种。目标文件就是源代码编译后但未进行链接的那些中间文件(Windows的.obj和Linux下的.o),
它跟可执行文件的内容与结构很相似,所以一般跟可执行文件格式一起采用种格式存储。

   从广义上看,目标文件与可执行文件的格式其实几乎是一样的,所以我们可以广义地将目标文件
与可执行文件看成是一种类型的文件,在Windows下,我们可以统称它们为PE-COFF文件格式。
在Linux下,我们可以将它们统称为ELF文件。其他不太常见的可执行文件格式还有Intel/Microsoft
的OMF( Object Module Format)、Unix a.out格式和MS-DOS.COM格式等。


    不光是可执行文件(Windows 的.exe和 Linux下的ELF可执行文件)按照可执行文件格式存储。
动态链接库(DLL,Dynamic linking Library)(Windows 的.dll和 Linux 的.so)及静态链接库
(Static linking Library)( Windows 的.lib和Linux的.a)文件都按照可执行文件格式存储。它
们在 Windows下都按照PE-COFF格式存储,Linux下按照ELF格式存储。静态链接库稍有不同,它是把
很多目标文件捆绑在一起形成一个文件,再加上一些索引,你可以简单地把它理解为一个包含有很多目
标文件的文件包。ELF文件标准里面把系统中采用ELF格式的文件归为如表3-1所列举的4类。

 

我们可以在Linux 下使用file命令来查看相应的文件格式,上面几种文件在file命令下会显示
出相应的类型:

1.2 目标文件与可执行文件格式的小历史
    目标文件与可执行文件格式跟操作系统和编译器密切相关,所以不同的系统平台下会有不同的格式,
但这些格式又大同小异,目标文件格式与可执行文件格式的历史几乎是操作系统的发展史。
    COFF 是由Unix System V Release 3首先提出并且使用的格式规范,后来微软公司基于COFF格式,
制定了PE格式标准,并将其用于当时的 Windows NT系统。System Release 4在 COFF的基础上引入了
ELF格式,目前流行的 Linux系统也以ELF作为基本可执行文件格式。这也就是为什么目前PE和ELF如此
相似的主要原因,因为它们都是源于同一种可执行文件格式COFF。
    Unix最早的可执行文件格式为a.out格式,它的设计非常地简单,以至于后来共享库这个概念出现
的时候,a.out格式就变得捉襟见肘了。于是人们设计了COFF格式来解决这些问题,这个设计非常通用,
以至于COFF的继承者到目前还在被广泛地使用。

    COFF的主要贡献是在目标文件里面引入了“段”的机制,不同的目标文件可以拥有不同数量及不同类
型的“段”。另外,它还定义了调试数据格式。

    下文的剖析我们以ELF结构为主。然后会专门分析PE-COFF文件结构,并对比其与ELF的异同。


 2.目标文件是什么样的 2.1 程序与目标文件简介
    我们大概能猜到,目标文件中的内容至少有编译后的机器指令代码、数据。没错,除了这些
内容以外,目标文件中还包括了链接时所须要的一些信息,比如符号表、调试信息、字符串等。
一般目标文件将这些信息按不同的属性,以“节”( Section)的形式存储,有时候也叫“段”
(Segment),在一般情况下,它们都表示一个一定长度的区域,基本上不加以区别,唯一的
区别是在ELF的链接视图和装载视图的时候,后面会专门提到。在本书中,默认情况下统一将
它们称为“段”。
    程序源代码编译后的机器指令经常被放在代码段(Code Section)里,代码段常见的名字
有“.code”或“.text";全局变量和局部静态变量数据经常放在数据段(Data Section),数据
段的一般名字都叫“.data”。让我们来看一个简单的程序被编译成目标文件后的结构,如图3-1
所示。

 

      假设图3-1的可执行文件(目标文件)的格式是ELF,从图中可以看到:

      ELF文件的开头是一个“文件头”,它描述了整个文件的文件属性,包括文件是否可执行、
是静态链接还是动态链接及入口地址(如果是可执行文件)、目标硬件、目标操作系统等信息,
文件头还包括一个段表(Section Table),段表其实是一个描述文件中各个段的数组。段表
描述了文件中各个段在文件中的偏移位置及段的属性等,从段表里面可以得到每个段的所有信息。
文件头后面就是各个段的内容,比如代码段保存的就是程序的指令,数据段保存的就是程序的静
态变量等。


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


2.2 BSS历史 
    BSS ( Block Started by Symbol)这个词最初是UA-SAP汇编器(United AircraftSymbolic 
Assembly Program)中的一个伪指令,用于为符号预留一块内存空间。该汇编器由美国联合航空公司
于20世纪50年代中期为IBM 704大型机所开发。后来BSS这个词被作为关键字引入到了IBM 709和
7090/94机型上的标准汇编器FAP(Fortran Assembly Program),用于定义符号并且为该符号预
留给定数量的未初始化空间。

    Unix FAQ section 1.3 (http://www.faqs.org/faqs/unix-faq/faq/part1/section-3.html)
里面有Unix和C语言之父Dennis Rithcie对BSS这个词由来的解释。
2.3 程序源代码为什么要分成程序指令和程序数据?
    总体来说,程序源代码被编译以后主要分成两种段:程序指令和程序数据。代码段属于程序指令,
而数据段和.bss段属于程序数据。

    很多人可能会有疑问:为什么要那么麻烦,把程序的指令和数据的存放分开?混杂地放在一个段里
面不是更加简单?其实数据和指令分段的好处有很多。主要有如下几个方面。

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

(2)对于现代的CPU来说,它们有着极为强大的缓存(Cache)体系。由于缓存在现代的计算机中地位非
常重要,所以程序必须尽量提高缓存的命中率。指令区和数据区的分离有利于提高程序的局部性。现代
CPU的缓存一般都被设计成数据缓存和指令缓存分离,所以程序的指令和数据被分开存放对CPU的缓存命
中率提高有好处。

(3)第三个原因,其实也是最重要的原因,就是当系统中运行着多个该程序的副本时,它们的指令都是
一样的,所以内存中只须要保存一份改程序的指令部分。对于指令这种只读的区域来说是这样,对于其
他的只读数据也一样,比如很多程序里面带有的图标、图片、文本等资源也是属于可以共享的。当然每
个副本进程的数据区域是不一样的,它们是进程私有的。不要小看这个共享指令的概念,它在现代的操
作系统里面占据了极为重要的地位,特别是在有动态链接的系统中,可以节省大量的内存。比如我们常
用的 Windows Internet Explorer 7.0运行起来以后,它的总虚存空间为112844 KB,它的私有部分
数据为15 944 KB,即有96 90O KB的空间是共享部分。如果系统中运行了数百个进程,可以想象共享的
方法来节省大量空间。关于内存共享的更为深入的内容我们将在装载这一章探讨。

3.挖掘SimpleSection.o
前面对于目标文件只是作了概念上的阐述,如果不彻底深入目标文件的具体细节,相信这样的分析也
只是泛泛而谈,没有真正深入理解的效果。就像知道TCP/IP协议是基于包的结构,但是从来却没有
看到过包的结构是怎样的,包的头部有哪些内容?目标地址和源地址是怎么存放的?如果不了解这些,
那么对于TCP/P的了解是粗略的,不够细致的。很多问题其实在表面上看似很简单,其实深入内部会
发现很多鲜为人知的秘密,或者发现以前自己认为理所当然的东西居然是错误的,或者是有偏差的。
对于系统软件也是如此,不了解ELF文件的结构细节就像学习了TCP/IP网络没有了解IP包头的结构一
样。本节后面的内容就是以ELF目标文件格式作为例子,彻底深入剖析目标文件,争取不放过任何一个
字节。真正了不起的程序员对自己的程序的每一个字节都了如指掌。

    我们就以前面提到过的SimpleSection.c编译出来的目标文件作为分析对象,这个程序是经过精心
挑选的,具有一定的代表性而又不至于过于繁琐和复杂。在接下来所进行的一系列编译、链接和相关的
实验过程中,我们将会用到第1章所提到过的工具套件,比如GCC编译器、binutils等工.具,如果你忘
了这些工具怎么使用,那么在阅读过程中可以再回去参考本书第Ⅰ部分的内容。图3-1中的程序代码如清
单3-1所示。

清单3-1

#include 
int globa1_init_var = 84; 
int global_uninit_var;
void func1 ( int i ) 
{   
    printf ("%dn", 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 0;
}



如不加说明,则以下所分析的都是32位Intel x86平台下的ELF文件格式。
我们使用GCC来编译这个文件(参数-c表示只编译不链接):
$ gcc  -c -m32  SimpleSection.c
$ gcc -c SimpleSection.c

    我们得到了一个1104字节(该文件大小可能会因为编译器版本以及机器平台不同而变化)的SimpleSection.o目标文件。我们可以使用binutils的工具objdump来查看object内部的结构,
这个工具在第1部分已经介绍过了,它可以用来查看各种目标文件的结构和内容。运行以下命令:

$objdump -h SimpleSection.o
[muten@master C]$ objdump -h SimpleSection.o

SimpleSection.o:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000054  00000000  00000000  00000034  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000008  00000000  00000000  00000088  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000004  00000000  00000000  00000090  2**2
                  ALLOC
  3 .rodata       00000004  00000000  00000000  00000090  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .comment      0000002e  00000000  00000000  00000094  2**0
                  CONTENTS, READonLY
  5 .note.GNU-stack 00000000  00000000  00000000  000000c2  2**0
                  CONTENTS, READonLY
  6 .eh_frame     00000058  00000000  00000000  000000c4  2**2
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA


$ objdump -h SimpleSection.o

SimpleSection.o:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         0000005b  00000000  00000000  00000034  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000008  00000000  00000000  00000090  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000004  00000000  00000000  00000098  2**2
                  ALLOC
  3 .rodata       00000004  00000000  00000000  00000098  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .comment      0000002e  00000000  00000000  0000009c  2**0
                  CONTENTS, READonLY
  5 .note.GNU-stack 00000000  00000000  00000000  000000c6  2**0
                  CONTENTS, READonLY



    GCC和binutils可被移植到各种平台.上,所以它们支持多种目标文件格式。比如Windows
下的GCC和 binutils支持PE文件格式、Linux版本支持ELF格式。Linux还有一个很不错的工具
叫readelf,它是专门针对ELF文件格式的解析器,很多时候它对ELF文件的分析可以跟objdump
相互对照,所以我们下面会经常用到这个工具。

    参数“-h”就是把ELF文件的各个段的基本信息打印出来。我们也可以使用“objdump-x”把更
多的信息打印出来,但是“-x”输出的这些信息又多又复杂,对于不熟悉ELF和objdump 的读者来
说可能会很陌生。我们还是先把ELF段的结构分析清楚。从上面的结果来看,SimpleSection.o 
的段的数量比我们想象中的要多,除了最基本的代码段、数据段和BSS段以外,还有3个段分别是
只读数据段(.rodata)、注释信息段(.comment)和堆栈提示段(.note.GNU-stack),这3个额外
的段的意义我们暂且不去细究。

     先来看看几个重要的段的属性,其中最容易理解的是段的长度(Size)和段所在的位置
(File Offset),每个段的第⒉行中的“CONTENTS"、“ALLOC”等表示段的各种属性,“CONTENTS”
表示该段在文件中存在。我们可以看到BSS段没有“CONTENTS",表示它实际上在ELF文件中不存在内
容。".note.GNU-stack”段虽然有“CONTENTS",但它的长度为0,这是个很古怪的段,我们暂且忽
略它,认为它在ELF文件中也不存在。那么ELF文件中实际存在的也就是".text"、“.data”、".rodata”
和“.comment”这4个段了,它们的长度和在文件中的偏移位置我们已经用粗体表示出来了。它们在ELF
中的结构如图3-3所示。

 

了解了这几个段在SimpleSection.o的基本分布,接着将逐个来看这几个段,看看它们
包含了什么内容。


有一个专门的命令叫做“size”,它可以用来查看ELF文件的代码段、数据段和BSS段的
长度(dec表示3个段长度的和的十进制,hex表示长度和的十六进制):

size simp1esection.o
text  data bss    dec   hex        fi1ename
95     8    4     107    6b        SimpleSection.o


为什么这里的text是95呢?bjdump -h SimpleSection.o中得到的.text的长度是是91(0x5b),
这两者个text的长度有什么关系呢?

 3.1 代码段
挖掘各个段的内容,我们还是离不开objdump这个利器。objdump的“-s”参数可以将所有段的内容
以十六进制的方式打印出来,“-d”参数可以将所有包含指令的段反汇编。我们将objdump输出中关
于代码段的内容提取出来,分析一下关于代码段的内容(省略号表示略去无关内容):

 

“Contents of section .text”就是.text的数据以十六进制方式打印出来的内容,总共Ox5b
字节,跟前面我们了解到的“.text”段长度相符合,最左面一列是偏移量,中间4列是十六进制内
容,最右面一列是.text 段的ASCII码形式。对照下面的反汇编结果,可以很明显地看到,.text
段里所包含的正是SimpleSection.c里两个函数func1()和 main()的指令。.text 段的第一个字
节“Ox55”就是“func1()”函数的第一条“push %ebp”指令,而最后一个字节Oxc3正是main()函数
的最后一条指令“ret”。
3.2 数据段和只读数据段 
    .data段保存的是那些已经初始化了的全局静态变量和局部静态变量。前面的SimpleSection.c
代码里面一共有两个这样的变量,分别是global_init_varabal 与static_var。这两个变量每个4
个字节,一共刚好8个字节,所以“.data”这个段的大小为8个字节。

    SimpleSection.c里面我们在调用“printf”的时候,用到了一个字符串常量“%dn",它是一种
只读数据,所以它被放到了“.rodata”段,我们可以从输出结果看到“.rodata”这个段的4个字节刚好
是这个字符串常量的ASCII字节序,最后以结尾。
  

    ".rodata”段存放的是只读数据,一般是程序里面的只读变量(如 const修饰的变量)和字符串
常量。单独设立“.rodata”段有很多好处,不光是在语义上支持了C++的const关键字,而且操作系
统在加载的时候可以将“.rodata”段的属性映射成只读,这样对于这个段的任何修改操作都会作为非
法操作处理,保证了程序的安全性。另外在某些嵌入式平台下,有些存储区域是采用只读存储器的,
如ROM,这样将“.rodata”段放在该存储区域中就可以保证程序访问存储器的正确性。

    另外值得一提的是,有时候编译器会把字符串常量放到“.data”段,而不会单独放在“.rodata”段。
有兴趣的读者可以试着把SimpleSection.c的文件名改成SimpleSection.cpp,然后用各种MSVC编译器
编译一下看看字符串常量的存放情况。

$objdump -x -s -d simplesection.o
我们看到“.data”段里的前4个字节,从低到高分别为0x54、Ox00、Ox00、Ox00。这
个值刚好是 global_init_varabal,即十进制的84。global_init_varabal是个4
字节长度的 int类型,为什么存放的次序为0x54、Ox00、Ox00、Ox00而不是Ox0O、
0x00、Ox00、0x54?这涉及CPU的字节序(Byte Order)的问题,也就是所谓的大端
(Big-endian)和小端(Little-endian)的问题。关于字节序的问题本书的附录有
详细的介绍。而最后4个字节刚好是static_init_var的值,即85。
3.3 BSS段  
    .bss段存放的是未初始化的全局变量和局部静态变量,如上述代码中 global_uninit_var
和static_var2就是被存放在.bss段,其实更准确的说法是.bss 段为它们预留了空间。但是我们
可以看到该段的大小只有4个字节,这与global_uninit_var和static_var2的大小的8个字节不符。

       其实我们可以通过符号表(Symbol Table)(后面章节介绍符号表)看到,只有static_var2
被存放在.了.bss段,而global_uninit_var却没有被存放在任何段,只是一个未定义的“COMMON符号”。
这其实是跟不同的语言与不同的编译器实现有关,有些编译器会将全局的未初始化变量存放在目标文件
.bss段,有些则不存放,只是预留一个未定义的全局变量符号,等到最终链接成可执行文件的时候再在
.bss段分配空间。我们将在“弱符号与强符号”和“COMMON块”这两个章节深入分析这个问题。原则上讲,
我们可以简单地把它当作全局未初始化变量存放在.bss段。值得提的是编译单元内部可见的静态变量
(比如给global_uninit_var 加上 static修饰)的确是存放在.bss段的,这一点很容易理解。

$objdump -x -s -d SimpleSection.o

Quiz变量存放位置
现在让我们来做一个小的测试,请看以下代码:
static int x1  = 0;
static int x2  = 1;

x1和x2会被放在什么段中呢?
x1会被放在.bss 中,x2会被放在.data中。为什么一个在.bss段,一个在.data段?因为xl为0,
可以认为是未初始化的,因为未初始化的都是0,所以被优化掉了可以放在.bss,这样可以节省
磁盘空间,因为.bss不占磁盘空间。另外一个变量x2初始化值为1,是初始化的,所以放在.data
段中。


注意:
    这种类似的编译器的优化会对我们分析系统软件背后的机制带来很多障碍,使得很多问题不
能一目了然,本书将尽量避开这些优化过程,还原机制和原理本身。
 3.4 其他段  
    除了.text、.data、.bss这3个最常用的段之外,ELF文件也有可能包含其他的段,用来保存
与程序相关的其他信息。表3-2中列举了ELF的一些常见的段。

 

    这些段的名字都是由“.”作为前缀,表示这些表的名字是系统保留的,应用程序也可以使用一些
非系统保留的名字作为段名。比如我们可以在ELF文件中插入一个“music”的段,里面存放了一首MP3
音乐,当ELF文件运行起来以后可以读取这个段播放这首MP3.但是应用程序自定义的段名不能使用“.”
作为前缀,否则容易跟系统保留段名冲突。一个ELF文件也可以拥有几个相同段名的段,比如一个ELF
文件中可能有两个或两个以上叫做“.text”的段。还有一些保留的段名是因为ELF文件历史遗留问题
造成的,以前用过的一些名字:
    如sdata、.tdesc、.sbss、.lit4、.lit8、.reginfo、.gptab、.liblist、.conflict。
可以不用理会这些段,它们已经被遗弃了。

Q:如果我们要将一个二进制文件,比如图片、MP3音乐、词典一类的东西作为目标文件中的一个段,
  该怎么做?
A:可以使用objcopy 工具,比如我们有一个图片文件“image.jpg”,大小为0x82100字节。

$ objcopy -I binary -o elf32-i386 -B i386 image.jpg image.o
$ objdump -ht image.o


符号“_binary_image_jpg._start”、“_binary_image_jpg_end”和“_binary_image_jpg_size"
分别表示该图片文件在内存中的起始地址、结束地址和大小,我们可以在程序里面直接声明并使用它
们。
3.5 自定义段
    正常情况下,GCC 编译出来的目标文件中,代码会被放到“.text”段,全局变量和静态变量
会被放到“.data”和“.bss”段,正如我们前面所分析的。但是有时候你可能希望变量或某些部分
代码能够放到你所指定的段中去,以实现某些特定的功能。比如为了满足某些硬件的内存和IO的
地址布局,或者是像Linux操作系统内核中用来完成一些初始化和用户空间复制时出现页错误异
常等。GCC提供了一个扩展机制,使得程序员可以指定变量所处的段:


_attribute__((section ("FOO"))) int global = 42;
_attribute__((section ("BAR"))) void foo ()
{
}

    我们在全局变量或函数之前加上“_attribute__((section("name")))”属性就可以把相应
的变量或函数放到以“name”作为段名的段中。
 4.ELF文件结构描述
    我们已经通过SimpleSection.o的结构大致了解了ELF文件的轮廓,接着就来看看ELF
文件的结构格式。图3-4描述的是ELF目标文件的总体结构,我们省去了ELF一些繁琐的结构,
把最重要的结构提取出来,形成了如图3-4所示的ELF文件基本结构图,随着我们讨论的展开,
ELF文件结构会在这个基本结构之上慢慢变得复杂起来。

 

    ELF目标文件格式的最前部是ELF文件头(ELF Header),它包含了描述整个文件的基本属性,
比如ELF文件版本、目标机器型号、程序入口地址等。紧接着是ELF文件各个段。其中ELF文件中与
段有关的重要结构就是段表(Section Header Table),该表描述了ELF文件包含的所有段的信息,
比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其他属性。接着将详细分析ELF
文件头、段表等ELF关键的结构。另外还会介绍一些ELF中辅助的结构,比如字符串表、符号表等,
这些结构我们在本节只是简单介绍一下,到相关章节中再详细展开。
4.1 文件头 

 

5.链接的接口-符号 6.调试信息 7.本章小结

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/297327.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号