Android保活技术实现

背景

随着Android阵营的各大手机厂商对于续航的高度重视,两三年前的手机发布会更是把反保活作为一个系统的卖点,不断提出了各种反保活的方案,导致现在想实现应用保活简直难于上青天,甚至都需要一个团队来专门研究这个事情。
随着Android系统的不断升级,即时通讯网技术群和社区里的IM和推送开发的程序员们,对于进程保活这件事是越来越悲观,必竟系统对各种保活黑科技的限制越来越多了,想超越系统的挚肘,难度越来越大。

但保活这件事就像“激情”之后的余味,总是让人欲罢不能,想放弃又不甘心。那么,除了像《2020年了,Android后台保活还有戏吗?看我如何优雅的实现!》这样的正经白名单方式,不正经的“黑科技”是否还有发挥的余地?

答案是肯定的,“黑科技”仍发挥的余地。不是“黑科技”不行,而是技术没到位。

研究TIM的保活是一次偶然机会,发现在安全中心关闭了它的自启动功能的情况下, 一键清理、强力清理等各大招都无法彻底杀掉TIM,系统的自启动拦截也没能阻止TIM的永生。

一直以来,App 进程保活都是各大厂商,特别是头部应用开发商永恒的追求。毕竟App 进程死了,就什么也干不了了;一旦 App 进程死亡,那就再也无法在用户的手机上开展任何业务,所有的商业模型在用户侧都没有立足之地了。
早期的 Android 系统不完善,导致 App 侧有很多空子可以钻,因此它们有着有着各种各样的姿势进行保活。

譬如说在 Android 5.0 以前,App 内部通过 native 方式 fork 出来的进程是不受系统管控的,系统在杀 App 进程的时候,只会去杀 App 启动的 Java 进程;因此诞生了一大批“毒瘤”,他们通过 fork native 进程,在 App 的 Java 进程被杀死的时候通过 am命令拉起自己从而实现永生。

那时候的 Android 可谓是魑魅横行,群魔乱舞;系统根本管不住应用,因此长期以来被人诟病耗电、卡顿。同时,系统的软弱导致了 Xposed 框架、阻止运行、绿色守护、黑域、冰箱等一系列管制系统后台进程的框架和 App 出现。

不过,随着 Android 系统的发展,这一切都在往好的方向演变。Android 5.0 以上,系统杀进程以 uid 为标识,通过杀死整个进程组来杀进程,因此 native 进程也躲不过系统的法眼。

Android 6.0 引入了待机模式(doze),一旦用户拔下设备的电源插头,并在屏幕关闭后的一段时间内使其保持不活动状态,设备会进入低电耗模式,在该模式下设备会尝试让系统保持休眠状态。

Android 7.0 加强了之前鸡肋的待机模式(不再要求设备静止状态),同时对开启了 Project Svelte,Project Svelte 是专门用来优化 Android 系统后台的项目,在 Android 7.0 上直接移除了一些隐式广播,App 无法再通过监听这些广播拉起自己。

Android 8.0 进一步加强了应用后台执行限制:一旦应用进入已缓存状态时,如果没有活动的组件,系统将解除应用具有的所有唤醒锁。

Android 9.0 新增Adaptive Battery(电量自适应)可以最大限度的降低后台占用,从而提升电池续航能力
当你的麦克风、摄像头或传感器空闲时,应用程序将不再能够访问它们
JobScheduler可以使用运营商提供的网络状态信号来改善网络相关作业的处理
进一步改进了省电模式的功能并加入了应用待机分组,长时间不用的 App 会被打入冷宫;另外,系统监测到应用消耗过多资源时,系统会通知并询问用户是否需要限制该应用的后台活动。

Android 10.0 更严格的权限,并限制了数据应用程序的使用。有关如何在应用程序中支持这些功能的详细信息

另外,系统会限制未在前台运行的应用的某些行为,比如说应用的后台服务的访问受到限制,也无法使用 Mainifest 注册大部分隐式广播。

然而,道高一尺,魔高一丈。系统在不断演进,保活方法也在不断发展。

大约在 4 年前出现过一个 MarsDaemon,这个库通过双进程守护的方式实现保活,一时间风头无两。不过好景不长,进入 Android 8.0 时代之后,这个库就逐渐消亡。

一般来说,Android 进程保活分为两个方面:保持进程不被系统杀死。进程被系统杀死之后,可以重新复活。

随着 Android 系统变得越来越完善,单单通过自己拉活自己逐渐变得不可能了;因此后面的所谓「保活」基本上是两条路:1. 提升自己进程的优先级,让系统不要轻易弄死自己;2. App 之间互相结盟,一个兄弟死了其他兄弟把它拉起来。

当然,还有一种终极方法,那就是跟各大系统厂商建立 PY 关系,把自己加入系统内存清理的白名单;比如说国民应用微信。当然这条路一般人是没有资格走的。

大约一年以前,大神 gityuan 在其博客上公布了 TIM 使用的一种可以称之为「终极永生术」的保活方法;这种方法在当前 Android 内核的实现上可以大大提升进程的存活率。

这种保活思路的实现原理,并且提供了一个参考实现 Leoric

接下来就给大家分享一下这个终极保活黑科技的实现原理。保活的底层技术原理知己知彼,百战不殆。既然我们想要保活,那么首先得知道我们是怎么死的。

一般来说,系统杀进程有两种方法,这两个方法都通过 ActivityManagerService 提供:killBackgroundProcessesforceStopPackage在原生系统上,很多时候杀进程是通过第一种方式,除非用户主动在 App 的设置界面点击「强制停止」。

不过国内各厂商以及一加三星等 ROM 现在一般使用第二种方法。第一种方法太过温柔,根本治不住想要搞事情的应用。第二种方法就比较强力了,一般来说被 force-stop 之后,App 就只能乖乖等死了。因此,要实现保活,我们就得知道 force-stop 到底是如何运作的,可以跟踪一下系统的 forceStopPackage 这个方法的执行流程

连微信这种超级APP,也要拜倒在反保活的石榴裙下,允许后台启动太费电,不允许后台启动就收不到消息。

Android发现了一个保活野路子就堵一条,然而很多场景是有保活的强需求的,有木有考虑过我们开发者的感受,自己人何必为难自己人😭。

这是一个Android设计的不合理的地方,路子可以堵,但还是有必要留一个统一的保活接口的。这个接口由Google实现也好,厂商来实现也好,总好过现在很笨拙的系统自启动管理或者是JobScheduler。

从本质上来说,让应用开发者想尽各种办法去做保活,这个事情是没有意义的,保活的路子被封了,但保活还是需要做,保活的成本也提高了,简直浪费生命。Android的锅

保活是”应用的蜜罐,系统的肿瘤“,应用高保活率给自己赢得在线时长,甚至做各种应用想做而用户不期望的行为,给系统带来的是不必要的耗电,以及系统额外的性能负担。

保活技术阶段回顾

Android保活技术的进化,可以分为几个阶段。
第一个阶段:
也就是各种“黑科技”盛行的时代,比如某Q搞出来的1像素、后台无声音乐(某运动计步APP就干过)等等。
这个阶段的一些典型主要技术手段,可以看以下这几篇文章:

第二个阶段:
到了Android 6.0时代以后,Android保活就开始有点技术难度了,之前的各种无脑保活方法开始慢慢失效。
这个阶段的一些典型技术手段,可以读读以下这几篇文章:

第三个阶段:
进入Android 8.0时代,Android直接在系统层面进行了各种越来越严格的管控,可以用的保活手段越来越少,保活技术的发展方向已发分化为两个方向——要么用白名单的方式走正经的保活路径、要么越来越“黑”一“黑”到底(比如本文将要介绍的TIM的保活手段)。
这个阶段可以用的保活已经手段不多了,以下几篇盘点了目前的一些技术可行性现状等:

什么是保活?

保活就是在用户主动杀进程,或者系统基于当前内存不足状态而触发清理进程后,该进程设法让自己免于被杀的命运或者被杀后能立刻重生的手段。
保活是”应用的蜜罐,系统的肿瘤“,应用高保活率给自己赢得在线时长,甚至做各种应用想做而用户不期望的行为,给系统带来的是不必要的耗电,以及系统额外的性能负担。
保活方案一直就层出不穷,APP开发们不断地绞尽脑汁让自己的应用能存活得时间更长, 主要思路有以下两个。

提升进程优先级,降低被杀概率:

  • 1)比如监听SCREEN_ON/OFF广播,启动一像素的透明Activity;
  • 2)启动空通知,提升fg-service;
  • … …

进程被杀后,重新拉起进程:

  • 1)监听系统或者第3方广播拉起进程。但目前安全中心/Whetstone已拦截;
  • 2)Native fork进程相互监听,监听到父进程被杀,则通过am命令启动进程。force-stop会杀整个进程组,所以这个方法几乎很难生效了。

规纳如下

  • 提升进程优先级,降低被杀概率
  • 比如监听SCREEN_ON/OFF广播 启动一像素的透明Activity
  • 启动空通知,提升fg-service
  • 进程被杀后,重新拉起进程
  • 监听系统或者第3方广播拉起进程。目前安全中心/Whetstone已拦截
  • Native fork进程相互监听,监听到父进程被杀,则通过am命令启动进程。force-stop会杀整个进程组,几乎很难生效

如何实现

通过ioctl跟binder驱动交互,实现以最快的方式唤醒新的保活服务,最大程度防止保活失败。

关于binder

如果从Android层开始,一层套一层,会让我们迷茫不知所措。而ioctl才是驱动整个binder运行的引擎。
我们可以不用binder,可以实现另外一种类似binder的机制,通过ioctl可以觉察到整个的数据流向,本来进程通信表面意思就是进程间传递数据,不围绕数据的流向,常常会迷失在大量的接口类中不知所措。

使用binder

ioctl 根据第二个参数,确定BINDER WRITE READ,然后阅读相关的linux驱动源码,解析参数。
进程间传递数据发送方要把指针放在其中。binder_transaction_data,如果A进程要发送数据给B,那么填充方式就是在write_buffer中写入数据的指针。

  • 看上去binder是双工的,每一个进程通过调用ioctl既可以发送数据也可以收到数据,在ioctl执行前hook只能得到发送的数据,执行后即可得到接收的数据。所以binder的read_buffer就相当于socket的recvfrom write_buffer相当于socket的sendto*

  • 进程之间的通信和进程服务之间的通信好像并不完全一样,而大部分资料介绍的几乎都是进程和服务之间的通信。这里通过A向B发送数据来看下binder底层是如何工作的。*

  • 首先A生成write_buffer:binder_transaction_data,标记为BC_TRANSACTION,数据指针中保存实际的数据。调用ioctl,binder驱动保存数据后填充read_buffer为BR_TRANSACTION_COMPLETE,虽然数据还没有到B,但是已经到了驱动中,就算是成功了。*

  • 然后通过某些方法告诉B可以接收数据了,测试发现,B填充的read_buffer中填充了BR_TRANSACTION_COMPLETE write_buffer中填充了BC_FREE_BUFFER是否有意义*?然后B调用ioctl,binder驱动填充read_buffer* ,标记为BR_REPLY 表示成功接收到了数据*

进程保活原理

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/Leoric 这个方案是当检测到进程被杀时,会通过JNI的方式,调用Java层的方法来复活进程。为了实现稳定的保活,尤其是系统杀进程只给了5ms复活的机会,使用JNI这种方式复活进程现在达不到最优的效果。

分析TIM

执行命令adb shell ps | grep tencent.tim,可见TIM共有4个进程, 其父进程都是Zygote

1
2
3
4
5
ps | grep tencent.tim
u0_a146 27965 551 1230992 43964 SyS_epoll_ 00f6df4bf0 S com.tencent.tim:Daemon
u0_a146 27996 551 1252492 54032 SyS_epoll_ 00f6df4bf0 S com.tencent.tim:MSF
u0_a146 28364 551 1348616 89204 SyS_epoll_ 00f6df4bf0 S com.tencent.tim:mail
u0_a146 31587 551 1406128 147976 SyS_epoll_ 00f6df4bf0 S com.tencent.tim

一键清理看现象,排查初步怀疑

以下是对TIM执行一键清理后的日志:

1
2
3
4
5
6
12-21 21:12:20.265  1053  1075 I am_kill : [0,4892,com.tencent.tim:Daemon,5,stop com.tencent.tim: from pid 4617]
12-21 21:12:20.272 1053 1075 I am_kill : [0,5276,com.tencent.tim:mail,2,stop com.tencent.tim: from pid 4617]
12-21 21:12:20.305 1053 1075 I am_kill : [0,4928,com.tencent.tim,2,stop com.tencent.tim: from pid 4617]
12-21 21:12:20.330 1053 1075 I am_kill : [0,4910,com.tencent.tim:MSF,0,stop com.tencent.tim: from pid 4617]
12-21 21:13:59.920 1053 1466 I am_proc_start: [0,5487,10146,com.tencent.tim:MSF,service,com.tencent.tim/com.tencent.mobileqq.app.DaemonMsfService]
12-21 21:13:59.984 1053 1604 I am_proc_start: [0,5516,10146,com.tencent.tim,content provider,com.tencent.tim/com.tencent.mqq.shared_file_accessor.ContentProviderImpl]
1
2
3
4
5
6
7
# MIUI
``12-21 21:12:20.265 1053 1075 I am_kill : [0,4892,com.tencent.tim:Daemon,5,stop com.tencent.tim: from pid 4617]
``12-21 21:12:20.272 1053 1075 I am_kill : [0,5276,com.tencent.tim:mail,2,stop com.tencent.tim: from pid 4617]
``12-21 21:12:20.305 1053 1075 I am_kill : [0,4928,com.tencent.tim,2,stop com.tencent.tim: from pid 4617]
``12-21 21:12:20.330 1053 1075 I am_kill : [0,4910,com.tencent.tim:MSF,0,stop com.tencent.tim: from pid 4617]
``12-21 21:13:59.920 1053 1466 I am_proc_start: [0,5487,10146,com.tencent.tim:MSF,service,com.tencent.tim``/com``.tencent.mobileqq.app.DaemonMsfService]
``12-21 21:13:59.984 1053 1604 I am_proc_start: [0,5516,10146,com.tencent.tim,content provider,com.tencent.tim``/com``.tencent.mqq.shared_file_accessor.ContentProviderImpl]

Force-stop是系统提供的杀进程最为彻底的方式,详见文章Android进程绝杀技–forceStop。从日志可以发现一键清理后TIM的4个进程全部都已被Force-stop。但进程com.tencent.tim:MSF立刻就被DaemonMsfService服务启动过程而拉起。

问题1:安全中心已配置了禁止TIM的自启动, 并且安全中心和系统都有对进程自启动以及级联启动的严格限制,为何会有漏网之鱼?

怀疑1: 是否安全中心自启动没能有效限制,以及微信/QQ跟TIM有所级联,比如com.tencent.mobileqq.app.DaemonMsfService服务名中以com.tencent.mobileqq(QQ的包名)开头,经过dumpsys以及反复验证后排除了这种可能性,如下:

1
2
3
4
5
6
12-21 21:12:20.266  1053  1075 I AutoStartManagerService: MIUILOG- Reject RestartService packageName :com.tencent.tim uid : 10146
12-21 21:12:20.291 1053 1075 I AutoStartManagerService: MIUILOG- Reject RestartService packageName :com.tencent.tim uid : 10146
12-21 21:12:20.323 1053 1075 I AutoStartManagerService: MIUILOG- Reject RestartService packageName :com.tencent.tim uid : 10146
12-21 21:12:20.323 1053 1075 I AutoStartManagerService: MIUILOG- Reject RestartService packageName :com.tencent.tim uid : 10146
12-21 21:12:20.331 1053 1075 I AutoStartManagerService: MIUILOG- Reject RestartService packageName :com.tencent.tim uid : 10146
12-21 21:12:20.332 1053 1075 I AutoStartManagerService: MIUILOG- Reject RestartService packageName :com.tencent.tim uid : 10146

怀疑2: 是否在TIM进程被杀后, 收到BinderDied后的死亡回调过程中将Service再次拉起,这个情况也很快就被排除, 因为force-stop这种冷面强力杀手, 并不会等到死亡回调再去清理进程相关信息,而是直接连根拔起,并不会走到AMS的死亡回调。

怀疑3: TIM设置了alarm机制,在callApp为空符合特征, 但经过分析这里就是普通的startService, 非startServiceInPackage(), 也排除了这种可能性

1
2
3
//启动DaemonAssistService时,callApp为空,只有通过PendingIntent方式才可能出现这种情况
12-21 21:56:54.653 3181 3195 I am_start_service: [-1,NULL,10146,com.tencent.tim:Daemon,com.tencent.tim/com.tencent.mobileqq.app.DaemonAssistService,{cmp=com.tencent.tim/com.tencent.mobileqq.app.DaemonAssistService}]
12-21 21:56:56.666 3181 3827 I am_start_service: [-1,NULL,10146,com.tencent.tim:MSF,com.tencent.tim/com.tencent.mobileqq.app.DaemonMsfService,{cmp=com.tencent.tim/com.tencent.mobileqq.app.DaemonMsfService}]

既然排除以上3种可能,直接上断点来看看吧

Android Studio断点分析

一上断点就发现了意外的一幕:

tim_1

问题2:startService()的callingPid怎么可能等于0?

分析callingPid=0

为什么说上面是意外的一幕呢?这需要对binder底层原理有一定深入理解,才能看出一些端倪,那就是此处callingPid=0是不合理逻辑的。很多人可能不太理解为何就不合乎逻辑, 这要从Binder原理说起, startService()这个Binder call是属于同步binder调用, 对于binder调用过程,只有异步Binder调用的情况下callingPid=0才会为空, 因为不需要reply应答数据给发送binder请求的那一端。 但如果是同步的,则必须要给出callingPid,否则无法将应答数据回传给发送方。 这是由Binder Driver所决定的,见如下Binder Driver核心代码:

(1)Binder发起端:根据当前ONE_WAY来决定是否设置from线程

1
2
3
4
5
6
7
8
9
binder_transaction(...) {
...
if (!reply && !(tr->flags & TF_ONE_WAY))
t->from = thread;
else
t->from = NULL;
}
...
}

(2)Binder接收端: 根据from线程是否为空, 来决定sender_pid是否为0. 这便是Java层所说的callingPid

1
2
3
4
5
6
7
8
9
10
11
12
13
binder_thread_read(...) {
...
t_from = binder_get_txn_from(t);
if (t_from) {
struct task_struct *sender = t_from->proc->tsk;

tr.sender_pid = task_tgid_nr_ns(sender,
task_active_pid_ns(current));
} else {
tr.sender_pid = 0;
}
...
}

上述代码表明: 同步的Binder调用的情况下则callingPid必定不等于0

下面告诉大家如何看一个Binder调用是否同步, 如下图最后一个参数代表的是FLAG_ONEWAY值,等于0则代表的是同步, 等于1则代表的是异步。

tim1_2

以上代码是framework的框架代码,startService最终都会调用到这里来,所以callingPid必然是不可能出现为0的情况,让我们看不透到底哪个进程把com.tencent.tim:Daemon拉起的。

揭秘

从前面的分析来看callingPid是不可能为0的, 但从结果来看的确是0, 出现矛盾就一定有反常规存在,难道是存在同步的Binder调用,也存在同时callingPid=0的case?答案是No.

从源码角度来看是没有这种可能性存在,后面再进一步追踪flags值的变化,从如下的flags=17,可以确定的是此处的startService的binder call是ONE_WAY的,这就可以确定的确是发起了异步的Binder调用,代码如下:

tim_2

虽然callingPid=0,但从callUid=10146可以确定的一点是com.tencent.tim:Daemon进程是被来自TIM应用自身的某个进程所拉起的。

小结

通过前面的初步分析,先整理一下思路,有以下初步结论:

  • TIM至少有4个进程,且都是由Zygote进程fork, 保活是通过startService被拉起
  • 排除 安全中心的对TIM限制自启动功能失效的情况
  • 排除 TIM进程被杀后的Binder死亡回调过程通过Service重新拉起进程
  • 排除 alarm机制 拉起进程
  • 从callingPid=0,可以得出TIM没有走常规的系统框架中提供的startService()接口来启动服务,而是自定义的方式
  • 从callingUid=10146, 可以得出TIM救活自己的方式,是通过TIM自身,而非系统或者第三方应用拉起

到此不难得出一个猜想: 首先TIM应用能做到监听应用进程被杀的情况, 其次是TIM应用自身替换掉或者自定义一套Binder调用,主动跟Binder驱动进行数据交互。

深入分析

寻求规律

TIM应用有4个进程,不断反复地尝试杀TIM每一个进程后,观察自启动的情况后。 发现了一个规律:com.tencent.tim:Daemon和com.tencent.tim:MSF进程任一被杀,都会先把对方进程拉起,然后跟着自杀后,再重启。

接下来就把范围锁定在这两个进程,然后来tracing信号处理情况。

从signal角度来分析

打开signal开关

1
2
echo 1 > /d/tracing/events/signal/enable
echo 1 > /d/tracing/tracing_on

执行如下命令抓取tracing log

1
cat /d/tracing/trace_pipe

日志如下:

1
2
3
4
5
6
7
8
//通过adb shell kill -9 10649,  将com.tencent.tim:Daemon进程杀掉
sh-22775 [000] d..2 18844.276419: signal_generate: sig=9 errno=0 code=0 comm=cent.tim:Daemon pid=10649 grp=1 res=0
// 线程Thread-89 将tencent.tim:MSF进程也杀掉了
Thread-89-10712 [000] dn.2 18844.340735: signal_generate: sig=9 errno=0 code=0 comm=tencent.tim:MSF pid=10669 grp=1 res=0
Binder:14682_4-14845 [000] d..2 18844.340779: signal_deliver: sig=9 errno=0 code=0 sa_handler=0 sa_flags=0
Binder:14682_1-14694 [000] d..2 18844.341418: signal_deliver: sig=9 errno=0 code=0 sa_handler=0 sa_flags=0
Binder:14682_2-14697 [000] d..2 18844.345075: signal_deliver: sig=9 errno=0 code=0 sa_handler=0 sa_flags=0
tencent.tim:MSF-14682 [000] dn.2 18844.345115: signal_deliver: sig=9 errno=0 code=0 sa_handler=0 sa_flags=

从这里,可以发现com.tencent.tim:Daemon进程是由于其中一个线程Thread-89所杀,但从名字来看Thread-xxx,很明显是系统自动生成的编号。

问题3:进程内的名叫“Thread-89”的线程具有什么特点,如何做到把进程杀掉?
从下面的截图,可以看出MSF进程的这个特殊的线程当前在执行flock_lock操作,这个明显是一个文件加锁的操作, 这个方法很快就引起了我的注意。同理Daemon进程也有一个这样的线程, 离真相有近了一步。

tim_3_2

tim_3

再来看看调用栈情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Cmd line: com.tencent.tim:Daemon
"Thread-89" prio=10 tid=12 Native
| group="main" sCount=1 dsCount=0 obj=0x32c07460 self=0xf3382000
| sysTid=10712 nice=-8 cgrp=bg_non_interactive sched=0/0 handle=0xee824930
| state=S schedstat=( 44972457 14188383 124 ) utm=1 stm=3 core=0 HZ=100
| stack=0xee722000-0xee724000 stackSize=1038KB
| held mutexes=
kernel: __switch_to+0x74/0x8c
kernel: flock_lock_file_wait+0x2a4/0x318
kernel: SyS_flock+0x19c/0x1a8
kernel: el0_svc_naked+0x20/0x28
native: #00 pc 000423d4 /system/lib/libc.so (flock+8)
native: #01 pc 0000195d /data/app/com.tencent.tim-1/lib/arm/libdaemon_acc.so (_Z9lock_filePc+64)
...
native: #29 pc 0000191f /data/app/com.tencent.tim-1/lib/arm/libdaemon_acc.so (_Z9lock_filePc+2)
native: #30 pc 0000191d /data/app/com.tencent.tim-1/lib/arm/libdaemon_acc.so (_Z9lock_filePc)
native: #31 pc 0000191b /data/app/com.tencent.tim-1/lib/arm/libdaemon_acc.so (_Z18notify_and_waitforPcS_+102)
...
native: #63 pc 000018d1 /data/app/com.tencent.tim-1/lib/arm/libdaemon_acc.so (_Z18notify_and_waitforPcS_+28)
at com.libwatermelon.WaterDaemon.doDaemon2(Native method)
at com.libwatermelon.strategy.WaterStrategy2$2.run(WaterStrategy2.java:111)

从这个线程的调用栈中的名字, notify_and_waitfor让我想到了这极有可能用于监听文件来获知进程是否存活。 为了进一步观察这个特殊线程的工作使命, 这里还不需要GDB, 祭出strace大招应该就差不多

利用strace分析

strace -CttTip 22829 -CttTip 22793

结果如下:

tim7

flock基础知识简介:

flock是Linux文件锁,用于多个进程同时操作同一个文件时,通过加锁机制保证数据的完整,flock使用场景之一,便是用于检测进程是否存在。flock属于建议性的锁,而非强制性锁,只是进程可以直接操作正被另一个进程用flock锁住的文件, 原因在于flock只检测文件是否加锁,内核并不会强制阻塞其他进程的读写操作,这便是建议性锁的内核策略。

方法原型: int flock(int fd, int operation)

第一个参数是文件描述符,第二参数指定锁的类型,有以下3个可选值:

  • LOCK_SH: 共享锁, 同一时间运行多个进程同时持有该共享锁
  • LOCK_EX: 排它锁,只允许一个进程持有该锁
  • LOCK_UN: 移除该进程的该文件所持有的锁

从strace可以推测出:com.tencent.tim:MSF进程的监控线程执行排它锁LOCK_EX类型的flock,尝试去获取某个文件,而该文件已被com.tencent.tim:Daemon进程所持有,所以MSF进程会被阻塞知道锁的释放,而一旦Daemon进程被杀,系统就会回收所有资源(包括文件),这是Linux内核负责完成的。

当Daemon进程的文件被回收,就会释放flock, 从而MSF进程可以获取该锁,从而吐出“lock file success”的信息。 MSF得知Daemon进程被杀,然后执行一行ioctl(11, BINDER_WRITE_READ, 0xffffffffee823ed0) = 0 <0.000867> ,

这个应该就是TIM进程自身实现了一套执行startService的Binder调用,向Binder驱动发送 BINDER_WRITE_READ的ioctl命令。 再然后发送kill SIGKILL将自身MSF进程杀掉,同样的道理可以再次被拉起。

分析到这里,看执行了writev操作, 应该就是Log操作, 有一个关键词到Watermelon吸引了我的注意力, 搜索Watermelon关键词,果然找到新的一片天地。

TIM日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//旧的MSF进程
24538 24562 D Watermelon: lock file success >> /data/user/0/com.tencent.tim/app_indicators/indicator_p2
24538 24562 E Watermelon: Watch >>>>Daemon<<<<< Daed !!
24538 24562 E Watermelon: java_callback:onDaemonDead
24538 24562 V Watermelon: onDaemonDead
24576 24576 D Watermelon: lock file success >> /data/user/0/com.tencent.tim/app_indicators/indicator_d1
24576 24576 E Watermelon: Watch >>>>Daemon<<<<< Daed !!
24576 24576 E Watermelon: process exit
//新daemon进程
25103 25103 V Watermelon: initDaemon processName=com.tencent.tim:Daemon
25103 25103 E Watermelon: onDaemonAssistantCreate
25134 25134 D Watermelon: start daemon24=/data/user/0/com.tencent.tim/app_bin/daemon2
//app_d进程
25137 25137 D Watermelon: pipe read datasize >> 316 <<
25137 25137 D Watermelon: indicator_self_path >> /data/user/0/com.tencent.tim/app_indicators/indicator_d1
25137 25137 D Watermelon: observer_daemon_path >> /data/user/0/com.tencent.tim/app_indicators/observer_p1
25137 25137 I Watermelon: sIActivityManager==NULL
25137 25137 I Watermelon: BpActivityManager init
//新daemon
25103 25120 D Watermelon: start try to lock file >> /data/user/0/com.tencent.tim/app_indicators/indicator_p2
25103 25120 D Watermelon: lock file success >> /data/user/0/com.tencent.tim/app_indicators/indicator_p2
25137 25137 I Watermelon: BpActivityManager init end
//app_d进程
25137 25137 D Watermelon: start try to lock file >> /data/user/0/com.tencent.tim/app_indicators/indicator_d1
25137 25137 D Watermelon: lock file success >> /data/user/0/com.tencent.tim/app_indicators/indicator_d1
//新MSF进程
25119 25119 V Watermelon: initDaemon processName=com.tencent.tim:MSF
25119 25119 V Watermelon: mConfigurations.PERSISTENT_CONFIG.PROCESS_NAME=com.tencent.tim:MSF
25119 25119 E Watermelon: onPersistentCreate
25153 25153 D Watermelon: start daemon24=/data/user/0/com.tencent.tim/app_bin/daemon2
25119 25144 D Watermelon: pipe write len=324
25159 25159 D Watermelon: pipe read datasize >> 324 <<
25159 25159 D Watermelon: indicator_self_path >> /data/user/0/com.tencent.tim/app_indicators/indicator_p1
25159 25159 D Watermelon: observer_daemon_path >> /data/user/0/com.tencent.tim/app_indicators/observer_d1
25159 25159 I Watermelon: sIActivityManager==NULL
25159 25159 I Watermelon: BpActivityManager init
25119 25144 D Watermelon: start try to lock file >> /data/user/0/com.tencent.tim/app_indicators/indicator_d2
25119 25144 D Watermelon: lock file success >> /data/user/0/com.tencent.tim/app_indicators/indicator_d2
25159 25159 I Watermelon: BpActivityManager init end
//各进程进入监听就绪状态
25159 25159 D Watermelon: start try to lock file >> /data/user/0/com.tencent.tim/app_indicators/indicator_p1
25159 25159 D Watermelon: lock file success >> /data/user/0/com.tencent.tim/app_indicators/indicator_p1
25119 25144 E Watermelon: Watched >>>>OBSERVER<<<< has been ready...
25119 25144 D Watermelon: start try to lock file >> /data/user/0/com.tencent.tim/app_indicators/indicator_p2
25159 25159 E Watermelon: Watched >>>>OBSERVER<<<< has been ready...
25159 25159 D Watermelon: start try to lock file >> /data/user/0/com.tencent.tim/app_indicators/indicator_d1
25137 25137 E Watermelon: Watched >>>>OBSERVER<<<< has been ready...
25137 25137 D Watermelon: start try to lock file >> /data/user/0/com.tencent.tim/app_indicators/indicator_p1
25103 25120 E Watermelon: Watched >>>>OBSERVER<<<< has been ready...
25103 25120 D Watermelon: start try to lock file >> /data/user/0/com.tencent.tim/app_indicators/indicator_d2

再从其中的截取核心片段:

1
2
3
25159 25159 I Watermelon: BpActivityManager init
25119 25144 D Watermelon: start try to lock file >> /data/user/0/com.tencent.tim/app_indicators/indicator_d2
25119 25144 D Watermelon: lock file success >> /data/user/0/com.tencent.tim/app_indicators/indicator_d2

不难看出:

  • TIM自身通过向servicemanager查询来获取AMS的代理BpActivityManager, 然后自己去写startService通信过程的数据
  • TIM通过两个进程通过flock来相互监听对方进程存活状态
  • 监听的文件有比如:/data/user/0/com.tencent.tim/app_indicators/indicator_d2

indicator文件

进一步查看TIM所监听的路径下/data/user/0/com.tencent.tim/app_indicators/, 发现有4个监听文件:

tim4

问题4:为何需要4个indicator文件?

进一步延伸:通过查看flock,再次发现新大陆,原来除了Daemon和MSF进程各有一个监听文件的线程, 还有两个由init进程作为父进程的app_d进程也监听文件

1
2
3
4
5
adb shell ps -t | grep -i flock
u0_a146 10668 10649 1143304 85876 flock_lock 00f6e1e3d8 S Thread-85
u0_a146 10712 10669 1158552 89664 flock_lock 00f6e1e3d8 S Thread-89
u0_a146 10687 1 12768 564 flock_lock 00f73113d8 S app_d
u0_a146 10717 1 12768 560 flock_lock 00f74353d8 S app_d

不难发现,以上几个进程/线程的uid=10146,进一步通过ps命名查找。

再一次刷新对TIM应用的认识: 原来TIM有6个进程,其中还有2个是挂在init进程下,名字跟tencent没有关系,差点错过了这两个特殊的进程

tim5_2

这两个app_d进程其实也是做着同样的相互监听的工作, 应该是备选方案。当有概率恰巧Daemon和MSF进程同时被杀而来不及互保的情况下,那么可以走紧急通道app_d 将TIM进程拉起。可谓是暗藏玄机, 6个进程中有4个进程可以相互保活, 以保证TIM进程永生。

问题5: 这4个进程到达是什么如何相互监听的呢?

通过不断分析被杀与重启前后的规律与特征,得出进程与监听文件的关系图:

tim5_3

进一步揭露面纱,得到如下结论:

  • Daemon与MSF两进程等待对方所持有的锁,两个app_d进程相互等待对方所持有的锁
  • app_d1进程被杀, 则app_d2观察后通过拉起DaemonMsfService服务来启动MSF进程,然后跟着被杀
  • app_d2进程被杀,则app_d1观察后通过拉起DaemonAssistService服务来启动Daemon进程,然后跟着被杀
  • Daemon与MSF两进程, 如果杀掉其中一个,则另个一个进程观察后通过拉起服务方式来启动对方进程,然后跟着被杀;然后app_d两个进程也跟着重启

另外,猜想:监测indicator_p1和indicator_p2的两个进程有关联,indicator_d1和indicator_d2的进程有关联,后面会验证。

到这里又有出现新的疑问,Daemon进程死后,MSF进程通过flock能监测到该事件,可是app_d进程又是如何得知的呢? app_d得知之后,又为何要再次自杀重启?

从cgroup角度来分析

1
2
3
4
5
6
cat cgroup.procs
10649 //Daemon
10687 //app_d
cat cgroup.procs
10669 //MSF
10717 //app_d

从而,进一步获取更多关于TIM深层次的关联,通过查看cgroup发现,Daemon和app_d1是同一个group的, MSF和app_d2是同一个group的。

问题6: app_d到底是如何创建出来?又是如何成为init进程的子进程的?

从进程创建与退出的角度来看看来看

1
2
3
4
5
6
7
// 5170(MSF进程) --> 5192 --> 5201(退出) --> 5211(存活)
tencent.tim:MSF-5170 [001] ...1 55659.446062: sched_process_fork: comm=tencent.tim:MSF pid=5170 child_comm=tencent.tim:MSF child_pid=519
Thread-300-5192 [000] ...1 55659.489621: sched_process_fork: comm=Thread-300 pid=5192 child_comm=Thread-300 child_pid=5201
<...>-5201 [003] ...1 55659.501074: sched_process_exec: filename=/data/user/0/com.tencent.tim/app_bin/daemon2 pid=5201 old_pid=5201
daemon2-5201 [009] ...1 55659.533492: sched_process_fork: comm=daemon2 pid=5201 child_comm=daemon2 child_pid=5211
daemon2-5201 [009] ...1 55659.535169: sched_process_exit: comm=daemon2 pid=5201 prio=120
daemon2-5201 [009] d..3 55659.535341: signal_generate: sig=17 errno=0 code=262145 comm=Thread-300 pid=5192 grp=1 res=1

说明:其中一个app_d进程是由MSF进程,通过两次fork,然后父进程退出,从而成为了孤儿进程,然后托孤给init进程,这是Linux进程机制所保证的。 同理,另一个app_d进程是由Daemon进程所fork。到这里,那么总算是认清的app_d的由来。 app_d是由于cgroup关联所以可以得知Daemon进程的情况。 关于重启的原因是为了重新建立互动的关系

问题7:为何单杀daemon,会牵连app_d进程被杀,这是什么原理?

解答:从杀进程的日志上来是调用killProcessGroup()杀进程,可事实上adb只调用kill -9 pid的方式,单杀一个进程,怎么就牵连了app_d进程。 这是由于当daemon进程被杀后,死亡回调会回来后,在binderDied()的过程执行了killProcessGroup()。

如果从Linux内核层面,研究过Binder死亡回调机制的童鞋,到这里还就会有想到一个新的疑问如下:

问题8:app_d是由daemon进程间接fork出来的, 会共享binder fd,所以即便daemon进程被杀,死亡回调也不会触发,这又是何触发的呢?

解答:由于app_d进程被fork后,马上执行了exec()系的函数, 而在ProcessState打开Binder驱动的时候, 有一个非常重要的flag, 那就是O_CLOEXEC。

采用O_CLOEXEC方式打开的问题,当新创建的进程调用exec()函数成功后,文件描述符会自动关闭, 代码如下:

tim6_1

剖根问底

问题9:TIM到底对Binder框架做了什么级别的修改?这4个互保进程,既然callingPid=0,有没有办法知道到底是由谁拉起谁的?

前面既然说了,TIM强行修改了ONEWAY的方式。可以去掉该flags, 为了调试,这里就针对TIM,并且code=34(即START_SERVICE_TRANSACTION), 并且修改flag的case下:

tim_6

从实验结果来看,通过修改IPCThreadState.cpp代码, 完成control住了 TIM的所有修改, 这里可以说明:

TIM分别在Java层和Native层,主动向ServiceManager进程查询AMS后,获取BpActivityManager代理对象,然后继续使用框架中的IPCThreadState跟Binder驱动交互,并没有替换掉libbinder.so。

其实,还可以更高级的玩法,连IPCThreadState这些框架通信代码也不使用, 彻底地去自定义Binder交互代码,类似于servicemanager的方式。可以自己封装ioctl(),直接talkWithDriver。TIM保活还有改进空间, 提供保活变种方案,这样的话,上面的调试代码也拦截不了其对flags修改为ONEWAY的过程。 即使如此,一切都在Control之中, 完全可以在Binder Driver中拦截再定位其策略, 玩得再高级也主要活动在用户态, 内核态的策略还是相对安全的, 此所谓“魔高一座,道高一尺”。

另外,通过增加上面的临时代码,再次多次实验对比,可以得出如下关系图:

tim

二度fork是指前面介绍了,fork后再fork,然后托孤,无论如何跟最初的进程都属于同一个group,有着级联被杀关系。

  • 杀掉Daemon进程,则MSF进程观察到会去拉起Daemon进程; 同时app_d1因为同一个group而被杀,则app_d2进程观察到也拉起Daemon进程,这就是双保险;
  • 杀掉app_d1进程, 则app_d2进程观察到会拉起MSF进程;
  • 直接force-stop进程, 则6个进程都会被杀,只是杀的过程并非所有进程同一时刻点被杀,而是有前后顺序,所以造成能自启。

分析思路归纳

  • 先有了初步分析过程中对一些常规套路的可能性的排除,并嗅到callingPid=0的异常举动
  • 沿着蛛丝马迹,不断反复尝试杀进程,从中寻找更多的规律,不断地向自己提出疑问
  • 结合signal,strace, traces,ps,binder,linux,kill等技能 不断地解答自己的疑惑

解系统层的问题,更像是侦探破案的感觉,要有敏锐的嗅觉,抓住蛛丝马迹,加上”大胆猜想,小心验证“ , 终究能找到案件的真相。 此所谓”点动成线,线动成面,面动成体“, 从零星的点滴勾画出全方面立体化的真相。

归纳下,主要提出过这些疑惑:
问题1:安全中心已配置了禁止TIM的自启动, 并且安全中心和Whetstone都有对进程自启动以及级联启动的严格限制, 为何会有漏网之鱼?
问题2:startService()的callingPid怎么可能等于0?
问题3:进程内的名叫“Thread-89”的线程具有什么特点,如何做到把进程杀掉?
问题4:为何需要4个indicator文件?
问题5:这4个进程到达是什么如何相互监听的呢?
问题6:app_d到底是如何创建出来?又是如何成为init进程的子进程的?
问题7:为何单杀daemon,会牵连app_d进程被杀,这是什么原理?
问题8:app_d是由daemon进程间接fork出来的, 会共享binder fd,所以即便daemon进程被杀,死亡回调也不会触发,这又是何触发的呢?
问题9:TIM到底对Binder框架做了什么级别的修改?这4个互保进程,既然callingPid=0,有没有办法知道到底是由谁拉起谁的?

总结以上理论

保活技术点

  • 通过flock的文件排它锁方式来监听进程存活状态
  • 先采用一对普通的进程Daemon和MSF相互监听文件的方式来获得对方进程是否存活的状态;
  • 同时再采用一对退孤给init进程的app_d进程相互监听文件的方式来获得对方进程是否存活的状态; 而这两个进程都有间接由Daemon和MSF进程所fork而来;双重保险
  • 不采用系统框架中startService的Binder框架代码,而是自身在Native层通过自己去查询获取BpActivityManager代理对象, 然后自己实现startService接口,并修改为ONEWAY的binder调用,既增加分析问题的难度,也进一步隐藏自身策略;
  • 当监听进程死亡,则通过自身实现的StartService的Binder call去拉起对方进程,系统对于这种方式启动进程并没有拦截机制。

这种flock的方式至少比网上常说的通过循环监听的方式,要强很多;比往常的互保更厉害的是TIM共有6个进程(说明:使用过程也还会创建一些进程),其中4个进程,形成两组互动进程,其中一组利用Linux进程托孤原理,可谓是隐藏得很深来互保,进一步确保进程永生; 当然,进程收到signal信号后,如果恰巧这四个进程在同一个时刻点退出,那么还是有概率会被杀。 不走系统框架代码,自己去实现启动服务的binder call也是一大亮点,不过还有更高级的玩法,直接封装ioctl跟驱动交互。之前针对这个问题,做过反保活方案,后来为了某些功能缘故又放开对这个的限制,这里就不再继续展开了。

附录:有关IM/推送的进程保活/网络保活方成的文章汇总

应用保活终极总结(一):Android6.0以下的双进程守护保活实践
应用保活终极总结(二):Android6.0及以上的保活实践(进程防杀篇)
应用保活终极总结(三):Android6.0及以上的保活实践(被杀复活篇)
Android进程保活详解:一篇文章解决你的所有疑问
Android端消息推送总结:实现原理、心跳保活、遇到的问题等
深入的聊聊Android消息推送这件小事
为何基于TCP协议的移动端IM仍然需要心跳保活机制?
微信团队原创分享:Android版微信后台保活实战分享(进程保活篇)
微信团队原创分享:Android版微信后台保活实战分享(网络保活篇)
移动端IM实践:实现Android版微信的智能心跳机制
移动端IM实践:WhatsApp、Line、微信的心跳策略分析
Android P正式版即将到来:后台应用保活、消息推送的真正噩梦
全面盘点当前Android后台保活方案的真实运行效果(截止2019年前)
一文读懂即时通讯应用中的网络心跳包机制:作用、原理、实现思路等
融云技术分享:融云安卓端IM产品的网络链路保活技术实践
正确理解IM长连接的心跳及重连机制,并动手实现(有完整IM源码)
2020年了,Android后台保活还有戏吗?看我如何优雅的实现!
史上最强Android保活思路:深入剖析腾讯TIM的进程永生技术
Android进程永生技术终极揭密:进程被杀底层原理、APP对抗被杀技巧

实际实现

Java 层复活进程

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

1
2
3
4
5
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:

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

封装启动Service的Parcel:

1
2
3
4
5
6
7
8
9
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:

1
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层。

1
2
3
4
5
6
7
8
9
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变量:

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

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

1
2
3
4
5
6
7
8
9
10
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指针:

1
Parcel *parcel = (Parcel *) parcel_ptr;

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

提取libbinder.so

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

script
1
2
3
4
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。

script
1
2
3
4
5
# osx下解压img
brew install simg2img ext4fuse
simg2img ./data.img ./data_raw.img
ext4fuse data_raw.img ./data
Windows下使用BrandZip或者好压即可
1
2
3
4
5
6
7
8
9
10
11
12
13
ls -l binder_libs
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修改:

1
2
3
4
5
6
7
8
# 链接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对象

Parcel能直接拿到数据地址,并提供了构造方法。所以我们可以通过管道把Parcel数据传输到其它进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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:

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

传输Parcel数据

1
2
3
4
5
6
// 获取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是一个linux标准方法,那么我们就直奔主题看看,binder是什么,ioctl怎么跟binder driver通信。

Binder介绍

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

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

img

注册服务、获取服务、使用服务,都是需要经过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接口是一个独立的系统调用,通过它用户空间可以跟设备驱动沟通了。函数原型:

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

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

  • fd 是用户程序打开设备时使用open函数返回的文件描述符
  • request是用户程序对设备的控制命令
  • 后面的省略号是一些补充参数,和cmd的意义相关

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

1
2
3
4
5
6
7
8
9
10
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,
};

它的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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常量的定义。

[惊不惊喜]
[意不意外]
[手动滑稽]

1
2
3
4
5
6
7
8
9
10
#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):

1
2
3
4
5
ProcessState::ProcessState(const char *driver)
: mDriverName(String8(driver))
, mDriverFD(open_driver(driver))
, ...
{}

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

1
2
3
4
5
6
7
8
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在哪里实例化呢?

1
2
3
4
5
6
7
sp<ProcessState> ProcessState::self() {
if (gProcess != nullptr) {
return gProcess;
}
gProcess = new ProcessState(kDefaultDriver);
return gProcess;
}

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

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

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

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

看看文件权限:

script
1
2
3
adb shell
ls -l /dev/binder
crw-rw-rw- 1 root root 10, 49 1972-07-03 18:46 /dev/binder

可以看到,/dev/binder设备对所有用户可读可写。
再看看,SELinux权限:

script
1
2
ls -Z /dev/binder
u:object_r:binder_device:s0 /dev/binder

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

script
1
allow domain binder_device:chr_file rw_file_perms;

也就是所有domain对binder的字符设备有读写权限,而普通应用属于domain。
既然这样,*肝它!*

写个Demo试一下

验证一下上面的想法,看看ioctl给binder driver发数据好不好使。
1、打开设备

1
2
3
4
5
6
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

1
2
3
4
5
6
7
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、查看日志

script
1
2
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对象。

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

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

1
2
3
4
5
6
7
8
9
10
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方法调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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调用的时序图:

img

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

1
2
3
4
5
6
7
8
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的步骤:

1
2
3
4
5
6
// 获取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的单例。

1
2
3
4
5
6
7
8
9
sp<IServiceManager> defaultServiceManager() {
if (gDefaultServiceManager != NULL) return gDefaultServiceManager;
while (gDefaultServiceManager == NULL) {
gDefaultServiceManager = interface_cast<IServiceManager>(
ProcessState::self()->getContextObject(NULL));
}
}
return gDefaultServiceManager;
}

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

1
2
3
4
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++方法是:

1
sp<IBinder> binder = serviceManager->getService(String16("activity"));

BpServiceManager.getService:

1
2
3
4
5
virtual sp<IBinder> getService(const String16& name) const {
sp<IBinder> svc = checkService(name);
if (svc != NULL) return svc;
return NULL;
}

BpServiceManager.checkService:

1
2
3
4
5
6
7
8
9
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()方法搞了什么鬼:

1
2
3
4
5
6
7
8
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方法的定义:

1
2
3
4
5
6
7
8
9
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:

1
2
3
4
5
6
7
8
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句柄:

1
2
const flat_binder_object* flat = reply->readObject(false);
return flat->handle;

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

1
2
3
4
5
6
7
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对象的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
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中的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 打开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加上一些日志,打印结果如下:

script
1
2
D/KeepAlive: BR_DEAD_REPLY
D/KeepAlive: write_transact failed, error=-32

reply中始终读不到数据。这是为什么?现在已经不报Invalid argument的错误了,说明Parcel数据格式可能没问题了。但是不能成功把数据写给ServiceManager,或者ServiceManager返回的数据不能成功写回来。
想到Binder是基于内存的一种IPC机制,数据都是对的,那问题就出在内存上了。这就要说到Binder基本原理以及Binder内存转移关系。

Binder基本原理:
img

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

数据发送方写数据时,内核态通过copy_from_user()方法把它的数据拷贝到数据接收方映射(mmap)到内核空间的地址上。这样,只需要一次数据拷贝过程,就可以完成进程间通信。
由此可知,没有这块内核空间是没办法完成IPC通信的。Demo失败的原因就是缺少了一个mmap过程,以映射一块内存到内核空间。修改如下:

1
2
3
#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);

日志:

script
1
2
D/KeepAlive: BR_REPLY
D/KeepAlive: write_transact handle is:1

搞定!

最后

当然,这个保活的办法虽然很强,但现在也只能活在模拟器和部分机型中了。
尤其是MIUI12挂了,最新的EMUI挂了

说一下我的方法论。

1、确定问题和目标。

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

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

2、时序图很重要。

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

3、实践出真知。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 nathanwriting@126.com

文章标题:Android保活技术实现

字数:16.7k

本文作者:Nathaniel

发布时间:2020-09-19, 15:28:59

最后更新:2023-11-06, 22:59:18

原始链接:http://example.com/2020/09/19/keep-app-alive/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

×

喜欢就点赞,疼爱就打赏