连接原理

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


1 链接器的任务

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

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

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


2 目标文件

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

(1) 可重定位目标文件this

    这种文件包含二进制代码和数据,这些代码和数据已经转换成了机器指令代码和数据,可是还不能够直接执行。由于这些指令和数据中每每引用其余模块(目标文件)中的符号,这些其余模块的符号对于本模块来讲是未知的,这些符号的解析须要连接器将全部模块进行连接。这种操做称为“重定位”,所以,这种目标文件被称为“可重定位的目标文件”,后缀名一般为*.ospa

(2) 可执行目标文件debug

    这种文件一样包含了二进制代码和数据。所不一样的是,这种文件已经通过了连接操做,和全部的模块(目标文件)都产生了联系。连接器将全部须要的可重定位目标文件链接成一个可执行目标文件。这时,每一个目标文件中引用其余目标文件中的符号都已经获得了解析和重定位。所以,每一个符号都是已知的了,该文件能够被机器直接执行。3d

(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命令来链接这两个文件,最终运行效果以下:

xiaomanon@xiaomanon-machine:~/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'
xiaomanon@xiaomanon-machine:~/Documents/c_code$ gcc add.o main.o -o main
xiaomanon@xiaomanon-machine:~/Documents/c_code$ ./main 12 19
Sum = 31

补充关于ld的用法?

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

xiaomanon@xiaomanon-machine:~/Documents/c_code$ ls
add.c  add.o  main.c  main.o
xiaomanon@xiaomanon-machine:~/Documents/c_code$ ld -e main main.o
main.o: In function `main':
main.c:(.text+0x29): undefined reference to `add'

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

xiaomanon@xiaomanon-machine:~/Documents/c_code$ ld -e main main.o add.o
xiaomanon@xiaomanon-machine:~/Documents/c_code$ ls
add.c  add.o  a.out  main.c  main.o

咱们能够看到,最终生成了a.out文件,运行它:

xiaomanon@xiaomanon-machine:~/Documents/c_code$ ./a.out
Segmentation fault (core dumped)

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


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

    ELF(Excutable Linkable File)是Linux环境下最经常使用的目标文件格式,在大多数状况下,不管是可重定位的目标文件仍是可执行的目标文件都可采用这种格式。ELF格式的目标文件中不只包含了二进制的代码和数据,还包括不少帮助连接器解析符号和解释目标文件的信息。下图展现了一个典型的ELF格式的可重定位目标文件的结构。

image

    该目标文件主要由两部分组成: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工具查看可重定位目标文件内容,该工具能够读物目标文件的符号表,从而获得每个符号的信息。

xiaomanon@xiaomanon-machine:~/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) 重定位段

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

1

 

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

(2) 重定位符号引用

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


6 符号的重定位信息

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

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

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

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

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

相关文章
相关标签/搜索