什么是链接?
简单来说链接的本质就是将多个可重定位的目标文件合并成一个可执行文件。
链接的主要过程有三个:
- 地址与空间分配。被分配的地址和空间为链接后输出的可执行文件中的空间以及装载后的虚拟地址中的虚拟地址空间。在链接时,链接器需要根据输入的目标文件中的各个段长度,计算出他们在最终输出的可执行文件中的长度以及位置,建立映射关系。
- 符号解析。根据输入的目标文件,将符号的定义和符号的引用对应并关联起来。
- 重定位。在地址和空间分配之后,链接器已经知道了所有符号和指令在可执行文件中的相对地址,链接器就可以根据这些地址对所有引用的符号进行地址修改,让所有符号的引用都能指向正确的地址。
静态链接
静态链接库在 linux 下通常为 .a 文件,在 windows 下通常为 .lib 文件。
静态链接简单来说就是在链接时将程序需要的所有依赖或者模块合并到最终输出的可执行文件中,最终输出的可执行程序可以独立运行,不需要再从其他地方加载库文件。
静态链接库的一般生成过程。
假设程序员在 foo.c 定义中了两个方法 foo() 、test():
int foo(int a){
return a + 7;
}
int test(int b){
return b -7;
}
程序员想在另一个文件 hello.c 中调用 foo.c 中的 foo() 函数:
#include<stdio.h>
int main(){
int val = 20;
int temp = foo(val);
return 0;
}
这种情况下就可以使用静态链接的方式。
先将 foo.c 进行编译(-c,不进行链接)生成目标文件:
riscv32-unknown-elf-gcc -Os foo.c -c foo.o
再使用 ar 工具生成库文件:
riscv32-unknown-elf-ar -cr libfoo.a foo.o
此时目录下新生成的 libfoo.a 便是一个静态库文件,如果我们需要正确编译 hello.c ,只需要在链接的时候指定我们生成的静态库:
# 编译 hello.c
riscv32-unknown-elf-gcc -Os hello.c -c hello.o
# 链接
riscv32-unknown-elf-ld hello.o libfoo.a -o hello
链接后将生成一个可执行程序 hello,让我们使用 objdump 工具反汇编出它的汇编代码:
riscv32-unknown-elf-objdump -d hello > 1.txt
从返回的结果 1.txt 当中我们可以看到以下结构:
00010054 <main>:
10054: 1141 addi sp,sp,-16
10056: 4551 li a0,20
10058: c606 sw ra,12(sp)
1005a: 2029 jal 10064 <foo>
1005c: 40b2 lw ra,12(sp)
1005e: 4501 li a0,0
10060: 0141 addi sp,sp,16
10062: 8082 ret
00010064 <foo>:
10064: 051d addi a0,a0,7
10066: 8082 ret
00010068 <test>:
10068: 1565 addi a0,a0,-7
1006a: 8082 ret
在静态链接的情况下,即使我们只引用了静态库中的一个方法 foo(),但是在最终生成的可执行文件当中,静态库中的其他模块(test方法) 也被装载了进来。
这样做的一个好处是,无论链接库里的一些模块我们是否需要,它们都将装载到我们的最终程序当中,这使得我们的程序能够独立运行,而不需要依赖任何其他的文件或者模块,这也加快了程序的执行速度,但是代价是我们生成的程序有很多冗余的模块,当静态库较大时,每个程序都将浪费大量的内存空间。另外,如果库函数中的代码进行了修改,就必须重新编译静态库和重新链接生成可执行程序,这就导致了静态库更新非常麻烦。
动态链接
动态链接库在 linux 下通常为 .so 文件,在 windows 下通常为 .dll 文件。
动态链接简单来说就是在链接时程序所需部分依赖和模块不需要合并到最终输出的可执行文件中,该可执行文件在运行时再加载所需的库文件,不可以独立运行。
假设有个动态库 libfoo.so,两个依赖此动态库的目标文件 hello1.o、hello2.o,当我们运行 hello1.o 时,系统会发现 hello1.o 依赖于动态库 libfoo.so ,那么系统就会将 libfoo.so 加载到内存中。当我们继续执行 hello2.o 时,系统发现 hello2.o 依赖 libfoo.so ,而此时 libfoo.so 已经在内存中,那么系统就会将内存中备份的 libfoo.so 映射到 hello2.o 的虚拟地址中,然后执行。
由于在使用动态链接的时候,被多个程序依赖的库只需要加载一次,而不是像静态链接那样每一次链接时都要加载,这样就节省了很多的内存;并且当我们对动态库里的代码进行修改时只需要重新生成一次动态库,而不需要将所有依赖此库的程序重新编译一次,当这些程序下一次执行时,新的动态库将会加载到内存中。但是代价是由于每次执行程序时都要进行链接的操作,程序的执行速度将会收到影响。