加入收藏 | 设为首页 | 会员中心 | 我要投稿 武汉站长网 (https://www.027zz.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 服务器 > 搭建环境 > Linux > 正文

Linux如何实现动态链接

发布时间:2022-12-09 14:35:00 所属栏目:Linux 来源:未知
导读: Linux通过使用动态库的方式使得多个程序可以共享一个动态库,节省了很多的存储空间和运行空间。那么这个是如何实现的呢?我之前也一直比较疑惑,通过一段时间的研究算是搞懂了一些。
我们可

Linux通过使用动态库的方式使得多个程序可以共享一个动态库,节省了很多的存储空间和运行空间。那么这个是如何实现的呢?我之前也一直比较疑惑,通过一段时间的研究算是搞懂了一些。

我们可以想一下,链接是要干什么事情?其实链接最重要的一件事情就是确定好代码的运行地址,不管是变量还是函数都要把地址确定好这样才能够正确的执行。静态链接就是把所有用到的函数和变量都聚合到一个文件中,然后把地址确定好,最后就形成了一个可执行文件。但是动态链接不是这样,除了可执行文件还有一块是作为一个动态库独立的存在,只能在运行的时候才能重新把地址排布好,专业术语叫做重定位。

下面我们看如果要实现在运行的时候动态重定位,对可执行文件和动态库都有什么要求。对于动态库来说,就得要求它的代码里面不能有绝对地址存在,必须都是相对地址访问,因为如果是绝对地址的话那么一旦重定位那么这个绝对地址就会出错。对于可执行文件来说,就得有个地方记住哪些函数或者变量是来自于动态库中,还得有个地方能够用来保存重定位后的这些函数或者变量的真实地址。前面这个要求容易做到,只要在可执行文件中拿出一部分空间,在gcc编译的时候写入进去就可以了。但是后面这个要求就不是那么好做,要知道程序的代码段是只读的,肯定不能在运行的时候修改代码段,那么能修改的只能是数据段,但是数据段虽然可写但是不能执行,所以中间还得有个跳板才行。基本思想就是这样,下面看看代码要如何实现。

首先看看如何让动态库的程序实现相对地址访问,可以在编译的时候采取下面的方式:

gcc -fPIC --shared test_lib.c -g -o libtest_lib.so  
#这里的-fPIC就是让gcc生成相对地址访问的程序,也叫做位置无关

我们可以反汇编看下代码

首先可以看到,程序的执行地址都很小是从0地址开始的(这个程序是x86体系)而正常x86上面的程序都是从0x804xxxxx开始的。另外就是看下红框部分的指令:e8 28 00 00,其中e8是相对地址调用,相对地址大小就是后面的0x28加上它的下一条指令地址也就是0x578所以0x28+0x578=0x5a0。可以看到这里都是用的这种相对地址访问,就是为了避免使用绝对地址导致重定位后程序执行出问题。

android 动态加载so库_linux动态库_友立数码影像动态素材库80cd

动态库的问题解决了,下面来看可执行文件这边如何来实现跳板以及修改重定位后的地址,这个相对会复杂一些不过也很有意思,里面的思想也可以迁移到其它地方来作为解决问题的办法。为了实现上面所说的,可执行文件中保存了两个表:PLT和GOT。其中PLT位于代码段可读可执行但是不可写,它起到跳板的作用。GOT位于数据段可读可写但是不可执行,它作为最终动态库函数真实地址存放的地方。我们通过代码来具体分析一下是如何实现的。

可执行文件的测试代码:

test_main.c

#include 
#include 
extern int b;
int main(void)
{
    //func1();
    b = 5;

    func2();
    while(1) sleep(2);
    return 0;
}

动态库的代码:

test_lib.c

#include 
int b = 1;
void func1(void)
{
    b = 2;
}
void func2(void)
{
    printf("func2 b=%d\n", b);
}

进行编译,生成可执行文件

gcc -fPIC --shared test_lib.c -g -o libtest_lib.so
gcc -g -o test_main test_main.c ./libtest_lib.so

接下来就要反汇编可执行文件:test_main来看看里面的细节了

可以看到main函数中调用func1的地方是调用了func1@plt

android 动态加载so库_linux动态库_友立数码影像动态素材库80cd

下面看看func1@plt。可以看到这个就是PLT表了,我们的func1@plt就在这里。上面我有说这个是跳板,我们继续看下是怎么个跳法。这个函数的第一条指令就是一个跳转指令,它是要跳到0x804a00c这个内存地址保存的值的位置,注意它不是跳到0x804a00c哦,因为前面带了一个"*"表示取地址。

linux动态库_友立数码影像动态素材库80cd_android 动态加载so库

接下就要看这个0x804a00c保存了什么内容。可以看到0x804a00c就是在GOT表中,GOT表是从0x804a000地址开始的,那么0x804a00c地址里面保存的值就是56840408,但是要注意x86是小端地址所以56840408真实值就是:0x08048456,可以发现这个地址就是PLT表中func1@plt的第二条指令地址,就是又跳回去了。

看到这里的时候就估计会很困惑,前面讲了GOT表中不是应该保存的是func1的真实地址吗?为什么又跳回来了?我刚开始的时候也是很不理解,后来通过看一些资料后才终于搞明白是怎么回事。而且这个正是动态连接的其中一个精髓。我们先想一个问题,如果我们要执行一个动态链接的可执行程序,为了确保正常它在运行前就会查找所有在动态库中的函数和变量,然后把它们都填到程序的GOT表中。想象一下,如果这个程序使用的动态库中函数和变量比较多,动态库也比较大的话,整个过程是不是就会需要耗费不少时间,直观的感觉就是程序运行会比较慢。为了解决这个问题,Linux就使用了一种叫做用时加载的方法。这个思想在Linux中被很多地方应用到,比如分配内存的时候,只有当向这块内存里面写数据的时候,才会真正分配内存。所以我们就看到了,第一次调用func1函数的时候,GOT表中并没有保存它的真实地址。那么继续往下看是什么时候以及怎么把真实地址保存进来的。

跳转到0x8048456后执行的是一条压栈指令,然后接下来又是一条跳转指令,转到了func1@plt-0x10。这里首先是一条压栈指令,接着就是一个跳转到0x804a008内存地址处保存的值,同样要注意这里不是跳转到0x804a008。

友立数码影像动态素材库80cd_linux动态库_android 动态加载so库

这个地址同样是在GOT表中,可以看下它里面的值是0xb7ff0000

看下0xb7ff0000这个地址是什么东西。可以看到这是一个函数地址linux动态库,这个函数是:_dl_runtime_resolve。这个是glibc中的一个函数,作用就是查找动态库中函数的位置并将其写入到程序的GOT表中。这个函数比较复杂,下次找机会再来分析。

linux动态库_android 动态加载so库_友立数码影像动态素材库80cd

到这里整个过程基本就清晰了,中间涉及到不少汇编代码的分析。下面用一幅图来描述这个过程

友立数码影像动态素材库80cd_android 动态加载so库_linux动态库

当第一次执行完后,再次执行func1这个函数的时候就不用再反复跳转跑到_dl_runtime_resolve这里来了,因为GOT表中已经保存了func1的真实地址。可以看下执行了第一次调用后再GOT表里面的信息:

里面的值变成了0xb7fd4550,不再是0x08048456了。

看下0xb7fd4550这个地址是什么东西。发现就是func1函数的地址。

linux动态库_android 动态加载so库_友立数码影像动态素材库80cd

这基本就是动态链接的大致过程了,还是花了不少时间来盘它。主要是很多背景知识自己还是缺乏,比如汇编、ELF文件格式等等。技术一条无底洞越学越觉得要学习的东西太多,不过当把背景知识补齐然后搞通了一个技术点后,还是非常有成就感的。

(编辑:武汉站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!