案例 | 奇怪,为什么Hook不生效?

最近有一位大厂的兄弟联系我,说他们通过代理替换的方式hook方法时,有的可以生效,有的就不行,这让他百思不得其解。关于代理替换来实现hook的这种方式,可以参考维术的这篇文章,简言之,就是用一个新的对象来替换原有对象,而新的对象所属的类是我们通过静态或动态代理人为构造的类,这样一来,后续通过这个对象开展的方法调用就会进入到人为构造的类中。

问题

下面我们来看看这个问题的具体描述,为了聚焦问题,代码已做精简。

目标是替换Choreographer中的mHandler字段,hook它的dispatchMessagesendMessageAtTime方法。

public final class Choreographer {
    ...
    private final FrameHandler mHandler;
    ...
    private final class FrameHandler extends Handler {
        public FrameHandler(Looper looper) {
            super(looper);
        }
        @Override
        public void handleMessage(Message msg) {
            ...
        }
    }
    ...
}

为了实现这个目标,构造一个新类CustomHandler继承于Handler,然后实例化CustomHandler对象来替换Choreographer中的mHandler字段。修改完后,在release包里运行时出现了神奇的一幕:dispatchMeassage中的log会打印,但sendMessageAtTime中的log却没有打印;可是如果切换到debug包,两个方法的log又都会打印。

public class CustomHandler extends Handler {
    @Override
    public void dispatchMessage(Message msg) {
        Log.i(TAG, "dispatchMessage is called.");
        super.dispatchMessage(msg);
    }
    @Override
    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        Log.i(TAG, "sendMessageAtTime is called.");
        return super.sendMessageAtTime(msg, uptimeMillis);
    }
}

首先怀疑是不是sendMessageAtTime没有被调用到,增加调试信息后这个怀疑被排除,以下是该方法被调用的一个位置:

public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
        VsyncEventData vsyncEventData) {
    ...
    Message msg = Message.obtain(mHandler, this);
    msg.setAsynchronous(true);
    mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
    ...
}

如果是这个情况的话,那么问题只可能出在底层,因为上层的使用找不出纰漏。

侦探

像Handler和Choreographer这种framework中的代码,大部分都会提前编译,最终形成boot-framework.oat文件,在zygote启动时被加载进来。因此我们可以通过oatdump boot-framework.oat拿到Choreographer$FrameDisplayEventReceiver.onVsync方法的DEX字节码和编译后的机器码。为了简化讨论,下面只截取最关键的信息。

void android.view.Choreographer$FrameDisplayEventReceiver.onVsync(long, long, int, android.view.DisplayEventReceiver$VsyncEventData) (dex_method_idx=27333)
DEX CODE:
  0x0074: 6e40 ce6a 4576            | invoke-virtual {v5, v4, v6, v7}, boolean android.view.Choreographer$FrameHandler.sendMessageAtTime(android.os.Message, long) // method@27342
CODE: (code_offset=0x006c0710 size=904)...
  0x006c09f4: aa0003e2  mov x2, x0
  0x006c09f8: aa0203fc  mov x28, x2
  0x006c09fc: aa1503e1  mov x1, x21
  0x006c0a00: aa0303fd  mov x29, x3
  0x006c0a04: b0ff8520  adrp x0, 
  0x006c0a08: 910e8000  add x0, x0, 
  0x006c0a0c: f9400c1e  ldr lr, [x0, 
  0x006c0a10: d63f03c0  blr lr
    StackMap[23] (native_pc=0x6c0a14, dex_pc=0x74, register_mask=0x14600000, stack_mask=0b10000010000000000000000000000000000000000)
      v0:#8 v1:#0 v2:r27 v3:r27/hi v4:r28 v5:r21 v6:r29 v7:r29/hi v8:r22 v9:r23 v10:r23/hi v11:r24 v12:r24/hi v13:r25 v14:r26

DEX字节码的指令为invoke-virtual,表明调用的sendMessageAtTime是virtual method。按照常规的理解,虚方法的调用需要通过vtable来找到具体实现,而vtable的查找则依赖对象所属的类。如果是这样的话,hook没理由不生效,因为对象所属的类已经被修改了。

可是机器码和我们的认知并不吻合:

adrp x0, #-0xf5b000 (addr -0x89b000)
add x0, x0, #0x3a0 (928)
ldr lr, [x0, #24]
blr lr

下面针对这四条指令做些详细的解释。

  1. 首先将该指令的地址按页(4KB)向下对齐:指令地址为0x6c0a04(文件内的相对地址,并非加载到内存里的绝对地址),4KB向下对齐后为0x6c0000。接着和立即数-0xf5b000相加:0x6c0000 + (-0xf5b000) = -0x89b000。最后将这个结果写入x0寄存器。它的真实目的是将boot-framework.oat文件向前偏移0x89b000的地址写入x0。那么这个文件向前偏移0x89b000指向什么呢?实际上它指向的是boot-framework.art的内存空间。.oat.art文件不同,.oat里保存的主要是机器码,而.art保存的主要是Class、ArtMethod、ArtField这样的数据。
  2. 将x0加上0x3a0,结果依然保存到x0中。这么做的目的是找到Handler类中sendMessageAtTime对应的ArtMethod的起始地址。-0xf5b000和0x3a0都是dex2oat编译期间根据内存布局计算而得的。
  3. 将x0加上24得到一个新的地址,取出该地址处的值,存入lr寄存器中。这么做的目的是取出ArtMethod的entry_point_from_quick_compiled_code_,因为该字段在ArtMethod内部的偏移就是24。
  4. 跳转到entry_point_from_quick_compiled_code_指向的地方去执行,这么做的目的是进行方法调用。

综合上面的分析可以看出,获得ArtMethod并不需要对象的类参与计算,也不需要vtable。这就是hook无法生效的原因,因为ArtMethod早已固定,根本不受代理对象的影响。可这与我们对虚方法的理解并不一致,若想彻底弄清楚这背后的原因,还得深入到dex2oat的源码里。

原因

代码编译环节,如果能在编译期间确定所调用的虚方法的最终实现,那么就可以将它转成static/direct(这二者处理方式一致)来处理,这算是编译器的某种优化(属于Sharpening优化的一部分)。具体的调用路径如下,感兴趣研究源码的兄弟可以查看这几个函数。

调用逻辑.png

这项优化由下面这笔改动引入,按理说Android 12及之后的版本都会受到影响。

优化的CL.png

Devirtualize以后,HInvokeStaticOrDirect所生成的机器码并非只有一种,它取决于方法所在的位置。通常来说有三种情况,如下图所示。

SharpenLoadMethod.png

第一种方式适用于代码位于Boot Image,且方法也位于Boot Image的情况;第二种方式适用于代码位于app,但方法位于Boot Image的情况;第三种方式适用于ArtMethod不位于Boot Image的情况,这些方法一般是运行时生成的。Choreographer$FrameDisplayEventReceiver.onVsync生成的机器码位于boot-framework.oat,而它调用的Handler.sendMessageAtTime(由于Choreographer$FrameHandler没有override该方法,所以最终调用的是父类的方法)所对应的ArtMethod位于boot-framework.art中,二者同属于Boot Image,所以适用于第一种情况,因此最终生成的机器码也如上文所述。

那么什么样的invoke-virtual会被devirtualize成invoke-static/direct呢?上文只提到了“如果能在编译期间确定所调用的虚方法的最终实现”,那么具体情况又是如何呢?

判断是否可以被devirtualize的逻辑位于FindVirtualOrInterfaceTarget函数,其中逻辑较多,最为常见的是该方法或者方法所属的类是否声明为final。如果声明为final,则表明方法不会被override,因此不会存在子类重新实现的情况,也就可以在编译期间确定invoke-virtual最终的调用目标。回到之前的源码,我们可以发现Choreographer$FrameHandler被声明为final,因此符合devirtualize的条件。

private final class FrameHandler extends Handler

行文至此,其实还有两个问题没有解释。

一个是dispatchMessage为什么可以hook成功?该方法主要调用的地方是Looper.loopOnce。查看它的机器码,可以发现这里采用的是vtable的方式,因此代理对象的类会参与ArtMethod的查询过程。

DEX CODE:
0x00a7: 6e20 8fa9 d500              | invoke-virtual {v5, v13}, void android.os.Handler.dispatchMessage(android.os.Message) 
CODE:
ldr w0, [x1]
ldr x0, [x0, #224]
ldr lr, [x0, #24]
blr lr

上面的机器码中,ldr w0, [x1]是从对象中取出class,ldr x0, [x0, #224]是从vtable中取出具体的ArtMethod指针。这里省去vtable pointer的获取,是因为vtable被embedded到class的尾部,如下所示。

EmbeddedVtable.png

那么这里为什么没有被devirtualize呢?原因是Message里的target字段属于Handler类,Handler并非final,且dispatchMessage也没有被声明为final,因此这个方法的具体实现在编译期间无法确定,只能等到运行时才知道。

[loopOnce]
msg.target.dispatchMessage(msg)
[Message.java]
/*package*/ Handler target

第二个问题是为什么debug包里sendMessageAtTime也可以hook成功?原因是debuggable条件下,程序会走解释执行,因此AOT里的优化不起效果。

优化评估

采用vtable方式的invoke-virtual生成出来的机器码有三条load指令,而devirtualize成static/direct方式的机器码只有一条或两条load指令。更少的连续且独立的load指令意味着更好的性能,尤其当这些数据不在cache中的时候。

后记

通过代理替换的方式来hook虚方法,应该是不少App都会采用的方式。但随着虚拟机优化策略的调整,这种方式在某种条件下可能会失效。对于方法hook而言,替换ArtMethod的entrypoint会有更广的覆盖面,我之前的文章有过介绍,但它也会受到JIT的影响。所以世间难得万全法,最终还是回到那八个字:具体情况,具体分析。

阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/archives/22579,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?