Binder | 代理对象的泄露及其检测

·  阅读 2479

本文分析基于Android S(12)

前言

谈起“泄露”,相信大家都能想到内存泄漏,而它是OOM的重要原因之一。在内存泄露的语境中,并非是GC算法出了问题导致未被引用的对象没有被回收,而是程序逻辑上已经不需要的对象却依然被引用着。对于Binder通信而言,Server端将Binder对象发送到Client进程后会转变为BinderProxy对象,如果Server端短期内发送了许多不同的Binder对象给Client进程,则Client进程中也会创建很多BinderProxy对象。这些对象一方面消耗着Client进程的内存,同时也会在早期版本中产生"global reference table overflow"的问题。

这种BinderProxy创建过多的情况我们就称为Binder代理对象的泄露,它对system_server的影响尤为突出。因为system_server(Client)会接收来自各个APP进程(Server)的Binder对象,如果每个进程都发过来很多Binder对象,那么system_server的内存早就消耗殆尽了。至于为什么APP进程会给system_server发送很多Binder对象,这种要么是恶意行为,要么是程序设计有缺陷,都是需要被禁止的行为。

基于这样一种想法,BpBinder和BinderProxy的构造过程分别加入了检测,当来自某个UID的代理对象创建过多时,就会打印一些警告信息。如果是system_server进程的话,它还会杀掉该UID对应的APP(bad app)。

1. 缘起

在Android P以前,创建Binder代理对象是没有检测的。之所以后来的版本加入了检测,是因为小米给Google提了一个问题(感谢小米)。这个问题(链接)目前是公开的,因此大家都可以了解。下面我来简单分析下这个问题。

1.1 问题场景

在APP中插入下列代码,循环地调用registerContentObserver,当调用次数在24000次左右时,system_server崩溃,系统重启,必现。

public static void triggerWithContent(final Context context) {
    final ContentResolver contentResolver = context.getContentResolver();
    new Thread(new Runnable() {
        Uri uri = Uri.parse("content://sms/sent");
        @Override
        public void run() {
            while (true) {
                ContentObserver contentObserver = new MyObserver(handler);
                contentResolver.registerContentObserver(uri, true, contentObserver);
            }

        }
    }).start();
}

class MyObserver extends ContentObserver {
    public MyObserver(Handler handler) {
        super(handler);
    }

    @Override
    public void onChange(boolean selfChange) {
        this.onChange(selfChange, null);
    }

    @Override
    public void onChange(boolean selfChange, Uri uri) {
    }
}
复制代码

这个问题的复现条件很简单,下面看看system_server的报错信息(tombstone文件)。

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'xiaomi/mido/mido:7.0/NRD90M/1.1.1:user/test-keys'
Revision: '0'
ABI: 'arm64'
pid: 1633, tid: 4450, name: Binder:1633_1B  >>> system_server <<<
signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
Abort message: 'art/runtime/indirect_reference_table.cc:116] JNI ERROR (app bug): global reference table overflow (max=51200)'
x0   0000000000000000  x1   0000000000001162  x2   0000000000000006  x3   0000000000000008
x4   000000000000008c  x5   0000007f78f83880  x6   4437f8787f000000  x7   0000007f78f83744
x8   0000000000000083  x9   ffffffffffffffdf  x10  0000000000000000  x11  0000000000000001
x12  ffffffffffffffff  x13  00000000ffffffff  x14  000000000008cc40  x15  0000000000001fe3
x16  0000007f78fc1ed8  x17  0000007f78f6f4d0  x18  0000000000000000  x19  0000007f41b1d4f8
x20  0000000000000006  x21  0000007f41b1d450  x22  000000000000000b  x23  000000000000389b
x24  ffffffffffffffff  x25  0000007f779fc730  x26  0000007f77985ec8  x27  0000007f41b1b341
x28  0000007f7794c09b  x29  0000007f41b1b270  x30  0000007f78f6c960
sp   0000007f41b1b250  pc   0000007f78f6f4d8  pstate 0000000000000000
复制代码

堆栈就不必看了,跟问题的原因不相关。注意上述的Abort message,它表示目前system_server中存活的global reference已经超过了上限(51200),而它正是system_server崩溃的直接原因。

想不到吧,APP循环调用一个系统方法还能够让手机重启,而它根本的原因正是Binder代理对象创建过多。

1.2 问题原因

首先我们来回答一个问题:为什么Android系统会对global reference的数量做限制?我相信很多人了解这个限制,但是没有思考过为什么要限制。本人斗胆做下猜测,如下。

  • Global reference主要用于native层对Java对象的引用。由于native层的对象不会被GC扫描到,因此我们必须通过global reference来显式地告知collector:这个Java对象正在被人引用,你别回收它了。
  • 被global reference的Java对象在GC时将会作为GC Roots参与扫描,因此不会被回收。
  • Global reference的释放游离在GC体系之外,是否释放只能由开发者在代码中写明,如果开发者稍有疏忽,则会导致这些对象被泄露。
  • 增加global reference的数量限制可以让开发者提前知道这些泄露情况。

了解完global reference的限制以后,我们再来分析下为什么上述操作会触发global reference的上限。

public final void registerContentObserver(Uri uri, boolean notifyForDescendents,
        ContentObserver observer, @UserIdInt int userHandle) {
    try {
        getContentService().registerContentObserver(uri, notifyForDescendents,
                observer.getContentObserver(), userHandle, mTargetSdkVersion);
    } catch (RemoteException e) {
    }
}
复制代码

ContentResolver.registerContentObserver方法中有两个重要的点值得关注:

  1. getContentService方法可以获取ContentService的代理对象,因此这次调用会发起Binder通信,而通信对端为system_server。
  2. observer.getContentObserver方法将获取到一个Binder对象(属于Transport类,继承于IContentObserver.Stub类),将它作为参数会在system_server进程中创建出BinderProxy对象和IContentObserver.Stub.Proxy对象。

BinderProxy的创建过程中,需要为该对象的一个字段(WeakReference类型)增加global reference,目的是让Native层的BpBinder对象可以找到Java层的BinderProxy对象。

jobject javaObjectForIBinder(JNIEnv* env, const sp<IBinder>& val)
{
    ...
    if (object != NULL) {
        ....
        // The native object needs to hold a weak reference back to the
        // proxy, so we can retrieve the same proxy if it is still active.
        jobject refObject = env->NewGlobalRef(
                env->GetObjectField(object, gBinderProxyOffsets.mSelf));
        val->attachObject(&gBinderProxyOffsets, refObject,
                jnienv_to_javavm(env), proxy_cleanup);
        ...
    }
    return object;
}
复制代码

另外,在system_server中创建出的IContentObserver.Stub.Proxy对象会被包装成ObserverEntry对象并加入一个ArrayList中,如下面代码所示。

private void addObserverLocked(Uri uri, int index, IContentObserver observer,
                               boolean notifyForDescendants, Object observersLock,
                               int uid, int pid, int userHandle) {
    // If this is the leaf node add the observer
    if (index == countUriSegments(uri)) {
        mObservers.add(new ObserverEntry(observer, notifyForDescendants, observersLock,
                uid, pid, userHandle));
        return;
    }
复制代码

ObserverEntry类的构造方法中会执行linkToDeath操作,注册对APP进程中Transport对象死亡通知的监听(通常是APP进程死亡时system_server才会接到该对象的死亡通知)。

public ObserverEntry(IContentObserver o, boolean n, Object observersLock,
                     int _uid, int _pid, int _userHandle) {
    this.observersLock = observersLock;
    observer = o;
    uid = _uid;
    pid = _pid;
    userHandle = _userHandle;
    notifyForDescendants = n;
    try {
        observer.asBinder().linkToDeath(this, 0);
    } catch (RemoteException e) {
        binderDied();
    }
}
复制代码

而linkToDeath的具体实现里,最终也会为ObserverEntry对象增加global reference,保证在BpBinder对象存在时死亡通知的处理对象不能被回收。

static void android_os_BinderProxy_linkToDeath(JNIEnv* env, jobject obj,
        jobject recipient, jint flags) // throws RemoteException
{
    ...
    if (!target->localBinder()) {
        DeathRecipientList* list = (DeathRecipientList*)
                env->GetLongField(obj, gBinderProxyOffsets.mOrgue);
        sp<JavaDeathRecipient> jdr = new JavaDeathRecipient(env, recipient, list);
        status_t err = target->linkToDeath(jdr, NULL, flags);
        ...
    }
}
复制代码
JavaDeathRecipient(JNIEnv* env, jobject object, const sp<DeathRecipientList>& list)
    : mVM(jnienv_to_javavm(env)), mObject(env->NewGlobalRef(object)),  //增加global reference
      mObjectWeak(NULL), mList(list)
{
    ...
}
复制代码

因此,一个IContentObserver.Stub.Proxy对象的创建将会伴随着两个global reference的创建。而且由于这些代理对象最终加入到一个ArrayList中,所以也无法被回收。

当APP循环调用24000次左右registerContentObserver时,system_server侧会对应产生48000次左右的global reference,再加上自己正常消耗的global reference,那么很容易就达到51200的上限。

2. 检测方案

2.1 BpBinder创建过程中的检测

为了防止那些"bad behaving apps"的疯狂操作,系统有必要在核心系统进程中增加对代理对象的检测。而不论是Java层的代理对象还是Native层的代理对象,都需要生成BpBinder对象。因此我们可以在它的构造过程中增加检测。

sp<BpBinder> BpBinder::create(int32_t handle) {
    int32_t trackedUid = -1;
    if (sCountByUidEnabled) {
        trackedUid = IPCThreadState::self()->getCallingUid();
        AutoMutex _l(sTrackingLock);
        uint32_t trackedValue = sTrackingMap[trackedUid];
        if (CC_UNLIKELY(trackedValue & LIMIT_REACHED_MASK)) {
            if (sBinderProxyThrottleCreate) {
                return nullptr;
            }
        } else {
            if ((trackedValue & COUNTING_VALUE_MASK) >= sBinderProxyCountHighWatermark) {
                ALOGE("Too many binder proxy objects sent to uid %d from uid %d (%d proxies held)",
                      getuid(), trackedUid, trackedValue);
                sTrackingMap[trackedUid] |= LIMIT_REACHED_MASK;
                if (sLimitCallback) sLimitCallback(trackedUid);
                if (sBinderProxyThrottleCreate) {
                    ALOGI("Throttling binder proxy creates from uid %d in uid %d until binder proxy"
                          " count drops below %d",
                          trackedUid, getuid(), sBinderProxyCountLowWatermark);
                    return nullptr;
                }
            }
        }
        sTrackingMap[trackedUid]++;
    }
    return sp<BpBinder>::make(BinderHandle{handle}, trackedUid);
}
复制代码

只有当sCountByUidEnabled为true时,才会进行检测。系统中默认只为system_server和systemui两个进程打开了这项检测,原因是这两个进程会和比较多的APP进程打交道。systemui只在debug build下打开这项检测,因此普通用户的手机是不会打开的;而system_server的检测是任何时候都会打开的。

[/frameworks/base/packages/SystemUI/src/com/android/systemui/SystemUIService.java]

if (Build.IS_DEBUGGABLE) {
    // b/71353150 - looking for leaked binder proxies
    BinderInternal.nSetBinderProxyCountEnabled(true);
    BinderInternal.nSetBinderProxyCountWatermarks(1000,900);
    BinderInternal.setBinderProxyCountCallback(
            new BinderInternal.BinderProxyLimitListener() {
                @Override
                public void onLimitReached(int uid) {
                    Slog.w(SystemUIApplication.TAG,
                            "uid " + uid + " sent too many Binder proxies to uid "
                            + Process.myUid());
                }
            }, mMainHandler);
}
复制代码

[/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java]

BinderInternal.nSetBinderProxyCountWatermarks(BINDER_PROXY_HIGH_WATERMARK,
        BINDER_PROXY_LOW_WATERMARK);
BinderInternal.nSetBinderProxyCountEnabled(true);
BinderInternal.setBinderProxyCountCallback(
        (uid) -> {
            Slog.wtf(TAG, "Uid " + uid + " sent too many Binders to uid "
                    + Process.myUid());
            BinderProxy.dumpProxyDebugInfo();
            if (uid == Process.SYSTEM_UID) {
                Slog.i(TAG, "Skipping kill (uid is SYSTEM)");
            } else {
                killUid(UserHandle.getAppId(uid), UserHandle.getUserId(uid),
                        "Too many Binders sent to SYSTEM");
            }
        }, mHandler);
复制代码
/**
 * The number of binder proxies we need to have before we start warning and
 * dumping debug info.
 */
private static final int BINDER_PROXY_HIGH_WATERMARK = 6000;

/**
 * Low watermark that needs to be met before we consider dumping info again,
 * after already hitting the high watermark.
 */
private static final int BINDER_PROXY_LOW_WATERMARK = 5500;
复制代码

下面以system_server为例,用图片的方式描述检测算法的运行规则。检测发生在BpBinder::Create的时候,但是是否开启检测也和BpBinder的销毁过程有关。另外设置下水位值的目的是为了防止onLimitReached被回调太多次从而影响程序性能。

BpBinder检测机制.png

system_server的onLimitReached回调方法会做两件事:

  1. 调用BinderProxy.dumpProxyDebugInfo方法来输出system_server中创建的BinderProxy信息。

    public static void dumpProxyDebugInfo() {
        if (Build.IS_DEBUGGABLE) {
            sProxyMap.dumpProxyInterfaceCounts();
            sProxyMap.dumpPerUidProxyCounts();
        }
    }
    复制代码
  2. 如果该UID并非system UID的话,杀死该UID所属的APP。

2.2 BinderProxy创建过程中的检测

除了BpBinder中为每个UID限定的创建上限,BinderProxy的创建过程中也会对整个进程代理对象的总量做限制(不管来自哪个UID)。

BinderProxy通过ProxyMap来管理,用Native层BinderProxyNativeData的地址作为key,WeakReference<BinderProxy>对象作为value。这样可以建立BinderProxyNativeData对象和BinderProxy对象一对一的映射关系。当BinderProxy对象创建出来后,需要往ProxyMap中增加一个元素。在元素增加的过程中会检测ProxyMap中所有value的个数,当超过某一个数值时便会抛出异常。

if (size >= mWarnBucketSize) {
    final int totalSize = size();
    Log.v(Binder.TAG, "BinderProxy map growth! bucket size = " + size
            + " total = " + totalSize);
    mWarnBucketSize += WARN_INCREMENT;
    if (Build.IS_DEBUGGABLE && totalSize >= CRASH_AT_SIZE) {
        // Use the number of uncleared entries to determine whether we should
        // really report a histogram and crash. We don't want to fundamentally
        // change behavior for a debuggable process, so we GC only if we are
        // about to crash.
        final int totalUnclearedSize = unclearedSize();
        if (totalUnclearedSize >= CRASH_AT_SIZE) {
            dumpProxyInterfaceCounts();
            dumpPerUidProxyCounts();
            Runtime.getRuntime().gc();
            throw new AssertionError("Binder ProxyMap has too many entries: "
                    + totalSize + " (total), " + totalUnclearedSize + " (uncleared), "
                    + unclearedSize() + " (uncleared after GC). BinderProxy leak?");
        } else if (totalSize > 3 * totalUnclearedSize / 2) {
            Log.v(Binder.TAG, "BinderProxy map has many cleared entries: "
                    + (totalSize - totalUnclearedSize) + " of " + totalSize
                    + " are cleared");
        }
    }
}
复制代码

由于ProxyMap中的value记录的是WeakReference对象,因此它所对应的BinderProxy对象是可能不存在的,这个得通过WeakReference.get() != null来判断。那么上述几个变量的具体含义如下:

  • totalSize:记录的是WeakReference对象的个数,因此≥BinderProxy对象的个数。
  • totalUnclearedSize:表示的是BinderProxy对象的个数。
  • GC之后的unclearedSize():有些BinderProxy由于缺乏强引用,可能会在此次GC过程中被回收,因此该值将≤totalUnclearedSize。

CRASH_AT_SIZE值为20000,表明当一个进程中存在的BinderProxy数超过20000时,将会抛出异常。不过该检测只在Build.IS_DEBUGGABLE的版本中打开,普通用户感知不到。

2.3 调试信息

在调试版本中,不论是BpBinder中的检测还是BinderProxy中的检测,当超过水位线时都会尝试输出一些调试信息,帮助开发者了解改进的方向。调试信息输出的内容有两种,一种是"interface counts",按接口名称来归类,另一种是"uid counts",按所属uid来归类。

2.3.1 dumpProxyInterfaceCounts

Dump出来的信息如下所示,记录了排名前十的接口名称,x符号后面是该接口所对应的BinderProxy对象的数量,下面做详细的解读。

Binder  : BinderProxy descriptor histogram (top 10):
Binder  :  #1: <proxy to dead node> x13298
Binder  :  #2: android.view.IWindow x3732
Binder  :  #3: android.view.accessibility.IAccessibilityManagerClient x1109
Binder  :  #4: android.content.IIntentReceiver x707
Binder  :  #5: android.database.IContentObserver x676
Binder  :  #6:  x512
Binder  :  #7: <cleared weak-ref> x222
Binder  :  #8: com.android.internal.textservice.ISpellCheckerService x127
Binder  :  #9: android.view.accessibility.IAccessibilityInteractionConnection x112
Binder  :  #10: android.content.IContentProvider x69
复制代码

首先是接口名称获取的方法,底层最终会调用BpBinder的getInterfaceDescriptor方法。如果是第一次尝试获取,则会发起一次Binder通信来得到这个信息,因为接口名称通常是存在Stub对象中的。

const String16& BpBinder::getInterfaceDescriptor() const
{
    if (isDescriptorCached() == false) {
        sp<BpBinder> thiz = sp<BpBinder>::fromExisting(const_cast<BpBinder*>(this));

        Parcel data;
        data.markForBinder(thiz);
        Parcel reply;
        // do the IPC without a lock held.
        status_t err = thiz->transact(INTERFACE_TRANSACTION, data, &reply);
        if (err == NO_ERROR) {
            String16 res(reply.readString16());
            Mutex::Autolock _l(mLock);
            // mDescriptorCache could have been assigned while the lock was
            // released.
            if (mDescriptorCache.size() == 0)
                mDescriptorCache = res;
        }
    }

    // we're returning a reference to a non-static object here. Usually this
    // is not something smart to do, however, with binder objects it is
    // (usually) safe because they are reference-counted.

    return mDescriptorCache;
}
复制代码

既然获取接口名称的过程会发起Binder通信,那么也就意味着这是一个可能被对方进程阻塞的过程。因此dumpProxyInterfaceCounts中单独创建了一个线程来做这件事,并且设置了20秒的超时。

接着是接口名称的含义。类似于android.view.IWindow这种还是比较容易看懂的,可是上述列表中还有几个奇怪的名称。

  • #1: <proxy to dead node> x13298:这里表示端进程已经死掉的情况,死去进程里的所有Binder对象对应的BinderProxy都会记录在这个条目里,而不管它真实的接口名称(事实上我们也无法知道他们的真实名称了,因为没法通过Binder通信获取这个信息)。
  • #6: x512:名称为空,这通常是因为所对应的Binder对象是直接通过new Binder()的方式实例化出来的,而不是通过Binder的子类实例化出来的。
  • #7: <cleared weak-ref> x222:WeakReference对象存在,而所对应的BinderProxy对象已经被回收了。

像这种问题的后续追踪,要去从logcat中查看刚刚被杀掉的进程以及被杀掉的原因,因为不是本文重点,不再赘述。

2.3.2 dumpPerUidProxyCounts

每个UID的BpBinder的数量信息都存储在BpBinder::sTrackingMap中,因此dumpPerUidProxyCounts本质上就是从native层读回这些信息。读回的信息按照UID的顺序进行排列,且所有与system_server有交互(往system_server中发送Binder对象)的UID都会记录在列。

Binder  : Per Uid Binder Proxy Counts:
Binder  : UID : 0  count = 40
Binder  : UID : 1000  count = 576
Binder  : UID : 1001  count = 155
...
Binder  : UID : 10128  count = 4
Binder  : UID : 10131  count = 212
Binder  : UID : 10138  count = 566
Binder  : UID : 10144  count = 4
Binder  : UID : 10148  count = 5194
Binder  : UID : 10154  count = 164
Binder  : UID : 10161  count = 5
Binder  : UID : 10164  count = 13
Binder  : UID : 10166  count = 893
Binder  : UID : 10174  count = 9
Binder  : UID : 10175  count = 636
Binder  : UID : 10178  count = 3
Binder  : UID : 10180  count = 4
Binder  : UID : 10183  count = 5
...
复制代码

结语

这篇文章依然是个小众话题,不过难度较低,适合茶余饭后的消遣。另外这类问题的调试信息还有些优化空间,但估计改进不大。题外话,后续想写一篇稍微“热门”点的文章,也即调用栈中经常可见的"art::GoToRunnable"的原因,这涉及到signal_catcher的工作原理。

分类:
Android
标签:
分类:
Android
标签: