启动Service:
mRemote.transact(transactCode, mServiceData, null, 1);
在Java层做进程复活的工作,这个方式是比较低效的,最好的方式是在 native 层使用纯 C/C++来复活进程。方案有两个。
其一,维术大佬给出的方案是利用libbinder.so, 利用Android提供的C++接口,跟ActivityManagerService通信,以唤醒新进程。
其二,Gityuan大佬则认为使用 ioctl 直接给 binder 驱动发送数据以唤醒进程,才是更高效的做法。然而,这个方法,大佬们并没有提供思路。
那么今天,我们就来实现这两种在 native 层进行 Binder 调用的骚操作。
上面在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的版本兼容问题,这里我们可以采用一个更简单的方式,拿到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修改:
link_directories(binder_libs/${CMAKE_ANDROID_ARCH_ABI})
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include/)
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 sm = defaultServiceManager();
// 获取ActivityManager binder
sp binder = sm->getService(String16(“activity”));
// 传输parcel
int result = binder.get()->transact(code, parcel, NULL, 0);
方式一让我尝到了一点甜头,实现了大佬的思路,不禁让鄙人浮想联翩,感慨万千,鄙人的造诣已经如此之深,不久就会人在美国,刚下飞机,迎娶白富美,走向人生巅峰矣…
[图片上传中…(image-237bb2-1586154434812-4)]
咳咳。不禁想到ioctl的方式我也可以尝试着实现一下。ioctl是一个linux标准方法,那么我们就直奔主题看看,binder是什么,ioctl怎么跟binder driver通信。
Binder是Android系统提供的一种IPC机制。每个Android的进程,都可以有一块用户空间和内核空间。用户空间在不同进程间不能共享,内核空间可以共享。Binder就是一个利用可以共享的内核空间,完成高性能的进程间通信的方案。
Binder通信采用C/S架构,从组件视角来说,包含Client、Server、ServiceManager以及binder驱动,其中ServiceManager用于管理系统中的各种服务。如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gQjBVPkD-1651037634129)(//upload-images.jianshu.io/upload_images/19956127-21ed577f4ce0562a.png?imageMogr2/auto-orient/strip|imageView2/2/w/921/format/webp)]
可以看到,注册服务、获取服务、使用服务,都是需要经过binder通信的。
Binder通信的代表类是BpBinder(客户端)和BBinder(服务端)。
ps:有关binder的详细知识,大家可以查看Gityuan大佬的Binder系列文章。
ioctl(input/output control)是一个专用于设备输入输出操作的系统调用,它诞生在这样一个背景下:
操作一个设备的IO的传统做法,是在设备驱动程序中实现write的时候检查一下是否有特殊约定的数据流通过,如果有的话,后面就跟着控制命令(socket编程中常常这样做)。但是这样做的话,会导致代码分工不明,程序结构混乱。所以就有了ioctl函数,专门向驱动层发送或接收指令。
Linux操作系统分为了两层,用户层和内核层。我们的普通应用程序处于用户层,系统底层程序,比如网络栈、设备驱动程序,处于内核层。为了保证安全,操作系统要阻止用户态的程序直接访问内核资源。一个Ioctl接口是一个独立的系统调用,通过它用户空间可以跟设备驱动沟通了。函数原型:
int ioctl(int fd, int request, …);
作用:通过IOCTL函数实现指令的传递
应用程序在调用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写数据呢?
阅读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
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系统调用了!
再来看看mProcess->mDriverFD是什么东西。mProcess也就是[ProcessState.cpp](()(源码目录:frameworks/native/libs/binder/ProcessState.cpp):
从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::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。
既然这样,肝它!
验证一下上面的想法,看看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数据有问题。来看看数据应该是什么样的。
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));
lkWithDriver方法中,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));