静态链接与动态链接

引入:理解链接过程

由一个.c源文件得到一个二进制可执行文件需要经历预处理、编译、汇编和链接:

  • 预处理:包括头文件的包含、宏定义的扩展、条件编译的选择等 gcc -E hello.c

  • 编译:经过词法分析、语法分析、语义分析,将源代码翻译成汇编代码 gcc -S hello.c

  • 汇编:把作为中间结果的汇编代码翻译成了机器代码,即目标代码 gcc -c hello.s

代码在链接之前经历:源码文件(.c)---> 汇编代码(.s)---> 目标文件(.o),此时得到的目标文件还不是可以运行的二进制文件,利用 readelf -e hello.o 可以看到hello.o只有elf头和节头,没有程序头,它不是一个可执行文件,它是一个可重定位文件,需要经过链接将它制作成可执行文件

链接:由于项目很可能有多个文件(模块)相互依赖、相互引用,因此需要通过链接处理好各个模块之间的相互引用关系。链接过程做的事包括:

  • 符号决议
  • 地址空间分配
  • 符号重定位

Q:为什么要链接?gcc -c产生的编译结果的elf文件不是已经包含了能够执行的汇编代码吗?

A:编译过程只针对单个文件进行,将高级程序语言的代码翻译成机器码,而没有考虑具体的运行时,如程序的加载地址、外部的变量符号函数等。举个例子:

1.c

#include <stdio.h>

extern int a;
int main()
{
	a += 1;
	printf("hello\n");
	return 0;
}

2.c

int a = 1;

源文件1.c引用了2.c中的变量a。编译它们得到1.o2.o

gcc -c 1.c 2.c

然后利用objdump -d 1.o查看反汇编代码:

qxy@qxy-XPS-13-9360:~/Desktop/test$ objdump -d 1.o

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


Disassembly of section .text:

0000000000000000 <main>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	8b 05 00 00 00 00    	mov    0x0(%rip),%eax        # a <main+0xa>
   a:	83 c0 01             	add    $0x1,%eax
   d:	89 05 00 00 00 00    	mov    %eax,0x0(%rip)        # 13 <main+0x13>
  13:	bf 00 00 00 00       	mov    $0x0,%edi
  18:	e8 00 00 00 00       	callq  1d <main+0x1d>
  1d:	b8 00 00 00 00       	mov    $0x0,%eax
  22:	5d                   	pop    %rbp
  23:	c3                   	retq   

此时编译得到的代码只是单纯的对源文件的c代码进行转译,而并没有得到正确的变量a的值。同时也可以查看该目标代码的elf头:

qxy@qxy-XPS-13-9360:~/Desktop/test$ readelf -h 1.o
ELF 头:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  类别:                              ELF64
  数据:                              2 补码,小端序 (little endian)
  版本:                              1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  类型:                              REL (可重定位文件)
  系统架构:                          Advanced Micro Devices X86-64
  版本:                              0x1
  入口点地址:               0x0
  程序头起点:          0 (bytes into file)
  Start of section headers:          736 (bytes into file)
  标志:             0x0
  本头的大小:       64 (字节)
  程序头大小:       0 (字节)
  Number of program headers:         0
  节头大小:         64 (字节)
  节头数量:         13
  字符串表索引节头: 12

它的入口点地址是0x0,也不是程序最终的入口虚拟地址

然后再使用gcc -o hello 1.o 2.o将两个目标文件进行链接,得到可执行文件hello,此时再看main函数的反汇编代码和文件的elf头:

qxy@qxy-XPS-13-9360:~/Desktop/test$ objdump -d hello
...
00000000004005fd <main>:
  4005fd:	55                   	push   %rbp
  4005fe:	48 89 e5             	mov    %rsp,%rbp
  400601:	8b 05 29 0a 20 00    	mov    0x200a29(%rip),%eax        # 601030 <a>
  400607:	83 c0 01             	add    $0x1,%eax
  40060a:	89 05 20 0a 20 00    	mov    %eax,0x200a20(%rip)        # 601030 <a>
  400610:	bf b4 06 40 00       	mov    $0x4006b4,%edi
  400615:	e8 d6 fe ff ff       	callq  4004f0 <puts@plt>
  40061a:	b8 00 00 00 00       	mov    $0x0,%eax
  40061f:	5d                   	pop    %rbp
  400620:	c3                   	retq   
  400621:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
  400628:	00 00 00 
  40062b:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)
  ...
  
  qxy@qxy-XPS-13-9360:~/Desktop/test$ readelf -h hello
ELF 头:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  类别:                              ELF64
  数据:                              2 补码,小端序 (little endian)
  版本:                              1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  类型:                              EXEC (可执行文件)
  系统架构:                          Advanced Micro Devices X86-64
  版本:                              0x1
  入口点地址:               0x400500
  程序头起点:          64 (bytes into file)
  Start of section headers:          6488 (bytes into file)
  标志:             0x0
  本头的大小:       64 (字节)
  程序头大小:       56 (字节)
  Number of program headers:         9
  节头大小:         64 (字节)
  节头数量:         30
  字符串表索引节头: 29

显而易见有几处发生了变化:文件从重定位文件变成了可执行文件,入口地址变更为一个合适的虚拟地址,add语句中找到了正确的变量a。这就是前文所说的“符号决议,地址空间和分配符号重定位”。链接器在重定位的过程中对目标文件中未定义的部分发生修改

静态链接与动态链接

静态链接: 所有目标文件和外部库静态地绑定在一起。在最终的可执行文件中,所有符号都被解析出来,运行时不依赖任何外部库

动态链接: 外部内容没有被完整地拷贝进最终的可执行文件,而是在运行时动态地加载。程序运行时必须能够找到这些库,解析动态链接进来的符号引用,然后才能真正开始执行程序

继续以上述代码举例。首先是静态库:

$ gcc -c 1.c 2.c
$ ar rv lib2static.a 2.o
$ gcc -o hello_static 1.o -L. -l2static

2.o打包成静态库,然后链接静态库得到可执行文件hello_static由于静态库的内容已经完整的整合到hello_static中,因此可以在任何目录下执行hello_static,无论当前目录中有没有lib2static.a

静态库libxx.a相当于是若干个x.o的整合包,对他们进行归档形成一个静态库文件。链接一个静态库与链接库中包含的所有.o文件效果等同

然后是动态库:

$ gcc -c 1.c 2.c
$ gcc --shared 2.o -o lib2dynamic.so
$ gcc -o hello_dynamic 1.o -L. -l2dynamic

由于动态库目录指定为当前目录,链接得到的可执行文件hello_dynamic只能在与动态库lib2dynamic.so同目录下运行。此时如果重命名、删除lib2dynamic.so,或将hello_dynamic拷贝到没有lib2dynamic.so的目录下运行,会得到错误:

qxy@qxy-XPS-13-9360:~/Desktop/test$ ./hello_dynamic
./hello_dynamic: error while loading shared libraries: lib2dynamic.so: cannot open shared object file: No such file or directory

因为程序在执行的过程中动态加载动态库的内容,但发现找不到这个文件

posted @ 2019-04-24 19:20  sssaltyfish  阅读(363)  评论(0编辑  收藏  举报