彻底理解Linux下动态替换.so的方法

0x00 背景

hdfs增加了一个native方法,打成了libhadoop.so这个动态库。需要分发到线上的各个Datanode上以便升级。在灰度分发到datanode时遇到了可复现的问题,即datanode进程肯定会core dump。分析core dump时产生的hs_err_pid.log文件后,发现最后的执行现场都是在执行native方法。怀疑和替换.so文件有关。

Google了一下,关键词:“如何动态替换.so文件”。确实能够解决问题,即我之前用的是cp -rf这种粗鲁的覆盖文件的手段,优雅的方法是mv+cp的方式。按照网络上的方法修改了分发脚本后,问题解决。

解决问题就结束了?这不是忘记了《码农翻身》里大佬的谆谆教诲了么?非常好奇为什么会有这种现象。网上的文章没有一个说清楚的。自己来吧。

本文不光会解释这个现象的原理,还会进行试验验证,还还还会提出一些自己问题(给自己挖坑)后面继续深挖。

Let's GO!!!

0x01 原理

首先需要一些关于inode的前置知识,更详细的内容放在本文最后的参考链接【1】【2】,有兴趣的朋友可以学习下。

这里简单介绍一下:
linux操作系统中,每一个文件都有对应的inode,里面有关于文件的一些信息,例如:文件的字节数、文件拥有者的User ID、文件的Group ID、文件的读、写、执行权限等、链接数,即有多少文件名指向这个inode、文件数据block的位置。总之,除了文件名以外的所有文件信息,都存在inode之中。

还要记住两个至关重要的知识点:
①移动文件或重命名文件(mv命令),只是改变文件名,不影响inode号码。
②打开一个文件以后,系统就以inode号码来识别这个文件,不再考虑文件名。

也就是说mv操作是不改变已经加载到内存的文件的inode号的。

我们来看下cp和mv操作对文件的inode都有什么样的影响。

hadoop@dn1949:~$ touch test1 test2 && ls -i test1 test2
2174306 test1  2183331 test2   //新建的两个文件,前面的数字代表inode号
hadoop@dn1949:~$ cp test1 test2 && ls -i test1 test2
2174306 test1  2183331 test2  // cp覆盖,将test2覆盖成test1,但是test2的inode号还是原来的。
hadoop@dn1949:~$ mv test1 test2 && ls -i test2
2174306 test2  //将test1 mv 成test2,test2的inode号是test1的inode号。
hadoop@dn1949:~$ cp test2 test3 && ls -i test2 test3
2174306 test2  2183331 test3  //将test2 cp成之前并未存在的test3文件,test3使用新的inode号。
hadoop@dn1949:~$

总结:

cp命令

  1. inode号分配
  • 如果目标文件不存在,分配一个未使用的inode号,在inode表中添加一个新项目;
  • 如果目标文件存在,则inode号采用被覆盖之前的目标文件的inode号;
  1. 在目录中新建一个dentry,并指向步骤1中的inode。

  2. 把数据复制到block中。

mv命令

  1. 如果mv命令的目标和源文件所在的文件系统相同:
  • 使用新文件名建立dentry(文件名 -> inode)
  • 删除带有原来文件名的dentry; 【该操作对inode表没有影响(除时间戳),对数据的位置也没有影响,不移动任何数据。(即使是mv到一个已经存在的目标文件,新目录项指源文件inode,会先删除目标文件的dentry)】
  1. 如果目标和源文件所在文件系统不相同,就是cp和rm;

好了,前置知识差不多说完了,接下来是解释为什么cp -rf覆盖.so文件的方式会导致进程core dump。其实发生了如下事情:

①进程启动时,通过dlopen打开.so,内核会通过mmap把.so文件加载到进程的地址空间,对应了内存中的几个page。(这里我是查阅了jdk加载.so的源码,最后会使用dlopen这个库函数加载。关于dlopen的实现,我去看了glibc的源码,c语言知识浅薄再加上Clion这个IDE有时候没法跳转到函数,没找到调用mmap函数)
②在这个过程中loader会把so里面引用的外部符号例如malloc printf等解析成真正的虚存地址。
③我们执行cp -rf覆盖.so文件时会对.so进行truncate,当so被trunc时,kernel会把so文件在虚拟内的页清理掉。

怎么知道cp命令会truncate的呢?
strace cp test2 test3
使用strace追踪cp的系统调用过程,有如下:
fstat(3, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
open("test3", O_WRONLY|O_TRUNC) = 4
cp的实现是如果目标文件存在会先truncate清空目标文件内容,然后再把数据写到目标文件里。
顺带提一句,mv命令执行的系统调用是rename,不会截断文件。

④当运行到so里面的代码时,因为物理内存中不再有实际的数据(仅存在于虚存空间内),会产生一次缺页中断。
⑤Kernel从so文件中copy一份到内存中去。这时就会发生下面几种情况:
a.如果需要的文件偏移大于新的so的地址范围,就会产生bus error。(此处待深入)
b.如果so里面依赖了外部符号,但是这时的全局符号表并没有经过重新解析,当调用到时就产生segment fault。(我遇到的线上问题就是属于这种情况)。
c.如果so里面没有依赖外部符号,程序侥幸可以继续运行。 (后续进行了第二次测试验证,发现如果程序启动时加载了最新的.so,那么即使cp -rf覆盖相同的内容,也不会导致程序core dump)

0x02 实验验证

验证目标:
mmap加载到内存中的文件,如果被cp -rf覆盖后会产生缺页中断。

测试方案:

  1. 创建1个文件test.txt,用mmap方法加载到内存中。
  2. 分别使用mv + cp 以及 cp -rf 覆盖的方式,进行替换test.txt
  3. 分别观察缺页中断的次数,使用命令:sudo perf stat -e faults -p 进程id

预期结果:

  1. 使用mv+cp的方式,进程不会产生缺页中断。
  2. 使用cp -rf覆盖的方式,进程会产生缺页中断。

实验程序代码如下:
主要参考了mmap手册中的demo,稍加修改,增加了一个无线循环,读映射到内存中文件。

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main (int argc, char **argv) 
{
    int fd, nread, i;
    struct stat sb;
    char *mapped;
    
    if ( argc <= 1 ) {
        printf("%s: Need file path! \n",argv[0]);
        exit(-1);
    }

    /* 打开文件 */
    if ((fd = open (argv[1], O_RDWR)) < 0) {
        perror ("open");
    }

    /* 获取文件的属性 */
    if ((fstat (fd, &sb)) == -1) {
        perror ("fstat");
    }

    /* 将文件映射至进程的地址空间 */

    if ((mapped = (char *) mmap (NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == (void *) -1) {
        perror ("mmap");
    }

    /* 映射完后, 关闭文件也可以操纵内存 */
    close (fd);

    //printf ("%s", mapped);
  
    /* 修改一个字符,同步到磁盘文件 */
    //int i = 0;
    while (1) {
        char ch = mapped[0];
        //mapped[0] = i;
        sleep(1);
    }

    if ((msync ((void *) mapped, sb.st_size, MS_SYNC)) == -1) {
        perror ("msync");
    }

    /* 释放存储映射区 */
    if ((munmap ((void *) mapped, sb.st_size)) == -1) {
        perror ("munmap");
    }

    return 0;
 }

编译运行(我的文件名叫testMmap.cpp):

g++ testMmap.cpp -o testMmap
./testMmap test1.txt

实验结果

一、mv + cp的方式

启动程序后,使用sudo perf stat -e faults -p 进程id 监控进程的缺页中断情况。
程序运行起来后,我们什么也不操作的话,是没有page fault的。因为我们的程序就是特意这么编写的,为了排除其他因素的干扰。

启动程序

接着使用mv命令把mmap的文件test1.txt重命名成test2.txt。
然后用cp命令把zhanghaobo/test1.txt 复制到当前目录下。

mv+cp

然后来看缺页中断的结果:

0次,符合预期。

二、cp -rf覆盖的方式

在进行cp -rf的实验之前,我们需要停止程序。(如果不停止的话,需要把下面cp -rf 的被替换文件改成第一个实验里的test2.txt,因为第一个实验里我们进行了mv操作)

首先我们来看cp -rf覆盖的方式。cp -rf 一个同名文件。
执行cp -rf。

cp -rf

可以从下图发现确实有缺页中断产生。

所以这也就验证了使用直接覆盖动态库的方式会因为truncate文件导致进程缺页中断,后发现覆盖后的.so中依赖了未解析的符号(比如调用了新的函数等),就会导致进程core dump。

参考资料
【1】http://www.ruanyifeng.com/blog/2011/12/inode.html
【2】https://developer.aliyun.com/article/6371
【3】https://www.gnu.org/software/libc/
【4】https://man7.org/linux/man-pages/man2/mmap.2.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

叹了口丶气

觉得有收获就支持一下吧~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值