zoukankan      html  css  js  c++  java
  • Linker加载so失败问题分析

    WeTest 导读

    近期测试反馈一个问题,在旧版本微视基础上覆盖安装新版本的微视APP,首次打开拍摄页录制视频合成时高概率出现crash。

     


     

    那么我们直奔主题,看看日志:

     

     

    另外复现的日志中还出现如下信息:

    '/data/data/com.tencent.weishi/appresArchiveExtra/res1bodydetect/bodydetect/libxnet.so: strtab out of bounds error

     

     

    后经过测试,发现覆盖安装后首次使用美体功能也会出现crash,日志如下:

     

     

    由于出现问题的场景都是覆盖安装首次使用,并且涉及到人体检测相关的so,似乎存在某种共同的原因。

     

    因此Abort异常比起fault addr类问题更容易分析,先从前面Linker出现Abort异常的位置开始着手。

     

    Linker是so链接和加载的关键,属于系统可执行文件,因此分析起来比较棘手。好在手上正好有一台刚刷完自己编译的Android AOSP的Pixel,做一些实验变得更轻松了。

    出现异常的Linker代码linker_soinfo.cpp如下:

     

    const char* soinfo::get_string(ElfW(Word) index) const {
     if (has_min_version(1) && (index >= strtab_size_)) {
       async_safe_fatal("%s: strtab out of bounds error; STRSZ=%zd, name=%d",
           get_realpath(), strtab_size_, index);
     }

     return strtab_ + index;
    }

    bool soinfo::elf_lookup(SymbolName& symbol_name,
                           const version_info* vi,
                           uint32_t* symbol_index) const {
     uint32_t hash = symbol_name.elf_hash();

     TRACE_TYPE(LOOKUP, "SEARCH %s in %s@%p h=%x(elf) %zd",
                symbol_name.get_name(), get_realpath(),
                reinterpret_cast<void*>(base), hash, hash % nbucket_);

     ElfW(Versym) verneed = 0;
     if (!find_verdef_version_index(this, vi, &verneed)) {
       return false;
     }

     for (uint32_t n = bucket_[hash % nbucket_]; n != 0; n = chain_[n]) {
       ElfW(Sym)* s = symtab_ + n;
       const ElfW(Versym)* verdef = get_versym(n);

       // skip hidden versions when verneed == 0
       if (verneed == kVersymNotNeeded && is_versym_hidden(verdef)) {
           continue;
       }

       if (check_symbol_version(verneed, verdef) &&
           strcmp(get_string(s->st_name), symbol_name.get_name()) == 0 &&
           is_symbol_global_and_defined(this, s)) {
         TRACE_TYPE(LOOKUP, "FOUND %s in %s (%p) %zd",
                    symbol_name.get_name(), get_realpath(),
                    reinterpret_cast<void*>(s->st_value),
                    static_cast<size_t>(s->st_size));
         *symbol_index = n;
         return true;
       }
     }

     TRACE_TYPE(LOOKUP, "NOT FOUND %s in %s@%p %x %zd",
                symbol_name.get_name(), get_realpath(),
                reinterpret_cast<void*>(base), hash, hash % nbucket_);

     *symbol_index = 0;
     return true;
    }

     

    从代码上看,是在so的symtab中查找某个符号时ElfW(Sym)* s的地址出现异常,导致s->st_name获取到错误的数据。

     

    通过复现问题,可以抓到更完整的 /data/tombstone日志,得到如下完整的信息:

     

     

    尽管从tombstone中我们可以看到一些寄存器数据及寄存处地址附近内存数据,同时也可以看到crash时的虚拟内存映射表,仍然无法获取有价值的信息。另外通过几次复现,发现并不是每次Crash都是SIGABRT,也出现不少SIGSEGV信号,而调用栈和之前都是一样的,比如这个:

     

     

    这基本上可以说明,并不是so本身的代码存在异常,只可能是加载的so出现了文件异常。

     

    另外通过在linker中增加日志,并重新编译linker替换到/system/lib/linker中:

     

     

    可以获取到如下的地址信息:

     

     

    通过根据tombstone中的/proc/<poc>/maps的虚拟内存地址与日志打印的地址进行对比,可以发现最为符号表地址的s并没有指向so文件在虚拟内存中的地址段,因此可以怀疑,so加载确实出现了异常。

     

    因为手机root,可以直接获取到crash时的so文件(adb pull /data/data/com.tencent.weishi/appresArchiveExtra/res1bodydetect/bodydetect/libxnet.so),导出来对比md5,然而发现与正常情况下的so是一模一样的:

     

     

    既然前面的这些实验都没有得出什么有意义的结论,那么我回过头来分析一下,与问题关联的so加载到底有什么特殊性。

     

    实际上,微视为了减包,将一部分so文件进行下发,由于so也处于不断迭代的过程中,新版本的微视可能会在后台更新so文件,那么客户端一旦发现新的版本有新的so,就会去下载so并进行本地替换。

     

    那么这个过程有什么问题呢?唯一可能的问题,就是先加载了旧的so,之后下载新的so进行了热更新。

     

    我们先看下微视中是否有这种现象。要观察这种现象,我们可以打开linker自身的调试开关,开启so加载的日志。通过设置系统属性,我们可以很容易地进行开启LD_LOG日志:

    adb shell setprop debug.ld.all dlerror,dlopen

     

     

    当然我们也可以只针对某个应用开启这个日志(设置系统属性debug.ld.app.)。另外,为了开启linker中更多的日志,比如DEBUG打印的信息等,我们只需要在adb shell中设置环境变量:

    export LD_DEBUG=10

     

     

     

    那么,我们重新复现问题,可以看到如下so加载过程:

     

     

    这个过程表明:旧的so先被加载了,然后下载了新版本的so,并进行了替换。

     

    这个过程有什么问题呢?根据《理解inode》一文我们可以得知,linux的文件系统使用的inode机制支持了so文件的热更新(动态更新),即每个文件都有一个唯一的inode号,打开文件后使用inode号区分文件而不是文件名:

     

    八、inode的特殊作用

    由于inode号码与文件名分离,这种机制导致了一些Unix/Linux系统特有的现象。

     

    1. 有时,文件名包含特殊字符,无法正常删除。这时,直接删除inode节点,就能起到删除文件的作用。

    2. 移动文件或重命名文件,只是改变文件名,不影响inode号码。

    3. 打开一个文件以后,系统就以inode号码来识别这个文件,不再考虑文件名。因此,通常来说,系统无法从inode号码得知文件名。

     

    第3点使得软件更新变得简单,可以在不关闭软件的情况下进行更新,不需要重启。因为系统通过inode号码,识别运行中的文件,不通过文件名。更新的时候,新版文件以同样的文件名,生成一个新的inode,不会影响到运行中的文件。等到下一次运行这个软件的时候,文件名就自动指向新版文件,旧版文件的inode则被回收。

     

    但是问题就出在这里,如果替换文件使用的是cp这样的操作,会导致原来的so文件截断,然后重新写入数据,但是inode并没有更新号,磁盘与内存中的信息出现不一致,这种情况在linux中很常见,比如这篇文章就进行了分析:

     

    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命令看到一样。

     

    还有更深入的解释:

     

    Linux由于Demand Paging机制的关系,必须确保正在运行中的程序镜像(注意,并非文件本身)不被意外修改,因此内核在启动程序后会绑定 内存页 到这个so的inode,而一旦此inode文件被open函数O_TRUNC掉,则kernel会把so文件对应在虚存的页清空,这样当运行到so里面的代码时,因为物理内存中不再有实际的数据(仅存在于虚存空间内),会产生一次缺页中断。Kernel从so文件中copy一份到内存中去,a)但是这时的全局符号表并没有经过解析,当调用到时就产生segment fault , b)如果需要的文件偏移大于新的so的地址范围,就会产生bus error。

     

    那么问题基本清晰了。我们在回去看看微视的代码,这里下载了so之后直接unzip到原来的路径,并没有先进行rm操作。

     

    更近一步,我们自己写个demo测试下刚才的问题(2个按钮,一个加载指定so,一个调用so中的native方法):

     

     

    代码不能再简单了:

     

     

    正常加载so然后执行native方法都是ok的,使用rm+mv替换或者adb push替换也都是ok的,最后再按照错误的方法操作,步骤为:

     

    1. 启动app,点击加载so;

    2. 通过cp命令替换so;

    3. 点击执行native方法;

     

     

    结果确实是crash了:

     

     

    日志如下,是不是很最开始的日志信息一样呢:

     

     

    到此,我们有两种解决办法:

    1. 如果so有升级,先不加载旧的so,等新的so下载完成之后再加载;

    2. 可以先加载旧的so,但是下载了新的so之后,要删除旧的so,再进行替换。

     


     

     

    目前,“自动化兼容测试” 提供云端自动化兼容服务,提交云端百台真机,并行测试。快速发现游戏/应用兼容性和性能问题,覆盖安卓主流机型。

     

    点击:https://wetest.qq.com/product/auto-compatibility-testing 即可体验。

     

    如果使用当中有任何疑问,欢迎联系腾讯WeTest企业QQ:2852350015

  • 相关阅读:
    深入理解计算机系统 第六章 存储器层次结构 第二遍
    深入理解计算机系统 第六章 存储器层次结构
    深入理解计算机系统 第八章 异常控制流 Part2 第二遍
    深入理解计算机系统 第八章 异常控制流 part2
    深入理解计算机系统 第八章 异常控制流 Part1 第二遍
    深入理解计算机系统 第八章 异常控制流 part1
    深入理解计算机系统 第三章 程序的机器级表示 Part2 第二遍
    深入理解计算机系统 第三章 程序的机器级表示 part2
    深入理解计算机系统 第三章 程序的机器级表示 Part1 第二遍
    深入理解计算机系统 第三章 程序的机器级表示 part1
  • 原文地址:https://www.cnblogs.com/wetest/p/9959866.html
Copyright © 2011-2022 走看看