Linux LD链接器 -静、动库编译or链接

程序链接

动态链接

首先存在如下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.ofunc()函数了:

$ 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
  1. 同样这里先编译func.cfunc.o
  2. 添加-shared-fpic参数将func.o共享对象文件转为动态共享库的形式,libfunc.so这是一个动态共享库的命令规范libxxx.so

在编译动态库时,一般需要使用位置无关代码(Position-Independent Code,PIC)来确保库可以在内存中的任何位置加载并运行

  1. 最后编译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
  1. 步骤和上面步骤类似,不过打包不一样

  2. 使用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处)这里就完成了真实地址的填充

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值