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

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 sm = defaultServiceManager();
// 获取ActivityManager binder
sp binder = sm->getService(String16(“activity”));
// 传输parcel
int result = binder.get()->transact(code, parcel, NULL, 0);

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

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

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

咳咳。不禁想到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::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 sm = defaultServiceManager();
// 获取ActivityManager binder
sp 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 defaultServiceManager() {
if (gDefaultServiceManager != NULL) return gDefaultServiceManager;
while (gDefaultServiceManager == NULL) {
gDefaultServiceManager = interface_cast(
ProcessState::self()->getContextObject(NULL));
}
}
return gDefaultServiceManager;
}

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

sp ProcessState::getContextObject(const sp& /caller/) {
sp context = getStrongProxyForHandle(0);
return context;
}

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

2、获取ActivityManager的mHandle句柄

获取ActivityManager的c++方法是:

sp binder = serviceManager->getService(String16(“activity”));

BpServiceManager.getService:

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

,18年进入阿里一直到现在。**

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-AkFtYHHS-1715111016217)]

[外链图片转存中…(img-bqzZJZ0Q-1715111016217)]

[外链图片转存中…(img-GR8gXYWg-1715111016218)]

[外链图片转存中…(img-Zi8ryfRT-1715111016218)]

[外链图片转存中…(img-QdSQoG6K-1715111016218)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值