阅读 9198

Android最强保活黑科技的最强技术实现

大家好,我是老玩童。今天来跟大家分享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); // resolvedType
mServiceData.writeInt(0);
mServiceData.writeString(context.getPackageName()); // callingPackage
mServiceData.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); // resolvedType
mServiceData.writeInt(0);
mServiceData.writeString(context.getPackageName()); // callingPackage
mServiceData.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数据

// 获取ServiceManager
sp<IServiceManager> sm = defaultServiceManager();
// 获取ActivityManager binder
sp<IBinder> binder = sm->getService(String16("activity"));
// 传输parcel
int result = binder.get()->transact(code, parcel, NULL, 0);
复制代码

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

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

咳咳。不禁想到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 shell
chiron:/ $ ls -l /dev/binder
crw-rw-rw- 1 root root 10,  49 1972-07-03 18:46 /dev/binder
复制代码

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

再看看,SELinux权限:

chiron:/ $ ls -Z /dev/binder
u: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 35
D/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;
}
复制代码

BpBinder::transact方法的code/data/reply/flags这几个参数都是调用的地方传过来的,现在唯一不知道的就是mHandle是什么东西。mHandle是BpBinder(也就是Binder Client)的一个int类型的局部变量(句柄),只要拿到了这个handle就相当于拿到了BpBinder。

ioctl启动Service分几步?

下面是在依赖libbinder.so时,启动Service的步骤:

// 获取ServiceManager
sp<IServiceManager> sm = defaultServiceManager();
// 获取ActivityManager binder
sp<IBinder> binder = sm->getService(String16("activity"));
// 传输parcel
int result = binder.get()->transact(code, parcel, NULL, 0);
复制代码

1、获取到IServiceManager Binder Client;

2、从ServiceManager中获取到ActivityManager Binder Client;

3、调用ActivityManager binder的transact方法传输Service的Parcel数据。

通过ioctl启动Service也应该是类似的步骤:

1、获取到ServiceManager的mHandle句柄;

2、进行binder调用获取到ActivityManager的mHandle句柄;

3、进行binder调用传输启动Service的指令数据。

这里有几个问题:

1、不依赖libbinder.so时,ndk中没有Parcel类的定义,parcel数据哪里来,怎么封装?

2、如何获取到BpBinder的mHandle句柄?

如何封装Parcel数据

Parcel类是Binder进程间通信的一个基础的、必不可少的数据结构,往Parcel中写入的数据实际上是写入到了一块内部分配的内存上,最后把这个内存地址封装到binder_write_read结构体中。Parcel作为一个基础的数据结构,和Binder相关类是可以解耦的,可以直接拿过来使用,我们可以根据需要对有耦合性的一些方法进行裁剪。

c++ Parcel类路径:frameworks/native/libs/binder/Parcel.cpp

jni Parcel类路径:frameworks/base/core/jni/android_os_Parcel.cpp

如何获取到BpBinder的mHandle句柄

具体流程参考Binder系列4—获取ServiceManager

1、获取ServiceManager的mHandle句柄

defaultServiceManager()方法用来获取gDefaultServiceManager对象,gDefaultServiceManager是ServiceManager的单例。

sp<IServiceManager> defaultServiceManager() {
    if (gDefaultServiceManager != NULL) return gDefaultServiceManager;
    while (gDefaultServiceManager == NULL) {
            gDefaultServiceManager = interface_cast<IServiceManager>(
                ProcessState::self()->getContextObject(NULL));
        }
    }
    return gDefaultServiceManager;
}
复制代码

getContextObject方法用来获取BpServiceManager对象(BpBinder),查看其定义:

sp<IBinder> ProcessState::getContextObject(const sp<IBinder>& /*caller*/) {
    sp<IBinder> context = getStrongProxyForHandle(0);
    return context;
}
复制代码

可以发现,getStrongProxyForHandle是一个根据handle获取IBinder对象的方法,而这里handle的值为0,可以得知,ServiceManager的mHandle恒为0

2、获取ActivityManager的mHandle句柄

获取ActivityManager的c++方法是:

sp<IBinder> binder = serviceManager->getService(String16("activity"));
复制代码

BpServiceManager.getService:

virtual sp<IBinder> getService(const String16& name) const {
	sp<IBinder> svc = checkService(name);
	if (svc != NULL) return svc;
	return NULL;
}
复制代码

BpServiceManager.checkService:

virtual sp<IBinder> checkService( const String16& name) const {
    Parcel data, reply;
    //写入RPC头
    data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor());
    //写入服务名
    data.writeString16(name);
    remote()->transact(CHECK_SERVICE_TRANSACTION, data, &reply);
    return reply.readStrongBinder();
}
复制代码

可以看到,CHECK_SERVICE_TRANSACTION这个binder调用是有返回值的,返回值会写到reply中,通过reply.readStrongBinder()方法,即可从reply这个Parcel对象中读取到ActivityManager的IBinder。每个Binder对象必须要有它自己的mHandle句柄,不然,transact操作是没办法进行的。所以,很有可能,Binder的mHandle的值是写到reply这个Parcel里面的。

看看reply.readStrongBinder()方法搞了什么鬼:

sp<IBinder> Parcel::readStrongBinder() const {
    sp<IBinder> val;
    readNullableStrongBinder(&val);
    return val;
}
status_t Parcel::readNullableStrongBinder(sp<IBinder>* val) const {
    return unflattenBinder(val);
}
复制代码

调用到了Parcel::unflattenBinder方法,顾名思义,函数最终想要得到的是一个Binder对象,而Parcel中存放的是二进制的数据,unflattenBinder很可能是把Parcel中的一个结构体数据给转成Binder对象。

看看Parcel::unflattenBinder方法的定义:

status_t Parcel::unflattenBinder(sp<IBinder>* out) const {
    const flat_binder_object* flat = readObject(false);
    if (flat) {
        ...
        sp<IBinder> binder =
            ProcessState::self()->getStrongProxyForHandle(flat->handle);
    }
    return BAD_TYPE;
}
复制代码

果然如此,从Parcel中可以得到一个flat_binder_object结构体,这个结构体重有一个handle变量,这个变量就是BpBinder中的mHandle句柄。

因此,在不依赖libbinder.so的情况下,我们可以自己组装数据发送给ServiceManager,进而获取到ActivityManager的mHandle句柄。

IPCThreadState是一个被Binder依赖的类,它是可以从源码中抽离出来为我们所用的。上一节中说到,Parcel类也是可以从源码中抽离出来的。

通过如下的操作,我们就可以实现ioctl获取到ActivityManager对应的Parcel对象reply:

Parcel data, reply;
// data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor());
// IServiceManager::getInterfaceDescriptor()的值是android.app.IActivityManager
data.writeInterfaceToken(String16("android.app.IActivityManager"));
data.writeString16(String16("activity"));
IPCThreadState::self()->transact(
	0/*ServiceManger的mHandle句柄恒为0*/, 
CHECK_SERVICE_TRANSACTION, data, reply, 0);
复制代码

reply变量也就是我们想要的包含了flat_binder_object结构体的Parcel对象,再经过如下的操作就可以得到ActivityManager的mHandle句柄:

const flat_binder_object* flat = reply->readObject(false);
return flat->handle;
复制代码

3、传输启动指定Service的Parcel数据

上一步已经拿到ActivityManger的mHandle句柄,比如值为1。这一步的过程和上一步类似,自己封装Parcel,然后调用IPCThreadState::transact方法传输数据,伪代码如下:

Parcel data;
// 把Service相关信息写到parcel中
writeService(data, packageName, serviceName, sdk_version);
IPCThreadState::self()->transact(
	1/*上一步获取的ActivityManger的mHandle句柄值是1*/, 
	CHECK_SERVICE_TRANSACTION, data, reply, 
	1/*TF_ONE_WAY*/);
复制代码

4、writeService方法需要做什么事情?

下面这段代码是Java中封装Parcel对象的方法:

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

Parcel mServiceData = Parcel.obtain();
mServiceData.writeInterfaceToken("android.app.IActivityManager");
mServiceData.writeStrongBinder(null);
mServiceData.writeInt(1);
intent.writeToParcel(mServiceData, 0);
mServiceData.writeString(null); // resolvedType
mServiceData.writeInt(0);
mServiceData.writeString(context.getPackageName()); // callingPackage
mServiceData.writeInt(0);
复制代码

可以看到,有Intent类转Parcel,ComponentName类转Parcel,这些类在c++中是没有对应的类的。所以需要我们参考intent.writeToParcel/ComponentName.writeToParcel等方法的源码的实现,自行封装数据。下面这段代码就是把启动Service的Intent写到Parcel中的方法:

void writeIntent(Parcel &out, const char *mPackage, const char *mClass) {
    // mAction
    out.writeString16(NULL, 0);
    // uri mData
    out.writeInt32(0);
    // mType
    out.writeString16(NULL, 0);
//    // mIdentifier
    out.writeString16(NULL, 0);
    // mFlags
    out.writeInt32(0);
    // mPackage
    out.writeString16(NULL, 0);
    // mComponent
    out.writeString16(String16(mPackage));
    out.writeString16(String16(mClass));
    // mSourceBounds
    out.writeInt32(0);
    // mCategories
    out.writeInt32(0);
    // mSelector
    out.writeInt32(0);
    // mClipData
    out.writeInt32(0);
    // mContentUserHint
    out.writeInt32(-2);
    // mExtras
    out.writeInt32(-1);
}

复制代码

继续写Demo试一下

上面已经知道了怎么通过ioctl获取到ActivityManager,可以写demo试一下:

// 打开binder设备
int fd = open("/dev/binder", O_RDWR | O_CLOEXEC);
Parcel data, reply;
// data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor());
// IServiceManager::getInterfaceDescriptor()的值是android.app.IActivityManager
data.writeInterfaceToken(String16("android.app.IActivityManager"));
data.writeString16(String16("activity"));
IPCThreadState::self()->transact(
	0/*ServiceManger的mHandle句柄恒为0*/, 
CHECK_SERVICE_TRANSACTION, data, reply, 0);

const flat_binder_object *flat = reply->readObject(false);
if (flat) {
    LOGD("write_transact handle is:%llu", flat->handle);
}else {
    LOGD("write_transact failed, error=%d", status);
}
复制代码

给IPCThreadState::transact加上一些日志,打印结果如下:

D/KeepAlive: BR_DEAD_REPLY
D/KeepAlive: write_transact failed, error=-32
复制代码

reply中始终读不到数据。这是为什么?现在已经不报Invalid argument的错误了,说明Parcel数据格式可能没问题了。但是不能成功把数据写给ServiceManager,或者ServiceManager返回的数据不能成功写回来。

想到Binder是基于内存的一种IPC机制,数据都是对的,那问题就出在内存上了。这就要说到Binder基本原理以及Binder内存转移关系。

Binder基本原理:

Binder的Client端和Server端位于不同的进程,它们的用户空间是相互隔离。而内核空间由Linux内核进程来维护,在安全性上是有保障的。所以,Binder的精髓就是在内核态开辟了一块共享内存。

数据发送方写数据时,内核态通过copy_from_user()方法把它的数据拷贝到数据接收方映射(mmap)到内核空间的地址上。这样,只需要一次数据拷贝过程,就可以完成进程间通信。

由此可知,没有这块内核空间是没办法完成IPC通信的。Demo失败的原因就是缺少了一个mmap过程,以映射一块内存到内核空间。修改如下:

#define BINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2)
int mDriverFD = open("/dev/binder", O_RDWR | O_CLOEXEC);
mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);
复制代码

日志:

D/KeepAlive: BR_REPLY
D/KeepAlive: write_transact handle is:1
复制代码

搞定!

最后

相关的代码我已经发布到Github(lcodecorex/KeepAlive),master分支是利用 libbinder.so 与 ActivityManagerService 通信的版本,ioctl分支是使用 ioctl 与 binder 驱动通信的版本。

当然,这个保活的办法虽然很强,但现在也只能活在模拟器里了。

说一下我的方法论。

1、确定问题和目标。

研究一个比较复杂的东西的时候,我们比较难有一个大局观。这个时候,就需要明确自己需要什么?有问题,才能推动自己学习,然后顺腾摸瓜,最后弄清自己的模块在系统中的位置。

这篇文章,我们确定了目标是直接通过ioctl进行Binder通信,进而确定Binder通信的关键是拿到mHandle句柄。同时也理清了Binder通信的一个基本流程。

2、时序图很重要。

大佬们画的时序图,可快帮助我们快速理清框架的思路。

3、实践出真知。

纸上得来终觉浅,绝知此事要躬行。我一直践行的一个学习方式是学以致用,可以及时写Demo帮助我们巩固知识以及分析问题。

鸣谢

Gityuan大佬的Binder系列

Android 黑科技保活实现原理揭秘

binder Driver (binder IPC) 功能介绍与分析

Binder驱动之设备控制binder_ioctl

Google官方Android源码浏览网站