本文分析基于Android T(13)
上篇文章介绍了fd bug的检测东西fdsan,这篇文章再介绍fd leak的检测东西fdtrack。它们互为补充,成为Android世界中消除fd问题的两柄利剑。
对于leak问题,咱们一般的解决方案是dump出一切信息,找出其间呈现频率最高的那一个。之后再进一步分析引证联系或调用栈,从而找出leak的代码方位。
fdtrack自然也是沿袭了这一套leak检测的套路。现在咱们企图站在原作者的视角,一起来推演下东西诞生的过程。
对fd leak而言,root cause的找出需求依赖fd创立时的调用栈,有了调用栈才能判别出代码方位。因而,咱们需求在一切fd创立的根底函数(比如open, dup, pipe)中添加hook,在hook函数中去搜集当时调用栈。下方是一个示例,其间FDTRACK_CREATE会搜集当时调用栈。
int open(const char* pathname, int flags, ...) {
mode_t mode = 0;
if (needs_mode(flags)) {
va_list args;
va_start(args, flags);
mode = static_cast<mode_t>(va_arg(args, int));
va_end(args);
}
return FDTRACK_CREATE(__openat(AT_FDCWD, pathname, force_O_LARGEFILE(flags), mode));
}
接下来的问题是怎么寄存这些调用栈数据。fdtrack中使用了一个长度为4096的大局数组,经过fd索引便可得到该fd创立时的调用栈信息。数组长度仅为4096,而并非单个进程中fd的最大创立个数32768,是因为部分分析根本能够排查出leak的原因,这样也能够节省内存。别的,每个fd的调用栈最多只保存32帧(去除最顶部的libfdtrack.so帧,因为它们不提供信息)。 调用栈的数据有地方寄存了,那么接下来便是hook函数怎么生效的问题。首要能够必定的是,咱们不能大局默许翻开这个功用,因为这会给功用带来灾难。此外,fd leak一般产生在特定进程,因而该功用最好能针对进程翻开。常规的思路是经过体系特点,每次进入hook函数后判别特点值和当时进程名是否相等,从而决议功用是否使能。fdtrack则将此功用封装成了libfdtrack.so库,经过进程对库的自动加载来决议功用是否翻开。这样便将功用和进程绑定起来了。但是凡事有利有弊,这么做的缺点是功用开启必需求添加如下代码,对于那些没有源码的三方进程,开发者就无法调试了。
dlopen("libfdtrack.so", RTLD_GLOBAL)
libfdtrack.so中有一个函数ctor(如下所示)经过__attribute__((constructor))特点声明,在动态库被加载时,该函数会得到履行。其间重点进行了信号BIONIC_SIGNAL_FDTRACK(39)的注册以及功用的使能。之所以要注册信号,是因为搜集到的fd信息终归要展示出来。而信号便是输出这些信息的“发令枪”。
__attribute__((constructor)) static void ctor() {
for (auto& entry : stack_traces) {
entry.backtrace.reserve(kStackDepth);
}
struct sigaction sa = {};
sa.sa_sigaction = [](int, siginfo_t* siginfo, void*) {
if (siginfo->si_code == SI_QUEUE && siginfo->si_int == 1) {
fdtrack_dump_fatal();
} else {
fdtrack_dump();
}
};
sa.sa_flags = SA_SIGINFO | SA_ONSTACK;
sigaction(BIONIC_SIGNAL_FDTRACK, &sa, nullptr);
if (Maps().Parse()) {
ProcessMemory() = unwindstack::Memory::CreateProcessMemoryThreadCached(getpid());
android_fdtrack_hook_t expected = nullptr;
installed = android_fdtrack_compare_exchange_hook(&expected, &fd_hook);
}
android_fdtrack_set_globally_enabled(true);
}
信号的发送有多种方法,常规的有kill,复杂点的有sigqueue,能够带些参数。不管使用哪种方法,搜集到的fd信息都会打印,但只要经过sigqueue且参数si_int为1时,进程才会终止(FATAL形式)。如下方代码所示,system_server检测到fd leak后就会完结自己。
static void android_server_SystemServer_fdtrackAbort(JNIEnv*, jobject) {
sigval val;
val.sival_int = 1;
sigqueue(getpid(), BIONIC_SIGNAL_FDTRACK, val);
}
这两种信号发送方法,除了决议进程是否完结外还有一个区别。常规形式会按照fd从小到大的次序打印出每个fd创立时的调用栈信息。而FATAL形式除了打印这些外,还会在终究的abort message中打印频率最高的调用栈。在核算调用栈呈现频率时,需求判别两个调用栈是否一致。fdtrack中定义了一个hash_stack函数(如下所示),将一长串的调用栈哈希为一个长整型的数字。经过比对两个数字就能够判别调用栈是否相同。这种熵减行为理论上会导致两个不相同的调用栈核算得到相同的数字,但只要位数够多,产生这种问题的概率就很小。
static size_t hash_stack(const char* const* function_names, const uint64_t* function_offsets,
size_t stack_depth) {
size_t hash = 0;
for (size_t i = 0; i < stack_depth; ++i) {
// To future maintainers: if a libc++ update ever makes this invalid, replace this with +.
hash = std::__hash_combine(hash, std::hash<std::string_view>()(function_names[i]));
hash = std::__hash_combine(hash, std::hash<uint64_t>()(function_offsets[i]));
}
return hash;
}
至此,这个东西呈现在了咱们面前。它很强大,但仍然有缺点。还记得咱们之前提过需求在一切fd创立的根底函数(比如open, dup, pipe)中添加hook么?那么这些函数中添加hook就能追寻一切的fd创立么?答案是否定的。Android世界中还有很多的fd是在内核态创立的,它们不会经过上述函数,也就不会被追寻。举个比如,一切经过binder传递的fd都会在binder driver中进行“翻译”,它们也不会被fdtrack追寻。自己前段时间调试过一个ION buffer leak的问题,走漏的fd正是HIDL通讯过程中创立的,因而fdtrack获取不到任何有效信息。
原理现已说完,下面再谈谈使用。system_server作为fdtrack第一个也是最重要的使用者,在debug build中会默许翻开检测。为了合作fdtrack工作,system_server新启了一个线程,每120秒轮询一下当时的fd数量。当数量大于1024时翻开fdtrack,比及数量超过2048时给自己发送信号BIONIC_SIGNAL_FDTRACK,生成的abort message如下所示。
Cmdline: system_server
pid: 1556, tid: 16001, name: system_server >>> system_server <<<
uid: 1000
tagged_addr_ctrl: 0000000000000001
signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr --------
Abort message: 'aborting due to fd leak: most common stack =
0: fcntl+488
1: _ZNK7android6Parcel30readUniqueParcelFileDescriptorEPNS_4base14unique_fd_implINS1_13DefaultCloserEEE+56
2: _ZNK7android6Parcel8readDataINSt3__18optionalINS_2os20ParcelFileDescriptorEEELb1EEEiPT_+220
3: AParcel_readParcelFileDescriptor+64
4: _ZL23Bitmap_createFromParcelP7_JNIEnvP8_jobjectS2_+912
5: art_jni_trampoline+112
6: android.graphics.Bitmap$1.createFromParcel+92
7: android.os.Parcel.readParcelable+132
8: android.telephony.SubscriptionInfo$1.createFromParcel+564
9: [DEDUPED] ?.createFromParcel+52
10: android.os.Parcel.createTypedArrayList+220
11: nterp_helper+4020
12: nterp_helper+9296
13: com.android.server.vcn.TelephonySubscriptionTracker.handleSubscriptionsChanged+172
14: [DEDUPED]+64
15: android.telephony.TelephonyRegistryManager$1.lambda$onSubscriptionsChanged$0+44
16: nterp_helper+156
17: android.os.Handler.dispatchMessage+84
18: android.os.Looper.loopOnce+1036
19: android.os.Looper.loop+520
20: '
...
backtrace:
#00 pc 00000000000516a4 /apex/com.android.runtime/lib64/bionic/libc.so (abort+168)
#01 pc 000000000000fffc /system/lib64/libfdtrack.so (void* std::__1::__thread_proxy<std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct> >, fdtrack_dump_impl(bool)::$_1> >(void*)+40)
#02 pc 00000000000b6954 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+264)
#03 pc 0000000000052fd8 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+68)
每一帧调用栈都包含函数名和函数内的汇编指令偏移,如果有对应的symbol库,那么就能够康复详细的行号。此外,[DEDUPED]表明JNI跳板函数(详细能够参阅这篇文章),同参数类型的JNI函数共享一个,对于问题分析来说不重要。最终,因为fdtrack使用了libunwindstack,所以调用栈的回溯能够一并将java帧康复出来。