百度App性能优化工具篇 - Thor原理及实践

490次阅读  |  发布于2年以前

01 背景

App开发过程中,如果遇到一些疑难问题或者性能问题(如低端机卡顿),由于没法拿到更多系统的有效信息很难有效定位。这时,Hook不失为一种好的解决方案,Hook技术是在程序运行的过程中,动态的修改代码,植入自己的代码逻辑,修改原有程序执行流程的技术。Hook技术有如下几点能力:

耗时监控】在代码前后动态插入Trace打点,统计耗时;

性能监控】IO监控、内存监控、View绘制监控、大图检测等;

安全审查】Hook敏感API(例如定位),用以安全评审;

逆向和脱壳】对病毒样本进行逆向和脱壳分析;

Hook技术由来已久,目前业界Java和Native Hook都有不少优秀的开源框架,但是如果我们需要将Hook能力使用到线上,都或多或少有些问题,例如兼容性、稳定性、动态性等等。鉴于此,我们开发了一套Thor容器框架,提供标准的Hook接口,降低学习成本,同时将开源框架按照接口适配成插件动态下发按需安装,保证Hook能力的完备和轻量性,并且后续出现更加优秀以及自研的框架的可以无缝的接入和Hook能力拓展,并且不需要上层业务代码和插件进行适配,保证兼容性。

02 现状

Android系统的编程语言主要分为Java和C/C++,Hook方向也主要分为Native和Java Hook两种,其中Native Hook原理也主要分为PLT / Inline Hook两大类,然后Java Hook也分为替换入口点Hook(Replace Entrypoint Hook)和类Inline Hook两大类。Native 方法执行流程大概如下:

Native 方法执行过程中会先通过PLT表找到GOT表中函数全局偏移地址,然后执行其机器码指令,PLT Hook主要是指通过修改GOT中函数的全局偏移地址来达到Hook的目的,代表框架如:xHook、bHook等;Inline Hook则主要是指直接将函数执行的机器码指令进行修改和指令修复来达到Hook的目的,代表框架如:Android-Inline-Hook等。

GOT(Global Offset Table):全局偏移表用于记录在 ELF 文件中所用到的共享库中符号的绝对地址。PLT(Procedure Linkage Table):过程链接表的作用是将位置无关的符号转移到绝对地址。当一个外部符号被调用时,PLT 去引用 GOT 中的其符号对应的绝对地址,然后转入并执行。

Java 方法执行流程大概如下:

Java 方法执行过程中会通过方法在虚拟机中对应的结构Method或ArtMethod结构体中的入口点(Entrypoint),来找到对应的字节码/机器码指令执行。替换入口点Hook(Replace Entrypoint Hook)是指替换Method/ArtMethod中的入口点来达到Hook的目的,代表框架如:Xposed、Dexposed、YAHFA等;类Inline Hook是指将入口点对应的字节码/机器码进指令进行修改和指令修复来达到Hook的目的,代表框架如:Epic等,由于安卓虚拟机的JIT/AOT机制的存在,函数执行地址可能会进行各种变化,所以通常会将字节码强行编译成机器码,然后统一通过修改机器码指令来Hook。

2.1 常见Native Hook框架

2.1.1 xHook框架

xHook框架通过PLT Hook方案来实现的,PLT Hook是通过直接修改GOT表,使得在调用该共享库的函数时跳转到的是用户自定义的Hook功能代码。流程如下:

了解PLT Hook的原理之后,知道该Hook方式有如下特点:

2.1.1 Andorid-Inline-Hook框架

Inline Hook的原理则是直接修改函数在.text实际执行的机器码来实现Hook,不仅对所有SO生效,还能Hook非export导出函数,补齐了PLT Hook方法的不足。流程如下: 但是由于你直接修改的是机器码指令,由于指令架构版本的差异以及后续要进行指令修复,容易有兼容性的问题。

2.2 常见Java Hook框架

2.2.1 Dexposed框架

Dexposed框架只支持Dalvik虚拟机,此虚拟机通过Method结构体中accessFlags字段来判断当前方法是字节码还是机器码。该框架通过修改accessFlags字段为ACC_NATIVE,将Java原方法注册为Native方法,调用时会先调用Native Hook方法,再反射调用Java原方法来实现Hook的目的,流程图如下所示:

2.2.2 Epic框架

Epic框架则是在Dexposed的基础上,增加了对ART虚拟机Hook的能力。由于ART虚拟机的复杂性(AOT和JIT),Java代码执行的入口可能随时都在变化,如果通过ArtMethod中的entry_point_from_quick_compiled_code_字段入口进行Hook,可能会发生不可预期的崩溃。Epic则是在 Wißfeld, Marvin 的论文ArtHook: Callee-side Method Hook Injection on the New Android Runtime ART基础上做了实现,大概思路是把entry_point_from_quick_compiled_code_指向的机器码地址(未编译的字节码也会强制编译成机器码,类似于Inline Hook)进行修改,跳转到跳板代码,然后通过跳转代码进行分发,调用Hook方法之后再调用原方法,来达到Hook的目的。流程图如下:

2.3 常见框架对比

通过分析和对比可知,开源框架存在比较典型的几个问题如下:

03 方案选型

从现有状况来看,如果同时需要Java/Native Hook的能力,那么至少需要集成两个框架,业务代码也只能在主包中编写,增加包体积。其次如果替换使用更加优秀或者自研的框架时,所有的业务代码也要跟着修改,学习和适配兼容的成本巨大。最后Hook框架导致的崩溃,因为没有动态能力和容灾机制也只能重新发布应用和铺渠道,影响用户体验。

虽然每个框架都有各自的一些问题,但是要求我们从头开始开发一款同时支持Java和Native Hook的框架,没有稳定性问题并且兼容所有安卓版本、轻量且容灾的框架,重复造轮子并且ROI太低,所以我们要开发自己的一套容器框架,取长处补短板,充分利用好已有的框架来实现目标。百度App作为超级App,本身就是一个航空母舰,容器框架要在其上线至少需要达到以下几点要求:

04 Thor揭秘

为了满足上述要求,我们开发了Thor容器框架,提供标准的Hook接口,包含Java和Native Hook接口,业务方不需要关心底层实现逻辑(如同虚拟文件系统VFS),只需要了解如何使用这些接口即可,极大的降低学习接入成本。同时将稳定的开源框架按照接口适配成插件,将这些Hook能力进行抽象,按需动态的安装加载,保证Hook能力的完备性和轻量性。并且后续出现更加优秀以及自研的框架的可以无缝的接入,对上层也是无感知的,不需要上层业务代码和插件进行适配,很好的保证了兼容性。

4.1 Thor整体结构

4.1.1 Thor架构图

Thor实现层插件和Thor应用层插件都是apk的形式存在,但是也可以以组件源码的形式集成打包到宿主中。

4.2 Thor核心优势

4.2.1 易用性

Thor只开发抽象层接口,底层实现对业务是不可见的,不需要反复学习,这样最大程度的保证了易用性。Java/Native Hook都提供了标准的接口供业务方使用,接口如下:

public interface IHookEntity {
    ......
    /**
     * Hook指定的方法
     *
     * @param hookedMethod 待Hook的方法Method对象
     * @param hookCallback Hook回调{@link IHookCallback}
     */
    void hookMethod(Member hookedMethod, IHookCallback hookCallback);

    ......
}

如果是Java Hook使用方只需要直接使用该接口的能力即可;如果是能力提供方,则需要将Java Hook能力注入到Thor抽象层的Java Hook接口实现中。

struct thor_abstract {
    // 函数定义:PLT Hook实现框架的函数指针
    // lib_name            被Hook的so库的名称
    // symbol              被Hook的函数名
    // hook_func           Hook方法
    // backup_func         原方法备份(函数指针)
    int (*thor_plt_hook)(const char *lib_name, const char *symbol, void *hook_func, void **backup_func);
    // 函数定义:Inline Hook实现框架的函数指针
    // target_func         原方法
    // hook_func           Hook方法
    // backup_func         原方法备份(函数指针)
    int (*thor_inline_hook)(void *target_func, void *hook_func, void **backup_func);
    // PLT Hook二期(新增接口,支持批量plt hook)
    struct thor_plt_ext *plt_ext;
};

如果是Nava Hook使用方只需要直接使用该接口的能力即可;如果是能力提供方,则需要将Nava Hook能力注入到Thor抽象层的Native Hook接口实现中。


public abstract class ThorModule implements IThorModule {
    /**
     * 调度插件的加载生命周期
     */
    public abstract void handleLoadModule();

    /**
     * 宿主通知和更新插件配置信息生命周期
     */
    public void onPluginFuncControl(PluginFuncInfo pluginFuncInfo) {
    }
}

主要提供给业务模块使用,如果需要使用Hook能力,直接在handleLoadModule子类实现中调用Thor的各个Hook能力即可(不是必须使用的,Thor作为容器框架只是额外提供了Hook的能力而已)。

4.2.2 完备性

该框架同时支持Java / Native Hook的能力,具有完备的Hook能力。上小节讲解了提供给业务方的Java/Native Hook和 Thor Module业务模块等抽象层接口,底层实现则根据接口进行适配之后,通过静态代码依赖注入或动态模块加载注入到抽象层实现中,这样Thor就具备了完备的Hook能力。

Hook Handler#dispatchMessage方法,代码如下:


ThorHook.findAndHookMethod(Handler.class, "dispatchMessage", new IHookCallback() {
    @Override
    public void beforeHookedMethod(IHookParam param) {
        Message msg = (Message) param.getArguments()[0];
        Log.d(TAG, ">>>>>>>>>>>dispatchMessage: " + msg);
    }

    @Override
    public void afterHookedMethod(IHookParam param) {
        Log.d(TAG, "<<<<<<<<<<<<dispatchMessage: ");
    }
}, Message.class);

继续看Thor#findAndHookMethod的逻辑,代码如下:

/**
  * 寻找方法并将其Hook,最后一个参数必须是Hook方法的回调
  *
  * @param clazz          Hook方法所在类的类名称
  * @param methodName     Hook方法名
  * @param hookCallback   回调{@link IHookCallback}
  * @param parameterTypes Hook方法的参数类型
  */
public static void findAndHookMethod(Class<?> clazz, String methodName,
                                     IHookCallback hookCallback, Class<?>... parameterTypes) {
    ......                                 
    Method methodExact = ThorHelpers.findMethodExact(clazz, methodName, parameterTypes);
    hookMethod(methodExact, hookCallback);
    ......
}

ThorHook#findAndHookMethod通过类的类类型、函数名和参数,找到相应的Method,再调用ThorHook#hookMethod进行Hook,继续看如下代码:


/**
 * Hook指定的方法
 *
 * @param hookedMethod 待Hook的方法Method对象
 * @param hookCallback Hook回调{@link IHookCallback}
 */
public static void hookMethod(Member hookedMethod, IHookCallback hookCallback) throws HookMethodException {
    ......
    CallbacksHandler callbacksHandler;
    synchronized (sHookedMethodCallbacks) {
        callbacksHandler = sHookedMethodCallbacks.get(hookedMethod);
        if (callbacksHandler == null) { // 未Hook过的Method
            callbacksHandler = new CallbacksHandler();
            callbacksHandler.register(hookCallback);
            sHookedMethodCallbacks.put(hookedMethod, callbacksHandler);
        } else { // Hook过的Method,只需要注册回调即可
            callbacksHandler.register(hookCallback);
            return;
        }
    }

    ThorManager.getInstance().getHookEntity().hookMethod(hookedMethod, callbacksHandler);
}

多个业务方如果Hook了同一个java 方法,会被加到缓存中,Hook回调的时候再逐个进行分发;继续可以看到hookMethod最后调用到了getHookEntity#hookMethod方法,最终会调用到具体Java Hook框架实现的hookMethod方法,例如Epic的适配代码如下:


/**
 * Epic框架适配类
 */
public class EpicHookEntity implements IHookEntity {
    @Override
    public void hookMethod(Member hookedMethod, final IHookCallback hookCallback) {
        // Epic Hook方法回调
        XC_MethodHook xc_methodHook = new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                // 将Epic Hook方法信息包装成抽象层Hook方法信息
                IHookParam hookParam = new EpicHookParam(param);

                if (hookCallback != null) {
                    // 调用before回调
                    hookCallback.beforeHookedMethod(hookParam);
                }
            }

            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                // 将Epic Hook方法信息包装成抽象层Hook方法信息
                IHookParam hookParam = new EpicHookParam(param);

                if (hookCallback != null) {
                    // 调用after回调
                    hookCallback.afterHookedMethod(hookParam);
                }
            }
        };

        // Epic Hook Method
        DexposedBridge.hookMethod(hookedMethod, xc_methodHook);
    }
}

使用PLT Hook 对应SO所在PLT表的open函数,Inline Hook puts方法,部分代码如下:


thor_abstract *impl = reinterpret_cast<thor_abstract *>(nativePtr);

// plt hook open
thor->thor_plt_hook(so_name, "open", (void *) ProxyOpen, (void **) &original_open);

// inline hook puts
impl->thor_inline_hook((void *) puts, (void *) new_puts, (void **) &origin_puts);

根据4.2.1中的Native Hook接口可知,thor_plt_hook和thor_inline_hook成员都是函数指针,指针只有指向真正的Native Hook能力,代码才会生效,所以相应的Hook框架也需要根据Native Hook接口进行适配,例如xHook适配PLT Hook部分代码如下:


thor_abstract *thor = reinterpret_cast<thor_abstract *>(nativePtr);
// plt hook函数指针赋值
thor->thor_plt_hook = xhook_impl_plt_hook;
.....

// xhook适配部分代码
int xhook_impl_plt_hook(const char *so_name, const char *symbol, void *new_func, void **old_func) {
    void *soinfo = xhook_elf_open(so_name);
    if (!soinfo) {
        return -1;
    }

    if (xhook_hook_symbol(soinfo, symbol, new_func, old_func) != 0) {
        return -2;
    }

    xhook_elf_close(soinfo);
    return 0;
}

Android-Inline-Hook适配Inline Hook接口部分示例代码如下:


// inline hook函数指针赋值
thor->thor_inline_hook = impl_inline_hook;

// andorid-inline-hook适配部分代码
int impl_inline_hook(void *target_func, void *new_func, void **old_func) {
    if (registerInlineHook((uint32_t) target_func, (uint32_t) new_func, (uint32_t **) old_func)) {
        return -1;
    }

    if (inlineHook((uint32_t) target_func) != ELE7EN_OK) {
        return -2;
    }

    return 0;
}

我们在使用这些底层Hook框架适配组件(插件)的过程中,也遇到了一些问题,例如Epic在Hook Handler#dispatchMessage的过程中,会发生不符合预期的崩溃,但是在进一步调研了SandHook可以解决该问题之后,马上就适配了SandHook的实现来解决问题,业务方的代码不需要做任何修改和适配,再例如xHook的作者新写了一款PLT Hook框架bHook,解决了xHook的一些问题(例如增量Hook,unHook能力等等),我们也很快跟进对bHook框架进行了调研和适配,同样业务方也是无感知的,这两个例子从侧面佐证了Thor容器框架具有良好的兼容性和可扩展性。

同时同学们可能会有如下疑惑,如果Hook框架出问题,难道只能去找更好的开源方案进行适配吗?有没有银弹呢?这其实就回到了方案选型时所说的,由于安卓的碎片化和复杂性,从头开始开发一款同时支持Java和Native Hook的框架,没有稳定性问题并且兼容所有安卓版本、轻量且容灾的框架,重复造轮子并且ROI太低,所以我们要开发自己的一套容器框架,取长处补短板,充分利用好已有的框架来实现目标,当然也不排除在所有开源方案都不满足的情况下,进行深度二次开发或者自研底层Hook框架,不过这些对业务代码都不可见,不需要修改适配。

4.2.3 轻量动态性

百度App作为一个航母级应用,对于包体积大小还是比较敏感的,根据Google Store的数据,包体积每增加6M,就降低1%的转化率,影响巨大,所以Thor容器框架要尽可能的做到轻量,基于此,我们需要把业务代码做成动态加载的插件,甚至是底层适配的Hook实现也要做成动态可加载的插件。

业务代码可以不在宿主中编写,只在插件代码中编写,然后将生成的插件动态下发到手机上,再通过插件加载模块动态加载生效。例如:在需要监控应用IO的情况下,下发IO插件和xHook插件到手机上安装,Hook IO操作(例如:open、read、write等),将不合理的IO操作上报给平台,同时在不需要监控的时候动态卸载关闭即可。

插件动态加载生效的大致流程如下:

/**
 * 先加载Thor插件中的类,再加载宿主中的类
 *
 * @param name
 */
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    Class<?> clazz = null;
    try {
        // 加载插件中的类
        clazz = findOwnClass(name);
    } catch (ClassNotFoundException ignored) {
        // ignored
    }

    if (clazz != null) {
        return clazz;
    }

    // 加载宿主中的类
    return mHostClassLoader.loadClass(name);
}
  1. 如果是Java Hook接口实现类,则注入Java Hook实例能力给Thor抽象层;
  2. 如果是Native Hook接口实现类,则注入Native Hook实例能力给Thor抽象层;
  3. 如果是Thor Module业务接口实现类,则将业务实例存储到map中,等待后续插件管理模块调度相应的生命周期。

大概流程图如下:

这里大家可能会有以下疑问:

Thor有一个pending模式会等到实现层安装生效之后,业务层的逻辑再开始执行生效;

通过翻看源码,Binder调用中的Parcel类拥有Native对象的内存指针,所以我们也借鉴相同的方法,将Native对象内存指针地址通过Java层进行传递,然后使用拥有相同内存布局的struct结构体进行转换,这样就可以拿到Native Hook实现了。

4.2.4 容灾性

Hook技术毕竟是一个比较hack的技术,谁也无法保证百分百的兼容和稳定性,所以我们要针对这种特殊的崩溃情况进行兜底,将该框架可能造成的影响降到最低。目前有三个容灾能力:

通过这三个容灾能力,基本能够保证百度App不会因为Thor容器框架发生大规模的崩溃影响用户体验,能够较好的管理风险。GEEK TALK

05 业务实践案例

Thor框架作为一套动态插件容器基础设施,真正让其起作用的是丰富的插件生态(如IO、内存、线程、隐私等等),可以根据实际需要,大胆的发挥想象,开发适合业务场景的插件。目前该框架可以应用于线下RD开发、线下流水线和线上云控开启,由于篇幅限制,摘选其中一些案例讲述。

5.1 线程插件

由于在开发过程中随手就可以创建一个线程运行,也没有强制约束使用线程池,这样会导致很多游离线程,线程过多不仅会提高内存使用导致IO放大和OOM崩溃,并且有频繁的上下文切换会导致启动和流畅度问题。线程插件则通过Thor框架的PLT Hook能力Hook libart.so库中的pthead_create的函数,来监控线程的创建。核心代码如下:**

// 原始被方法函数指针
static void *(*origin_pthread_create)(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg) = NULL;

// Hook之后的Proxy方法
void *proxy_pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg) {
    ......

    // 调用原始方法
    void *ret = origin_pthread_create(thread, attr, start_routine, arg);
    // 打印Java堆栈
    printJavaStackTrace();
    return ret;
}

void doHook(long nativePtr){
    thor_abstract *impl = reinterpret_cast<thor_abstract *>(nativePtr);
    // plt hook pthread_create
    impl->thor_plt_hook("libart.so", "pthread_create", (void *) proxy_pthread_create, (void **) &origin_pthread_create);}
}

Hook完成之后,会在创建线程的过程中先调用 proxy_pthread_create 的代理方法再调用原始的创建线程方法,在代理方法中通过反射打印创建当前线程的Java堆栈。在百度App启动阶段通过线程插件监控记录发现有100+个SP线程,和50+非线程池管理的线程,严重影响启动速度和用户体验。协助组内同学进行优化后(SP迁移KV组件,所有线程通过线程池管理),降低启动过程中线程数100+,优化TTI(Time To Interactive) 1s+。

5.2 IO插件

由于在开发过程中有同学会把一些IO操作在主线程中执行,例如文件读写、网络传输,这样会导致主线程卡顿,影响启动速度、流畅度等,即使是小文件也可能因为内存不足和磁盘不足等原因导致IO读写放大,从而导致长耗时的IO,同时还有一些不合理的IO,例如:读写buffer过小会导致频繁的系统调用影响性能,以及重复读同一个文件等。IO插件则通过Thor框架的 PLT Hook能力 Hook IO操作(open、read和write、close等),用来记录监控主线程不合理的IO。核心代码如下:

......
thor->thor_plt_hook(so_name, "open", (void *) ProxyOpen, (void **) &original_open);
thor->thor_plt_hook(so_name, "read", (void *) ProxyRead, (void **) &original_read);
thor->thor_plt_hook(so_name, "write", (void *) ProxyWrite, (void **) &original_write);
thor->thor_plt_hook(so_name, "close", (void *) ProxyClose, (void **) &original_close);
......

调用open时会先调用ProxyOpen,ProxyOpen中会存储fd(文件描述符)和IOInfo的map映射关系,后续的ProxyRead、ProxyWrite和ProxyClose则通过fd来完善IOInfo的信息,IOInfo部分字段如下:


class IOInfo {
    public:
        // 文件路径
        const std::string file_path_;
        // 开始时间
        int64_t start_time_μs_;
        // buffer大小
        long buffer_size_ = 0;
        // 连续读写次数
        long max_continual_rw_cnt_ = 0;
        // 文件大小
        long file_size_ = 0;
        // 总耗时
        long total_cost_μs_ = 0;
};

在最后文件Close的时候通过分析IOInfo即可分析出不合理的IO操作(例如主线程IO耗时过长、IO的buffer过小(导致系统调用增多)、重复读等)。在百度App启动过程中通过IO插件监控记录发现有20+不合理的IO操作,与各个业务方的同学进行协同和优化,最终启动速度TTI优化400ms+,提升了用户体验。

5.3 隐私合规插件

由于个人信息法的颁布,应用不可以在隐私弹窗确认前获取用户个人信息,基于此,隐私合规插件使用Thor框架的Java Hook的能力,监控记录隐私弹窗前不合理的隐私API调用(例如定位、WI-FI、蓝牙等等),部分代码如下:


// hook getDeviceId
ThorHook.findAndHookMethod(TelephonyManager.class, "getDeviceId", new IHookCallbackImpl(), String.class);

隐私合规插件结合了手百内部通用防劣化流水线的能力(这里不展开讲解),每天自动编译打包内置隐私合规插件,然后自动在真机上测试,监控记录隐私弹框前的隐私问题,最后自动分析、分发问题卡片给相应的业务同学进行修改,有效的规避了合规风险,防止被下架整改;

5.4 内存插件

内存优化是性能和稳定性优化中的一大课题,如果内存占用过大,轻则导致频繁GC造成卡顿,重则内存溢出导致OOM应用崩溃。内存插件则通过Thor框架PLT Hook的能力,监控记录Java堆内存和Native内存(监控 malloc 和 free等函数)。内存插件目前有两个使用场景:

06 总结

Hook这个话题由来以久,框架种类繁多,但是没有一款全面性、动态性以及兼容性好的框架,但是正是有这些优秀的框架(Xposed、Dexposed、Epic、xHook等),我们才能学习和借鉴其优秀的设计和理念,补齐不足,Thor只是在这条道路上迈出了一小步,后面需要更加完善和夯实Thor基础设施,并且丰富插件生态,在Android性能和稳定性治理上添砖加瓦。

相关链接:

[1] Dexposed链接:https://github.com/alibaba/dexposed

[2] ArtHook论文链接:http://publications.cispa.saarland/143/

[3] Epic链接:https://github.com/tiann/epic

[4] xHook链接:https://github.com/iqiyi/xHook

[5] Android-Inline-Hook链接:https://github.com/ele7enxxh/Android-Inline-Hook

[6] Tailor链接:https://github.com/bytedance/tailor

[7] Matrix链接:https://github.com/Tencent/matrix/

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8