Android 中的指令修改与指令删除(基于 AArch64)

近况

呜呼!天气也是越来越冷了,不知不觉也快到年末了。我在回顾一些技术分享的时候,刚好看到几个大厂们常"玩"的概念,指令修改指令擦除。 比如抖音基础技术里面就有介绍到,抖音在进行一些性能优化的时候,通过一些手段进行了指令集的修改,从而达到了一些优化的目的。

针对架构指令级别的"核武器"

在认识指令修改指令擦除之前,我们来一段非常简单的C代码

csharp 复制代码
void callee(int num){
    __android_log_print(ANDROID_LOG_ERROR, "mooner", "%s %d", " 我是callee num is",num);
}

void caller(){
    callee(1);
}

当我调用caller的时候,会传递一个数值1作为参数调用callee函数,从而打印了本次的一个数值。这段代码即是没有学过C语言的同学,应该都能够看懂。

那么我们来反思一下,这一串代码,背后的逻辑是什么?我们都知道Android可以运行在ARM架构与X86等架构平台之上,手机上CPU也基本是ARM架构。那么CPU会认识C语言吗?显然不会,它不认识Java,也不认识C语言。因此运行在CPU上的,一定是CPU认识的指令集。ARM架构中有很多架构分类,每个架构也有着其运行的状态,比如ARMv8 上面有着64位运行状态AArch64 与 32位运行状态AArch32

通常,我们会在APP项目中gradle配置支持的架构,其实意义就在这里

当然,现在市面上大部分的手机CPU架构,基本都是基于ARMv8架构,运行在64位运行状态,即我们写的C语言或者Android本身的so,会被翻译成符合A64的指令集。

那么,我们写的caller函数,会被编译器翻译成符合A64指令集的汇编代码,我们可以通过objdump这个ndk工具,帮助我们去查看

复制代码
objdump -d xxx.so 

objdump工具包伴随着NDK的下载就带有

我们可以通过把caller函数打包成so,最后查看编译的指令集如下:

需要注意的是,不同的指令集中,生成的指令代码也不一样,比如上图抖音ppt中,就是thumb指令集,即满足aarch32状态的指令集。而我们的是满足aarch64状态的64位指令集,也符合大部分手机。

那么怎么看汇编代码呢?首先我们要建立一个前提,上面一行代码对应着指令,指令由操作码+若干的操作数组成

比如常见的stp指令,其实就是把两个寄存器异常压入操作数中所给的地址中,比如

在aarch64状态中X代表着一个64位寄存器,W也代表着同一个寄存器,只是它只有32位被使用。在A64中,一共有31个通用寄存器,他们会根据一个调用规范(AAPSC64),各司其职。

理解了这些基本概念之后,大家就可以去找ARM的文档,查看各个指令的含义了,下面我们来简单解析我们的caller函数的汇编代码的含义

解析caller函数汇编代码

  1. stp x29,x30 [sp,#-16]! :这条指令的操作码是stp,含义就是存储x29,x30 的内容到sp-16(压栈)的地方(这里面!代表着先计算,有兴趣的小伙伴可以多了解一下A64的调用,我这里就不再细节讲了)

  2. mov x29,sp:就是把当前sp的值赋值给x29,即FP寄存器,这样我们后续做FP回溯拿堆栈的时候,就会带上了,这也是为什么aarch64状态能够用fp回溯,而aarch32默认不行的原因了(默认可以不遵守fp寄存器的调用规范)

  3. mov w0,#0x1 :接着就是把1这个是数,赋值给w0寄存器(x0寄存器同一个,只是它使用了32位),还记得我们调用callee函数吗,传递的数值正是1,又根据上图我们看到的AAPSC64调用规范,第一个参数是不是由X0(W0)寄存器负责存放呀?没错,这里是不是就对上了。

  4. bl 6460callee@plt :这里就是跳转到了callee函数的got表地址,callee函数会通过got/plt那一套解析,最终在got表找到真正的callee函数(got/plt hook的知识有说到噢)

  5. ldp x29,x30,[sp],#16 :bl指令会把下一条指令PC值保存到LR寄存器中;即执行完callee@plt会回到下一条指令,下一条就是ldp这一条指令。这里就是跟stp一一对应,把sp的数值依次赋值给x29与x30,然后sp = sp+16,其实就是函数结束了,要把栈空间给腾回来。 6.ret :结束调用,其实相当于 b LR 寄存器这个指令一样,跳转到LR寄存器的内容地址以执行下一条指令

至此,我们就结束了整个函数的汇编代码分析,当然,里面涉及很多细节我们还是没有说的,大家可以多参考官网指令集进行更多了解,见AArch64官网

指令修改

上文中,我们针对caller函数的汇编代码进行了分析,下面我们来进行指令修。我们不改动caller函数源代码情况下,把传入callee的参数从1变成2,这究竟要怎么做呢?

很多情况下,我们都不能修改到源代码,因此这个例子是有实际工程意义的,比如修改某个参数为固定某个值避免crash等等,在性能优化中都会用到。

我们从上文分析了caller函数的汇编代码,我们发现,caller调用callee函数进行参数赋值是,整数通过W0寄存器进行复制的,内容就是1

dart 复制代码
void callee(int num){
    __android_log_print(ANDROID_LOG_ERROR, "mooner", "%s %d", " 我是callee num is",num);
}

如果我们能够修改这个参数为2,是不是就实现了我们的目的!我们只需要把mov w0,#0x1 变成 mov w0,#0x2 即可,前面的指令保持不变。

那么我们怎么应用呢?一个函数的指针,所指向的内容,就是具体的汇编指令,指令以16进行的形式存储,我们看看修改mov指令后的16进程代码是什么,然后赋值过去就可以了,这里有一个小工具,大家可以编写汇编代码然后转换为16进制

汇编转换16进制

使用后我们就拿到了符合ARM64的指令码,这个时候我们只需要替换前面12字节的数据就可以了

替换前:

替换后:

替换的时候,我们可以通过memcpy的方式,把我们的汇编代码替换原本的汇编代码,值得注意的是,默认代码段是不可写的,因此我们需要调用mprotect赋予可读可写权限(按照页为单位),同时修改完成之后,还别忘了清除指令缓存

这个时候我们调用以下代码,在手机中可以看到

scss 复制代码
caller();
hook();
caller();

很棒,调用callee的函数输入就变成了2,符合我们的预期

这个就是指令修改的一个小实现,实际情况下,我们可以通过这种方式,去修改我们想要的指令。inline hook 相关其实也是指令修改的实现之一,只不过它属于嵌入跳转指令。修改指令的前提是我们要对指令集足够了解。

指令删除

学习完指令修改之后,我们学习指令删除就比较轻松了。比如我们想要调用caller函数的时候,不去调用callee函数,那么我们怎么实现呢?

还是回到caller函数的汇编代码,我们看到跳转callee函数的代码,其实是通过bl 6460callee@plt 的方式去实现的,因此这个案例中,想要不去执行callee函数,我们把bl指令删除即可。当然,这里需要注意的是,因为调用callee函数是没有副作用的,即没有修改到其他依赖,因此我们可以直接把这条指令删除,实际情况下,我们还要考虑多种情况,比如bl指令下一条指令是否收到影响等等。

这里比较简单,我们只需要删除bl指令或者在bl指令替换成NOP指令,就能够完成我们的目的了,同样的,我们写上汇编代码,只需要替换bl指令为NOP指令(1F2003D5),

scss 复制代码
void hook(){
    uintptr_t pv = (uintptr_t)caller;
    uintptr_t pu = (pv | (PAGE_SIZE - 1)) + 1u;
    uintptr_t pd = (pv & ~(PAGE_SIZE - 1));
    mprotect((void *) pd, pv + 8u >= pu ? PAGE_SIZE * 2u : PAGE_SIZE,
             PROT_READ | PROT_WRITE | PROT_EXEC);

    memcpy((void *const) caller, "\xFD\x7B\xBF\xA9\xFD\x03\x00\x91\x20\x00\x80\x52\x1F\x20\x03\xD5\xFD\x7B\xC1\xA8\xC0\x03\x5F\xD6", 24);
    __builtin___clear_cache((void *)PAGE_START(pv), (void *)PAGE_END(pv));
}

调用hook函数

scss 复制代码
caller();
hook();
caller();

__android_log_print(ANDROID_LOG_ERROR, "mooner", "%s", " caller 完成");

当然,本例子中可以这么简单,因为caller和callee函数足够简单,实际复杂任务上,还有考虑进行指令的补齐等等操作,我们可以具体案例具体分析~

总结

通过本文,我们了解到了指令修改与指令删除的基本实现,这些方式在性能优化中或者解决一些疑难杂症时有着奇效,因此也出现在很多国内大厂的方案中。我们了解了这些之后,才能更好的读懂分享出来的方案,从而落地到自己项目当中!

Android中涉及的技术栈可以很深也可以很广,看到这里,说明你已经很棒了!加油吧~

相关推荐
雨白13 小时前
Android 快捷方式实战指南:静态、动态与固定快捷方式详解
android
hqk13 小时前
鸿蒙项目实战:手把手带你实现 WanAndroid 布局与交互
android·前端·harmonyos
LING14 小时前
RN容器启动优化实践
android·react native
侑虎科技16 小时前
在UE5中,预测脚步IK实现-PredictFootIK
性能优化·unreal engine
恋猫de小郭17 小时前
Flutter 发布官方 Skills ,Flutter 在 AI 领域再添一助力
android·前端·flutter
Kapaseker1 天前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴1 天前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
恋猫de小郭1 天前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
冬奇Lab1 天前
PowerManagerService(上):电源状态与WakeLock管理
android·源码阅读
BoomHe2 天前
Now in Android 架构模式全面分析
android·android jetpack