Linux下程序运行中替换动态链接库引起崩溃详解

笔者在工作过程中,遇到过好多次未停止进程的情况下,直接替换了动态库,导致程序崩溃产生coredump的场景。一直也没有深究过其原因,最近准备总结一下,搜索了很多资料,总结一下。

具体场景是:在Linux服务器上。可执行程序A,dlopen方式打开了动态链接库B.so,调用了B库中的函数进行业务处理。未关闭程序A的情况下,用xftp替换了一个新版本的B.so到服务器上。结果A业务上再次调用到B中的函数时,触发了coredump。

为了简化问题,这边用一个demo程序来进行问题的说明。下面是可执行程序dlopenTest的程序实现。它就是打开了一个libcaculate.so,获取了里面的add函数指针,每隔5秒调用一次这个add函数,实现2和7两个数的相加。



下面是libcaculate.so的实现,它实现了一个add函数,打印了入参和出参,同时返回了两个数相加的结果。



下面我们来模拟coredump文件的产生。首先运行dlopenTest程序,在运行过程中用命令cp libcaculate.so.bak libcaculate.so 覆盖libcaculate.so程序。注意这里libcaculate.so.bak是和libcaculdate完全相同的程序项,只是提前拷贝换了个名字而已。果然产生了coredump。



下面我们用gdb分析这个coredump文件。查看堆栈,发现0号栈帧显示??,上层堆栈是对应calc.c的第六行,也就是add函数的结束大括号。这个没法继续分析了,我们尝试看看汇编语句,能不能找到更多的线索。



切换到1号栈,查看反汇编代码,发现程序是在callq 命令后,执行return a+b之前即发生了崩溃。Callq 对应的应该就是调用printf函数。



分析到现在,猜测问题就大概率出现在调用printf函数这里了,我们必须进一步分析调用printf函数的过程,才能继续深入定位问题所在。下面我们就先学习下动态链接函数调用相关的知识。

大家都知道printf是标准C库的函数,我们使用gcc编译链接生成libcaculate.so程序的时候,虽然没有显式的写出来printf所在的动态链接库,但是编译器会默认帮我们链接libc库,这些标准C库的函数就定义在libc库里面(可以通过ldd libcaculate命令看到其依赖的动态链接库)。但是这里有个问题,程序的汇编代码是在编译链接时就生成好的,编译器是怎么知道libc库中printf函数在内存中地址是多少呢?(这个只有在运行期才能知道啊)。这里也就引出了Linux系统处理动态链接函数调用的两个关键表,PLT(程序链接表,Procedure Link Table)和GOT(全局偏移表,Global Offset Table)。

PLT和GOT简而言之,就是libcaculate.so中add函数调用printf语句生成的汇编代码,调用的其实是自身PLT表项中的一项plt[printf]。而plt[printf]则指向了GOT表项中的其中一项GOT[printf]。在程序未运行时,GOT[printf]中的内容是空的。在进程启动的过程,动态加载器ld-linux-x86-64会负责解析printf函数在libc库中的地址,填充到GOT[printf]表项中去。这样进程就可以通过plt->got->libc中的printf函数了。当然,我这个只是简略的说法,便于理解,完整的过程建议查看博文https://blog.csdn.net/linyt/article/details/51635768 ,里面解释的非常好。

我们通过objdump –D libcaculate.so 看看so中汇编代码的内容,验证下上述的理论基础。



从上图可以看到,add函数生成的确实是调用的是4d0地址的printf@plt内容



4d0地址所在的printf@plt表项,则是一个跳转GLOBAL_OFFSET_TABLE(GOT)+0x8地址的表项。这个0x8偏移的GOT表项,到时候会被动态加载器用libc库printf函数函数地址填充。在静态文件中,这一项是个无意义的项。



静态分析后,我们再通过gdb单步指令调试,验证下上述流程。

回到上面的例子,我们在add函数的printf语句设个断点,然后通过display/i $pc 同时展示源代码和汇编代码。再通过stepi命令单步执行一条汇编代码。

从下图中可以看到,printf@plt指向printf@got表项的地址为0x7ffff7fee920,



而替换so后再次触发断点时,则触发了coredump,崩溃的原因是访问了非法内存地址0x4d6,查看got表项的地址,可以看到内容也变成了0x000004d6。这个0x4d6是不是有点熟悉,没错,就是我们用objdump分析程序项时,静态程序代码段中plt项的一个地址。看样子,是由于so被覆盖,导致内存中的GOT表项也同步变了(正常程序加载时,加载器对把程序代码段中的静态地址重定向为真实内存地址,并写入GOT表项),而且变成了一个非法的未重定向地址。



那么为什么磁盘文件的变动,会影响进程的内存空间内容呢,我们通过strace命令跟踪看看dlopenTest是怎么处理动态链接库的。通过下图可以看出,通过dlopen加载动态库时,使用的是mmap文件映射的方式。将磁盘内容映射到了内存页中。所以磁盘内容的变化会体现在内存中。



再通过strace命令看看cp libcaculate.so.bak libcaculate.so时,系统做了什么处理,从下图可以看到,系统打开目标文件时使用了O_TRUNC选项,会对目标文件进行清空,然后再写入libcaculate.so.bak的内容。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
在 Android 平台上,可以通过使用 JNI(Java Native Interface)来调用 C++ 动态链接库(DLL)。JNI 是一个 Java 虚拟机规范,用于实现 Java 代码与本地代码的交互。通过 JNI,可以在 Java 声明本地方法,并将其实现委托给本地代码,以便在 Java 调用本地方法。 要在 Android 调用 C++ DLL,需要先将 DLL 编译为适用于 Android 平台的共享库(.so 文件),然后在 Java 代码使用 JNI 调用该共享库。具体步骤如下: 1. 在 C++ 编写动态链接库,并将其编译为适用于 Android 平台的共享库(.so 文件)。 2. 在 Java 声明 native 方法,以便在 Java 调用 C++ 方法。例如: ```java public class MyNativeClass { public native int myNativeMethod(int arg); } ``` 3. 在 C++ 实现该方法,并将其封装为静态库(.a 文件)或共享库(.so 文件)。 4. 将 C++ 库与 Java 代码链接,并打包为 Android 应用程序。 5. 在 Java 代码,使用 System.loadLibrary() 方法加载 C++ 库,并调用 native 方法。例如: ```java public class MyJavaClass { static { System.loadLibrary("MyNativeLibrary"); } public static void main(String[] args) { MyNativeClass myNativeClass = new MyNativeClass(); int result = myNativeClass.myNativeMethod(42); System.out.println("Result: " + result); } } ``` 需要注意的是,JNI 调用 C++ 方法的过程,需要进行参数类型转换和内存管理等操作,因此需要谨慎处理。另外,由于 Android 平台涉及多种架构,需要针对不同的架构编译对应的共享库。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值