一、前言
通过修复历史遗留的Crash漏报问题(包括端侧SDK采集的兼容性优化及Crash平台的数据消费机制完善),得物Android端的Crash监控体系得到显著增强,使得历史Crash数据的完整捕获能力得到系统性改善,相应Crash指标也有所上升,经过架构以及各团队的共同努力下,崩溃率已从最高的万2降至目前的万1.1到万1.5,其中疑难问题占比约90%、因系统bug导致的Crash占比约40%,在本文中将简要介绍一些较典型的系统Crash的治理过程。
二、DNS解析崩溃
背景
Android11及以下版本在DNS解析过程中的有几率产生野指针问题导致的Native Crash,其中Android9占比最高。
堆栈与上报趋势
java
at libcore.io.Linux.android_getaddrinfo(Linux.java)
at libcore.io.BlockGuardOs.android_getaddrinfo(BlockGuardOs.java:172)
at java.net.InetAddress.parseNumericAddressNoThrow(InetAddress.java:1631)
at java.net.Inet6AddressImpl.lookupAllHostAddr(Inet6AddressImpl.java:96)
at java.net.InetAddress.getAllByName(InetAddress.java:1154)
#00 pc 000000000003b938 /system/lib64/libc.so (android_detectaddrtype+1164)
#01 pc 000000000003b454 /system/lib64/libc.so (android_getaddrinfofornet+72)
#02 pc 000000000002b5f4 /system/lib64/libjavacore.so (_ZL25Linux_android_getaddrinfoP7_JNIEnvP8_jobjectP8_jstringS2_i+336)
问题分析
崩溃入口方法InetAddress.getAllByName用于根据指定的主机名返回与之关联的所有 IP 地址,它会根据系统配置的名称服务进行解析,沿着调用链查看源码发现在parseNumericAddressNoThrow方法内部调用Libcore.os.android_getaddrinfo时中有try catch的容错逻辑,继续查看后续调用的c++的源码,在调用android_getaddrinfofornet函数返回值不为0时抛出GaiException异常。
java
https://cs.android.com/android/platform/superproject/+/android-9.0.0_r49:libcore/ojluni/src/main/java/java/net/InetAddress.java
static InetAddress parseNumericAddressNoThrow(String address) {
// Accept IPv6 addresses (only) in square brackets for compatibility.
if (address.startsWith("[") && address.endsWith("]") && address.indexOf(':') != -1) {
address = address.substring(1, address.length() - 1);
}
StructAddrinfo hints = new StructAddrinfo();
hints.ai_flags = AI_NUMERICHOST;
InetAddress[] addresses = null;
try {
addresses = Libcore.os.android_getaddrinfo(address, hints, NETID_UNSET);
} catch (GaiException ignored) {
}
return (addresses != null) ? addresses[0] : null;
}
java
https://cs.android.com/android/platform/superproject/+/master:libcore/luni/src/main/native/libcore_io_Linux.cpp?q=Linux_android_getaddrinfo&ss=android%2Fplatform%2Fsuperproject
static jobjectArray Linux_android_getaddrinfo(JNIEnv* env, jobject, jstring javaNode,
jobject javaHints, jint netId) {
......
int rc = android_getaddrinfofornet(node.c_str(), NULL, &hints, netId, 0, &addressList);
std::unique_ptr<addrinfo, addrinfo_deleter> addressListDeleter(addressList);
if (rc != 0) {
throwGaiException(env, "android_getaddrinfo", rc);
return NULL;
}
......
return result;
}
解决过程
解决思路是代理android_getaddrinfofornet函数,捕捉调用原函数过程中出现的段错误信号,接着吃掉这个信号并返回-1,使之转换为JAVA异常进而走进parseNumericAddressNoThrow方法的容错逻辑,和负责网络的同学提前做了沟通,确定此流程对业务没有影响后开始解决。
首先使用inline-hook代理了android_getaddrinfofornet函数,接着使用字节封装好的native try catch工具做吃掉段错误信号并返回-1的,字节工具内部原理是在try块的开始使用sigsetjmp打个锚点并快照当前寄存器的值,然后设置信号量处理器并关联当前线程,在catch块中解绑线程与信号的关联并执行业务兜底代码,在捕捉到信号时通过siglongjmp函数长跳转到catch块中,感兴趣的同学可以用下面精简后的demo试试,以下代码保存为mem_err.c,执行gcc ./mem_err.c;./a.out
java
#include <stdio.h>
#include <signal.h>
#include <setjmp.h>
struct sigaction old;
static sigjmp_buf buf;
void SIGSEGV_handler(int sig, siginfo_t *info, void *ucontext) {
printf("信号处理 sig: %d, code: %d\n", sig, info->si_code);
siglongjmp(buf, -1);
}
int main() {
if (!sigsetjmp(buf, 0)) {
struct sigaction sa;
sa.sa_sigaction = SIGSEGV_handler;
sigaction(SIGSEGV, &sa, &old);
printf("try exec\n");
//产生段错误
int *ptr = NULL;
*ptr = 1;
printf("try-block end\n");//走不到
} else {
printf("catch exec\n");
sigaction(SIGSEGV, &old, NULL);
}
printf("main func end\n");
return 0;
}
//输出以下日志
//try exec
//信号处理 sig: 11, code: 2
//catch exec
//main func end
inline-hook库: github.com/bytedance/a...
字节native try catch工具: github.com/bytedance/a...
三、MediaCodec 状态异常崩溃
背景
在Android 11系统库的音视频播放过程中,偶尔会出现因状态异常导致的SIGABRT崩溃。音视频团队反馈指出,这是Android 11的一个系统bug。随后,我们协助音视频团队通过hook解决了这一问题。
堆栈与上报趋势
java
#00 pc 0000000000089b1c /apex/com.android.runtime/lib64/bionic/libc.so (abort+164)
#01 pc 000000000055ed78 /apex/com.android.art/lib64/libart.so (_ZN3art7Runtime5AbortEPKc+2308)
#02 pc 0000000000013978 /system/lib64/libbase.so (_ZZN7android4base10SetAborterEONSt3__18functionIFvPKcEEEEN3$_38__invokeES4_+76)
#03 pc 0000000000006e30 /system/lib64/liblog.so (__android_log_assert+336)
#04 pc 0000000000122074 /system/lib64/libstagefright.so (_ZN7android10MediaCodec37postPendingRepliesAndDeferredMessagesENSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEERKNS_2spINS_8AMessageEEE+720)
#05 pc 00000000001215cc /system/lib64/libstagefright.so (_ZN7android10MediaCodec37postPendingRepliesAndDeferredMessagesENSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEEi+244)
#06 pc 000000000011c308 /system/lib64/libstagefright.so (_ZN7android10MediaCodec17onMessageReceivedERKNS_2spINS_8AMessageEEE+8752)
#07 pc 0000000000017814 /system/lib64/libstagefright_foundation.so (_ZN7android8AHandler14deliverMessageERKNS_2spINS_8AMessageEEE+84)
#08 pc 000000000001d9cc /system/lib64/libstagefright_foundation.so (_ZN7android8AMessage7deliverEv+188)
#09 pc 0000000000018b48 /system/lib64/libstagefright_foundation.so (_ZN7android7ALooper4loopEv+572)
#10 pc 0000000000015598 /system/lib64/libutils.so (_ZN7android6Thread11_threadLoopEPv+460)
#11 pc 00000000000a1d6c /system/lib64/libandroid_runtime.so (_ZN7android14AndroidRuntime15javaThreadShellEPv+144)
#12 pc 0000000000014d94 /system/lib64/libutils.so (_ZN13thread_data_t10trampolineEPKS_+412)
#13 pc 00000000000eba94 /apex/com.android.runtime/lib64/bionic/libc.so (_ZL15__pthread_startPv+64)
#14 pc 000000000008bd80 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64)
问题分析
根据堆栈内容分析Android11的源码以及结合SIGABRT信号采集到的信息(postPendingRepliesAndDeferredMessages: mReplyID == null, from kWhatRelease:STOPPING following kWhatError:STOPPING),找到崩溃发生在onMessageReceived函数处理kWhatRelease类型消息的过程中,onMessageReceived函数连续收到两条消息,第一条是kWhatError:STOPPING,第二条是kWhatRelease:STOPPING此时因mReplyID已经被置为空,因此走到判空抛异常的逻辑。
对比Android12的源码,在处理kWhatRelease事件且状态为STOPPING抛异常前,增加了对mReplyID不为空的判断来规避这个问题。
解决过程
Android12的修复方式意味着上述三个条件结合下吃掉异常是符合预期的,接下来就是想办法通过hook Android11使逻辑对齐Android12。
【初探】最先想到的办法是代理相关函数通过判断走到这个场景时提前return出去来规避,音视频的同学尝试后发现不可行,原因如下:
- void MediaCodec::postPendingRepliesAndDeferredMessages(std::string origin, status_t err): 匹配origin是否为特征字符串(postPendingRepliesAndDeferredMessages: mReplyID == null, from kWhatRelease:STOPPING following kWhatError:STOPPING);很多设备找不到这个符号不可行;
- void MediaCodec::onMessageReceived(const sp&msg): 已知MediaCodec实例的内存首地址,需要通过hardcode偏移量来获取mReplay、mState两个字段,这里又缺少可供校验正确性的特征,风险略大担心有不同机型的兼容性问题(不同机型新增、删除字段导致偏移量不准)。
【踩坑】接着尝试使用与修复DNS崩溃类似思路的保护方案,使用inline-hook代理onMessageReceived函数调用原函数时使用setjmp打锚点,然后使用plt hook代理_android_log_assert函数并在内部检测错误信息为特征字符串时通过longjmp跳转到onMessageReceived函数的锚点并作return操作,精简后的demo如下:
Plt-hook 库: github.com/iqiyi/xHook
java
#include <iostream>
#include <setjmp.h>
#include <csignal>
static thread_local jmp_buf _buf;
void *origin_onMessageReceived = nullptr;
void *origin__android_log_assert = nullptr;
void _android_log_assert_proxy(const char* cond, const char *tag, const char* fmt, ...) {
//模拟liblog.so的__android_log_assert函数
std::cout << "__android_log_assert start" << std::endl;
if (!strncmp(fmt, "postPendingRepliesAndDeferredMessages: mReplyID == null", 55)) {
longjmp(_buf, -1);
}
//模拟调用origin__android_log_assert,产生崩溃
raise(SIGABRT);
}
void onMessageReceived_proxy(void *thiz, void *msg) {
std::cout << "onMessageReceived_proxy start" << std::endl;
if (!setjmp(_buf)) {
//模拟调用onMessageReceived原函数(origin_onMessageReceived)进入崩溃流程
std::cout << "onMessageReceived_proxy 1" << std::endl;
_android_log_assert_proxy(nullptr, nullptr, "postPendingRepliesAndDeferredMessages: mReplyID == null, from kWhatRelease:STOPPING following kWhatError:STOPPING");
std::cout << "onMessageReceived_proxy 2" << std::endl;//走不到
} else {
//保护后从此处返回
std::cout << "onMessageReceived_proxy 3" << std::endl;
}
std::cout << "onMessageReceived_proxy end" << std::endl;
}
int main() {
std::cout << "main func start" << std::endl;
/**
inline-hook: shadowhook_hook_sym_name("libstagefright.so","_ZN7android10MediaCodec17onMessageReceivedERKNS_2spINS_8AMessageEEE",(void *) onMessageReceived_proxy, (void **) &origin_onMessageReceived);
plhook: xh_core_register("libstagefright.so", "__android_log_assert", (void *) (_android_log_assert_proxy), (void **) (&origin__android_log_assert));
*/
//模拟调用libstagefright.so的_ZN7android10MediaCodec17onMessageReceivedERKNS_2spINS_8AMessageEEE函数
onMessageReceived_proxy(nullptr, nullptr);
std::cout << "main func end" << std::endl;
return 0;
}
/**
日志输出
main func start
onMessageReceived_proxy start
onMessageReceived_proxy 1
__android_log_assert start
onMessageReceived_proxy 3
onMessageReceived_proxy end
main func end
*/
线下一阵操作猛如虎经测试保护逻辑符合预期,但是在灰度期间踩到栈溢出保护导致错误转移的坑,堆栈如下:
java
#00 pc 000000000004e40c /apex/com.android.runtime/lib64/bionic/libc.so (abort+164)
#01 pc 0000000000062730 /apex/com.android.runtime/lib64/bionic/libc.so (__stack_chk_fail+20)
#02 pc 000000000000a768 /data/app/~~JaQm4SU8wxP7T2GaSWxYkQ==/com.shizhuang.duapp-N5RFIB8WurdccMgAVsBang==/lib/arm64/libduhook.so (_ZN25CrashMediaCodecProtection5proxyEPvS0_)
#03 pc 0000000001091c0c [anon:scudo:primary]
*关于栈溢出保护机制感兴趣的同学可以参考这篇文章bbs.kanxue.com/thread-2217...
(CSPP 第3版 "3.10.3 内存越界引用和缓冲区溢出"章节讲的更详细)*
longjmp函数只是恢复寄存器的值后从锚点处再次返回,过程中也唯一可能会操作栈祯只有inline-hook,当时怀疑是与setjmp/longjmp机制不兼容,由于inline-hook内部逻辑大量使用汇编来实现排查起来比较困难,因此这个问题困扰比较久,网上的资料提到可以使用代理出错函数(__stack_chk_fail)或者编译so时增加参数不让编译器生成保护代码来绕过,这两种方式影响面都比较大所以未采用。有了前面的怀疑点想到使用c++的try catch机制来做跨函数域的跳转,大致的思路同上只是把setjmp替换为c++的try catch,把longjmp替换为throw exception,精简后的demo如下:
c++异常机制介绍: baiy.cn/doc/cpp/ins...
java
#include <iostream>
#include <csignal>
void *origin_onMessageReceived = nullptr;
void *origin__android_log_assert = nullptr;
class MyCustomException : public std::exception {
public:
explicit MyCustomException(const std::string& message)
: msg_(message) {}
virtual const char* what() const noexcept override {
return msg_.c_str();
}
private:
std::string msg_;
};
void _android_log_assert_proxy(const char* cond, const char *tag, const char* fmt, ...) {
//模拟liblog.so的__android_log_assert函数
std::cout << "__android_log_assert start" << std::endl;
if (!strncmp(fmt, "postPendingRepliesAndDeferredMessages: mReplyID == null", 55)) {
throw MyCustomException("postPendingRepliesAndDeferredMessages: mReplyID == null");
}
//模拟调用origin__android_log_assert,产生崩溃
raise(SIGABRT);
}
void onMessageReceived_proxy(void *thiz, void *msg) {
std::cout << "onMessageReceived_proxy start" << std::endl;
try {
//模拟调用onMessageReceived原函数(origin_onMessageReceived)进入崩溃流程
std::cout << "onMessageReceived_proxy 1" << std::endl;
_android_log_assert_proxy(nullptr, nullptr, "postPendingRepliesAndDeferredMessages: mReplyID == null, from kWhatRelease:STOPPING following kWhatError:STOPPING");
std::cout << "onMessageReceived_proxy 2" << std::endl;//走不到
} catch (const MyCustomException& e) {
//保护后从此处返回
std::cout << "onMessageReceived_proxy 3" << std::endl;
}
std::cout << "onMessageReceived_proxy end" << std::endl;
}
int main() {
std::cout << "main func start" << std::endl;
/**
inline-hook: shadowhook_hook_sym_name("libstagefright.so","_ZN7android10MediaCodec17onMessageReceivedERKNS_2spINS_8AMessageEEE",(void *) onMessageReceived_proxy, (void **) &origin_onMessageReceived);
plhook: xh_core_register("libstagefright.so", "__android_log_assert", (void *) (_android_log_assert_proxy), (void **) (&origin__android_log_assert));
*/
//模拟调用libstagefright.so的_ZN7android10MediaCodec17onMessageReceivedERKNS_2spINS_8AMessageEEE函数
onMessageReceived_proxy(nullptr, nullptr);
std::cout << "main func end" << std::endl;
return 0;
}
/**
日志输出
main func start
onMessageReceived_proxy start
onMessageReceived_proxy 1
__android_log_assert start
onMessageReceived_proxy 3
onMessageReceived_proxy end
main func end
*/
灰度上线后发现有设备走到了_android_log_assert代理函数中的throw逻辑,但是未按预期走到catch块而是把错误又转移为" terminating with uncaught exception of type" ,有点搞心态啊。
【柳暗花明】C++的异常处理机制在throw执行时,会开始在调用栈中向上查找匹配的catch块,检查每一个函数直到找到一个具有合适类型的catch块,上述的错误信息代表未找到匹配的catch块。从转移的堆栈中注意到没有onMessageReceived代理函数的堆栈,此时基于inline-hook的原理(修改原函数前面的汇编代码跳转到代理函数)又怀疑到它身上,再次排查代码时发现代理函数开头漏写了一个宏,在inline-hook中SHADOWHOOK_STACK_SCOPE就是来管理栈祯的,因此出现找不到catch块以及前面longjmp的问题就不奇怪了。加上这个宏以后柳暗花明,重新放量后保护逻辑按预期执行并且保护生效后视频播放正常。和音视频的小伙伴一努力下,经历了几个版本终于解决了这个系统bug,目前仅剩老版本App有零星的上报。
四、bio多线程环境崩溃
背景
Android 11 Socket close过程中在多线程场景下有几率产生野指针问题导致Native Crash,现象是多个线程同时close连接时,一个线程已销毁了bio的上下文,另外一个线程仍执行close并在此过程中尝试获取这个bio有多少未写出去的字节数时出现野指针导致的段错误。此问题从21年首次上报以来在得物的Crash列表中一直处于较前的位置。
堆栈与上报趋势
Java
at com.android.org.conscrypt.NativeCrypto.SSL_pending_written_bytes_in_BIO(Native method)
at com.android.org.conscrypt.NativeSsl$BioWrapper.getPendingWrittenBytes(NativeSsl.java:660)
at com.android.org.conscrypt.ConscryptEngine.pendingOutboundEncryptedBytes(ConscryptEngine.java:566)
at com.android.org.conscrypt.ConscryptEngineSocket.drainOutgoingQueue(ConscryptEngineSocket.java:584)
at com.android.org.conscrypt.ConscryptEngineSocket.close(ConscryptEngineSocket.java:480)
at okhttp3.internal.Util.closeQuietly_aroundBody0(Util.java:1)
at okhttp3.internal.Util$AjcClosure1.run(Util.java:1)
at org.aspectj.runtime.reflect.JoinPointImpl.proceed(JoinPointImpl.java:3)
at com.shizhuang.duapp.common.aspect.ThirdSdkAspect.t(ThirdSdkAspect.java:1)
at okhttp3.internal.Util.closeQuietly(Util.java:3)
at okhttp3.internal.connection.ExchangeFinder.findConnection(ExchangeFinder.java:42)
at okhttp3.internal.connection.ExchangeFinder.findHealthyConnection(ExchangeFinder.java:1)
at okhttp3.internal.connection.ExchangeFinder.find(ExchangeFinder.java:6)
at okhttp3.internal.connection.Transmitter.newExchange(Transmitter.java:5)
at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.java:5)
#00 pc 0000000000064060 /system/lib64/libcrypto.so (bio_ctrl+144)
#01 pc 00000000000615d8 /system/lib64/libcrypto.so (BIO_ctrl_pending+40)
#02 pc 00000000000387dc /apex/com.android.conscrypt/lib64/libjavacrypto.so (_ZL45NativeCrypto_SSL_pending_written_bytes_in_BIOP7_JNIEnvP7_jclassl+20)
问题分析
从设备分布上看,出问题都全是Android 11且各个国内厂商的设备都有,怀疑是Android 11引入的bug,对比了Android 11 和 Android 12的源码,发现在Android12 崩溃堆栈中的相关类 com.android.org.conscrypt.NativeSsl$BioWrapper有四个方法增加了读写锁,此时怀疑是多线程问题,通过搜索Android源码的相关issue以及差异代码的MR描述信息,进一步确认此结论。通过源码进一步分析发现NativeSsl的所有加锁的方法,会分发到NativeCrypto.java中的native方法,最终调用到native_crypto.cc中的JNI函数,如果能hook到相关的native函数并在Native层实现与Android12相同的读写锁逻辑,这个问题就可以解决了。
cs.android.com/android/pla... cs.android.com/android/pla... cs.android.com/android/pla...
解决过程
通过JNI hook代理Android12中增加锁的相关函数,当走到代理函数中时,先分发到JAVA层通过反射获取ReadWriteLock实例并上锁再通过跳板函数调用原来的JNI函数,此时就完成了对Android12 增量锁逻辑的复刻。经历了两个版本的灰度hook方案已稳定在线上运行,期间无因hook导致的网络不可用和其它崩溃问题,目前开关放全量的版本崩溃设备数已降为0。
JNI hook原理,以及详细修复过程: blog.dewu-inc.com/article/MTM...
五、小米Android15 焦点处理空指针崩溃
背景
随着Android15开放公测,焦点处理过程中发生的空指针问题逐步增多,并在1月份上升到Top。
堆栈与上报趋势
java
java.lang.NullPointerException: Attempt to invoke virtual method 'android.view.ViewGroup$LayoutParams android.view.View.getLayoutParams()' on a null object reference
at android.view.ViewRootImpl.handleWindowFocusChanged(ViewRootImpl.java:5307)
at android.view.ViewRootImpl.-$$Nest$mhandleWindowFocusChanged(Unknown Source:0)
at android.view.ViewRootImpl$ViewRootHandler.handleMessageImpl(ViewRootImpl.java:7715)
at android.view.ViewRootImpl$ViewRootHandler.handleMessage(ViewRootImpl.java:7611)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loopOnce(Looper.java:249)
at android.os.Looper.loop(Looper.java:337)
at android.app.ActivityThread.main(ActivityThread.java:9568)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:593)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:935)
问题分析
通过分析ASOP的源码,崩溃的触发点是mView字段为空。
源码中mView为空的情况有两种:
- 未调用setView方法前触发窗口焦点变化事件(只有setView方法才会给mView赋不为空的值)。
- 先正常调用setView使mView不为空,其它地方置为空。
结合前置判断了mAdded为true才会走到崩溃点,在源码中寻找到只有先正常调用setView以后在调用dispatchDetachedFromWindow时才满足mAdded=true、mView=null的条件,从采集的logcat日志中可以证明这一点,此时基本可以定位根因是窗口销毁与焦点事件处理的时序问题。
解决过程
在问题初期,尝试通过 Hook 拦截 handleWindowFocusChanged 方法增加防御:当检测到 mView 为空时直接中断后续逻辑执行。本地验证阶段,通过在 Android 15 设备上高频触发商详页 Dialog 弹窗的焦点获取与关闭操作,未复现线上崩溃问题。考虑到 Hook 方案的侵入性风险 ,且无法本地测试,最终放弃此方案上线。
通过崩溃日志分析发现,问题设备100% 集中在小米/红米机型,而该品牌在 Android 15 DAU中仅占 36% ,因此怀疑是MIUI对Android15某些定制功能有bug。经与小米技术团队数周的沟通与联合排查,最终小米在v2.0.28版本修复了此问题,需要用户升级ROM解决,目前>=2.0.28的MIUI设备无此问题的上报。
六、总结
通过上述问题的治理,系统bug类的崩溃显著减少,希望这些经验对大家有所帮助。
文 / 亚鹏
关注得物技术,每周更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。