写点什么

只要进程我复活的足够快,系统它就杀不死我!Android 最强保活黑科技的最强技术实现

用户头像
Android架构
关注
发布于: 刚刚

学习视频先行!学习视频与资料下载地址:https://shimo.im/docs/VW6gJxVvwGjtWtcv 复制即可获取!


前言

今天来跟大家分享 TIM 最强保活思路的几种实现方法。这篇文章我将通过 ioctl 跟 binder 驱动交互,实现以最快的方式唤醒新的保活服务,最大程度防止保活失败。同时,我也将跟您分享,我是怎么做到在不甚了解 binder 的情况下,快速实现 ioctl binder 这种高级操作。


声明:现在这个保活方式在 MIUI 等定制 Android 系统中已经不能保活,大部分时候只能活在模拟器中了。但对与我们的轻量定制的 Android 系统,一些系统级应用的保活,这个方案还是有用的。


随着 Android 阵营的各大手机厂商对于续航的高度重视,两三年前的手机发布会更是把反保活作为一个系统的卖点,不断提出了各种反保活的方案,导致现在想实现应用保活简直难于上青天,甚至都需要一个团队来专门研究这个事情。连微信这种超级 APP,也要拜倒在反保活的石榴裙下,允许后台启动太费电,不允许后台启动就收不到消息。。Android 发现了一个保活野路子就堵一条,然而很多场景是有保活的强需求的,有木有考虑过我们开发者的感受,自己人何必为难自己人。


我觉得这是一个 Android 设计的不合理的地方,路子可以堵,但还是有必要留一个统一的保活接口的。这个接口由 Google 实现也好,厂商来实现也好,总好过现在很笨拙的系统自启动管理或者是 JobScheduler。我觉得本质上来说,让应用开发者想尽各种办法去做保活,这个事情是没有意义的,保活的路子被封了,但保活还是需要做,保活的成本也提高了,简直浪费生命。Android 的锅。(仅代表个人观点)

黑科技进程保活原理

大概 2 个月前,Gityuan 大佬放出了一份分析 TIM 的黑科技保活的博客史上最强 Android 保活思路:深入剖析腾讯 TIM 的进程永生技术(后来不知道什么原因又删除了),顿时间掀起了一阵波澜,仿佛让开发者们又看到了应用保活的一丝希望。Gityuan 大佬通过超强的专业技术分析,为我们解开了 TIM 保活方案的终极奥义。


后来,为数不多的维术大佬在 Gityuan 大佬的基础上,发布了博客 Android 黑科技保活实现原理揭秘又进行了系统进程查杀相关的源码分析。为我们带来的结论是,Android 系统杀应用的时候,会去杀进程组,循环 40 遍不停地杀进程,每次杀完之后等 5ms


总之,引用维术的话语,原理如下:


  1. 利用 Linux 文件锁的原理,使用 2 个进程互相监听各自的文件锁,来感知彼此的死亡。

  2. 通过 fork 产生子进程,fork 的进程同属一个进程组,一个被杀之后会触发另外一个进程被杀,从而被文件锁感知。


具体来说,创建 2 个进程 p1, p2,这两个进程通过文件锁互相关联,一个被杀之后拉起另外一个;同时 p1 经过 2 次 fork 产生孤儿进程 c1,p2 经过 2 次 fork 产生孤儿进程 c2,c1 和 c2 之间建立文件锁关联。这样假设 p1 被杀,那么 p2 会立马感知到,然后 p1 和 c1 同属一个进程组,p1 被杀会触发 c1 被杀,c1 死后 c2 立马感受到从而拉起 p1,因此这四个进程三三之间形成了铁三角,从而保证了存活率。


按照维术大佬的理论,只要进程我复活的足够快,系统它就杀不死我,嘿嘿。


维术大佬写了一个简单的实现,代码在这里:github.com/tiann/Leori…,这个方案是当检测到进程被杀时,会通过 JNI 的方式,调用 Java 层的方法来复活进程。为了实现稳定的保活,尤其是系统杀进程只给了 5ms 复活的机会,使用 JNI 这种方式复活进程现在达不到最优的效果。

Java 层复活进程

复活进程,其实就是启动指定的 Service。当 native 层检测到有进程被杀时,为了能够快速启动新 Service。我们可以通过反射,拿到 ActivityManager 的 remote binder,直接通过这个 binder 发送数据,即可实现快速启动 Service。


Class<?> amnCls = Class.forName("android.app.ActivityManagerNative");amn = activityManagerNative.getMethod("getDefault").invoke(amnCls);Field mRemoteField = amn.getClass().getDeclaredField("mRemote");mRemoteField.setAccessible(true);mRemote = (IBinder) mRemoteField.get(amn);


启动 Service 的 Intent:


Intent intent = new Intent();ComponentName component = new ComponentName(context.getPackageName(), serviceName);intent.setComponent(component);


封装启动 Service 的 Parcel:


Parcel mServiceData = Parcel.obtain();mServiceData.writeInterfaceToken("android.app.IActivityManager");mServiceData.writeStrongBinder(null);mServiceData.writeInt(1);intent.writeToParcel(mServiceData, 0);mServiceData.writeString(null); // resolvedTypemServiceData.writeInt(0);mServiceData.writeString(context.getPackageName()); // callingPackagemServiceData.writeInt(0);


启动 Service:


mRemote.transact(transactCode, mServiceData, null, 1);

在 native 层进行 binder 通信

在 Java 层做进程复活的工作,这个方式是比较低效的,最好的方式是在 native 层使用纯 C/C++来复活进程。方案有两个。


其一,维术大佬给出的方案是利用 libbinder.so, 利用 Android 提供的 C++接口,跟 ActivityManagerService 通信,以唤醒新进程。


  1. Java 层创建 Parcel (含 Intent),拿到 Parcel 对象的 mNativePtr(native peer),传到 Native 层。

  2. native 层直接把 mNativePtr 强转为结构体指针。

  3. fork 子进程,建立管道,准备传输 parcel 数据。

  4. 子进程读管道,拿到二进制流,重组为 parcel。


其二,Gityuan 大佬则认为使用 ioctl 直接给 binder 驱动发送数据以唤醒进程,才是更高效的做法。然而,这个方法,大佬们并没有提供思路。


那么今天,我们就来实现这两种在 native 层进行 Binder 调用的骚操作。

方式一 利用 libbinder.so 与 ActivityManagerService 通信

上面在 Java 层复活进程一节中,是向 ActivityManagerService 发送特定的封装了 Intent 的 Parcel 包来实现唤醒进程。而在 native 层,没有 Intent 这个类。所以就需要在 Java 层创建好 Intent,然后写到 Parcel 里,再传到 Native 层。


Parcel mServiceData = Parcel.obtain();mServiceData.writeInterfaceToken("android.app.IActivityManager");mServiceData.writeStrongBinder(null);mServiceData.writeInt(1);intent.writeToParcel(mServiceData, 0);mServiceData.writeString(null); // resolvedTypemServiceData.writeInt(0);mServiceData.writeString(context.getPackageName()); // callingPackagemServiceData.writeInt(0);


查看 Parcel 的源码可以看到,Parcel 类有一个 mNativePtr 变量:


private long mNativePtr; // used by native code// android4.4 mNativePtr 是 int 类型


可以通过反射得到这个变量:


private static long getNativePtr(Parcel parcel) {try {Field ptrField = parcel.getClass().getDeclaredField("mNativePtr");ptrField.setAccessible(true);return (long) ptrField.get(parcel);} catch (Exception e) {e.printStackTrace();}return 0;}


这个变量对应了 C++中 Parcel 类的地址,因此可以强转得到 Parcel 指针:


Parcel *parcel = (Parcel *) parcel_ptr;


然而,NDK 中并没有提供 binder 这个模块,我们只能从 Android 源码中扒到 binder 相关的源码,再编译出 libbinder.so。腾讯 TIM 应该就是魔改了 binder 相关的源码。

提取 libbinder.so

为了避免 libbinder 的版本兼容问题,这里我们可以采用一个更简单的方式,拿到 binder 相关的头文件,再从系统中拿到 libbinder.so,当然 binder 模块还依赖了其它的几个 so,要一起拿到,不然编译的时候会报链接错误。


adb pull /system/lib/libbinder.so ./adb pull /system/lib/libcutils.so ./adb pull /system/lib/libc.so ./adb pull /system/lib/libutils.so ./复制代码


如果需要不同 SDK 版本,不同架构的系统 so 库,可以在 Google Factory Images 网页里找到适合的版本,下载相应的固件,然后解包 system.img(需要在 windows 或 linux 中操作),提取出目标 so。


binder_libs├── arm64-v8a│ ├── libbinder.so│ ├── libc.so│ ├── libcutils.so│ └── libutils.so├── armeabi-v7a│ ├── ...├── x86│ ├── ...└── x86_64├── ...


为了避免兼容问题,我这里只让这些 so 参与了 binder 相关的头文件的链接,而没有实际使用这些 so。这是利用了 so 的加载机制,如果应用 lib 目录没有相应的 so,则会到 system/lib 目录下查找。


SDK24 以上,系统禁止了从 system 中加载 so 的方式,所以使用这个方法务必保证 targetApi <24。

否则,将会报找不到 so 的错误。可以把上面的 so 放到 jniLibs 目录解决这个问题,但这样就会有兼容问题了。


CMake 修改:

链接 binder_libs 目录下的所有 so 库

link_directories(binder_libs/${CMAKE_ANDROID_ARCH_ABI})

引入 binder 相关的头文件

include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include/)

libbinder.so libcutils.so libutils.so libc.so 等库链接到 libkeep_alive.so

target_link_libraries(keep_alive${log-lib} binder cutils utils c)


进程间传输 Parcel 对象


C++里面还能传输对象?不存在的。好在 Parcel 能直接拿到数据地址,并提供了构造方法。所以我们可以通过管道把 Parcel 数据传输到其它进程。


Parcel *parcel = (Parcel *) parcel_ptr;size_t data_size = parcel->dataSize();int fd[2];// 创建管道 if (pipe(fd) < 0) {return;}


pid_t pid;// 创建子进程 if ((pid = fork()) < 0) {exit(-1);} else if (pid == 0) {//第一个子进程 if ((pid = fork()) < 0) {exit(-1);} else if (pid > 0) {// 托孤 exit(0);}


uint8_t data[data_size];// 托孤的子进程,读取管道中的数据 int result = read(fd[0], data, data_size);}


// 父进程向管道中写数据 int result = write(fd[1], parcel->data(), data_size);


重新创建 Parcel:


Parcel parcel;parcel.setData(data, data_size);


传输 Parcel 数据


// 获取 ServiceManagersp<IServiceManager> sm = defaultServiceManager();// 获取 ActivityManager bindersp<IBinder> binder = sm->getService(String16("activity"));// 传输 parcelint result = binder.get()->transact(code, parcel, NULL, 0);

方式二 使用 ioctl 与 binder 驱动通信

方式一让我尝到了一点甜头,实现了大佬的思路,不禁让鄙人浮想联翩,感慨万千,鄙人的造诣已经如此之深,不久就会人在美国,刚下飞机,迎娶白富美,走向人生巅峰矣......


[图片上传中...(image-237bb2-1586154434812-4)]


<figcaption></figcaption>


咳咳。不禁想到 ioctl 的方式我也可以尝试着实现一下。ioctl 是一个 linux 标准方法,那么我们就直奔主题看看,binder 是什么,ioctl 怎么跟 binder driver 通信。

Binder 介绍

Binder 是 Android 系统提供的一种 IPC 机制。每个 Android 的进程,都可以有一块用户空间和内核空间。用户空间在不同进程间不能共享,内核空间可以共享。Binder 就是一个利用可以共享的内核空间,完成高性能的进程间通信的方案。


Binder 通信采用 C/S 架构,从组件视角来说,包含 Client、Server、ServiceManager 以及 binder 驱动,其中 ServiceManager 用于管理系统中的各种服务。如图:



可以看到,注册服务、获取服务、使用服务,都是需要经过 binder 通信的。


  • Server 通过注册服务的 Binder 通信把自己托管到 ServiceManager

  • Client 端可以通过 ServiceManager 获取到 Server

  • Client 端获取到 Server 后就可以使用 Server 的接口了


Binder 通信的代表类是 BpBinder(客户端)和 BBinder(服务端)。


ps:有关 binder 的详细知识,大家可以查看 Gityuan 大佬的 Binder 系列文章。

ioctl 函数

ioctl(input/output control)是一个专用于设备输入输出操作的系统调用,它诞生在这样一个背景下:


操作一个设备的 IO 的传统做法,是在设备驱动程序中实现 write 的时候检查一下是否有特殊约定的数据流通过,如果有的话,后面就跟着控制命令(socket 编程中常常这样做)。但是这样做的话,会导致代码分工不明,程序结构混乱。所以就有了 ioctl 函数,专门向驱动层发送或接收指令。


Linux 操作系统分为了两层,用户层和内核层。我们的普通应用程序处于用户层,系统底层程序,比如网络栈、设备驱动程序,处于内核层。为了保证安全,操作系统要阻止用户态的程序直接访问内核资源。一个 Ioctl 接口是一个独立的系统调用,通过它用户空间可以跟设备驱动沟通了。函数原型:


int ioctl(int fd, int request, …);


作用:通过 IOCTL 函数实现指令的传递


  • fd 是用户程序打开设备时使用 open 函数返回的文件描述符

  • request 是用户程序对设备的控制命令

  • 后面的省略号是一些补充参数,和 cmd 的意义相关


应用程序在调用 ioctl 进行设备控制时,最后会调用到设备注册 struct file_operations 结构体对象时的 unlocked_ioctl 或者 compat_ioctl 两个钩子上,例如 Binder 驱动的这两个钩子是挂到了 binder_ioctl 方法上:


static const struct file_operations binder_fops = {.owner = THIS_MODULE,.poll = binder_poll,.unlocked_ioctl = binder_ioctl,.compat_ioctl = bi


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


nder_ioctl,.mmap = binder_mmap,.open = binder_open,.flush = binder_flush,.release = binder_release,};


它的实现如下:


static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg){/根据不同的命令,调用不同的处理函数进行处理/switch (cmd) {case BINDER_WRITE_READ:/读写命令,数据传输,binder IPC 通信的核心逻辑/ret = binder_ioctl_write_read(filp, cmd, arg, thread);break;case BINDER_SET_MAX_THREADS:/设置最大线程数,直接将值设置到 proc 结构的 max_threads 域中。/break;case BINDER_SET_CONTEXT_MGR:/设置 Context manager,即将自己设置为 ServiceManager,详见 3.3/break;case BINDER_THREAD_EXIT:/binder 线程退出命令,释放相关资源/break;case BINDER_VERSION: {/获取 binder 驱动版本号,在 kernel4.4 版本中,32 位该值为 7,64 位版本该值为 8/break;}return ret;}


具体内核层的实现,我们就不关心了。到这里我们了解到,Binder 在 Android 系统中会有一个设备节点,调用 ioctl 控制这个节点时,实际上会调用到内核态的 binder_ioctl 方法。


为了利用 ioctl 启动 Android Service,必然是需要用 ioctl 向 binder 驱动写数据,而这个控制命令就是 BINDER_WRITE_READ。binder 驱动层的一些细节我们在这里就不关心了。那么在什么地方会用 ioctl 向 binder 写数据呢?

IPCThreadState.talkWithDriver

阅读 Gityuan 的 Binder 系列 6—获取服务(getService)一节,在 binder 模块下 IPCThreadState.cpp 中有这样的实现(源码目录:frameworks/native/libs/binder/IPCThreadState.cpp):


status_t IPCThreadState::talkWithDriver(bool doReceive) {...binder_write_read bwr;bwr.write_buffer = (uintptr_t)mOut.data();status_t err;do {//通过 ioctl 不停的读写操作,跟 Binder Driver 进行通信 if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)err = NO_ERROR;...} while (err == -EINTR); //当被中断,则继续执行...return err;}


可以看到 ioctl 跟 binder driver 交互很简单,一个参数是 mProcess->mDriverFD,一个参数是 BINDER_WRITE_READ,另一个参数是 binder_write_read 结构体,很幸运的是,NDK 中提供了 linux/android/binder.h 这个头文件,里面就有 binder_write_read 这个结构体,以及 BINDER_WRITE_READ 常量的定义。


[惊不惊喜意不意外]


#include<linux/android/binder.h>struct binder_write_read {binder_size_t write_size;binder_size_t write_consumed;binder_uintptr_t write_buffer;binder_size_t read_size;binder_size_t read_consumed;binder_uintptr_t read_buffer;};#define BINDER_WRITE_READ _IOWR('b', 1, struct binder_write_read)


这意味着,这些结构体和宏定义很可能是版本兼容的。


那我们只需要到时候把数据揌到 binder_write_read 结构体里面,就可以进行 ioctl 系统调用了!

/dev/binder

再来看看 mProcess->mDriverFD 是什么东西。mProcess 也就是 ProcessState.cpp(源码目录:frameworks/native/libs/binder/ProcessState.cpp):


ProcessState::ProcessState(const char *driver): mDriverName(String8(driver)), mDriverFD(open_driver(driver)), ...{}


从 ProcessState 的构造函数中得知,mDriverFD 由 open_driver 方法初始化。


static int open_driver(const char *driver) {int fd = open(driver, O_RDWR | O_CLOEXEC);if (fd >= 0) {int vers = 0;status_t result = ioctl(fd, BINDER_VERSION, &vers);}return fd;}


ProcessState 在哪里实例化呢?


sp<ProcessState> ProcessState::self() {if (gProcess != nullptr) {return gProcess;}gProcess = new ProcessState(kDefaultDriver);return gProcess;}


可以看到,ProcessState 的 gProcess 是一个全局单例对象,这意味着,在当前进程中,open_driver 只会执行一次,得到的 mDriverFD 会一直被使用。


const char* kDefaultDriver = "/dev/binder";


而 open 函数操作的这个设备节点就是/dev/binder。


纳尼?在应用层直接操作设备节点?Gityuan 大佬不会骗我吧?一般来说,Android 系统在集成 SELinux 的安全机制之后,普通应用甚至是系统应用,都不能直接操作一些设备节点,除非有 SELinux 规则,给应用所属的域或者角色赋予了那样的权限。


看看文件权限:


? ~ adb shellchiron:/ $ ls -l /dev/bindercrw-rw-rw- 1 root root 10, 49 1972-07-03 18:46 /dev/binder


可以看到,/dev/binder 设备对所有用户可读可写。


再看看,SELinux 权限:


chiron:/ $ ls -Z /dev/binderu:object_r:binder_device:s0 /dev/binder


查看源码中对 binder_device 角色的 SELinux 规则描述:


allow domain binder_device:chr_file rw_file_perms;


也就是所有 domain 对 binder 的字符设备有读写权限,而普通应用属于 domain。


既然这样,肝它!

写个 Demo 试一下

验证一下上面的想法,看看 ioctl 给 binder driver 发数据好不好使。


1、打开设备


int fd = open("/dev/binder", O_RDWR | O_CLOEXEC);if (fd < 0) {LOGE("Opening '%s' failed: %s\n", "/dev/binder", strerror(errno));} else {LOGD("Opening '%s' success %d: %s\n", "/dev/binder", fd, strerror(errno));}


2、ioctl


Parcel *parcel = new Parcel;parcel->writeString16(String16("test"));binder_write_read bwr;bwr.write_size = parcel->dataSize();bwr.write_buffer = (binder_uintptr_t) parcel->data();int ret = ioctl(fd, BINDER_WRITE_READ, bwr);LOGD("ioctl result is %d: %s\n", ret, strerror(errno));


3、查看日志


D/KeepAlive: Opening '/dev/binder' success, fd is 35D/KeepAlive: ioctl result is -1: Invalid argument


打开设备节点成功了,耶??!但是 ioctl 失败了,失败原因是 Invalid argument,也就是说可以通信,但是 Parcel 数据有问题。来看看数据应该是什么样的。

binder_write_read 结构体数据封装

IPCThreadState.talkWithDriver 方法中,bwr.write_buffer 指针指向了 mOut.data(),显然 mOut 是一个 Parcel 对象。


binder_write_read bwr;bwr.write_buffer = (uintptr_t)mOut.data();


再来看看什么时候会向 mOut 中写数据:


status_t IPCThreadState::writeTransactionData(int32_t cmd, uint32_t binderFlags,int32_t handle, uint32_t code, const Parcel& data, status_t* statusBuffer){binder_transaction_data tr;tr.data.ptr.buffer = data.ipcData();...mOut.writeInt32(cmd);mOut.write(&tr, sizeof(tr));return NO_ERROR;}


writeTransactionData 方法中,会往 mOut 中写入一个 binder_transaction_data 结构体数据,binder_transaction_data 结构体中又包含了作为参数传进来的 data Parcel 对象。


writeTransactionData 方法会被 transact 方法调用:


status_t IPCThreadState::transact(int32_t handle, uint32_t code, const Parcel& data,Parcel* reply, uint32_t flags) {status_t err = data.errorCheck(); // 数据错误检查 flags |= TF_ACCEPT_FDS;if (err == NO_ERROR) {// 传输数据 err = writeTransactionData(BC_TRANSACTION, flags, handle, code, data, NULL);}...


// 默认情况下,都是采用非 oneway 的方式, 也就是需要等待服务端的返回结果 if ((flags & TF_ONE_WAY) == 0) {if (reply) {//等待回应事件 err = waitForResponse(reply);}else {Parcel fakeReply;

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
只要进程我复活的足够快,系统它就杀不死我!Android最强保活黑科技的最强技术实现