IT技术博客大学习 共学习 共进步
全部 移动开发 后端 数据库 AI 算法 安全 DevOps 前端 设计 开发者

linux下cp,mv进行动态库覆盖问题分析

博学无忧 2014-03-19 23:04:03 累计浏览 3,880 次
本机暂存

   本文是引用@五牧同学在阿里ata上发表的文章。感觉分析的比较透彻,分享给大家。

   问题的起因在来源于周会上钟老板提出的一个问题,cp新的so文件替换老的so,会导致程序core掉。这个问题引起了大家的热烈讨论,其中提及了的名词有inode,dentry,buserror等,比较混乱,由于功力浅薄,当时也没有十分清楚引起core掉的原因。于是乎趁着10.1的休息时间,闲里偷忙,理一理当时的问题,有不对之处,还请大家多多指出。

   文章主要分为下面几个部分

  • part1.inode,dentry名词介绍

  • part2.cp,mv操作对inode的影响

  • part3.cp,mv覆盖动态库的区别

  • part4.代码分析验证

  •    希望通过这几个部分的介绍,最终能说清楚这个问题:cp操作新的so文件替换老的so文件,程序会core掉的根本原因是什么?

       part1:inode,dentry名词介绍

       inode索引节点,dentry目录项。从这两个单词的中文意思也能简单猜测下,dentry就像书的目录一样,指向具体的inode号。事实上是不是这样呢,看下具体的介绍。

       inode和dentry都是linux下虚拟文件系统(vfs,vitual file system,图1)的重要概念。inode储存着文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等等,特别注意的是inode中不包括文件名信息,具体包含的内容如下(stat命令可以查看文件的inode信息):

*文件的字节数
*文件拥有者的User ID
*文件的Group ID
*文件的读、写、执行权限
*文件的时间戳,共有三个:ctime是inode上一次变动的时间,mtime指文件内容上一次变动的时间,atime指文件上一次打开的时间。
*链接数,即有多少目录项指向这个inode
*文件数据block的位置

   dentry是directory entry的缩写,直接翻译目录入口是不是更容易理解些;)。dentry中则包含具体的文件名和指向inode的指针等信息,也就是说通过dentry可以找到对应的inode,再通过inode找到文件存储的block位置。这里我画了一个简单的示例图(图1),来说明dentry和inode之间的具体关系。

   t_35661_1381231351_830522434

   每一个进程在pcb中保存着一份文件描述符表,而文件描述符就是这个表的索引,这里进程打开/home/wsl/test文件,文件描述符为3,其中文件描述符表项中又有一个指向已打开文件的指针,已打开的文件在内核中用file结构体表示,包括打开的标志位,读写的位置f_pos,引用计数(f_count)以及指向dentry结构体的指针(f_dentry)等信息。为了减少读盘次数,内核都缓存了目录的树状结构,称为dentry cache,这里面每一个节点都是一个dentry结构体【正如前面介绍的,dentry中保存着文件名信息】。dentry结构体中都有一个指针指向inode结构体,因此只要沿着路径各部分的dentry搜索即可找到进程要访问的文件的inode结构体,从而获取文件的inode信息,进行文件的具体操作。

   简单总结下,*nux系统内部不使用文件名,而是使用inode来识别文件,用户通过文件名打开文件,实际上是首先通过dentry获取文件的inode信息,然后根据读取的inode信息来进行文件的处理。

   part 2:cp,mv,rm操作对inode的影响

   在介绍完inode后,我们来看下cp和mv操作对文件的inode都有什么样的影响。

snail@ubuntu:~/test$ touch t1 t2 && ls -i t1 t2
792797 t1  792798 t2
snail@ubuntu:~/test$ cp t1 t2 && ls -i t1 t2
792797 t1  792798 t2//将t1 cp成t2,但t2的inode号和原始的t2保持一致
snail@ubuntu:~/test$ mv t1 t2 && ls -i t2
792797 t2 //将t1 mv成t2,t2的inode号为原始t1的inode号
snail@ubuntu:~/test$ cp t2 t3 && ls -i t2 t3
792797 t2  792846 t3//cp到一个不存在的文件t3,t3为新的inode号

   下面是一些测试结论直接来自参考文献2

   cp命令

   inode号分配

   如果目标文件不存在,分配一个未使用的inode号,在inode表中添加一个新项目;

   如果目标文件存在,则inode号采用被覆盖之前的目标文件的inode号

   在目录中新建一个dentry,并指向步骤1)中的inode;

   把数据复制到block中。

   我们接着来看下rm命令对inode会有什么样的影响

   mv命令

   a.如果mv命令的目标和源文件所在的文件系统相同:

   1)使用新文件名建立dentry

   2)删除带有原来文件名的dentry; 【该操作对inode表没有影响(除时间戳),对数据的位置也没有影响,不移动任何数据。(即使是mv到一个已经存在的目标文件,新目录项指源文件inode,会先删除目标文件的dentry)】

   b.如果目标和源文件所在文件系统不相同,就是cp和rm;

   然后我们来看下rm对inode的影响

   首先写了一个简单的python脚本,不停的网log文件里面写数据

[wsl@inc-search-150-67 tmp]$ cat test.py 
import time
file = open('log','w')
while(1):
    file.write("abc
");
    time.sleep(1)
    file.flush()
file.close()

   然后lsof命令查看log文件

   其中29908为进程号,120那一列为文件大小,35为inode号

[wsl@inc-search-150-67 tmp]$ /usr/sbin/lsof |grep /tmp/log
python    29908   wsl    3w      REG                8,5        96         35 /tmp/log

   最后删除此log文件,继续查看此命令

[wsl@inc-search-150-67 tmp]$ rm log
[wsl@inc-search-150-67 tmp]$ /usr/sbin/lsof |grep /tmp/log
python    29908   wsl    3w      REG                8,5       120         35 /tmp/log (deleted)//节点被标记为deleted
[wsl@inc-search-150-67 tmp]$ /usr/sbin/lsof |grep /tmp/log
python    29908   wsl    3w      REG                8,5       232         35 /tmp/log (deleted)//文件大小仍在增加
[wsl@inc-search-150-67 tmp]$ kill -9 29908
[wsl@inc-search-150-67 tmp]$ /usr/sbin/lsof |grep /tmp/log
[wsl@inc-search-150-67 tmp]$ 

   我们可以看到log文件被删除后,lsof可以看到此文件被标记为deleted,inode仍然存在,并且在没有kill掉进程的情况下,文件的大小仍在增加,只有进程被kill掉后,才释放掉此inode。先埋下这一观察到的现象,到文章的最后,我们在继续讨论这样的操作会有什么样的影响。

   下面一些是rm命令对文件inode的影响

   rm命令

   1)递减链接计数,从而释放inode号码,这个inode号码可以被重用

   2)把数据块挂到可用空间列表

   3)删除目录映射表中的相关行 但是底层数据实际上没有被删除,只是当数据块被另一个文件使用时,原来的数据才会被覆盖

   简单总结下:

   cp命令到一个已经存在的文件,inode号沿用已经存在文件的inode号;

   mv命令用新的inode号,也就是mv前的文件的inode号;

   rm命令删除的底层数据只有被使用的时候才会被覆盖。

   part3.cp,mv覆盖动态库的区别

   前面两部分是对这一部分的一个简单铺垫。现在我们来看下为什么使用cp对动态库进行覆盖,程序会core掉(或者说可能会core掉?)

   首先我们使用strace命令来跟踪cp命令的执行。【btw:strace命令可以跟踪到一个进程产生的系统调用,包括参数,返回值,执行消耗的时间,调试利器】

snail@ubuntu:~/test$ ls
new.so  old.so
snail@ubuntu:~/test$ cat new.so //new.so内容
this is new.so
haha!
snail@ubuntu:~/test$ strace cp new.so old.so
//......只列出重要的相关步骤
open("new.so", O_RDONLY)    = 3
fstat64(3, {st_mode=S_IFREG|0664, st_size=21, ...}) = 0
open("old.so", O_WRONLY|O_TRUNC) = 4
fstat64(4, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
read(3, "this is new.sonhaha!n", 32768) = 21
write(4, "this is new.sonhaha!n", 21) = 21
read(3, "", 32768)                      = 0
close(4)                                = 0
close(3)                                = 0
//......

   可以看到第8行以只读的方式打开了new.so,然后第10行以写加截断(O_WRONLY|O_TRUNC)的方式打开old.so。【O_TRUNC的含义:若文件存在,则长度被截为0,属性不变】,最后将new.so的内容写到old.so中,然后关闭文件。

   这个过程具体的发生的事情如下:

   1.应用程序通过dlopen打开so的时候,kernel通过mmap把so加载到进程地址空间,对应于vma里的几个page.

   2.在这个过程中loader会把so里面引用的外部符号例如malloc printf等解析成真正的虚存地址。

   3.当so被cp覆盖时,确切地说是被trunc时,kernel会把so文件在虚拟内的页清理掉。

   4.当运行到so里面的代码时,因为物理内存中不再有实际的数据(仅存在于虚存空间内),会产生一次缺页中断。

   5.Kernel从so文件中copy一份到内存中去。这时就会发生下面几种情况

   a)如果需要的文件偏移大于新的so的地址范围,就会产生bus error.这个在向宇大神的文章中有详细的介绍(摸我)

   b)如果so里面依赖了外部符号,但是这时的全局符号表并没有经过重新解析,当调用到时就产生segment fault

   c)如果so里面没有依赖外部符号,程序侥幸可以继续运行。

   mv命令新的so到老的so,关键代码就一句,一个重命名的过程,所以旧的so文件的inode号被替换新的so的inode号

//......
rename("new.so", "old.so")              = 0
//......

   part4.代码验证分析

   下面就出现的bc两种情况用代码分析验证下。情况a可以参考向宇大神的文章,不在赘述了。

//test.c
#include<stdio.h>
 
void test1(void){
    int j=0;
    printf("test1:j=%dn", j);
    return ;
}
  
void test2(void){
    int j=1;
    return ;
}

   执行下面命令生成so文件

   gcc -fPIC -shared -o libtest.so test.c -g

//main.c
#include <stdio.h>
#include <dlfcn.h> 
  
int main()
{
    void *lib_handle;
    void (*fn1)(void);
    void (*fn2)(void);
    char *error;
    //表示要将库装载到内存,准备使用
    lib_handle = dlopen("libtest.so", RTLD_LAZY);
    if (!lib_handle)
    {
        fprintf(stderr, "%sn", dlerror());
        return 1;
    }
    //获得指定函数(symbol)在内存中的位置(指针)
    fn1 = dlsym(lib_handle, "test1");
    if ((error = dlerror()) != NULL)
    {
        fprintf(stderr, "%sn", error);
        return 1;
    }
    printf("fn1:0x%xn", fn1);
  
    fn1();
  
    fn2 = dlsym(lib_handle, "test2");
    if ((error = dlerror()) != NULL)
    {
      fprintf(stderr, "%sn", error);
      return 1;
    }
  
    printf("fn2:0x%xn", fn2);
  
    fn2();
  
    dlclose(lib_handle);
  
    return 0;
}

   执行命令:gcc -o main main.c -ldl -g

   首先进行测试1,断点设置在27行,fn1()执行之前

Breakpoint 1, main () at main.c:27
//这时我们在另外一个终端执行下面的命令
//cp libtest.so libtest2.so 
//cp libtest2.so libtest.so
 
27      fn1();
(gdb) s
test1 () at test.c:4
4       int j=0; //没有报错
(gdb) n
5       printf("test1:j=%dn", j);
(gdb) n
//出错,因为引用了printf外部函数,而全局符号表并没有经过重新解析,找不到printf函数
Program received signal SIGSEGV, Segmentation fault.
0x00000396 in ?? ()
(gdb) bt
#0  0x00000396 in ?? ()
#1  0xb7fd84aa in test1 () at test.c:5
#2  0x08048622 in main () at main.c:27

   下面进行测试2,断点设置在38行,fn2执行之前。

   然后在另一个终端执行和测试1相同的cp操作

Breakpoint 1, main () at main.c:38
38      fn2();
(gdb) s
test2 () at test.c:10
10      int j=1;
(gdb) n
12  }
(gdb) n
main () at main.c:40
40      dlclose(lib_handle);
(gdb) n
42      return 0;
(gdb) 
43  }//程序正常结束

   从这两个测试例子中,我们可以得到这样的结论:

   当用新的so文件去覆盖老的so文件时候:

   A)如果so里面依赖了外部符号,程序会core掉

   B)如果so里面没有依赖外部符号,so部分代码可以正常运行

   总结:

   整理完这四部分,回到最开始的问题“为什么cp新的so文件替换老的so,程序会core掉的根本原因是什么?”,现在串联起来总结如下。

   1. cp new.so old.so,文件的inode号没有改变,dentry找到是新的so,但是cp过程中会把老的so截断为0,这时程序再次进行加载的时候,如果需要的文件偏移大于新的so的地址范围会生成buserror导致程序core掉,或者由于全局符号表没有更新,动态库依赖的外部函数无法解析,会产生sigsegv从而导致程序core掉,当然也有一定的可能性程序继续执行,但是十分危险。

   2. mv new.so old.so,文件的inode号会发生改变,但老的so的inode号依旧存在,这时程序必须停止重启服务才能继续使用新的so,否则程序继续执行,使用的还是老的so,所以程序不会core掉,就像我们在第二部分删除掉log文件,而依然能用lsof命令看到一样。

   ps.阿里的第一篇博客,以后尝试经常写写博客,把自己的思路理的更加清晰和有逻辑,如有不对的地方,还请大家多多指正。

同分类推荐文章

  1. 从零重建 macOS 开发机:可复现的环境初始化流程 (2026-06-14 20:36:00)
  2. 百度物理网络监控工具开源第二弹:毫秒级监控工具 baize,让你的网络问题无处遁形 (2026-06-11 08:10:28)
  3. How to Set Up Homebrew Tap for Private CLI Tools: A Complete Guide (2026-05-27 02:13:03)

查看更多 DevOps 文章 →

建议继续学习

  1. Linux如何统计进程的CPU利用率 (累计阅读 16,307)
  2. 我的 RHCA 之路 (累计阅读 14,012)
  3. Linux内存点滴 用户进程内存空间 (累计阅读 13,228)
  4. 给程序员新手的一些建议 (累计阅读 13,089)
  5. Linux 性能监控、测试、优化工具 (累计阅读 13,011)
  6. 关于linux内存free的一些事情 (累计阅读 12,867)
  7. ps - 按进程消耗内存多少排序 (累计阅读 12,686)
  8. Google怎么用linux (累计阅读 12,581)
  9. Linux Used内存到底哪里去了? (累计阅读 11,866)
  10. find命令的一点注意事项 (累计阅读 11,864)