《程序员的自我修养》读书笔记 -- 第三章
第三章 目标文件里有什么
3.1 目标文件的格式
1、目标文件就是源代码编译后还未进行链接的中间文件。因为目标文件与可执行文件的内容和结构很相似,所以一般跟可执行文件的存储形式相同,Linux下统称为ELF可执行文件。动态链接库与动态链接库也使用可执行文件格式存储。
2、ELF文件标准里面把ELF文件归为4类:
l 可重定位文件(这类文件包含代码和数据,可被用来链接成可执行文件,静态链接库属于此类。如linux下的.o文件和windows下的.obj )
l 可执行文件(这类文件包含可直接执行的程序,ELF可执行文件属于此类,一般没有扩展名。如/bin/bash文件,windows下的.exe文件)
l 共享目标文件(包含代码和数据,可以在以下两种情况下使用。A.链接器可以使用这种文件跟其它的可重定位文件和共享目标文件链接,产生新的目标文件。B、动态链接器可以将几个这种共享目标文件与可执行文件结合,作为进程映像的一部分来运行。如linux的.so,windows的DLL)
l 核心转储文件(进程意外终止时,系统将该进程的地址空间的内容及终止的其他信息转储到核心转储文件。如linux下的core dump文件)
3.2 目标文件是什么样的
1、目标文件将指令代码、数据、符号表、字符串等信息按段存储。
说明:未初始化的全局变量和静态变量默认值都是0,本来应该存在.data段,但是开辟空间存放0是没有必要的。因此存在.bss段,只是为未初始化的全局变量和静态变量预留位置而已,它并没有内容,在文件中也不占空间。
2、总体来说,程序被编译之后主要分为两种段:程序指令和程序数据。代码段属于指令,.bss和数据段属于程序数据。
3、指令和数据分开存放的原因:
(1)程序被装载之后,数据和指令分别被映射到两个虚存区域。数据区对进程来说可读写,但指令区对进程只可读。因此分开存放,可以防止程序的指令被改写。
(2)现在的CPU有强大的缓存体系,所以程序必须尽量提高缓存的命中率。指令和数据的分离有利于提高程序的局部性。CPU的缓存一般都被设计成数据缓存与指令缓存分离。
(3)当系统运行多个该程序的副本时,它们的指令是一样的,因此内存中只需要保存一份该程序的指令部分。共享指令,可以节省大量的内存。
3.3 挖掘simpleSection.o
我们编写一个例子程序,simpleSection.c,代码如下。
接着,使用gcc来编译这个文件。gcc –c simpleSection.c (-c参数表示只编译不链接)。现在我们已经得到了simpleSection.o目标文件,使用objdump工具来查看目标文件的内部结构。执行下面的命令:参数“-h”就是把ELF文件的各个段的基本信息打印出来。
在打印出来的信息中,我们可以看到,simpleSection.o一共包含6个段,分别是代码段、数据段、bss段,只读数据段(rodata),注释信息段(comment),堆栈提示段(.note.GUN-stack)。至于段的具体信息,最上面一行的Size和File off分别表示短的长度和位置。每段第二行的CONTENTS,ALLOC等,表示段的各种属性。CONTENTS,表示该段在文件中存在,bss段没有CONTENTS,就说明bss其实是不存在的。
注:上面没说.eh_frame段,《程序员的自我修养》一书中,给出的simpleSection.o段信息并没有eh_frame段,但我在自己的主机上查看,多了这个段。在网上查了下这个段的作用,是辅助调试时,保存一些编译的帧栈信息的。具体为什么要保存在这里,一篇博客作了解释:https://book.2cto.com/201309/33387.html
3.3.1代码段
使用objdump的-s参数可将所有段的内容以十六进制的方式打印出来,-d参数可以将所有包含指令的段反汇编。
text段的内容如上所示,上一节中,我们看到text的的长度为0x51,换算为10进制,则是81个字节。现在我们看到text的内容,2个16进制数为一个字节,刚好是81字节。
对照下面的反汇编结果,可以看到.text段中包含的就是main()和func1()的指令。
3.3.2 数据段和只读数据段
.data段中保存的是全局变量和静态变量(含全局和局部)。【在原书中,写的是保存已经初始化的全局静态变量和局部静态变量。我觉得这句话有误,初始化的全局普通变量也在.data段中存放,如global_init_data】
在simpleSection.c程序中,我们调用printf的时候,使用了一个字符串常量“%d\n”,这是一种只读数据,因此存放在.rodata中。
程序里的只读变量,如const修饰的变量和字符串常量,都会存放在.rodata中,单独设立只读数据段有很多好处,不仅在语义上支持了C++的const关键字,而且操作系统在加载的时候可以将.rodata段的属性映射成只读,保证了程序的安全性。
3.3.3 BSS段
.bss段存放的是未初始化的全局变量和局部静态变量。
我们看到,针对simpleSection.c程序,里面有两个未初始化的变量,global_uninit_var和static_var2,但是打印出来的段信息中.bss段的长度为4,显然只有一个变量存放在.bss段中了。这是因为不同的编译器实现的方式不同,有的编译器会把全局未初始化变量存进.bss,有些则不存放,只预留一个未定义的全局变量和“COMMON块”。
如上两行代码,x1和x2会被存放在什么段中呢?照常理,这两个变量都初始化了,应该存放在data段。然而事实是,x1存放在.bss段,x2存放在.data段。原因是x1的值为0,可以认为是未初始化的,所以被优化掉可以放在.bss,这样可以节省磁盘空间。
3.3.4 其它段
除了上述说的几个常用段之外,ELF文件也会包含其他的段,用来保存与程序有关的其它信息。段的名字以“.”作为前缀,表示这些段的名字是系统保留的。应用程序可以使用一些非系统保留的名字作为段名,不能以“.”作为前缀。