使用Android Native Hook技术解决VLC播放器闪退的问题

文章目录

1.概述

在做公司的一个TOB的需求时,发现调起Unity提供的3D播放器播放网络在线视频时闪退了,然后就拉着相关部门的人一起分析问题,最后定位到是VLC里面用到的系统日志打印函数在部分的系统上会出问题,于是各部门的同事就开始想各种解决方案,当时主要是两个部门的同事提出了两种解决方案,一方面是系统部门的人提出直接在系统上改,因为ROM是我们自己的所以可以改系统的代码。禁用掉日志打印函数中引发闪退的部分,但是这样就会导致其他APP使用这个日志打印函数时就无法获取到日志了。但也不失为一个解决办法。第二种解决办法是C++ 部门提出的,直接将VLC的源码下载下来,修改打印日志的函数。这种方法其实也行,就是比较耗费时间和精力,导致一直没有实施,最后就是系统部门的人改了一个版本验证没有闪退后,也不敢进ROM,怕影响其他APP。于是最后我就提出了使用native hook的技术去解决这个问题,本文就是介绍如何使用native hook技术解决这个问题。

2.问题描述

本问题主要是当打开播放器,传递网络视频链接播放的时候就会闪退,闪退的堆栈如下所示:

javascript 复制代码
 FORTIFY: %n not allowed on Android
2024-03-04 18:02:50.263 15889-15947 AndroidRuntime          xxx.xxx.xxx.vrplayer            
 E  FATAL EXCEPTION: UnityMain
 Process: xxx.xxx.xxx.vrplayer, PID: 15889
 java.lang.Error: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
 Version '2019.4.36f1 (660c164b2fc5)', Build type 'Release', Scripting Backend 'il2cpp', CPU 'arm64-v8a'
 Build fingerprint: 'xxx/xxx/xxx:10/QKQ1.211001.001/02241336:user/release-keys'
 Revision: '0'
 ABI: 'arm64'
 Timestamp: 2024-03-04 18:02:50+0800
 pid: 15889, tid: 16063, name: VlcObject  >>> xxx.xxx.xxx.vrplayer <<<
 uid: 1000
 signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr --------
     x0  0000000000000000  x1  0000000000003ebf  x2  0000000000000006  x3  0000007956eedd90
     x4  0000000000000000  x5  0000000000000000  x6  0000000000000000  x7  0000000000000018
     x8  00000000000000f0  x9  becc2cee9a43a307  x10 0000000000000001  x11 0000000000000000
     x12 fffffff0fffffbdf  x13 0000000065e59c4a  x14 0002348d9b6c4800  x15 00005b4371aa1524
     x16 00000079e94118c0  x17 00000079e93eda60  x18 0000000000000000  x19 0000000000003e11
     x20 0000000000003ebf  x21 00000000ffffffff  x22 00000000ffffffff  x23 0000007956eee954
     x24 0000007956eee170  x25 000000000000001d  x26 000000000000006e  x27 0000000000000000
  x28 0000007956ef0008  x29 0000007956eede30
  sp  0000007956eedd70  lr  00000079e939f0c4  pc  00000079e939f0f0
 
                                                                                                    backtrace:
 #00 pc 00000000000830f0  /apex/com.android.runtime/lib64/bionic/libc.so (abort+160) (BuildId: 35b174c1ce3028d241142fca9906cd01)
 #01 pc 00000000000c4984  /apex/com.android.runtime/lib64/bionic/libc.so (helpers::wcsconv(wchar_t*, int)) (BuildId: 35b174c1ce3028d241142fca9906cd01)
 #02 pc 00000000000c3f38  /apex/com.android.runtime/lib64/bionic/libc.so (__vfprintf+10956) (BuildId: 35b174c1ce3028d241142fca9906cd01)
 #03 pc 00000000000e171c  /apex/com.android.runtime/lib64/bionic/libc.so (snprintf+232) (BuildId: 35b174c1ce3028d241142fca9906cd01)
 #04 pc 00000000008bdabc  /data/app/xxx.xxx.xxx.vrplayer-UO0jeUwKH6BDD6eJ_Zrx6A==/base.apk (vasnprintf+3036) (BuildId: fbdb95762b44ab3a3cd3722aba5ebc064ca8abd7)
 #05 pc 00000000008bce50  /data/app/xxx.xxx.xxx.vrplayer-UO0jeUwKH6BDD6eJ_Zrx6A==/base.apk (rpl_snprintf+132) (BuildId: fbdb95762b44ab3a3cd3722aba5ebc064ca8abd7)
 #06 pc 00000000008ecee8  /data/app/xxx.xxx.xxx.vrplayer-UO0jeUwKH6BDD6eJ_Zrx6A==/base.apk (BuildId: fbdb95762b44ab3a3cd3722aba5ebc064ca8abd7)
 #07 pc 00000000008ecdd8  /data/app/xxx.xxx.xxx.vrplayer-UO0jeUwKH6BDD6eJ_Zrx6A==/base.apk (gnutls_x509_trust_list_add_trust_dir+68) (BuildId: fbdb95762b44ab3a3cd3722aba5ebc064ca8abd7)
 #08 pc 00000000009275e0  /data/app/xxx.xxx.xxx.vrplayer-UO0jeUwKH6BDD6eJ_Zrx6A==/base.apk (gnutls_x509_trust_list_add_system_trust+76) (BuildId: fbdb95762b44ab3a3cd3722aba5ebc064ca8abd7)
 #09 pc 000000000063ca28  /data/app/xxx.xxx.xxx.vrplayer-UO0jeUwKH6BDD6eJ_Zrx6A==/base.apk (BuildId: fbdb95762b44ab3a3cd3722aba5ebc064ca8abd7)
 #10 pc 000000000074d190  /data/app/xxx.xxx.xxx.vrplayer-UO0jeUwKH6BDD6eJ_Zrx6A==/base.apk (vlc_module_load+1076) (BuildId: fbdb95762b44ab3a3cd3722aba5ebc064ca8abd7)
 #11 pc 0000000000785dd4  /data/app/xxx.xxx.xxx.vrplayer-UO0jeUwKH6BDD6eJ_Zrx6A==/base.apk (vlc_tls_ClientCreate+60) (BuildId: fbdb95762b44ab3a3cd3722aba5ebc064ca8abd7)
 #12 pc 000000000061f3b0  /data/app/xxx.xxx.xxx.vrplayer-UO0jeUwKH6BDD6eJ_Zrx6A==/base.apk (BuildId: fbdb95762b44ab3a3cd3722aba5ebc064ca8abd7)
 #13 pc 00000000006262d8  /data/app/xxx.xxx.xxx.vrplayer-UO0jeUwKH6BDD6eJ_Zrx6A==/base.apk (BuildId: fbdb95762b44ab3a3cd3722aba5ebc064ca8abd7)
 #14 pc 00000000006263d0  /data/app/xxx.xxx.xxx.vrplayer-UO0jeUwKH6BDD6eJ_Zrx6A==/base.apk (BuildId: fbdb95762b44ab3a3cd3722aba5ebc064ca8abd7)
 #15 pc 000000000061eac4  /data/app/xxx.xxx.xxx.vrplayer-UO0jeUwKH6BDD6eJ_Zrx6A==/base.apk (BuildId: fbdb95762b44ab3a3cd3722aba5ebc064ca8abd7)

堆栈做了脱敏处理,应该不影响问题分析。

3.问题分析

从上面的堆栈中,我们可以看到这个问题的一行日志如下:

结合日志,我们发现并不是我们打印的日志,最有可能是第三方库VLC或者是系统打印的,但是VLC我们暂时没有源码,更何况VLC是Unity插件中引用的,我们无法准确的知道对应的代码版本,所以就先从系统分析起,到系统源码中去搜索这行代码。幸运的是,我们在系统源码中找到了这行代码:
上图代码的地址

知道是系统打印的日志后就好办了,我们接着去找下都是谁调用了这个函数,这里需要看堆栈了。我们从堆栈中可以看到如下两个函数:

然后我们又可以根据日志中的信息:

推断出调用上面两个函数打印日志的是vlc播放器模块,接下来我们要想解决这个问题,主要有3种办法,第一种,系统修改,屏蔽掉这个日志函数。第二种。修改VLC源码。第三种就是本文介绍的从应用测通过native hook技术去修改。

4.问题解决

通过我的亲身经历也证明了,从系统侧改不现实,因为这是应用侧的库中的原因导致的闪退问题,系统侧没有责任也没有义务去为了一个应用冒着风险去修改这个地方,影响太大。第二,从VLC源码中修改重新编译,这也不现实,因为修改源码意味着需要花时间和精力去熟悉VLC源码,这需要公司投入人力,而且收益不高,并且VLC后续如果更新还得继续去维护,代价太大。所以我提出了native hook的技术。所谓的native hook,就是劫持函数的调用。让原本调用A函数的操作改为调用我自己定义的B函数。这样就绕过了A函数的执行。对应到当前的问题中就是,A函数调用了系统的日志打印函数,会引起闪退,那么我们就hook住A函数,让VLC不去调用A函数,去调用我们的B函数,这样就不会引起应用闪退了。接下来就看下如何去实现对应的函数hook.

通过分析日志我们发现,引起应用闪退的函数主要有两个。

所以我们只要hook住这两个函数,就可以避免这个native crash。知道了原理后,我们就可以开始hook代码的编写了。这里我们使用的是字节跳动开源的bhook库来hook native的函数调用。主要代码如下所示:

javascript 复制代码
static int my_vfprintf_proxy(FILE *fp, const char *path, va_list va) {
    debug("my_vfprintf_proxy", path, BYTEHOOK_RETURN_ADDRESS());
    LOG("zhongxj:my_vfprintf_proxy = %d,return fd: %d", 1129);
    BYTEHOOK_POP_STACK();
    return 1129;
}

static int my_rpl_snprintf_proxy(char * a, size_t s, const char * cc, ...) {
   // debug("my_rpl_snprintf_proxy", path, BYTEHOOK_RETURN_ADDRESS());
    LOG("zhongxj:my_vfprintf_proxy = %d,return fd: %d", 904);
    BYTEHOOK_POP_STACK();
    return 904;
}

static int hacker_hook_rpl_snprintf() {
    if (NULL != rplsnprintf_stub) return -1;
    void *rpl_snprintf_proxy;
    rpl_snprintf_proxy = (void *) my_rpl_snprintf_proxy;


    rplsnprintf_stub =
            bytehook_hook_single("libvlcjni.so",
                                 NULL,
                                 "rpl_snprintf",
                                 rpl_snprintf_proxy,
                                 rplsnprintf_hooked_callback,
                                 NULL);
    return 0;
}


static int hacker_hook_vasnprintf(JNIEnv *env, jobject thiz) {
    (void) env, (void) thiz;

    if (NULL != vfprintf_stub) return -1;
    void *vfprintf_proxy;
    vfprintf_proxy = (void *) my_vfprintf_proxy;

    vfprintf_stub =
            bytehook_hook_single("libvlcjni.so",
                                 NULL,
                                 "vasnprintf",
                                 vfprintf_proxy,
                                 vfprintf_hooked_callback,
                                 NULL
            );

    int res = hacker_hook_rpl_snprintf();
    LOG("zhongxj:hook rpl res: %d",res);
    return 0;
}



static int hacker_unhook(JNIEnv *env, jobject thiz) {
    (void) env, (void) thiz;
    if (NULL != vfprintf_stub) {
        bytehook_unhook(vfprintf_stub);
        vfprintf_stub = NULL;
    }

    if(NULL != rplsnprintf_stub){
        bytehook_unhook(rplsnprintf_stub);
        rplsnprintf_stub = NULL;
    }

    return 0;
}

然后在Java层使用对应的jni函数,在打开播放器的地方hook上面的两个函数vasnprintf,rpl_snprintf这样当VLC库调用这两个函数的时候,就会被hook住,然后调用我们的my_vfprintf_proxy,my_vfprintf_proxy函数,这样就避免了调用系统的函数导致闪退,然后我们的播放器停止退出的时候可以释放掉我们的hook点就可以了。有人会觉得这种native hook的技术好像不是啥好技术,可能会有害,但其实不是,bhook库被字节跳动广泛用于抖音,火山引擎,飞书等,所以就放心用吧,用得好的话收益很大的。

5.总结

本文主要介绍了使用native hook的方案解决第三方库中的函数调用引起的闪退,这里只是提供了这个思路和大体实现,具体的细节还需要读者自己添加上,比如何时需要hook,hook的时机等。解决这次问题的过程中,我也深深感受到了知识的力量。所以我们应该多读书,多看别人的经验。很多时候这种解决问题的方法是AI和搜索引擎无法提供的,很遗憾我即将30了才明白这个道理,希望年轻的读者,对编程感兴趣的一定要多学习,多看书,多动手。下篇文章我打算出一个bhook的使用方法示例。希望能帮到新手朋友。敬请期待。