链接原理

本文简单介绍了程序的链接原理。学习链接原理有助于程序员理解程序的本质,同时也可以为日后的大型软件的代码开发打下坚实的基础。由此可知链接原理的重要性,尤其是一些程序员被一些莫名其妙的错误困扰的时候,更加能够体会到这一点。


1 连接器的任务

连接器将多个目标文件链接成一个完整的、可加载、可执行的目标文件。其输入是一组可重定位的目标文件。链接的两个主要任务如下:

(1) 符号解析,将目标文件内的引用符号和该符号的定义联系起来。

(2) 将符号定义与存储器的位置联系起来,修改对这些符号的引用。


2 目标文件

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

(1) 可重定位目标文件

这种文件包含二进制代码和数据,这些代码和数据已经转换成了机器指令代码和数据,但是还不可以直接执行。因为这些指令和数据中往往引用其他模块(目标文件)中的符号,这些其他模块的符号对于本模块来说是未知的,这些符号的解析需要链接器将所有模块进行链接。这种操作称为“重定位”,因此,这种目标文件被称为“可重定位的目标文件”,后缀名通常为*.o

(2) 可执行目标文件

    这种文件同样包含了二进制代码和数据。所不同的是,这种文件已经经过了链接操作,和所有的模块(目标文件)都产生了联系。链接器将所有需要的可重定位目标文件连接成一个可执行目标文件。这时,每个目标文件中引用其他目标文件中的符号都已经得到了解析和重定位。因此,每个符号都是已知的了,该文件可以被机器直接执行。

(3) 共享目标文件

这是一种特殊的可定位目标文件,可以在需要它的程序运行或加载时,动态地加载到内存中运行。这种文件的后缀名通常是*.so。共享目标文件通常又被称为“动态库”文件或者“共享库”文件。

下面的示例演示了可重定位目标文件和可执行目标文件的产生。该程序使用两个简单的C语言源程序add.c和main.c文件,其中add.c中定义一个函数add(),实现两个整数相加;main.c中定义了main函数,在该函数中调用add()函数。

//@file add.c
//@brief sum 2 integers
int add(int a, int b)
{
    return (a+b);
}

//@file main.c
//@brief call add() from another file
#include <stdio.h>
#include <stdlib.h>
extern int add(int,int);

int main(int argc, char *argv[])
{
    int a, b;

    if (argc != 3)
    {
        printf("Usage: main a b\n");
        exit(-1);
    }
    a = atoi(argv[1]);
    b = atoi(argv[2]);
    printf("Sum = %d\n", add(a, b));

    return 0;
}

那么,我们使用ld命令链接两个文件,会提示以下错误,我个人觉得是因为代码中使用到了<stdio>和<stdlib>库中的函数,但是并没有指定对应的目标文件导致。当然,我函数习惯直接使用gcc命令来连接这两个文件,最终运行效果如下:

[email protected]:~/Documents/c_code$ ld add.o main.o –o main
ld: warning: cannot find entry symbol _start; defaulting to 0000000008048074
main.o: In function `main‘:
main.c:(.text+0x17): undefined reference to `puts‘
main.c:(.text+0x23): undefined reference to `exit‘
main.c:(.text+0x33): undefined reference to `atoi‘
main.c:(.text+0x47): undefined reference to `atoi‘
main.c:(.text+0x6f): undefined reference to `printf‘
[email protected]:~/Documents/c_code$ gcc add.o main.o -o main
[email protected]:~/Documents/c_code$ ./main 12 19
Sum = 31

补充关于ld的用法?

我们接下来就来解决上面使用ld命令链接可重定位目标文件时出错的问题。提示信息中,第一个warningd的意思是没有找到一个函数入口,我们可以使用ld命令的-e选项来指定:

[email protected]:~/Documents/c_code$ ls
add.c  add.o  main.c  main.o
[email protected]:~/Documents/c_code$ ld -e main main.o
main.o: In function `main‘:
main.c:(.text+0x29): undefined reference to `add‘

这里,又有一个错误提示:没有定义add,我们在其中添加对add.o的链接。

[email protected]:~/Documents/c_code$ ld -e main main.o add.o
[email protected]:~/Documents/c_code$ ls
add.c  add.o  a.out  main.c  main.o

我们可以看到,最终生成了a.out文件,运行它:

[email protected]:~/Documents/c_code$ ./a.out
Segmentation fault (core dumped)

结果出现了段错误,这是问什么呢?应该怎么解决?


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

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

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

除了ELF文件头之外,剩下的部分由目标文件的段组成。这些段是ELF文件中的核心部分。由以下几个段组成:

■ .text : 代码段,存储二进制的机器指令,这些指令可以被机器直接执行。

■ .rodata : 只读数据段,存储程序中使用的复杂常量,例如字符串等。

■ .data : 数据段,存储程序中已经被明确初始化的全局数据。包括C语言中的全局变量和静态变量。如果这些全局数据被初始化为0,则不存储在数据段中,而是被存储在块存储段中。C语言局部变量保存在栈上,不出现在数据段中。

■ .bss : 块存储段,存储未被明确初始化的全局数据。 在目标文件中这个段并不占用实际的空间,而仅仅是一个占位符,以告知指定位置上应当预留全局数据的空间。块存储段存在的原因是为了提高磁盘上存储空间的利用率。

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

■ .symtab : 符号表,存储定义和引用的函数和全局变量。每个可重定位的目标文件中都要有一个这样的表。在该表中,所有引用的本模块内的全局符号(包括函数和全局变量)以及其他模块(目标文件)中的全局符号都会有一个登记。链接中的重定位操作就是将这些引用的全局符号的位置确定。

■ .rel.text : 代码段需要重定位(relocate)的信息,存储需要靠重定位操作修改位置的符号的汇总。这些符号在代码段中,通常是一个函数名和标号。

■ .rel.data : 数据段需要重定位的信息,存储需要靠重定位操作修改位置的符号的汇总。这些符号在数据段中,是一些全局变量。

■ .debug : 调试信息,存储一个用于调试的符号表。在编译程序时使用gcc编译器的-g选项会生成该段,该表包括源程序中所有符号的引用和定义,有了这个段在使用gdb调试器对程序进行调试的时候才可以打印并观察变量的值。

■ .line : 源程序的行号映射,存储源程序中每一个语句的行号。在编译程序时使用gcc编译器的-g选项会生成该段,在使用gdb调试器对程序进行调试的时候这个段的作用很大。

■ .strtab : 字符串表,存储.symtab符号表和.debug符号表中符号的名字,这些名字是一些字符串,并且以‘\0’结尾。


4 目标文件中的符号表

符号解析是链接的主要任务之一。只有在正确解析了符号之后才能够更改引用符号的位置,从而完成重定位,生成一个可以被机器直接加载执行的可执行目标文件。每个可重定位目标文件都有一个符号表,在这个符号表中存储符号,这些符号分为3类:

(1) 本模块中引用的其他模块所定义的全局符号

(2) 本模块中定义的全局符号

(3) 本模块中定义和引用的局部符号

注意:局部变量和局部符号不是一回事。局部变量存储在栈中,是一个仅仅在内存中出现的概念;而局部符号包括静态变量和局部标号,这些内容也可能出现在磁盘文件中。

下面代码演示了在程序中使用局部符号。该程序声明了一个静态局部变量和一个局部变量,其中静态局部变量是一个局部符号。

//@file cnt.c
#include <stdio.h>

void f(int i)
{
    int static count = 10;
    int a = 0;

    count = i;
    count++;
    if (count >= 20)
        goto done;
    else{
        printf("the count is lower than 20\n");
        return;
    }
done:
    printf("the count is higher than 20\n");
    a = 20;
    printf("a is : %d\n", a);

    return;
}

int main(void)
{
    int i;
    scanf("%d", &i);
    f(i);
    return 0;
}

该程序中局部静态变量count和标号done都是局部符号,会出现在目标文件的符号表中,而局部变量a存储在栈上,因此不会出现在符号表中。

然后使用gcc –c cnt.c命令,编译得到可重定位目标文件cnt.o,这样我们就可以使用GNU的readelf工具查看可重定位目标文件内容,该工具可以读物目标文件的符号表,从而得到每一个符号的信息。

[email protected]:~/Documents/c_code$ readelf -a cnt.o
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2‘s complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          500 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           40 (bytes)
  Number of section headers:         13
  Section header string table index: 10

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00000000 000034 000095 00  AX  0   0  1
  [ 2] .rel.text         REL             00000000 000520 000068 08     11   1  4
  [ 3] .data             PROGBITS        00000000 0000cc 000004 00  WA  0   0  4
  [ 4] .bss              NOBITS          00000000 0000d0 000000 00  WA  0   0  1
  [ 5] .rodata           PROGBITS        00000000 0000d0 000045 00   A  0   0  1
  [ 6] .comment          PROGBITS        00000000 000115 000025 01  MS  0   0  1
  [ 7] .note.GNU-stack   PROGBITS        00000000 00013a 000000 00      0   0  1
  [ 8] .eh_frame         PROGBITS        00000000 00013c 000058 00   A  0   0  4
  [ 9] .rel.eh_frame     REL             00000000 000588 000010 08     11   8  4
  [10] .shstrtab         STRTAB          00000000 000194 00005f 00      0   0  1
  [11] .symtab           SYMTAB          00000000 0003fc 0000f0 10     12  10  4
  [12] .strtab           STRTAB          00000000 0004ec 000034 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

There are no section groups in this file.

There are no program headers in this file.

Relocation section ‘.rel.text‘ at offset 0x520 contains 13 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000011  00000301 R_386_32          00000000   .data
00000016  00000301 R_386_32          00000000   .data
0000001e  00000301 R_386_32          00000000   .data
00000023  00000301 R_386_32          00000000   .data
00000030  00000501 R_386_32          00000000   .rodata
00000035  00000b02 R_386_PC32        00000000   puts
0000004a  00000501 R_386_32          00000000   .rodata
0000004f  00000c02 R_386_PC32        00000000   printf
00000059  00000501 R_386_32          00000000   .rodata
0000005e  00000b02 R_386_PC32        00000000   puts
00000079  00000501 R_386_32          00000000   .rodata
0000007e  00000e02 R_386_PC32        00000000   __isoc99_scanf
0000008a  00000a02 R_386_PC32        00000000   f

Relocation section ‘.rel.eh_frame‘ at offset 0x588 contains 2 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000020  00000202 R_386_PC32        00000000   .text
00000040  00000202 R_386_PC32        00000000   .text

The decoding of unwind sections for machine type Intel 80386 is not currently supported.

Symbol table ‘.symtab‘ contains 15 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS cnt.c
     2: 00000000     0 SECTION LOCAL  DEFAULT    1
     3: 00000000     0 SECTION LOCAL  DEFAULT    3
     4: 00000000     0 SECTION LOCAL  DEFAULT    4
     5: 00000000     0 SECTION LOCAL  DEFAULT    5
     6: 00000000     4 OBJECT  LOCAL  DEFAULT    3 count.1826
     7: 00000000     0 SECTION LOCAL  DEFAULT    7
     8: 00000000     0 SECTION LOCAL  DEFAULT    8
     9: 00000000     0 SECTION LOCAL  DEFAULT    6
    10: 00000000   101 FUNC    GLOBAL DEFAULT    1 f
    11: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND puts
    12: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
    13: 00000065    48 FUNC    GLOBAL DEFAULT    1 main
    14: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND __isoc99_scanf

No version information found in this file.

可以看到,ELF格式文件输出信息的最后是一个符号表,这个符号表揭示了cnt.c源文件中符号的信息。符号表的第5列表示符号的作用域类型,LOCAL表示局部符号,而GLOBAL代表的是全局符号。

开始链接的时候,链接器首先完成的任务就是符号解析。由于符号已经被确定,链接器所要做的就是寻找所有参与链接的目标文件,查找这些文件中是否定义了本模块中尚未能解析的符号。

如果查找到未解析的符号的定义,则准备开始下一步重定位;如果寻找所有参与链接的目标文件后仍然找不到未解析的符号的定义,则认为该符号未定义,从而将出错信息输出给用户。当全部的符号都被解析之后,就可以开始链接的第二个任务——重定位。


5 重定位的概念

当符号解析结束之后,每个符号的定义位置以及大小都是已知的了。重定位操作只需要将这些符号链接起来。在这个步骤中,链接器需要将所有参与链接的目标文件合并,并且为每一个符号分配存储内容的运行时地址。重定位分为以下两步进行:

(1) 重定位段

这一步将所有目标文件中同类型的段合并,生成一个大段。例如,将所有参与链接的目标文件的数据段合并,生成一个大的数据段;所有目标文件的代码段也被合并,生成一个大的代码段,如下图所示。

合并之后,程序中的指令和变量就拥有一个统一的并且唯一的运行时地址了。

(2) 重定位符号引用

由于目标文件中相同的段已经合并,因此程序中对富豪的引用位置也就都作废了。这是链接器需要修改这些引用符号的地址,使其指向正确的运行时地址。


6 符号的重定位信息

当编译器生成一个目标文件后,其并不知道代码和变量最终的存储位置,也不知道定义在其他文件中的外部符号。因此,编译器会生成一个重定位表目,里面存储着关于每一个符号的信息。这个表目告知链接器在合并目标文件时应该如何修改每个目标文件中对符号的引用。这种重定位表目存储在.rel.text段和.rel.data段中。该表目可以理解为一个结构体,其中存储着每一个符号的重定位信息。

typedef struct {
    int offset;/*偏移值*/
    int symbol;/*所代表的符号*/
    int type;/*符号的类型*/
}symbol_rel;

offset表示该符号在存储的段中的偏移值。symbol代表该符号的名称,字符串实际存储在.strtab段中,这里存储的是该字符串首地址的下标。type表示重定位类型,链接器只关心两种类型,一种是与PC相关的重定位引用,另一种是绝对地址引用。

PC相关的重定位引用表示将当前的PC值(这个值通常是吓一跳指令的存储位置)加上该符号的偏移值。绝对地址引用表示将当前指令中已经指定的地址引用直接作为跳转的地址,不需要进行任何修改。

有了这些信息,链接器就可以将符号在存储段中的偏移值加上该段在重定位后的新地址,这样就得到了一个新的引用地址,而这个引用地址就是该符号的最终地址。同样,在程序中所有引用该地址的部分都要做修改,使用这个新的绝对地址代替旧的偏移地址。当新的符号地址被修改完毕以后,链接器的工作就结束了。

时间: 2024-08-09 10:38:01

链接原理的相关文章

搜索引擎原理之链接原理的简单分析

在google诞生以前,传统搜索引擎主要依靠页面内容中的关键词匹配搜索词进行排名.这种排名方式的短处现在看来显而易见,那就是很容易被刻意操纵.黑帽SEO在页面上推挤关键词,或加入与主题无关的热门关键词,都能提高排名,使搜索引擎排名结果质量大为下降.现在的搜索引擎都使用链接分析技术减少垃圾,提高用户体验.下面泡馆史明星就来简单的介绍链接在搜索引擎排名中的应用原理. 在排名中计入链接因素,不仅有助于减少垃圾,提高结果相关性,也使传统关键词匹配无法排名的文件能够被处理.比如图片.视频无法进行关键词匹配

迅雷专用链接原理及转换

迅雷专用链接原理及转换内容简介: 现在就链接的编码原理及转换详细说明一下,明白以后就可以用迅雷下载快车.旋风专用地址的软件啦. 首先要明白Base64编码是怎么一回事,不懂的先去百度一下再回来. Base64编码是一种加密算法,Email的原始信息就是由Base64编码构成的. 而这些专用链接都是通过Base64编码加工转换而成的. 迅雷专用地址例子:thunder://QUFodHRwOi8vc29zb2J0LmNvbS9aWg== 真实文件下载链接:http://sosobt.com/ 搞懂

gcc编译链接原理及使用

gcc 的使用方法: gcc    [选项]    文件名 gcc常用选项: -v:查看gcc 编译器的版本,显示gcc执行时的详细过程 -o    < file >             Place  the output  into   < file > 指定输出文件名为file,这个名称不能跟源文件名同名 -E Preprocess only; do not compile, assemble or link 只预处理,不会编译.汇编.链接 -S   Compile onl

浅析静态库链接原理

静态库的链接基本上同链接目标文件.obj/.o相同,但也有些不同的地方.本文简要描述linux下静态库在链接过程中的一些细节. 静态库文件格式 静态库远远不同于动态库,不涉及到符号重定位之类的问题.静态库本质上只是将一堆目标文件进行打包而已.静态库没有标准,不同的linux下都会有些细微的差别.大致的格式wiki上描述的较清楚: Global header ----------------- +------------------------------- File header 1 --->

复制、移动、删除、软链接、硬链接原理

复制是将一个文件流传输到另一个文件流,本质是新建 移动 如果在同一个分区内,移动文件,文件的inode信息是不会变的,如果跨分区,将变成删除本分区的文件,在另一个分区新建文件,将数据流拷贝过去 同分区移动文件 删除 linux中的删除是很快的,新建2个G的文件很慢,但是删除很快,是因为在删除的时候只是在文件的inode中标识一个未使用标志,这样其他进程就可以在这里写入数据,所以一般文件删除之后,只要没有再写入数据,都是可以找回来数据的 软链接 实际上就是一个快捷方式 硬链接 文件的拷贝,每硬链接

迅雷专用链接和旋风专用链接编码及转换方法(摘抄)

目前网上比较流行迅雷下载,迅雷专用链接原理及转换内容简介: 现在就链接的编码原理及转换详细说明一下,明白以后就可以用迅雷下载快车.旋风专用地址的软件啦. 首先要明白Base64编码是怎么一回事,Base64编码是一种加密算法,目前Email的原始信息就是由Base64编码构成的. 而这些专用链接都是通过Base64编码加工转换而成的. 拿迅雷专用地址例子:thunder://QUFodHRwOi8vc29zb2J0LmNvbS9aWg== 则真实文件下载链接:http://sosobt.com/

Linux下链接问题小结(undefined reference)

一直以来对Linux下编译链接产生的问题没有好好重视起来,出现问题就度娘一下,很多时候的确是在搜索帮助下解决了BUG,但由于对原因不求甚解,没有细细研究,结果总是在遇到在BUG时弄得手忙脚乱得. 甚至有时候为了一个问题查了半天的资料,好不容易解决了,却因为没有记录下来或者没有弄清楚真实原因,结果第二次碰到还是要去重复前次的折腾,很是尴尬无奈. 虽然,同样的错误信息,其产生的原因不一而足,但是,总结一下终归是好的,使不知变知之,只要不在同一件事情上重复同样的错误,发现的问题越多,解决的问题越多,未

Web的运行原理

1.web工作原理 我是学习PHP网站建设的,那么网站在客户端和服务端的运行是网站运行的根本所在,那个这个运行过程是怎样的呢?我们一探就将! Web:万维网(WorldWideWeb) 服务器:我们把提供(响应)服务的计算机称作服务器(Server).  客户端:接受(请求)服务的计算机称作客户机(Client),也叫工作站(Workstations). 1.1服务器端和客户端连接原理 很简明的意思,就是在客户端请求数据,然后把服务的请求执行结果反馈给客户端. 客户机/服务器的这种计算机间的协作

有关目录链接小结

有关目录链接小结 对于目录,不可以创建硬链接,但可以创建软链接. 对于目录的软链接是生产场景运维中常用的技巧(例子第二关Apache考试题) 目录的硬链接不能跨越文件系统(从硬链接原理可以理解) 每个下面都有一个硬链接"."号,和对应上级目录的硬链接"..". 在父目录里创建一个子目录,父目录的链接数增加1(子目录里都有..来指向父目录).但是在父目录里创建文件,父目录的链接数不会增加. 请问下面的目录的链接数为什么是3? [[email protected] ol