一文搞懂系列——替换动态库,为什么导致运行进程异常

发布时间:2025-08-11 15:22

为什么太阳花总是朝向太阳?因为它们不懂什么是方向。 #生活乐趣# #日常生活趣事# #日常生活笑话# #搞笑日常#

本文主要是介绍一文搞懂系列——替换动态库,为什么导致运行进程异常,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

背景

上周我的好同事·金,又遇到了一个crash问题,他是这么描述的:测试同事在验证新版本时,程序正在运行,将新版本的动态库替换到对应目录中,正在运行的程序会出现crash。我的第一反应是,替换磁盘中的动态库,对于正在运行的进程应该是没有影响的。因为,正在运行的进程依赖于旧动态库,即使执行rm lib.so指令,文件系统发现依然有程序对其引用,也不会立刻从磁盘删除,待进程停止后,没有对该动态库应用时,才会真正从磁盘删除。因为当时比较忙,也就没有关注了。

正所谓天道好轮回,苍天饶过谁。就在今天,我在做演示项目时,好像就遇到了类似的问题,经过细细对比,果然和金描述的一致。心想:既然你又找上了我,那我一定不让你失望。

经过分析排查,查阅资料。发现之前自己的理解比较片面,也学到了很多。希望本次分享能够帮助到遇到相似问题或有兴趣的朋友。本篇内容难度偏高,希望朋友们能够自己操作一遍。

示例

为了方面操作和理解,我写了以下示例代码。

//main.c #include<stdlib.h> #include<stdio.h> #include<unistd.h>extern int add(int,int);int main() {printf("start sleep\n");sleep(10);printf("over sleep\n");add(5,6);return 0; }//add.c #include<stdio.h> int add(int a, int b) {printf("a+b\n",a+b);return (a+b); }

编译:

gcc -g -fPIC -shared add.c -o libadd.so gcc -g main.c -o main -L. -ladd

创建lib目录,并将libadd.so拷贝一份到lib目录,其中,两个libadd.so内容是完全一样的。最终工程如下:

yihua@ubuntu:~/test/reloadDynamiclibs$ tree . ├── add.c ├── lib │ └── libadd.so ├── libadd.so ├── main └── main.c1 directory, 5 files yihua@ubuntu:~/test/reloadDynamiclibs$

运行:其中环境变量动态库加载路径,是期望main链接lib/libadd.so。

export LD_LIBRARY_PATH=lib/ ./main

正常情况下,输出如下:

yihua@ubuntu:~/test/reloadDynamiclibs$ ./main start sleep over sleep yihua@ubuntu:~/test/reloadDynamiclibs$

问题复现: main运行时,在sleep的10秒内,执行cp libadd.so lib/libadd.so 将lib/libadd.so替换。则输出如下:

yihua@ubuntu:~/test/reloadDynamiclibs$ ./main start sleep over sleep Segmentation fault (core dumped) yihua@ubuntu:~/test/reloadDynamiclibs$

知识点补充

在进入分析前,我们需要了解两个知识点:文件共享的方式、GOT及PLT表。

文件共享方式

一、文件在磁盘中存储的方式

我们知道文件存储在磁盘中,硬盘的最小存储单位叫做"扇区"(Sector)。每个扇区储存512字节。操作系统对磁盘文件存的取最小单位是“块",最常见的是4KB,即连续八个Sector组成一个block。

通常情况下,文件系统会将文件的实际内容和属性分开存放。

文件的属性保存在 inode 中(i 节点)中,每个 inode 都有自己的编号。每个文件各占用一个 inode。不仅如此,inode 中还记录着文件数据所在 block 块的编号;文件的实际内容保存在 block 中(数据块),类似衣柜的隔断,用来真正保存衣物。每个 block 都有属于自己的编号。当文件太大时,可能会占用多个block 块。

故文件系统会将磁盘格式化为两个部分inode和block。举例:若某文件的inode节点为3,其中记录着文件实际内容存储在block1、2、4、5中。那么操作系统读取文件的流程:

读取inode节点信息,获取文件所占blocks读取blocks,获取文件实际内容。

我们可以通过stat查看文件的inode信息。如下:

yihua@ubuntu:~/test/reloadDynamiclibs$ stat libadd.soFile: libadd.soSize: 8344 Blocks: 24 IO Block: 4096 regular file Device: 801h/2049d Inode: 1713034 Links: 1 Access: (0775/-rwxrwxr-x) Uid: ( 1000/ yihua) Gid: ( 1000/ yihua) Access: 2023-12-26 01:55:52.528861823 -0800 Modify: 2023-12-26 01:55:23.649023426 -0800 Change: 2023-12-26 01:55:23.649023426 -0800Birth: -

其中libadd.so的inode节点为1713034,实际占用24个block。

二、进程表和内核文件表

每个进程在进程表中都有一个关于文件描述符的记录部分,其中文件描述符中主要记录两点内容。

文件描述符。也就是我们常见的fd。文件指针。每个fd都对应一个文件指针,文件指针的作用,是指向内核文件表中的某一项。

内核为所有打开文件维持一张文件表。表中的每一项包含三点内容。

文件的状态标志。比如读、写、添写、同步和非阻塞等。当前文件偏移量指向该文件的v节点表项的指针。v节点包含文件类型、对此文件的各种操作函数的指针、以及文件对应的inode。

通过以上两点,我们大致可以得出进程访问文件的关系如下:

注:其中文件表项是每一个进程所独有的,但是v节点是文件独有的,即若存在多个进程打开同一个文件,其流程大致如下:

动态链接PLT和GOT表

PLT(Procedure Linkage Table)及GOT(Global Offset Table)是动态链接中的核心点,本文不会进行更进一步的介绍。若要深入了解,可参考我的专栏《程序员的自我修养》。由于libadd.so中引用可printf接口,并且printf定义在libc.so中,因此libadd.so对printf的引用采用运行时重定位。采用objdump -d libadd.so命令查看add函数的反汇编内容。

000000000000060a <add>:60a: 55 push %rbp60b: 48 89 e5 mov %rsp,%rbp60e: 48 83 ec 10 sub $0x10,%rsp612: 89 7d fc mov %edi,-0x4(%rbp)615: 89 75 f8 mov %esi,-0x8(%rbp)618: 8b 55 fc mov -0x4(%rbp),%edx61b: 8b 45 f8 mov -0x8(%rbp),%eax61e: 01 d0 add %edx,%eax620: 89 c6 mov %eax,%esi622: 48 8d 3d 20 00 00 00 lea 0x20(%rip),%rdi # 649 <_fini+0x9>629: b8 00 00 00 00 mov $0x0,%eax62e: e8 dd fe ff ff callq 510 <printf@plt>633: 8b 55 fc mov -0x4(%rbp),%edx636: 8b 45 f8 mov -0x8(%rbp),%eax639: 01 d0 add %edx,%eax63b: c9 leaveq63c: c3 retq

可知add函数并不是直接调用printf符号,而是printf@plt符号。其实际访问流程应该是add --> printf@plt --> printf@got --> printf。

问题分析

一、首先我们要先确认cp libadd.so lib/做了什么

通过命令前后对比,查看文件的inode值,可发现libadd.so文件的inode值并没有变,说明该操作将磁盘中文件原始数据重新写了一遍

yihua@ubuntu:~/test/reloadDynamiclibs$ stat lib/libadd.soFile: lib/libadd.soSize: 10376 Blocks: 24 IO Block: 4096 regular file Device: 801h/2049d Inode: 1714531 Links: 1 Access: (0775/-rwxrwxr-x) Uid: ( 1000/ yihua) Gid: ( 1000/ yihua) Access: 2023-12-26 23:29:55.494489759 -0800 Modify: 2023-12-26 23:29:50.338512140 -0800 Change: 2023-12-26 23:29:50.338512140 -0800Birth: - yihua@ubuntu:~/test/reloadDynamiclibs$ yihua@ubuntu:~/test/reloadDynamiclibs$ yihua@ubuntu:~/test/reloadDynamiclibs$ cp libadd.so lib/libadd.so yihua@ubuntu:~/test/reloadDynamiclibs$ stat lib/libadd.soFile: lib/libadd.soSize: 10376 Blocks: 24 IO Block: 4096 regular file Device: 801h/2049d Inode: 1714531 Links: 1 Access: (0775/-rwxrwxr-x) Uid: ( 1000/ yihua) Gid: ( 1000/ yihua) Access: 2023-12-26 23:29:55.494489759 -0800 Modify: 2023-12-27 00:06:31.073213774 -0800 Change: 2023-12-27 00:06:31.073213774 -0800Birth: - yihua@ubuntu:~/test/reloadDynamiclibs$

二、gdb 调试分析

通过gdb调试分析,发现收到错误信号SIGSEGV,访问非法地址,而0x0000000000000516也很明显是一个错误地址。

Program received signal SIGSEGV, Segmentation fault. 0x0000000000000516 in ?? () (gdb) bt #0 0x0000000000000516 in ?? () #1 0x00007ffff7bd1633 in add (a=5, b=6) at add.c:4 #2 0x00005555555547cf in main () at main.c:12 (gdb)

我们跟着堆栈信息不妨一起分析下,通过disassemble 命令,查看进程的汇编内容。

由堆栈分析:是在执行jmpq *0x200b02(%rip)指令出现了crash,并且经过打印可以虚拟地址0x7ffff7dd2018中的值的确是0x0000000000000516,与现象相符。我们可以通过打断点查看正常情况下,该地址中的值。

这时我们可以得到一个结论:替换运行时的动态库,改变了内存进程中的内存值

三、思考:为什么修改文件中的磁盘数据,会影响进程中的内存空间呢?
我们可以通过strace命令查看进程是如何加载libadd.so动态库的。如下:

由图可知,libadd.so是通过mmap将文件映射到内存中的,因此当重新写磁盘内容时,会实时修改进程中的对应内存空间

四、再思考:为什么拷贝前后的libadd.so一摸一样,还是会导致crash呢?

这和进程加载动态库过程有关,因为内容就比较深刻,不进一步详细分析。不过简单理解进程加载动态库中的过程为:

动态链接器将动态库libadd.sommap到内存中。对libadd.so中的got表进行重定位,即将上面的0x0000000000000516地址,更改为加载后的虚拟地址0x7ffff7bd1516。加载完成。

实际上此时内存中的文件数据,是经过重定位修改后的内容。与磁盘中libadd.so的原始数据并不一致。若此时,再将磁盘中的原始数据mmap到内存中,上述的步骤二不再执行,因而会出现访问非法内存的情况。

总结

至此,该问题终于解决了。内容较多,希望大家有所收获。

思考题:在知道根因后,我们不妨再想想,如何在程序运行时,替换动态库,却不影响正在程序

提示:先删除,再拷贝。即rm lib/libadd.so + cp libadd.so lib/libadd.so。不妨试试哦,可以把原理在评论区写出。

若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途

这篇关于一文搞懂系列——替换动态库,为什么导致运行进程异常的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!

网址:一文搞懂系列——替换动态库,为什么导致运行进程异常 https://klqsh.com/news/view/139672

相关内容

Linux环境下,在不停止程序的情况下,更换动态链接库
明星翻车成常态,靠AI换脸拯救一部戏可行吗?
汽车保养项目有哪些?一文搞懂,新手秒变专家!
驱动程序和下载常见问题
体育运动的小常识有哪些 体育运动时要注意什么
河南省全民健身中心健身指导系列活动走进郑州社区
为什么要学习新知?什么是新知体系?
搞笑日常文案
如何批量替换MongoDB中的文档?一次性更新多个文档的方法
读书系列活动策划书范文(通用6篇)

随便看看