文章目录
- 程序链接
- 动态链接
- 静态链接
- 目标文件链接
- 打包为动态
- 打包为静态
- 总结
- 动态链接
- - 动态链接:在运行、加载时,在内存中完成链接的过程
- - 动态共享库:用于动态链接的系统库、特性是可以加载无需重定位的代码
- got表(Global Offset Table)
- 延时绑定
程序链接
动态链接
首先存在如下main.c
:
#include "stdio.h" //因为这个文件使用了printf函数所以需要引入系统文件
int func(int a,int b);
int main(){
printf("func -> %d",func(1,2));;
return 0;
}
使用最简单编译命令gcc main.c
是会报错的,该命令会执行编译+链接操作,因为这个代码中func
函数只进行了声明,并没有定义,那么可以使用gcc -c main.c
来告诉编译器只需要编译,不链接
再来看看编译后的文件信息
$ file main.o
main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
#这个信息就表示输出文件为编译后、可重定位的 ELF 文件可以用于链接成可执行文件或共享对象文件,而被剥离的 ELF 文件可以去掉调试信息。SYSV 是 Unix 系统 V 的一种变体,是 Linux 系统采用的标准。
文件内容信息:
$ objdump -d --section=.text main.o
0000000000000000 <main>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: be 02 00 00 00 mov $0x2,%esi
d: bf 01 00 00 00 mov $0x1,%edi
12: e8 00 00 00 00 call 17 <main+0x17> #可以看到这里的函数调用没有实际的调用地址
17: 89 c6 mov %eax,%esi
19: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # 20 <main+0x20>
20: 48 89 c7 mov %rax,%rdi
23: b8 00 00 00 00 mov $0x0,%eax
28: e8 00 00 00 00 call 2d <main+0x2d> #同时调用的printf函数也没有地址
2d: b8 00 00 00 00 mov $0x0,%eax
32: 5d pop %rbp
33: c3 ret
再来实现上面的func(int a,int b)
函数定义,不过为了演示程序链接,我将函数的定义放在另一个文件中实现,新建文件func.c
:
int func(int a,int b){
return a+b;
}
同样使用gcc -c func.c
来编译这个文件为一个可用于链接共享程序
$ objdump -d --section=.text func.o
0000000000000000 <func>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 89 7d fc mov %edi,-0x4(%rbp)
b: 89 75 f8 mov %esi,-0x8(%rbp)
e: 8b 55 fc mov -0x4(%rbp),%edx
11: 8b 45 f8 mov -0x8(%rbp),%eax
14: 01 d0 add %edx,%eax
16: 5d pop %rbp
17: c3 ret
可以看到这个文件并没有依赖任何其他外部的引用,只不过这个程序没有main
函数并不能直接执行,所以它就起到了一个功能函数的作用,而进行文件链接原理就是将上面的这个汇编嵌入到你的主程序文件中去(本文则指main.o
)
好,到这文件都编译好了,就差将我的main.o
链接系统的printf
函数和我的另一个文件func.o
的func()函数
了:
$ gcc main.o func.o
#这条命令就是链接了main.o func.o 其原理就是调用ld 链接去单个链接,也包括链接调用的printf函数,为了不加大复杂度,这里直接用gcc来完成自动链接
最后生成a.out
文件就是编译+链接的可执行程序了
再来看看生成的程序内部信息:
$ objdump -d --section=.text a.out | grep -A 100 "<main>:"
0000000000001149 <main>:
1149: f3 0f 1e fa endbr64
114d: 55 push %rbp
114e: 48 89 e5 mov %rsp,%rbp
1151: be 02 00 00 00 mov $0x2,%esi
1156: bf 01 00 00 00 mov $0x1,%edi
115b: e8 1d 00 00 00 call 117d <func> #这里<func>为下面的汇编代码段,可以看到已经将我们刚才编译的func.c文件汇编代码嵌入进来了
1160: 89 c6 mov %eax,%esi
1162: 48 8d 05 9b 0e 00 00 lea 0xe9b(%rip),%rax # 2004 <_IO_stdin_used+0x4>
1169: 48 89 c7 mov %rax,%rdi
116c: b8 00 00 00 00 mov $0x0,%eax
1171: e8 da fe ff ff call 1050 <printf@plt> #可以看到这里的调用以及被填充了数据,至于为什么是<printf@plt> 下面会详细讲
1176: b8 00 00 00 00 mov $0x0,%eax
117b: 5d pop %rbp
117c: c3 ret
000000000000117d <func>:
117d: f3 0f 1e fa endbr64
1181: 55 push %rbp
1182: 48 89 e5 mov %rsp,%rbp
1185: 89 7d fc mov %edi,-0x4(%rbp)
1188: 89 75 f8 mov %esi,-0x8(%rbp)
118b: 8b 55 fc mov -0x4(%rbp),%edx
118e: 8b 45 f8 mov -0x8(%rbp),%eax
1191: 01 d0 add %edx,%eax
1193: 5d pop %rbp
1194: c3 ret
静态链接
但是还是有一个问题没有看到printf
函数的汇编代码,而是一个printf@plt那么再重新链接一下:
$ gcc main.o func.o -static
这次再重新看下a.out
文件的信息printf的一系列函数的汇编代码就嵌入进来了。
上面的-static
参数就是表示使用静态的方式去链接系统帮我们编译好的printf
函数库,也就是c
标准库,默认情况下的链接是动态链接,那么就是不会将源代码的汇编链接进我们的main.o
,需要通过系统加载的方式进入到c
标准库中执行汇编代码。
这也从表面了静态链接后的程序大小要比动态链接后的程序大小要大,一个是将源代码全部放入程序中,一个是源代码在我的文件系统里面只需要去调用。
目标文件链接
那么如何将我们刚才写的func.c-func()函数
编译成像printf
一样的函数直接去调用呢?
打包为动态
$ gcc -c func.c
$ gcc func.o -shared -fpic -o libfunc.so
$ gcc main.c -L. -lfunc
- 同样这里先编译
func.c
为func.o
- 添加
-shared
、-fpic
参数将func.o
共享对象文件转为动态共享库的形式,libfunc.so
这是一个动态共享库的命令规范libxxx.so
在编译动态库时,一般需要使用位置无关代码(Position-Independent Code,PIC)来确保库可以在内存中的任何位置加载并运行
- 最后编译
main.c
+链接libfunc.so
,其中-L.
表示告诉gcc编译器在当前目录找共享库,-lfunc
表示gcc会在所有的共享库目录找到一个名为libfunc.so
或者libfunc.a
的共享库
在实际开发中,某些算法的完成就是通过打包源代码为共享库(动、静都可以),然后发布出去给第三方,第三方拿到共享库就可以直接通过动态、静态的方式调用了
打包为静态
$ gcc -c func.c
$ ar rcs libfunc.a func.o
$ gcc main.c -L. -lfunc
-
步骤和上面步骤类似,不过打包不一样
-
使用ar命令将func.o目标文件打包为
libfunc.a
静态库,后缀名为.a
-
r选项表示将目标文件添加到静态库中
-
c选项表示如果静态库不存在则创建一个新的静态库
-
s选项表示在创建静态库时生成索引
-
总结
注意打包动态、静态共享库和链接动态、静态库的概念不一样:
- 使用动态、静态链接只会影响你链接后的可执行文件类型
$ gcc main.c func.o #动态链接
$ file a.out && du a.out
a.out: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=51c81ac9b9286d36b8cfffeb3f38ef6895d4c48a, for GNU/Linux 3.2.0, not stripped
16K a.out
$ gcc main.c func.o -static #静态链接
$ file a.out && du a.out
a.out: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=6be05823bc9a7da5d33a0b5796d5473957d0a9ea, for GNU/Linux 3.2.0, not stripped
880K a.out
- 使用动态、静态打包,并链接不会影响文件大小
如果需要编译静态程序那么链接的共享库是也必须是静态库
#动态链接 动态库
$ gcc func.o -shared -fpic -o libfunc.so && gcc main.c -L. -lfunc
$ file a.out && du a.out
a.out: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=1a2e010e10724e9151dd3ed5565e10a45d2d26c8, for GNU/Linux 3.2.0, not stripped
16K a.out
#动态链接 静态库
$ rm libfunc.so
$ ar rcs libfunc.a func.o && gcc main.c -L. -lfunc
$ file a.out && du a.out
a.out: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=51c81ac9b9286d36b8cfffeb3f38ef6895d4c48a, for GNU/Linux 3.2.0, not stripped
16K a.out
#静态链接 动态库 (报错,静态链接的话ld链接器只会去找libxxx.a格式的文件,不会找libxxx.so二进制文件)
$ gcc func.o -shared -fpic -o libfunc.so && gcc main.c -static -L. -lfunc
/usr/bin/ld: cannot find -lfunc: No such file or directory
collect2: error: ld returned 1 exit status
#静态链接 静态库
$ rm libfunc.so
$ file a.out && du a.out
a.out: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=6be05823bc9a7da5d33a0b5796d5473957d0a9ea, for GNU/Linux 3.2.0, not stripped
880K a.out
动态链接
在有大量的可执行文件时,比如大部分可执行文件都需要glibc,那么每调用一次静态库就要在该库引用libc.a这样会造成内存巨大,这时就有了动态链接。
再明确下两个重要的概念:
- 动态链接:在运行、加载时,在内存中完成链接的过程
- 动态共享库:用于动态链接的系统库、特性是可以加载无需重定位的代码
got表(Global Offset Table)
因为程序的数据段和代码段的相对距离是固定的,所以指令和变量的距离就是一个常量(偏移、相对地址)就有了全局偏移表(Global Offset Table),用于保存全局变量和库函数的引用,在加载时进行重定位填入真实地址
为了区分数据段和代码段,就有了.got节和.got.plt节。.got节保存着数据因为它不需要延时绑定,.got.plt保存着函数引用需要延时绑定
延时绑定
因为动态链接是在加载时进行的,当重定位的库函数多了后会影响性能,故有了延时绑定。
原理:在函数第一次被调用时,动态链接器才进行符号查找、重定位操作、如未调用则不进行绑定,这样就可以节省资源和性能
程序中的实现:通过plt表(Procedure Linkage Table)和got表配合实现,以上面func和main为例,当main函数要跳转到func函数时,执行call func@plt (这里用gdb动态调试观看过程,插件时gef只有gef才可以看plt前面的地址,如果运行之前编译的func.ELF2文件报错,需要把编译的so文件复制到/usr/bin目录下就可以正常运行)
此时的plt节
进入plt节后执行jmp指令跳转到got表处的条目,因为是第一次执行,这个时候got表处的条目还是plt节中的第二条目地址(func@plt+6),然后回到plt表将0x1(.rel.plt中的下标(.rel.plt是对函数引用的修正,它所修正的位置位于.got.plt))进行push压栈,然后再进入plt[0]
这里就是将got[1]压栈,再跳转到got[2](_dl_runtime_resolve函数)这步就是对符号(变量、函数)重定位操作,将func()真实地址填入func@got.plt也就是前面调用的got表(0x555555754fd0处)这里就完成了真实地址的填充
回到plt表将0x1(.rel.plt中的下标(.rel.plt是对函数引用的修正,它所修正的位置位于.got.plt))进行push压栈,然后再进入plt[0]
[外链图片转存中…(img-JzGi5NHd-1682698436198)]
这里就是将got[1]压栈,再跳转到got[2](_dl_runtime_resolve函数)这步就是对符号(变量、函数)重定位操作,将func()真实地址填入func@got.plt也就是前面调用的got表(0x555555754fd0处)这里就完成了真实地址的填充