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

continue;

}

try {

pm.setPackageStoppedState(packageName, true, user);

} catch (RemoteException e) {

} catch (IllegalArgumentException e) {

Slog.w(TAG, "Failed trying to unstop package "

  • packageName + ": " + e);

}

if (mUserController.isUserRunning(user, 0)) {

// 根据 UID 和包名杀进程

forceStopPackageLocked(packageName, pkgUid, "from pid " + callingPid);

finishForceStopPackageLocked(packageName, pkgUid);

}

}

}

} finally {

Binder.restoreCallingIdentity(callingId);

}

}

在这里我们可以知道,系统是通过 uid为单位 force-stop 进程的,因此不论你是 Native 进程还是 Java 进程,force-stop 都会将你统统杀死。我们继续跟踪 forceStopPackageLocked这个方法:

final boolean forceStopPackageLocked(String packageName, int appId,

boolean callerWillRestart, boolean purgeCache, boolean doit,

boolean evenPersistent, boolean uninstalling, int userId, String reason) {

int i;

// … 状态判断,省略

boolean didSomething = mProcessList.killPackageProcessesLocked(packageName, appId, userId,

ProcessList.INVALID_ADJ, callerWillRestart, true /* allowRestart */, doit,

evenPersistent, true /* setRemoved */,

packageName == null ? ("stop user " + userId) : ("stop " + packageName));

didSomething |=

mAtmInternal.onForceStopPackage(packageName, doit, evenPersistent, userId);

// 清理 service

// 清理 broadcastreceiver

// 清理 providers

// 清理其他

return didSomething;

}

这个方法实现很清晰:先杀死这个 App 内部的所有进程,然后清理残留在 system_server 内的四大组件信息。我们关心进程是如何被杀死的,因此继续跟踪 killPackageProcessesLocked,这个方法最终会调用到 ProcessList 内部的 removeProcessLocked 方法, removeProcessLocked 会调用 ProcessRecord 的 kill 方法,我们看看这个 kill:

void kill(String reason, boolean noisy) {

if (!killedByAm) {

Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, “kill”);

if (mService != null && (noisy || info.uid == mService.mCurOomAdjUid)) {

mService.reportUidInfoMessageLocked(TAG,

"Killing " + toShortString() + " (adj " + setAdj + "): " + reason,

info.uid);

}

if (pid > 0) {

EventLog.writeEvent(EventLogTags.AM_KILL, userId, pid, processName, setAdj, reason);

Process.killProcessQuiet(pid);

ProcessList.killProcessGroup(uid, pid);

} else {

pendingStart = false;

}

if (!mPersistent) {

killed = true;

killedByAm = true;

}

Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);

}

}

这里我们可以看到,首先杀掉了目标进程,然后会以uid为单位杀掉目标进程组。如果只杀掉目标进程,那么我们可以通过双进程守护的方式实现保活;关键就在于这个 killProcessGroup,继续跟踪之后发现这是一个 Native 方法,它的最终实现在libprocessgroup中,代码如下:

int killProcessGroup(uid_t uid, int initialPid, int signal) {

return KillProcessGroup(uid, initialPid, signal, 40 /retries/);

}

注意这里有个奇怪的数字:40。

我们继续跟踪:

static int KillProcessGroup(uid_t uid, int initialPid, int signal, int retries) {

// 省略

int retry = retries;

int processes;

while ((processes = DoKillProcessGroupOnce(cgroup, uid, initialPid, signal)) > 0) {

LOG(VERBOSE) << "Killed " << processes << " processes for processgroup " << initialPid;

if (retry > 0) {

std::this_thread::sleep_for(5ms);

–retry;

} else {

break;

}

}

// 省略

}

瞧瞧我们的系统做了什么骚操作?循环 40 遍不停滴杀进程,每次杀完之后等 5ms,循环完毕之后就算过去了。

看到这段代码,我想任何人都会蹦出一个疑问:假设经历连续 40 次的杀进程之后,如果 App 还有进程存在,那不就侥幸逃脱了吗?

实现方法


那么,如何实现这个目的呢?我们看这个关键的 5ms。假设,App 进程在被杀掉之后,能够以足够快的速度(5ms 内)启动一堆新的进程,那么系统在一次循环杀掉老的所有进程之后,sleep 5ms 之后又会遇到一堆新的进程;如此循环 40 次,只要我们每次都能够拉起新的进程,那我们的 App 就能逃过系统的追杀,实现永生。是的,炼狱般的 200ms,只要我们熬过 200ms 就能渡劫成功,得道飞升。不知道大家有没有玩过打地鼠这个游戏,整个过程非常类似,按下去一个又冒出一个,只要每次都能足够快地冒出来,我们就赢了。

现在问题的关键就在于:如何在 5ms 内启动一堆新的进程?

再回过头来看原来的保活方式,它们拉起进程最开始通过 am 命令,这个命令实际上是一个 java 程序,它会经历启动一个进程然后启动一个 ART 虚拟机,接着获取 AMS 的 Binder 代理,然后与 AMS 进行 Binder 同步通信。这个过程实在是太慢了,在这与死神赛跑的 5ms 里,它的速度的确是不敢恭维。

后来,MarsDaemon 提出了一种新的方式,它用 Binder 引用直接给 AMS 发送 Parcel,这个过程相比 am 命令快了很多,从而大大提高了成功率。其实这里还有改进的空间,毕竟这里还是在 Java 层调用,Java 语言在这种实时性要求极高的场合有一个非常令人诟病的特性:垃圾回收(GC);虽然我们在这 5ms 内直接碰上 GC 引发停顿的可能性非常小,但是由于 GC 的存在,ART 中的 Java 代码存在非常多的 checkpoint;想象一下你现在是一个信使,有重要军情要报告,但是在路上却碰到很多关隘,而且很可能被勒令暂时停止一下,这种情况是不可接受的。因此,最好的方法是通过 native code 给 AMS 发送 Binder 调用;当然,如果再底层一点,我们甚至可以通过 ioctl 直接给 Binder 驱动发送数据进而完成调用,但是这种方法的兼容性比较差,没有用 Native 方式省心。

通过在 Native 层给 AMS 发送 Binder 消息拉起进程,我们算是解决了「快速拉起进程」这个问题。但是这个还是不够。还是回到打地鼠这个游戏,假设你摁下一个地鼠,会冒起一个新的地鼠,那么你每次都能摁下去最后获取胜利的概率还是比较高的;但如果你每次摁下一个地鼠,其他所有地鼠都能冒出来呢?这个难度系数可是要高多了。如果我们的进程能够在任意一个进程死亡之后,都能让把其他所有进程全部拉起,这样系统就很难杀死我们了。

新的黑科技保活中通过 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,因此这四个进程三三之间形成了铁三角,从而保证了存活率。

分析到这里,这种方案的大致原理我们已经清晰了。基于以上原理,我写了一个简单的 PoC,代码在这里:https://github.com/tiann/Leoric 有兴趣的可以看一下。

改进空间


本方案的原理还是比较简单直观的,但是要实现稳定的保活,还需要很多细节要补充;特别是那与死神赛跑的 5ms,需要不计一切代价去优化才能提升成功率。具体来说,就是当前的实现是在 Java 层用 Binder 调用的,我们应该在 Native 层完成。笔者曾经实现过这个方案,但是这个库本质上是有损用户利益的,因此并不打算公开代码,这里简单提一下实现思路供大家学习。

如何在 Native 层进行 Binder 通信?

libbinder 是 NDK 公开库,拿到对应头文件,动态链接即可。

难点:依赖繁多,剥离头文件是个体力活。

如何组织 Binder 通信的数据?

通信的数据其实就是二进制流;具体表现就是 (C++/Java) Parcel 对象。Native 层没有对应的 Intent Parcel,兼容性差。

方案:

Java 层创建 Parcel (含 Intent),拿到 Parcel 对象的 mNativePtr(native peer),传到 Native 层。

native 层直接把 mNativePtr 强转为结构体指针。

fork 子进程,建立管道,准备传输 Parcel 数据。

子进程读管道,拿到二进制流,重组为 Parcel。

如何应对?

尾声

你不踏出去一步,永远不知道自己潜力有多大,千万别被这个社会套在我们身上的枷锁给捆住了,30岁我不怕,35岁我一样不怕,去做自己想做的事,为自己拼一把吧!不试试怎么知道你不行呢?

改变人生,没有什么捷径可言,这条路需要自己亲自去走一走,只有深入思考,不断反思总结,保持学习的热情,一步一步构建自己完整的知识体系,才是最终的制胜之道,也是程序员应该承担的使命。

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

id源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)**

[外链图片转存中…(img-0t3JDDGV-1714338716703)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值