静态链接

作者: Cool_Pomelo | 来源:发表于2020-04-01 09:33 被阅读0次

静态链接

当有两个目标文件时,如何将它们连接起来形成一个可执行文件?其中发生了什么?

使用两个源代码文件作为研究例子:

a.c


extern int shared;

int main()
{
    int a = 100;

    swap(&a,&shared);

}

b.c


int shared = 1;

void swap(int *a,int *b)
{
    *a^=*b^=*a^=*b;
}

使用gcc将两个文件编译成目标文件a.o和b.o


gcc -c a.c b.c

图示:

图1.png

b.c里面定义了两个全局符号,

  • shared
  • 函数swap

a.c里面定义了一个全局符号

  • main

a.c引用到了b.c里面的swao与shared,接下来就是把这两个目标文件链接在一起最终形成一个可执行文件

空间与地址分配

可执行文件中代码段和数据段都是输入的目标文件合并而来的,那么链接过程产生的第一个问题:对于多个输入目标文件,链接器如何将它们的各个段合并到输出文件?

按序叠加

一个方案就是按照输入次序叠加起来:

图2.png

这种做法很简单,但是有一个问题:

在很多输入文件的时候,输出文件将会有很多零散的段,这种做法很浪费空间,因为每个段都需要有一定的地址和空间对齐要求,而且还会造成碎片问题,所以这不是一个好的方案

相似段合并

一个更实际的方法是将相同性质的段合并到一起,比如将所有输入文件的.text合并到输出文件的.text,接着是.data,.bss

图示:

图3.png

.bss段在目标文件和可执行文件并不占用文件空间,但是在装载时占用地址空间,所以链接器在合并各个段的时候,需要将.bss也合并,并且分配虚拟空间

链接器为目标文件分配地址和空间中,地址和空间有两个含义:

  • 一个是在输出的可执行文件中的空间

  • 一个是在装载后的虚拟地址中的虚拟地址空间

对于有实际数据的段,如.text和.data,它们在文件中和虚拟地址中都有分配空间,因为它们在这两者中都存在,而对于.bss这样的段来说,分配空间的意义只局限于虚拟地址空间,因为它在文件中并没有内容

现在链接器空间分配策略采用第二种方法,使用这种方法的链接器一般采用两步链接的方法:

  • 空间和地址分配

扫描所有的输入目标文件,并且获得它们的各个段的长度,属性,位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放在一个全局符号表。这一步,链接器能够获得所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度和位置,并建立映射关系。

  • 符合解析与重定位

使用上面的第一步中收集到的所有信息,读取输入文件中段的数据,重定位信息,并且进行符合解析与重定位,调整代码中的地址等。

用链接器将a.o和b.o链接起来:


ld a.o b.o -e main -o ab

接下来使用objdump来查看链接前后地址的分配情况:

objdump -h a.o

输出:



a.o:     文件格式 elf64-x86-64

节:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000051  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000000  0000000000000000  0000000000000000  00000091  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  00000091  2**0
                  ALLOC
  3 .comment      0000002a  0000000000000000  0000000000000000  00000091  2**0
                  CONTENTS, READONLY
  4 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000bb  2**0
                  CONTENTS, READONLY
  5 .eh_frame     00000038  0000000000000000  0000000000000000  000000c0  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA


objdump -h b.o

输出



b.o:     文件格式 elf64-x86-64

节:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         0000004b  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         00000004  0000000000000000  0000000000000000  0000008c  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  00000090  2**0
                  ALLOC
  3 .comment      0000002a  0000000000000000  0000000000000000  00000090  2**0
                  CONTENTS, READONLY
  4 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000ba  2**0
                  CONTENTS, READONLY
  5 .eh_frame     00000038  0000000000000000  0000000000000000  000000c0  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA



objdump -h ab

输出:


ab:     文件格式 elf64-x86-64

节:
Idx Name          Size      VMA               LMA               File off  Algn
 13 .text         00000222  0000000000000560  0000000000000560  00000560  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 14 .fini         00000009  0000000000000784  0000000000000784  00000784  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 15 .rodata       00000004  0000000000000790  0000000000000790  00000790  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 16 .eh_frame_hdr 00000044  0000000000000794  0000000000000794  00000794  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 17 .eh_frame     00000128  00000000000007d8  00000000000007d8  000007d8  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 18 .init_array   00000008  0000000000200db8  0000000000200db8  00000db8  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 19 .fini_array   00000008  0000000000200dc0  0000000000200dc0  00000dc0  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 20 .dynamic      000001f0  0000000000200dc8  0000000000200dc8  00000dc8  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 21 .got          00000048  0000000000200fb8  0000000000200fb8  00000fb8  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 22 .data         00000014  0000000000201000  0000000000201000  00001000  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 23 .bss          00000004  0000000000201014  0000000000201014  00001014  2**0
                  ALLOC
 24 .comment      00000029  0000000000000000  0000000000000000  00001014  2**0
                  CONTENTS, READONLY


//省略了剩余内容

上述输出中:VMA:虚拟地址,LMA:加载地址

链接前后的程序中所使用的的地址已经是程序在进程中的虚拟地址,即各个段里面的VMA和Size,可以看到,在链接之前,目标文件中所有段的VMA都是0,因为虚拟地址空间还没分配,所以默认为0,链接之后,可执行文件ab中各个段都被分配到了相应的虚拟地址,

符号地址的确定

在第一步的扫描和空间分配阶段,链接器按照上面的空间分配方法进行分配,这时候输入文件中各个段在链接后的虚拟地址就已经确定了。

当前面这步完成后,链接器开始计算各个符号的虚拟地址,因为各个符号在段内的相对位置是固定的,只不过链接器要给每个符号加上一个偏移量,使得它们能够调整到正确的虚拟地址。

符号解析与重定位

重定位

完成空间与地址分配后,链接器就进入了符号解析与重定位的步骤

在分析符号解析与重定位之前,先看看a.o里面是怎样使用这两个外部符号的,也就是源代码里面的shared变量以及swap函数

编译器将a.c编译成指令后,如何访问shared和调用swap?

使用objdump的-d参数可以查看反汇编结果


objdump -d a.o




a.o:     文件格式 elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   48 83 ec 10             sub    $0x10,%rsp
   8:   64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
   f:   00 00 
  11:   48 89 45 f8             mov    %rax,-0x8(%rbp)
  15:   31 c0                   xor    %eax,%eax
  17:   c7 45 f4 64 00 00 00    movl   $0x64,-0xc(%rbp)
  1e:   48 8d 45 f4             lea    -0xc(%rbp),%rax
  22:   48 8d 35 00 00 00 00    lea    0x0(%rip),%rsi        # 29 <main+0x29>
  29:   48 89 c7                mov    %rax,%rdi
  2c:   b8 00 00 00 00          mov    $0x0,%eax
  31:   e8 00 00 00 00          callq  36 <main+0x36>
  36:   b8 00 00 00 00          mov    $0x0,%eax
  3b:   48 8b 55 f8             mov    -0x8(%rbp),%rdx
  3f:   64 48 33 14 25 28 00    xor    %fs:0x28,%rdx
  46:   00 00 
  48:   74 05                   je     4f <main+0x4f>
  4a:   e8 00 00 00 00          callq  4f <main+0x4f>
  4f:   c9                      leaveq 
  50:   c3                      retq   

程序代码里面使用的都是虚拟地址,可以发现main的起始地址为 0x00000000,这是因为在未进行空间分配之前,目标文件代码段中的起始地址以 0x00000000开始,等到空间分配完成以后,各个函数才会确定自己在虚拟地址中的位置

上边反汇编输出中:

  • 最左边那列是每条指令的偏移量。一行代表一条指令

接着反汇编输出ab的代码段,可以发现main函数的两个重定位入口被修正到了正确的位置:


objdump -d ab



000000000000066a <main>:
 66a:   55                      push   %rbp
 66b:   48 89 e5                mov    %rsp,%rbp
 66e:   48 83 ec 10             sub    $0x10,%rsp
 672:   64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
 679:   00 00 
 67b:   48 89 45 f8             mov    %rax,-0x8(%rbp)
 67f:   31 c0                   xor    %eax,%eax
 681:   c7 45 f4 64 00 00 00    movl   $0x64,-0xc(%rbp)
 688:   48 8d 45 f4             lea    -0xc(%rbp),%rax
 68c:   48 8d 35 7d 09 20 00    lea    0x20097d(%rip),%rsi        # 201010 <shared>
 693:   48 89 c7                mov    %rax,%rdi
 696:   b8 00 00 00 00          mov    $0x0,%eax
 69b:   e8 1b 00 00 00          callq  6bb <swap>
 6a0:   b8 00 00 00 00          mov    $0x0,%eax
 6a5:   48 8b 55 f8             mov    -0x8(%rbp),%rdx
 6a9:   64 48 33 14 25 28 00    xor    %fs:0x28,%rdx
 6b0:   00 00 
 6b2:   74 05                   je     6b9 <main+0x4f>
 6b4:   e8 87 fe ff ff          callq  540 <__stack_chk_fail@plt>
 6b9:   c9                      leaveq 
 6ba:   c3                      retq   

00000000000006bb <swap>:

重定位表

链接器如何知道哪些指令需要被调整?这些指令的哪些部分要被调整?怎么调整?

在ELF文件中,有一个叫做重定位表的结构专门用来保存这些与重定位相关的信息

对于可重定位的ELF文件来说,它必须具有重定位表,用来描述如何修改相应的段里的内容,而对于每个要被重定位的ELF段都有一个对应的重定位表,这一个重定位表往往就是ELF文件中的一个段,比如代码段.text如有要被重定位地方,那么会有一个相对于叫做.rel.text的段保存了代码段的重定位表

使用objdump查看:


objdump -r a.o


a.o:     文件格式 elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE 
0000000000000025 R_X86_64_PC32     shared-0x0000000000000004
0000000000000032 R_X86_64_PLT32    swap-0x0000000000000004
000000000000004b R_X86_64_PLT32    __stack_chk_fail-0x0000000000000004


RELOCATION RECORDS FOR [.eh_frame]:
OFFSET           TYPE              VALUE 
0000000000000020 R_X86_64_PC32     .text



每个要被重定位的地方叫一个重定位入口,重定位入口的偏移OFFSET 表示该入口在要被重定位的段中的位置,“RELOCATION RECORDS FOR [.text]:”表示这个重定位表是代码段的重定位表

符号解析

之所以链接时因为目标文件中用到的符号被定义在其他目标文件,所以要将它们链接起来,比如直接用ld来链接a.o,而不将b.o作为输入,就会报错


ld a.o


ld: 警告: 无法找到项目符号 _start; 缺省为 00000000004000b0
a.o:在函数‘main’中:
a.c:(.text+0x25):对‘shared’未定义的引用
a.c:(.text+0x32):对‘swap’未定义的引用
a.c:(.text+0x4b):对‘__stack_chk_fail’未定义的引用


重定位的过程也伴随着符号的解析过程,每个目标文件中都可能定义一些符号,也可能引用到定义在其它目标文件的符号,重定位的过程,每个重定位的入口都是对一个符号的引用,那么当链接器要对某个符号的引用进行重定位时,它就要确定这个符号的目标地址,这是链接器就去查找所有输入目标文件的符号表组成的全局符号,找到后进行重定位

比如查看a.o的符号表:


readelf -s a.o


Symbol table '.symtab' contains 13 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS a.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     8: 0000000000000000    81 FUNC    GLOBAL DEFAULT    1 main
     9: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND shared
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
    11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND swap
    12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND __stack_chk_fail

可以发现"shared"和"swap"都是UND,即“undefined”未定义类型

COMMON块

弱符号机制允许同一个符号的定义存在于多个文件中。目前的链接器本身并不支持符号的类型,即变量类型对于链接器来说是透明的,它只知道一个符号的名字,并不知道类型是否一致。

多个符号(同名)定义类型不一致的几种情况

  • 两个或两个以上强符号类型不一致
  • 有一个强符号,其他都是弱符号,出现类型不一致

  • 两个或两个以上弱符号类型不一致

针对第一种,无需额外处理,因为多个强符号定义本身就是非法的

现在的编译器和链接器都支持一种叫COMMON块的机制,来源于Fortran,早起Fortran没有动态分配空间的机制,程序员必须事先声明它所需的临时使用空间的大小

COMMON类型的链接规则:原则上讲最终链接后的输出文件中,弱符号的大小以输入文件中最大(所占空间)的那个为准

如果有 一个符号为强符号,那么最终输出结果所占空间大小与强符号相同。【如果链接过程中,有弱符号大于强符号,编译器会报警告】

未初始化的全局变量最终还是被放在BSS段的
一旦有一个未初始化的全局变量不是以COMMON块的形式存在,那么它就相当于一个强符号。

静态库链接

程序之所以有用,因为它会有输入输出,这些输入输出对象可以是数据,也可以是人,也可以是另外一个程序,还可以是另外一台计算机。但是一个程序如何做到输入输出呢?最简单的办法是使用操作系统提供的应用程序编程接口(API)。当然,操作系统也是一个程序。

程序如何使用操作系统提供的 API ? 一般情况下,一种语言的开发环境往往会附带语言库,这些库就是对操作系统 API 的包装。库里面还有一些很常用的函数,这部分函数不调用操作系统 API。

静态库可以简单的看成一组目标文件的集合,即很多目标文件打包后形成的一个文件

  • Linux中最常用的C语言静态库libc位于/usr/lib/libc.a,属于glibc项目的一部分

  • Windows上,最常用的C语言库是IDE附带的运行库

链接过程控制

绝大部分情况下,我们使用链接器提供的默认规则对目标文件进行链接。一般情况下没有问题,但对于一些特殊程序,比如操作系统内核,BIOS或在一些没有操作系统的情况下运行的程序(如引导程序 Boot Loader或者嵌入式系统的程序),以及另外的一些需要特殊的链接过程的程序,如一些内核驱动程序,它们往往受限于一些特殊的条件,如需要指定输出文件的各个段虚拟地址,段的名称,段的存放顺序等,因为这些特殊的环境,特别是某些硬件的限制,往往对程序的各个段的地址有特殊的要求。

由于整个链接过程有很多内容需要确定:

  • 使用哪些目标文件

  • 使用哪些库文件

  • 是否在最终可执行文件中保存调试信息

  • 输出文件格式(可执行文件还是动态链接库)

  • 还要考虑是否要导出某些符号以供调试器或程序本身或其他程序使用等

链接控制脚本

链接器一般都提供多种控制整个链接过程的方法,以用来产生用户所需要的文件。一般链接器有如下3种方法:

  • 1.使用命令行来给链接器指定参数,如 ld 使用的 -o, -e
  • 2.将链接器指令存放在目标文件里面,编译器进程会通过这种方法向链接器传递指令。
  • 3.使用链接控制脚本,也是最为灵活,最为强大的链接控制方法

最小的程序

这个最小的程序,功能就是在终端上输出:"Hello world!",但这个经典的hello wrold程序有点不同:

  • 之前的程序使用了printf函数,该函数是C语言库的一部分,这次希望这个小程序能够摆脱C语言运行库,使得它成为一个独立于任何库的纯正的程序

  • 经典的版本程序使用了库,所以必须由main函数,所以此次不适用main这个函数名了用nomain作为整个程序的入口

  • 经典的程序会产生许多段,这次将所有段都合并到一个叫做tinytext的段,这个段的名字是任意起的

TinyHelloWorld.c



char* str = "Hello World!\n";

void print(){
    asm( "movl $14, %%edx \n\t"     
        "movl  %0, %%ecx \n\t"      
        "movl $0, %%ebx \n\t"       
        "movl $4, %%eax \n\t"       
        "int $0x80     \n\t"
        ::"r"(str):"edx","ecx","ebx");  
}

void exit() {
    asm( "movl $42,%ebx \n\t"
        "movl $1,%eax \n\t"
        "int $0x80    \n\t");
}

void nomain() {
    print();
    exit();
}


程序入口函数为nomain函数,调用了print函数,打印“hello world”,接着调用exit函数,结束进程,上面使用了GCC内嵌汇编,

现在先使用普通命令行的方式来编译和链接TinyHelloWorld.c:


(1)gcc -c -fno-builtin -m32 TinyHelloWorld.c


(2)ld -static -m elf_i386  -e nomain -o TinyHelloWorld TinyHelloWorld.o

第一步用GCC将TinyHelloWorld.c编译成TinyHelloWorld.o,接着用ld将TinyHelloWorld.o链接成可执行文件TinyHelloWorld

其中一些参数的含义:

  • -fno-builtin,GCC提供了很多内置函数(Built-in Function),它会把一些常用的C库函数替换成编译器的内置函数,以达到优化的功能
  • -static,表示ld将使用静态链接的方式来链接程序,而不是使用默认的动态链接

  • -e nomain,表示该程序的入口函数为nomain

  • -o TinyHelloWorld,指定输出可执行文件名为TinyHelloWorld

ld链接脚本

如果把整个链接过程比作一台计算机,那么ld链接器就是计算机的CPU,所有的目标文件,库文件就是输入,链接结果输出的可执行文件就是输出,而链接控制脚本正是这台计算机的“程序”,

不论是输出文件还是输入文件,它们的主要的数据就是文件中的各个段,我们把输入文件中的段,叫输入段,输出文件中的段称为输出段。简单的说,控制链接过程无非就是控制输入段如何变成输出段,比如哪些输入段要合并成一个输出段,哪些输入段要丢弃;指定输出段的名字,装载地址,属性等。

TinyHelloWorld的链接脚本TinyHelloWorld.lds:


ENTRY(nomain) 
SECTIONS  
{
  . = 0x0804800 + SIZEOF_HEADERS;
  tinytext : { *(.text) *(.data) *(.rodata) }
  /DISCARD/ : { *(.comment) }
}

ENTRY :指定了程序的入口为 nomain() 函数
SECTIONS : 链接脚本的主体,这个命令制定了各个输入端到输出端的变换,SECTIONS 后面紧跟着一对大括号里面包含了 SECTIONS 变换规则,其中有3条语句,每条语句一行。第一行是赋值语句,后面2条是段转换规则,它们的含义如下:

    1. . = 0x0804800 + SIZEOF_HEADERS; 第一条赋值语句的意思是将当期虚拟地址设置为 0x0804800 + SIZEOF_HEADERS, SIZEOF_HEADERS 为输出文件的文件大小。
      "." 表示当前的虚拟地址,因为这条语句后面紧跟着输出端 "tinytext", 所以 "tinytext" 段的起始虚拟地址为 0x0804800 + SIZEOF_HEADERS。它将当期虚拟地址
      设置成一个比较巧妙的值,以便于装载时页面映射更为方便。
    1. tinytext : { *(.text) *(.data) *(.rodata) } 第二条是段转换规则,它的意思即所有的输入文件中的名字为 ".text", ".data" 或者 ".rodata" 的段依次合并
      到输出文件的 "tinytext"
    1. /DISCARD/ : { *(.comment) } 第三天规则为:将所有输入文件中的名字为 ".comment" 的段丢弃,不保存到输出文件中。

通过命令行来编译TinyHelloWorld,并启用该链接控制脚本:


ld -static -m elf_i386 -T TinyHelloWorld.lds -e nomain -o TinyHelloWorld TinyHelloWorld.o


使用objdum查看TinyHelloWorld的段,可以发现整个程序只有一个段“tinytext”

图4.png

参考资料

<<程序员的自我修养—链接、装载与库>>

https://blog.csdn.net/neuq_jtxw007/article/details/78112672

相关文章

  • 操作系统

    • Linux静态链接和动态链接; (转)静态链接和动态链接1、静态链接静态链接方法:#pragma comme...

  • 静态链接

    静态链接 静态链接涉及的内容包含如下 空间地址的分配 符号解析和重定位 静态库链接 本文的测试代码以及其他文件存在...

  • 静态链接

    编译和链接 预处理 编译扫描(词法分析)、语法分析、语义分析(静态语义是编译器所能分析的,动态语义要在运行期才能确...

  • 静态链接

    大多数编译系统提供编译器驱动程序( compiler driver ),它代表用户在需要时调用语言预处理器、编译器...

  • 静态链接

    静态链接 当有两个目标文件时,如何将它们连接起来形成一个可执行文件?其中发生了什么? 使用两个源代码文件作为研究例...

  • 动态链接与静态链接

    什么是静态链接 如何实现静态链接 静态链接的优点与缺点 什么是动态链接 如何实现动态链接 动态链接的优点与缺点 S...

  • seo中的url结构优化

    一、url结构(访问结构)(链接结构) url一般有:静态链接,动态链接,伪静态链接 伪静态对seo来说非常...

  • 静态库静态链接静态库

    静态库·静态链接·静态库 这么搞的都是有强迫症的人在做SDK。 not me. PS: 多次静态链接同一个第三方库...

  • 《程序员的自我修养》笔记

    第二章 静态链接 疑问: 问什么静态链接不会把所有代码链接进程序 为什么要静态链接 被隐藏的过程 gcc hell...

  • 静态编译、动态编译、静态链接、动态链接

    1、静态编译: 静态编译就是编译器在编译可执行文件时,将可执行文件需要调用的部分从对于动态库中提取出来,链接到可执...

网友评论

    本文标题:静态链接

    本文链接:https://www.haomeiwen.com/subject/vkcouhtx.html