0%

《程序员的自我修养》第3章总结

目标文件就是源代码经过编译后的,链接之前的文件,经过链接就变成了可执行文件,它也是一种二进制文件。

目标文件的格式其实与最终的可执行文件的格式是一致的,只是没有经过符号的链接过程。

在Windows环境下,可执行文件的格式是PE(Portable Executable),在Linux环境下,可执行文件的格式叫ELF(Executable Linkable Format),这两种格式差别不大,都是源自于COFF(Common File Format)格式的。

ELF文件

下面主要介绍一下ELF文件格式。

ELF文件下又可以分为4种类型:

  • 可重定位文件(Relocatable File),包含代码和数据,一般是指静态链接库文件。
  • 可执行文件(Executable File)
  • 共享目标文件(Shared Object File),也是一种可重定位文件,但是它可以被用于动态链接。
  • 核心转储文件(Core Dump File),进程意外终止的时候,系统把进程地址空间的内容和一些其他信息存放到核心转储文件中,可以用于排除BUG。

一个目标文件/可执行文件的基本组成部分都是段(Segment/Section),段就是把一个程序的不同部分分类存放,形成不同的段,一个目标文件最基本的段包括:

  • 代码段(程序段).text,代码段就是存放程序的逻辑代码部分。
  • 数据段.data,用于存放变量数据,但是这里存放的是已初始化的全局/静态变量。
  • BSS(Block Started by Symbol).bss,BSS段则是为未初始化的全局变量和局部的静态变量,为他们预留位置,BSS段的起源是一个历史原因,同时也可以节省目标文件占用的空间。

那么有人会问,那局部变量存放在哪呢?局部变量是存放在进程地址空间中的里,因为局部变量的生命周期是临时的一个函数调用期,而全局变量/静态变量的生命周期是整个程序运行期间,所以我们将全局/静态变量放在进程中单独的一个段里。

image-20210122183320332

目标文件的3个基础段

PS:段名都是以小数点.开头的。

同时,目标文件的最开始包含一个ELF文件头(ELF Header),它用来说明整个ELF文件各种信息,包括:是否可执行,是静态链接还是动态链接,入口地址(如果是可执行文件),目标硬件,以及段表(Section Header Table)的位置(文件位置偏移)等。这里的段表是一个重要的部分,它本质是一个数组,用来描述ELF文件中各个段的信息(名字、偏移and其他信息)。

目标文件中除了有各种“段”,还有各种“表”,上面说的段表就是一个,除此之外还有重定位表(Relocation Table)、字符串表(String Table)、符号表(Symbol Table)等等。每一个表也是通过段的形式存储在目标文件中的。

image-20210122181949194

ELF文件结构

每一个需要重定位的段(例如代码段、数据段),都会有一个对应的重定位表,用来描述该段中需要重定位的引用的信息。

字符串表是将程序中所用到的变量名、段名等字符串集中起来放在一个段里,通过偏移量来表示字符串的。

符号表就是用来存放代码中用到的各种符号(函数名或变量名)的信息。符号一般分为全局符号、局部符号、段名、行号等。这里的全局符号(全局变量、非static函数)不仅包括定义在本文件中的全局符号,也包括引用其他文件中定义的全局符号,局部符号则是仅本文件内部可见的符号(主要是局部变量,还有static的变量和函数)。链接过程处理的就是全局符号

符号

符号修饰问题

一个讲的比较好的blog:https://www.cnblogs.com/wfwenchao/articles/4140388.html

在“远古时代”,编译后目标文件中的符号名与源代码中的符号名是一致的,随着软件工程的发展,就出现了一个问题:一个软件可能由多个人共同开发完成,如果他们没有对各自负责的模块中的变量、函数命名进行沟通及规范的话,很可能会造成符号重复的问题,即,不同模块中不同功能的函数名(或全局变量名)可能是一样的,或者是开发者写的函数名与所用到的库中的某一个函数撞名了,那么链接器在符号决议过程中就会冲突报错。

为了解决这个问题,现在的编译器一般会把源代码中的符号,经过一种修饰的规则修饰之后放入目标文件中,称为符号修饰(Name Decoration)或符号改编(Name Mangling),不同的C++编译器对符号的修饰规则是不一样的(例如GNU的g++和微软的Visual C++编译器)。

有了符号修饰这个机制,就能很好地实现函数重载这类机制了,因为修饰机制一般会区分开同名函数的不同参数列表。同时,命名空间也是为了解决符号冲突而引入的一种机制。

一个函数对应一个唯一的函数签名(Function Signature),函数签名包括了确定一个函数需要的所有信息,例如:函数名、参数列表、命名空间等。一个函数签名对应一个修饰后的符号名称(Decorated Name)。

由于C和C++的修饰规则并不一样,所以为了在C++程序中使用C的修饰规则,我们可以使用extern "C"这个关键字来声明符号,例如:extern "C" double a;,那么编译器就会对变量a使用C语言的修饰规则。

强符号or弱符号、强引用or弱引用

一个讲的比较好的blog:https://www.cnblogs.com/downey-blog/p/10470674.html

强符号和弱符号

强符号和弱符号是针对定义来说的,在C/C++语言中,已初始化的全局变量是一种强符号,未初始化的则是弱符号。在符号决议的过程中如果发现了多个同名的强符号定义,那么就会报错;如果有一个强符号定义以及一些弱符号定义,则不会报错,且强符号定义会覆盖弱符号定义;如果没有强符号定义,只有弱符号定义,那么会选择占用空间最大的那个(例如double类型比int型占用的字节大)。

我们也可以手动定义一个初始化的强符号,只需使用__attribute__((weak))关键字,例如:__attribute__((weak)) int weak2 = 2;,不过这样手动定义的弱符号不能和强符号共存。

弱符号的一个用处是:在库中的定义是弱符号,用户可以重新定义一个自己使用的同名强符号,使得其覆盖库中版本,使用用户自定义版本的库函数的作用。

强引用和弱引用

一般没有特殊声明的引用都是强引用,如果一个强引用在链接的符号决议过程中没有找到定义,链接器会报符号未定义的错误。而弱引用在没有找到定义时则不会报错,链接器会用0或者一个特殊的值代替。

将一个符号的引用声明为弱引用需要用到__attribute__((weakref))这个关键字,例如:__attribute__((weakref)) foo(),就是声明了一个名为foo的弱引用函数。当然,为了避免弱引用没有定义就使用,在使用前最好用if判断一下foo是否是0。

弱引用一个重要的作用就是,使得程序的不同功能模块更容易地裁剪和组合。