写点什么

Android 最强进程保活黑科技实现原理解密及方法,我的头条面试经历分享

用户头像
Android架构
关注
发布于: 2 小时前

复活进程,其实就是启动指定的 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


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


();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 = binder_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;err = waitForResponse(&fakeReply);}} else {err = waitForResponse(NULL, NULL);}return err;}


IPCThreadState 是跟 binder driver 真正进行交互的类。每个线程都有一个IPCThreadState,每个IPCThreadState中都有一个 mIn、一个 mOut。成员变量 mProcess 保存了 ProcessState 变量(每个进程只有一个)。


接着看一下一次 Binder 调用的时序图:



Binder 介绍一节中说过,BpBinder 是 Binder Client,上层想进行进程间 Binder 通信时,会调用到 BpBinder 的 transact 方法,进而调用到 IPCThreadState 的 transact 方法。来看看 BpBinder 的 transact 方法的定义:


status_t BpBinder::transact(uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) {if (mAlive) {status_t status = IPCThreadState::self()->transact(mHandle, code, data, reply, flags);if (status == DEAD_OBJECT) mAlive = 0;return status;}return DEAD_OBJECT;}

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android最强进程保活黑科技实现原理解密及方法,我的头条面试经历分享