彻底理解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号;
2. 在⽬录中新建⼀个dentry,并指向步骤1中的inode。
3. 把数据复制到block中。
mv命令
1. 如果mv命令的⽬标和源⽂件所在的⽂件系统相同:
使⽤新⽂件名建⽴dentry(⽂件名 -> inode)
删除带有原来⽂件名的dentry; 【该操作对inode表没有影响(除时间戳),对数据的位置也没有影响,不移动任何数据。(即使是mv到⼀个已经存在的⽬标⽂件,新⽬录项指源⽂件inode,会先删除⽬标⽂件的dentry)】
2. 如果⽬标和源⽂件所在⽂件系统不相同,就是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个⽂件,⽤mmap⽅法加载到内存中。
2. 分别使⽤mv + cp 以及 cp -rf 覆盖的⽅式,进⾏替换
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");
linux下的sleep函数}
/* 释放存储映射区 */
if ((munmap ((void *) mapped, sb.st_size)) == -1) {
perror ("munmap");
}
return 0;
}
编译运⾏(我的⽂件名叫testMmap.cpp):
g++ testMmap.cpp -o testMmap
./
实验结果
⼀、mv + cp的⽅式
启动程序后,使⽤sudo perf stat -e faults -p 进程id 监控进程的缺页中断情况。
程序运⾏起来后,我们什么也不操作的话,是没有page fault的。因为我们的程序就是特意这么编写的,为了排除其他因素的⼲扰。
启动程序
接着使⽤mv命令把mmap的⽂件重命名成。
然后⽤cp命令把 复制到当前⽬录下。
mv+cp
然后来看缺页中断的结果:
0次,符合预期。
⼆、cp -rf覆盖的⽅式
在进⾏cp -rf的实验之前,我们需要停⽌程序。(如果不停⽌的话,需要把下⾯cp -rf 的被替换⽂件改成第⼀个实验⾥的,因为第⼀个实验⾥我们进⾏了mv操作)
⾸先我们来看cp -rf覆盖的⽅式。cp -rf ⼀个同名⽂件。
执⾏cp -rf。
cp -rf
可以从下图发现确实有缺页中断产⽣。
所以这也就验证了使⽤直接覆盖动态库的⽅式会因为truncate⽂件导致进程缺页中断,后发现覆盖后的.so中依赖了未解析的符号(⽐如调⽤了新的函数等),就会导致进程core dump。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。