C C++ 链接原理

连接器的任务

链接器将多个目标文件链接成为一个完整的,可加载,可执行的目标文件。其输入是一组可重定位的目标文件。链接的两个主要任务如下。
1.符号解析:将目标文件内的引用符号和该符号的定义联系起来。
2.将符号定义与存储的位置联系起来,修改对这些符号的引用。

典型的目标文件分为以下3种形式

1.可重定位目标文件:这种文件包含二进制代码和数据,这些代码和数据已经转换成了机器指令代码和数据。但是这种可重定位的目标文件还不可以直接执行,原因是这些指令和数据中往往引用其他模块(目标文件)中的符号。这些其他模块的符号对于本模块来说是未知。这些符号的解析需要链接器将所有的模块进行链接。这种操作称为“重定位”,因此这种目标文件成为“可重定位的目标文件”,后缀名通常是 .o 。
2.可执行目标文件:这种文件同样包含二进制代码和数据。所不同的是,这种文件已经经过了链接操作,和所有的模块(目标文件)都产生了联系。连接器将所有需要的可重定位目标文件链接为一个可执行目标文件。这时,每个目标文件中引用其他目标文件中的符号都已经得到了解析和重新定位。因此每个符号都是已知的,因此该文件可以被机器直接执行。
3.共享目标文件:这是一种特殊类型的可重定位目标文件,可以在需要它的程序运行或者加载时,动态地加载到内存中运行。这种文件的后缀名通常是“.so"。共享目标文件又称为“动态库”文件或者“共享库”文件。

代码演示

//add.c文件
int add (int a,int b){
 int c =  a+b;
 return c;
}
#include "stdio.h"

extern int add(int a,int b);
void main(void){
        int number = add(3,5);
        printf("number:%d",number);
}

接下来使用gcc编译一下文件。如果没有安装gcc编译工具,如下:

sudo apt-get update
sudo apt-get install gcc

接下来编译文件:

gcc -c add.c main.c  
ls

发现当前目录下多了两个文件,add.o main.o,使用链接器链接两个可重定位的目标文件,生成一个可执行的目标文件如下:

gcc add.o main.o -o app

当前路径下会多出一个app文件,然后在终端输入./app 可执行文件运行。

ELF 格式的可重定位目标文件

ELF(Executable Linkable File)是Linux环境下最常用的目标文件格式,在大多数情况下,无论是可重定位的目标文件还是可执行的目标文件均采用这种格式。ELF格式的目标文件中不仅包含二进制的代码和数据,还包括很多帮助连接器解析符号和解释目标文件的信息。如下图所示:

段头部表
.strtab
.line
.debug
.rel.data
.rel.text
symtab
.bss
.data
.rodata
.text
ELF头

该目标文件主要由两部分组成:ELF文件头和目标文件的段。ELF文件头的前16个字节构成了一个字节序,描述了生成该文件系统的字长以及字节序。剩下的部分包括了ELF文件的一些其他信息,其中包括ELF文件头的大小,目标文件的类型,目标机的类型,段头部表在目标文件内的文件偏移位置等。在链接和加载ELF格式的程序时,这些信息是很重要的。除了ELF文件头之外,剩下的部分由目标文件的段组成。这些段是ELF文件中的核心部分。由以下几个段组成。

  • .text:代码段,存储二进制的机器指令,这些指令可以被机器直接执行。
  • .rodata:只读数据段,存储程序中使用的复杂常量,例如字符串等。
  • .data:数据段。存储程序中已经被明确初始化的全局数据,包括C语言中的全局变量和静态变量。如果这些全局数据被初始化为0,则不存储在数据段中,而是存储在块存储段中。C语言局部变量保存在栈上,不出现在数据段中。
  • .bss:块存储段。存储未被明确初始化的全局数据。在目标文件中这个段并不占用实际的空间,而仅仅是一个占位符,以告知指定位置上应当预留全局数据的空间。块缓存段的存在原因是为了提高磁盘上存储空间的利用率。

注意

以上的4个段会实际加载到内存中,是实实在在的程序段。目标文件中还会有一些辅助程序进行链接和加载的信息,这些信息并不加载到内存中。事实上这些信息的生成最终的可执行目标文件时就已经被去掉了。

  • .symtab:符号表。存储定义和引用的函数和全局变量。每个可重定位的目标文件中都要有一个这样的表。在该表中,所有引用的本模块内的全局符号(包括函数和全局变量)以及其他模块(目标文件)中的全局符号都会有一个登记。链接中的重定位操作就是将这些引用的全局符号的位置确定。
  • .rel.text :代码段需要重定位的信息。存储需要靠重定位操作修改位置的符号汇总。这些符号在代码段中,通常是一些函数名和标号。
  • .rel.data:数据段需要重定位的信息。存储需要靠重定位操作修改位置的符号汇总。这些符号在数据段中,是一些全局变量。
  • .debug:调试信息。存储一个用于调试的符号表。在编译程序时使用gcc编译器的 -g 选项会生成该段。该表包括源程序中所有符号的引用和定义。有了这个段,在使用gdb调试器对程序进行调试时才可以打印并观察变量的值。
  • .line:源程序的行号映射。存储源程序中每一个语句的行号。在编译程序时使用gcc 编译器的 -g 选项会生成该段。在使用gdb调试器对程序进行调试时,这个段的作用时很大的。
  • .strtab:字符串表。存储.symtab符号表和.debug符号表中符号的名字,这些名字是一些字符串,并且以 ‘\0’ 结尾。
    注意:以上这些段不会出现在运行的程序中
    ,它们都是附加的信息。

你可能感兴趣的:(C C++ 链接原理)