本文分析基于Android S(12)
当App发生ANR或是System触发watchdog时,系统都希望生成一份trace文件,用来记录各个线程的调用栈信息,以及一些进程/线程的状态信息。这份文件通常存放在/data/anr
目录下,APP开发者拿不到。不过从Android R(11)开始,App便可以通过AMS的getHistoricalProcessExitReasons
接口读取该文件的详细信息。以下是一份典型trace文件中的内容。
----- pid 8331 at 2021-11-26 09:10:03 -----
Cmd line: com.hangl.test
Build fingerprint: xxx
ABI: 'arm64'
Build type: optimized
Zygote loaded classes=9118 post zygote classes=475
Dumping registered class loaders
#0 dalvik.system.PathClassLoader: [], parent #1
#1 java.lang.BootClassLoader: [], no parent
...
(进程整体的一些状态,譬如GC的统计数据)
suspend all histogram: Sum: 161us 99% C.I. 2us-60us Avg: 16.100us Max: 60us
DALVIK THREADS (14):
"Signal Catcher" daemon prio=5 tid=7 Runnable
| group="system" sCount=0 dsCount=0 flags=0 obj=0x14dc0298 self=0x7c4c962c00
...
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x7263ee78 self=0x7c4c7dcc00
| sysTid=8331 nice=-10 cgrp=default sched=0/0 handle=0x7c4dd45ed0
| state=S schedstat=( 387029514 32429484 166 ) utm=28 stm=10 core=6 HZ=100
| stack=0x7feacb5000-0x7feacb7000 stackSize=8192KB
| held mutexes=
native: #00 pc 00000000000d0f48 /apex/com.android.runtime/lib64/bionic/libc.so (__epoll_pwait+8)
native: #01 pc 00000000000180bc /system/lib64/libutils.so (android::Looper::pollInner(int)+144)
native: #02 pc 0000000000017f8c /system/lib64/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+56)
native: #03 pc 000000000013b920 /system/lib64/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce(_JNIEnv*, _jobject*, long, int)+44)
at android.os.MessageQueue.nativePollOnce(Native method)
at android.os.MessageQueue.next(MessageQueue.java:336)
at android.os.Looper.loop(Looper.java:174)
at android.app.ActivityThread.main(ActivityThread.java:7397)
at java.lang.reflect.Method.invoke(Native method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:935)
"Jit thread pool worker thread 0" daemon prio=5 tid=2 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x14dc0220 self=0x7bb9a05000
...
本文既无意于讨论ANR的触发类型,也无意于流水账式地展示每块内容的生成顺序,因为这些已经有不少文章写过,而且其中不乏精品。鉴于此,本文将重点分析调用栈的生成流程,而它将帮助我们更好地理解trace信息。
前言
不论是ANR还是Watchdog,trace的生成过程都放在target process中进行。以ANR为例,它的判定过程发生在system_server(AMS)中,而trace的生成过程却发生在APP中。那么如何让APP开始这个过程呢?答案是给它发送SIGQUIT(信号3)。之所以这样处理,是因为跨进程的信息收集通常采用ptrace方案,它要求收集方要么拥有特殊权限,要么满足进程间父子关系,而这些都没有进程内收集方便。
因此,分析的第一步便是去查看信号3在进程中的处理方式。
1. Signal Catcher线程
“Signal Catcher”线程在每个Java进程中都会存在。正常运行时,它将挂起等待信号3(以及信号10)的到来。当该进程接收到信号3时,将会交由”Signal Catcher”线程处理,处理的函数为HandleSigQuit
。
void SignalCatcher::HandleSigQuit() {
Runtime* runtime = Runtime::Current();
std::ostringstream os;
os GetFingerprint();
os GetInstructionSet()) DumpForSigQuit(os);
if ((false)) {
std::string maps;
if (android::base::ReadFileToString("/proc/self/maps", &maps)) {
os
中间的跳转过程就不展示了,直接进入我们关心的议题:调用栈的收集过程。通过ThreadList::Dump
函数,我们可以收集所有线程的调用栈信息。
void ThreadList::Dump(std::ostream& os, bool dump_native_stack) {
Thread* self = Thread::Current();
{
MutexLock mu(self, *Locks::thread_list_lock_);
os
其中关键的环节是执行RunCheckpoint函数。它将每个线程的信息收集分为单独的任务:**如果该线程正处在Runnable状态(运行java代码),则将收集的任务派发给线程自己处理;如果该线程处于其他状态,则由”Signal Catcher”线程代为完成。**请记住这句话,因为下面2、3小节分析的就是它的两种不同情况。
2. Checkpoint机制
将任务派发给Runnable状态的线程采用的是checkpoint机制,它分为两个部分:
- “Signal Catcher”线程调用RequestCheckpoint去改变目标线程的art::Thread对象的内部数据,具体而言改变的是以下两个字段。
tls32_.state_and_flags.as_struct.flags |= kCheckpointRequest;
tlsPtr_.checkpoint_function = function;
(tls32_和tlsPtr_均是art::Thread对象的内部数据)
- 对ART虚拟机而言,目标线程在每个方法的起始位置和循环语句的跳转位置都会去检查state_and_flags字段,如果checkpoint位被置上,则执行相应的checkpoint函数。这样安插检查点可以保证线程”及时“地处理checkpoint任务:因为所有向前执行(直线型、带条件分支都算)的代码都会在有限时间内执行完,而可能导致长时间执行的代码,要么是循环,要么是方法调用,所以只要在这两个地方插入检查点就可以保证及时性了。(参考R大知乎回答)
关于目标线程的检查点,这里我还想举个例子,让大家真切地感受到它的存在。
字节码在ART虚拟机中可以解释执行,也可以编译成机器码执行。当一个方法被编译为机器码后(如下所示),我们可以在函数的入口处看到有检测state_and_flags的操作。当有标志位被置上时,则执行pTestSuspend动作。
CODE: (code_offset=0x003f9ae0 size=788)...
0x003f9ae0: d1400bf0 sub x16, sp, #0x2000 (8192)
0x003f9ae4: b940021f ldr wzr, [x16]
StackMap[0] (native_pc=0x3f9ae8, dex_pc=0x0, register_mask=0x0, stack_mask=0b)
0x003f9ae8: f8180fe0 str x0, [sp, #-128]!
0x003f9aec: a9035bf5 stp x21, x22, [sp, #48]
0x003f9af0: a90463f7 stp x23, x24, [sp, #64]
0x003f9af4: a9056bf9 stp x25, x26, [sp, #80]
0x003f9af8: a90673fb stp x27, x28, [sp, #96]
0x003f9afc: a9077bfd stp x29, lr, [sp, #112]
0x003f9b00: b9008fe2 str w2, [sp, #140]
0x003f9b04: 79400270 ldrh w16, [tr] ; state_and_flags
0x003f9b08: 350016f0 cbnz w16, #+0x2dc (addr 0x3f9de4) //如果state_and_flags不为0,则跳转到0x3f9de4的位置
...
0x003f9de4: 940e62c3 bl #+0x398b0c (addr 0x7928f0) ; pTestSuspend //跳转进入pTestSuspend
几经跳转,pTestSuspend最终会调用Thread::CheckSuspend函数。当checkpoint位被置上时,则执行相应的checkpoint函数(RunCheckpointFunction)。
inline void Thread::CheckSuspend() {
DCHECK_EQ(Thread::Current(), this);
for (;;) {
if (ReadFlag(kCheckpointRequest)) {
RunCheckpointFunction();
} else if (ReadFlag(kSuspendRequest)) {
FullSuspendCheck();
} else if (ReadFlag(kEmptyCheckpointRequest)) {
RunEmptyCheckpoint();
} else {
break;
}
}
}
下面举一个Runnable线程自己收集调用栈的例子,2292行正好是writeNoException方法的第一行,与上述”在每个方法的起始位置插入检查点”的描述相吻合。
"Binder:2278_C" prio=5 tid=97 Runnable
| group="main" sCount=0 ucsCount=0 flags=0 obj=0x16104b20 self=0xb400007117c7afb0
| sysTid=2890 nice=0 cgrp=foreground sched=0/0 handle=0x6eafe24cb0
| state=R schedstat=( 47445156223 266433061959 175792 ) utm=1623 stm=3121 core=4 HZ=100
| stack=0x6eafd2d000-0x6eafd2f000 stackSize=991KB
| held mutexes= "mutator lock"(shared held)
at android.os.Parcel.writeNoException(Parcel.java:2292)
at android.os.IPowerManager$Stub.onTransact(IPowerManager.java:474)
at android.os.Binder.execTransactInternal(Binder.java:1184)
at android.os.Binder.execTransact(Binder.java:1143)
2291 public final void writeNoException() {
2292 AppOpsManager.prefixParcelWithAppOpsIfNeeded(this);
3. Suspend标志位
对于那些非Runnable状态的线程,收集的工作由”Signal Catcher”代为完成。这里我梳理了一下为单个线程”代工“的流程,总共分为4步。
thread->ModifySuspendCount(self, +1, nullptr, SuspendReason::kInternal);
checkpoint_function->Run(thread);
thread->ModifySuspendCount(self, -1, nullptr, SuspendReason::kInternal);
Thread::resume_cond_->Broadcast(self);
- 增加target thread的suspend count(+1),且给它置上suspend标志位。
- 运行相应的函数替target thread收集信息。
- 减少target thread的suspend count(-1),如果suspend count减为0,则清除suspend标志位。
- 调用resume_cond_条件变量的Broadcast函数,它会唤醒所有等待在它上面的线程。
流程的梳理总是简单,难的是理解流程设计背后的原因,下面来条分缕析。
- 为什么在执行信息收集前需要给target thread置上suspend标志位?
在回答这个问题前,我们需要补充些基础知识。每个Java线程本质上都是一个pthread线程,而它在内核中又对应一个task_struct对象,该对象是CPU调度的基本单元。从CPU的视角来看,该线程可以是R态、S态、D态等,它们的含义如下所示。不过虚拟机中又为Java线程记录了另一套状态,它反映的是虚拟机视角下的状态,具体分类如下。
R running or runnable (on run queue)
D uninterruptible sleep (usually IO)
S interruptible sleep (waiting for an event to complete)
```
enum ThreadState {
// Java
// Thread.State JDWP state
kTerminated = 66, // TERMINATED TS_ZOMBIE Thread.run has returned, but Thread* still around
kRunnable, // RUNNABLE TS_RUNNING runnable
kTimedWaiting, // TIMED_WAITING TS_WAIT in Object.wait() with a timeout
kSleeping, // TIMED_WAITING TS_SLEEPING in Thread.sleep()
kBlocked, // BLOCKED TS_MONITOR blocked on a monitor
kWaiting, // WAITING TS_WAIT in Object.wait()
kWaitingForLockInflation, // WAITING TS_WAIT blocked inflating a thin-lock
kWaitingForTaskProcessor, // WAITING TS_WAIT blocked waiting for taskProcessor
kWaitingForGcToComplete, // WAITING TS_WAIT blocked waiting for GC
kWaitingForCheckPointsToRun, // WAITING TS_WAIT GC waiting for checkpoints to run
kWaitingPerformingGc, // WAITING TS_WAIT performing GC
kWaitingForDebuggerSend, // WAITING TS_WAIT blocked waiting for events to be sent
kWaitingForDebuggerToAttach, // WAITING TS_WAIT blocked waiting for debugger to attach
kWaitingInMainDebuggerLoop, // WAITING TS_WAIT blocking/reading/processing debugger events
kWaitingForDebuggerSuspension, // WAITING TS_WAIT waiting for debugger suspend all
kWaitingForJniOnLoad, // WAITING TS_WAIT waiting for execution of dlopen and JNI on load code
kWaitingForSignalCatcherOutput, // WAITING TS_WAIT waiting for signal catcher IO to complete
kWaitingInMainSignalCatcherLoop, // WAITING TS_WAIT blocking/reading/processing signals
kWaitingForDeoptimization, // WAITING TS_WAIT waiting for deoptimization suspend all
kWaitingForMethodTracingStart, // WAITING TS_WAIT waiting for method tracing to start
kWaitingForVisitObjects, // WAITING TS_WAIT waiting for visiting objects
kWaitingForGetObjectsAllocated, // WAITING TS_WAIT waiting for getting the number of allocated objects
kWaitingWeakGcRootRead, // WAITING TS_WAIT waiting on the GC to read a weak root
kWaitingForGcThreadFlip, // WAITING TS_WAIT waiting on the GC thread flip (CC collector) to finish
kNativeForAbort, // WAITING TS_WAIT checking other threads are not run on abort.
kStarting, // NEW TS_WAIT native thread started, not yet ready to run managed code
kNative, // RUNNABLE TS_RUNNING running in a JNI native method
kSuspended, // RUNNABLE TS_RUNNING suspended by GC or debugger
};
```
一个处于R态的线程表明它逻辑上正在运行(由于调度的关系,他可能暂时未被执行,但总会被执行[一定时间内]),而它运行的代码可能位于kernel层,native层或java层。只有当它运行在java层时,虚拟机中记载的状态才是Runnable。
如果target thread处于非Runnable状态,也就意味它并未处于java层。可是不处在java层并不表示它不运行。在”Signal Catcher”代理target thread进行收集的过程中,target thread随时可能返回java层(结束了native层的工作或是发起了对java方法的调用)。一旦返回java层,java层的调用栈形态就会被改变。这样”Signal Catcher”和target thread之间对于调用栈整体形态就会存在竞争关系。
因此我们需要一种方案去解决这种竞争。
所有返回到java层的操作都需要进行线程状态切换,也即调用TransitionFromSuspendedToRunnable函数。该函数内部会判断suspend标志位,一旦它被置上,target thread就会等待在resume_cond_条件变量上。因此,置上suspend标志位可以保证target thread无法返回java层,也即无法改变java层的调用栈形态。(值得注意的是,网上有些言论认为置上suspend标志位是为了暂停线程,这其实是一种不严谨的认识。对于不想返回java层的线程而言,置上suspend标志位丝毫不影响它的运行。)
- 为什么需要在信息收集结束后调用resume_cond_条件变量的Broadcast函数?
因为有些准备返回java层的线程此时正等待在resume_cond_条件变量上(处于S态),当执行完收集操作后,我们有必要唤醒它们让它们继续工作。
- 分析了这么多,举个实际案例吧。通过native的#2可以知道,主线程已经结束了native层的工作,希望返回到java层。不过从堆栈中我们找不到TransitionFromSuspendedToRunnable的身影,原因是它被inline(内联)到GoToRunnable函数内部了。而#1 WaitingHoldingLocks等待的就是resume_cond_条件变量。
"main" prio=5 tid=1 Native
| group="main" sCount=1 ucsCount=0 flags=1 obj=0x71a33c18 self=0xb400006f417a1380
| sysTid=14756 nice=-10 cgrp=top-app sched=0/0 handle=0x71027344f8
| state=S schedstat=( 603683604122 79803215759 1916541 ) utm=43513 stm=16854 core=6 HZ=100
| stack=0x7fe8361000-0x7fe8363000 stackSize=8188KB
| held mutexes=
native: #00 pc 000000000004dff0 /apex/com.android.runtime/lib64/bionic/libc.so (syscall+32)
native: #01 pc 000000000028dc74 /apex/com.android.art/lib64/libart.so (art::ConditionVariable::WaitHoldingLocks(art::Thread*)+152)
native: #02 pc 000000000074c4ec /apex/com.android.art/lib64/libart.so (art::GoToRunnable(art::Thread*)+412)
native: #03 pc 000000000074c318 /apex/com.android.art/lib64/libart.so (art::JniMethodEnd(unsigned int, art::Thread*)+28)
at android.os.BinderProxy.transactNative(Native method)
at android.os.BinderProxy.transact(BinderProxy.java:571)
at com.android.internal.telephony.ISub$Stub$Proxy.getAvailableSubscriptionInfoList(ISub.java:1543)
at android.telephony.SubscriptionManager.getAvailableSubscriptionInfoList(SubscriptionManager.java:1640)
不过需要注意,这种trace只会在如下的时序条件下生成。如果运行在native层的函数没有结束,那么也就不需要返回java层,同时也就不会调用GoToRunnable。
因此,当我们发生ANR时看到主线程的调用栈如上所示,千万不要认为GoToRunnable才是ANR的元凶。它仅仅表明线程在执行过程中希望回到java层,而真正导致ANR的可能是一个消息的整体耗时。
4. Java调用栈收集
(这一小节较为粗略,如无兴趣可跳过)
通过调用StackDumpVisitor::WalkStack函数,我们可以收集到java层的调用栈信息。这个函数的内部比较复杂,想要完整的了解还要补充ArtMethod及DexFile等一系列知识。本文不打算完整介绍,只是概括性地总结。
机器码的每条指令都有编号,它在运行时表现为PC值。同样,Dex字节码的每条指令也有编号,它在Dex文件中表现为dex_pc(每个方法的dex_pc都从0开始编号)。譬如下方文件中的0x0003、0x0008等就是dex_pc。
DEX CODE:
0x0000: 7010 5350 0100 | invoke-direct {v1}, void android.media.IPlayer$Stub.() // method@20563
0x0003: 2200 791f | new-instance v0, java.lang.ref.WeakReference // type@TypeIndex[8057]
0x0005: 7020 84fa 2000 | invoke-direct {v0, v2}, void java.lang.ref.WeakReference.(java.lang.Object) // method@64132
0x0008: 5b10 582e | iput-object v0, v1, Ljava/lang/ref/WeakReference; android.media.PlayerBase$IPlayerWrapper.mWeakPB // field@11864
0x000a: 0e00 | return-void
字节码在实际运行时,可能被解释执行,也可能被编译成机器码执行(AOT或JIT),而这两种执行方式的调用栈回溯方法是不同的。原因是机器码执行时,java方法在栈帧结构上表现得就像纯native方法(S上引入了新的解释器nterp,它的栈帧结构和机器码执行也是一致的,因此性能比之前的mterp更好);而解释执行(这里指mterp解释器)会有专门的数据结构来记录dex_pc值。
当我们希望回溯出一帧java调用栈信息时,其实是想得到三个信息:方法名,文件名以及行号(至于锁信息,它并非每一帧都有,因此属于另一个话题,这里不做阐述)。
at android.os.Looper.loop(Looper.java:174)
想要得到这三个信息,其实依赖的数据也有三个:ArtMethod对象、DexFile信息及dex_pc值。由于DexFile信息可以通过ArtMethod间接得到,因此我们在回溯的过程中,主要目的就是为每一帧寻找它的ArtMehtod对象和dex_pc值。
这个寻找对于解释执行来说很简单,因为解释执行会有专门的数据结构来记录它,这个特定的数据结构就是ShadowFrame。
可是对于机器码执行来说,问题就变得复杂了很多。好在每一帧的机器码执行都遵循一个规律:栈顶存放当前执行方法的ArtMethod指针。因此当一连串的方法调用发生时,我们可以仅凭最后一帧的sp值就解析出所有信息,原理如下:
- 通过sp值,我们两次解引用可以获得当前运行方法的ArtMethod对象。
- 通过ArtMethod进一步获取FrameInfo,其中可知frame size。
- sp+frame size便可以得知上一帧的sp值。
- 通过上一帧的sp也可以获知返回地址的值,通常存于x30寄存器,方法调用时会压入栈中固定偏移的位置。
因此,我们可以获取每一帧的ArtMethod对象及pc值(最顶上的一帧要么是native方法,要么是runtime方法,都不需要恢复行号)。通过如下方法,便可以进一步得到dex_pc值,这样每一帧的详细信息便可以解析出来。
uint32_t StackVisitor::GetDexPc(bool abort_on_failure) const {
if (cur_shadow_frame_ != nullptr) {
return cur_shadow_frame_->GetDexPC();
} else if (cur_quick_frame_ != nullptr) {
if (IsInInlinedFrame()) {
return current_inline_frames_.back().GetDexPc();
} else if (cur_oat_quick_method_header_ == nullptr) {
return dex::kDexNoIndex;
} else if ((*GetCurrentQuickFrame())->IsNative()) {
return cur_oat_quick_method_header_->ToDexPc(
GetCurrentQuickFrame(), cur_quick_frame_pc_, abort_on_failure);
} else if (cur_oat_quick_method_header_->IsOptimized()) {
StackMap* stack_map = GetCurrentStackMap();
DCHECK(stack_map->IsValid());
return stack_map->GetDexPc();
} else {
DCHECK(cur_oat_quick_method_header_->IsNterpMethodHeader());
return NterpGetDexPC(cur_quick_frame_);
}
} else {
return 0;
}
}
不过我们上述的描述中还省略了一种情况,也即java inline的情况,它在unwind的过程中也是耗时的大户。
5. Native调用栈收集
在正常的trace生成过程中,一个线程的native调用栈是否收集取决于以下函数的判断,如下序号表示判断优先级。
static bool ShouldShowNativeStack(const Thread* thread)
REQUIRES_SHARED(Locks::mutator_lock_) {
ThreadState state = thread->GetState();
// In native code somewhere in the VM (one of the kWaitingFor* states)? That's interesting.
if (state > kWaiting && state HasManagedStack()) {
return true;
}
// In some other native method? That's interesting.
// We don't just check kNative because native methods will be in state kSuspended if they're
// calling back into the VM, or kBlocked if they're blocked on a monitor, or one of the
// thread-startup states if it's early enough in their life cycle (http://b/7432159).
ArtMethod* current_method = thread->GetCurrentMethod(nullptr);
return current_method != nullptr && current_method->IsNative();
}
- 当state是虚拟机相关的状态时,需要收集native调用栈。那什么是虚拟机相关的状态呢?譬如kWaitingForGcToComplete,它表示当前线程在等待GC结束。因此我们可以理解这些状态是因为虚拟机自身工作而影响到本线程运行的状态。
- 如果state是Waiting或Sleeping相关的状态时,则省略native调用栈的收集。因为处于此状态的线程,其native层的调用栈最终必然为futex系统调用,因此输出这些调用栈并不会给调试带来有价值的信息,故可省略。
- 如果该线程没有java层调用栈信息,则需要收集native调用栈,否则没有任何信息可输出。
- 如果java层调用栈的最后一帧为native方法,则需要收集native调用栈,以便了解native层具体的动作。
接下来就要讨论如何收集native调用栈,这个过程的专业术语叫做回溯或是展开(unwind),在Android中主要通过libunwindstack这个库来完成。
收集native调用栈,本质上是寻找每一帧的pc值。当我们拿到最后一帧的sp值后,便可以通过寻找返回地址的方式,不断回溯出其上的每一帧pc值。
因此,接下来的问题可以简化为两个:
- 如何寻找最后一帧的寄存器(sp/pc)值?
- 如何寻找每一帧的返回地址?
5.1 如何寻找最后一帧的寄存器值
寄存器信息本质上是线程相关的,因此这里分为两种情况来讨论。
- 本线程收集本线程的调用栈。
- “Signal Catcher”线程收集其他线程的调用栈。
本线程获取寄存器值较为简单,只需要一些基本的汇编指令便可以完成。譬如如下代码,可以将32个通用寄存器的值存到用户空间特定的数据结构中。
inline __attribute__((__always_inline__)) void AsmGetRegs(void* reg_data) {
asm volatile(
"1:n"
"stp x0, x1, [%[base], #0]n"
"stp x2, x3, [%[base], #16]n"
"stp x4, x5, [%[base], #32]n"
"stp x6, x7, [%[base], #48]n"
"stp x8, x9, [%[base], #64]n"
"stp x10, x11, [%[base], #80]n"
"stp x12, x13, [%[base], #96]n"
"stp x14, x15, [%[base], #112]n"
"stp x16, x17, [%[base], #128]n"
"stp x18, x19, [%[base], #144]n"
"stp x20, x21, [%[base], #160]n"
"stp x22, x23, [%[base], #176]n"
"stp x24, x25, [%[base], #192]n"
"stp x26, x27, [%[base], #208]n"
"stp x28, x29, [%[base], #224]n"
"str x30, [%[base], #240]n"
"mov x12, spn"
"adr x13, 1bn"
"stp x12, x13, [%[base], #248]n"
: [base] "+r"(reg_data)
:
: "x12", "x13", "memory");
}
可如果是跨线程(不是跨进程)来获取,该如何处理呢?
答案是通过信号。当目标线程运行在用户空间时,寄存器值是不会有备份的。只有当它发生用户态和内核态的切换时,信息才会被备份。此外,切换的过程也会检测信号,进而触发信号处理函数,因此刚刚备份的寄存器信息可以进一步传递给处理函数。而那里,才是我们跨线程获取寄存器值的真正位置。
Android中采用信号33(THREAD_SIGNAL)来完成这项工作,它的处理函数也比较简单,即将sigcontext中的寄存器信息拷贝到全局数据中,这样其他线程便可以获取它。
5.2 如何寻找每一帧的返回地址
当函数调用发生时,返回地址通常存在x30寄存器(AArch64)中。如果被调用者内部需要使用这个寄存器,那么它的起始片段肯定要将x30的值存到栈中,否则返回地址将丢失。可是x30的值到底存在栈中什么位置呢?
当开启-fomit-frame-pointer
编译选项时,x30存储的位置和x29(FP寄存器)相邻,因此很容易找出。可是没有此编译选项时,x30的值就得依赖更多的信息来获取。在64位库中,这个信息称为”Call Frame Information”,它存储在elf文件的.eh_frame段。微信技术团队的一篇文章关于这点描述的比较清楚,引用如下:
当你的代码执行到某一“行”时,根据此时的 pc 我们可以从”Call Frame Information”中查询到退出当前函数栈时各个寄存器该怎么进行恢复,比如它可能描述了寄存器的值该从当前栈的哪个位置上读回来。
除了可以unwind纯native的帧,libunwindstack库还支持AOT/JIT的帧以及解释执行的帧。这也就表明,通过libunwindstack收集的调用栈除了可以反映native层调用信息,还可以反映java层的调用信息,如下示例。
#00 pc 000aa0f8 /system/lib/libart.so (void std::__1::__tree_balance_after_insert*>(std::__1::__tree_node_base*, std::__1::__tree_balance_after_insert*>)+32)
#01 pc 001a0a35 /system/lib/libart.so (art::gc::space::LargeObjectMapSpace::Alloc(art::Thread*, unsigned int, unsigned int*, unsigned int*, unsigned int*)+180)
#02 pc 003cd4f5 /system/lib/libart.so (art::mirror::Object* art::gc::Heap::AllocLargeObject(art::Thread*, art::ObjPtr*, unsigned int, art::mirror::SetLengthVisitor const&)+108)
#03 pc 003cb659 /system/lib/libart.so (artAllocArrayFromCodeResolvedRegionTLAB+484)
#04 pc 00411613 /system/lib/libart.so (art_quick_alloc_array_resolved16_region_tlab+82)
#05 pc 0020cfe3 /system/framework/arm/boot-core-oj.oat (offset 0x10d000) (java.lang.AbstractStringBuilder.append+242)
#06 pc 002b809b /system/framework/arm/boot-core-oj.oat (offset 0x10d000) (java.lang.StringBuilder.append+50)
#07 pc 001199b7 /system/framework/arm/boot-core-libart.oat (offset 0x76000) (org.json.JSONTokener.nextString+214)
#08 pc 00119b73 /system/framework/arm/boot-core-libart.oat (offset 0x76000) (org.json.JSONTokener.nextValue+162)
#09 pc 001195db /system/framework/arm/boot-core-libart.oat (offset 0x76000) (org.json.JSONTokener.readObject+314)
#10 pc 00119b47 /system/framework/arm/boot-core-libart.oat (offset 0x76000) (org.json.JSONTokener.nextValue+118)
#11 pc 0040d775 /system/lib/libart.so (art_quick_invoke_stub_internal+68)
#12 pc 003e72c9 /system/lib/libart.so (art_quick_invoke_stub+224)
#13 pc 000a103d /system/lib/libart.so (art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)+136)
#14 pc 001e60f1 /system/lib/libart.so (art::interpreter::ArtInterpreterToCompiledCodeBridge(art::Thread*, art::ArtMethod*, art::ShadowFrame*, unsigned short, art::JValue*)+236)
#15 pc 001e0bdf /system/lib/libart.so (bool art::interpreter::DoCall(art::ArtMethod*, art::Thread*, art::ShadowFrame&, art::Instruction const*, unsigned short, art::JValue*)+814)
#16 pc 003e1f23 /system/lib/libart.so (MterpInvokeVirtual+442)
#17 pc 00400514 /system/lib/libart.so (ExecuteMterpImpl+14228)
#18 pc 002613ec /system/priv-app/ReusLauncherDev/ReusLauncherDev.apk (offset 0x9c9000) (com.reus.launcher.AsusAnimationIconReceiver.a+80)
#19 pc 001c535b /system/lib/libart.so (_ZN3art11interpreterL7ExecuteEPNS_6ThreadERKNS_20CodeItemDataAccessorERNS_11ShadowFrameENS_6JValueEb.llvm.866626450+378)
#20 pc 001c9a41 /system/lib/libart.so (art::interpreter::ArtInterpreterToInterpreterBridge(art::Thread*, art::CodeItemDataAccessor const&, art::ShadowFrame*, art::JValue*)+152)
#21 pc 001e0bc7 /system/lib/libart.so (bool art::interpreter::DoCall(art::ArtMethod*, art::Thread*, art::ShadowFrame&, art::Instruction const*, unsigned short, art::JValue*)+790)
#22 pc 003e2eff /system/lib/libart.so (MterpInvokeStatic+130)
#23 pc 00400694 /system/lib/libart.so (ExecuteMterpImpl+14612)
#24 pc 0028ae7a /system/priv-app/ReusLauncherDev/ReusLauncherDev.apk (offset 0x9c9000) (com.reus.launcher.d.run+1274)
#25 pc 001c535b /system/lib/libart.so (_ZN3art11interpreterL7ExecuteEPNS_6ThreadERKNS_20CodeItemDataAccessorERNS_11ShadowFrameENS_6JValueEb.llvm.866626450+378)
#26 pc 001c9987 /system/lib/libart.so (art::interpreter::EnterInterpreterFromEntryPoint(art::Thread*, art::CodeItemDataAccessor const&, art::ShadowFrame*)+82)
#32 pc 0040d775 /system/lib/libart.so (art_quick_invoke_stub_internal+68)
#33 pc 003e72c9 /system/lib/libart.so (art_quick_invoke_stub+224)
#34 pc 000a103d /system/lib/libart.so (art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)+136)
#36 pc 00348f6d /system/lib/libart.so (art::InvokeVirtualOrInterfaceWithJValues(art::ScopedObjectAccessAlreadyRunnable const&, _jobject*, _jmethodID*, jvalue*)+320)
#37 pc 00369ee7 /system/lib/libart.so (art::Thread::CreateCallback(void*)+866)
#38 pc 00072131 /system/lib/libc.so (__pthread_start(void*)+22)
#39 pc 0001e005 /system/lib/libc.so (__start_thread+24)
24是虚拟帧,也即它并非存在于栈上,而是辅助调试所增加的信息,它反映的就是2325这几帧解释器正在解释的java方法。0510反映的是机器码执行(AOT编译)的java方法。00~04反映的是纯native层(so库)的函数调用。
那么这时就有一个问题萦绕在我们心头:libunwindstack可以收集到java层的调用信息,那为什么trace文件中的native调用栈仅仅显示了native层的调用信息呢?
原因是trace文件在收集调用栈时做了截断和省略。具体的策略如下:
- 在回溯的过程中,如果碰到所属文件后缀为oat或odex的帧,则停止回溯。原因是JNI的跳板函数/AOT编译的java方法通常都在oat/odex文件中,碰到它们停止回溯,可以略去后续java方法的回溯。
backtrace_map_->SetSuffixesToIgnore(std::vector { "oat", "odex" });
- 回溯的栈帧中如果有落在”libunwindstack.so”和”libbacktrace.so”的帧,则不予显示。原因是这些帧反映的是调用栈收集过程,而非线程原始的调用逻辑。
std::vector skip_names{"libunwindstack.so", "libbacktrace.so"};
5.3 当前调用栈回溯的缺陷
仔细思考上述策略的第一条,其实可以发现它是有缺陷的。这个缺陷主要有两点:
- JNI跳板函数一定在oat/odex文件中么?
其实并不是。在dex2oat阶段,系统会为一个oat/odex文件中参数兼容(个数相同且类型相似)的native方法统一生成一个JNI跳板函数。这点可以展开举个例子。
#05 pc 00000000000eeb24 /system/lib64/libandroid_runtime.so (android::nativeCreate(_JNIEnv*, _jclass*, _jstring*, int)+132)
#06 pc 00000000003dff04 /system/framework/arm64/boot-framework.oat (offset 0x3d6000) (android.graphics.FontFamily.nInitBuilder [DEDUPED]+180)
#07 pc 000000000091414c /system/framework/arm64/boot-framework.oat (offset 0x3d6000) (android.database.CursorWindow.+172)
如上调用栈是Android S以前tombstone文件的典型输出。通过代码我们可以得知,#7中的CursorWindow构造方法明明调用的是nativeCreate方法,可为什么回溯出来的#6却是nInitBuilder?原因正是一个JNI跳板函数可以被多个native方法使用,而回溯时只是从众多native方法中挑了一个名字显示出来。因此后面的DEDUPED正是提醒我们,这一帧是不可信的。具体解释如下:
## DEDUPED frames
If the name of a Java method includes `[DEDUPED]`, this means that multiple
methods share the same code. ART only stores the name of a single one in its
metadata, which is displayed here. This is not necessarily the one that was
called.
继续查看nativeCreate和nInitBuilder的方法定义,可以发现它们的参数个数和类型均相同,因此在dex2oat后可以共用一个JNI跳板函数。
private static native long nativeCreate(String name, int cursorWindowSize);
private static native long nInitBuilder(String langs, int variant);
好在从Android S开始,这一帧不再显示具体的方法名,而是统一的art_jni_trampoline,这样可以减少对开发者的困扰。如下示例。
#05 pc 00000000004a600c /apex/com.android.art/lib64/libart.so (art::VMDebug_countInstancesOfClass(_JNIEnv*, _jclass*, _jclass*, unsigned char)+876) (BuildId: 2ede688a1cdde049a8439e413c1c41f8)
#06 pc 0000000000010fb4 /apex/com.android.art/javalib/arm64/boot-core-libart.oat (art_jni_trampoline+180) (BuildId: a58ab7e35be2dda5ad3453c56bfefea6edf331bf)
#07 pc 000000000064037c /system/framework/arm64/boot-framework.oat (android.os.Debug.countInstancesOfClass+44) (BuildId: e47113da18d4f822af52023fa19893d55035facd)
#08 pc 0000000000812930 /system/framework/arm64/boot-framework.oat (android.view.ViewDebug.getViewRootImplCount+48) (BuildId: e47113da18d4f822af52023fa19893d55035facd)
书归正题,dex2oat生成的JNI跳板函数其实是位于oat/odex文件中的。但还有一种情况,dex2oat并不会为native方法生成JNI跳板函数,而是在运行时采用统一的art_quick_generic_jni_trampoline来动态执行参数传递和状态切换。这时,art_quick_generic_jni_trampoline位于libart.so中,不符合oat/odex后缀的规律,因此调用栈回溯碰到这一帧时会继续进行。而如果后续的java方法全都是解释执行,那么解释执行的帧也将全部回溯出来,如下示例。
"Binder:1083_11" prio=5 tid=127 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x16002138 self=0xb40000715181e940
| sysTid=6990 nice=0 cgrp=default sched=0/0 handle=0x6f5580fcc0
| state=S schedstat=( 4739949803 13009985270 12510 ) utm=234 stm=239 core=3 HZ=100
| stack=0x6f55718000-0x6f5571a000 stackSize=995KB
| held mutexes=
native: #00 pc 000000000009aa34 /apex/com.android.runtime/lib64/bionic/libc.so (__ioctl+4)
native: #01 pc 0000000000057564 /apex/com.android.runtime/lib64/bionic/libc.so (ioctl+156)
native: #02 pc 00000000000999d4 /system/lib64/libhidlbase.so (android::hardware::IPCThreadState::transact(int, unsigned int, android::hardware::Parcel const&, android::hardware::Parcel*, unsigned int)+564)
native: #03 pc 0000000000094e84 /system/lib64/libhidlbase.so (android::hardware::BpHwBinder::transact(unsigned int, android::hardware::Parcel const&, android::hardware::Parcel*, unsigned int, std::__1::function)+76)
native: #04 pc 000000000000e538 /system/lib64/android.system.suspend@1.0.so (android::system::suspend::V1_0::BpHwSystemSuspend::_hidl_acquireWakeLock(android::hardware::IInterface*, android::hardware::details::HidlInstrumentor*, android::system::suspend::V1_0::WakeLockType, android::hardware::hidl_string const&)+324)
native: #05 pc 0000000000003178 /system/lib64/libhardware_legacy.so (acquire_wake_lock+356)
native: #06 pc 0000000000086648 /system/lib64/libandroid_servers.so (android::nativeAcquireSuspendBlocker(_JNIEnv*, _jclass*, _jstring*)+64)
native: #07 pc 000000000013ced4 /apex/com.android.art/lib64/libart.so (art_quick_generic_jni_trampoline+148)
native: #08 pc 00000000001337e8 /apex/com.android.art/lib64/libart.so (art_quick_invoke_static_stub+568)
native: #09 pc 00000000001a8a94 /apex/com.android.art/lib64/libart.so (art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)+228)
native: #10 pc 0000000000318240 /apex/com.android.art/lib64/libart.so (art::interpreter::ArtInterpreterToCompiledCodeBridge(art::Thread*, art::ArtMethod*, art::ShadowFrame*, unsigned short, art::JValue*)+376)
native: #11 pc 000000000030e56c /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall(art::ArtMethod*, art::Thread*, art::ShadowFrame&, art::Instruction const*, unsigned short, art::JValue*)+996)
native: #12 pc 000000000067e098 /apex/com.android.art/lib64/libart.so (MterpInvokeStatic+548)
native: #13 pc 000000000012d994 /apex/com.android.art/lib64/libart.so (mterp_op_invoke_static+20)
native: #14 pc 0000000000617e00 /system/framework/services.jar (com.android.server.power.PowerManagerService.access$600)
native: #15 pc 000000000067e33c /apex/com.android.art/lib64/libart.so (MterpInvokeStatic+1224)
native: #16 pc 000000000012d994 /apex/com.android.art/lib64/libart.so (mterp_op_invoke_static+20)
native: #17 pc 0000000000614fec /system/framework/services.jar (com.android.server.power.PowerManagerService$NativeWrapper.nativeAcquireSuspendBlocker)
native: #18 pc 000000000067b3e0 /apex/com.android.art/lib64/libart.so (MterpInvokeVirtual+1520)
native: #19 pc 000000000012d814 /apex/com.android.art/lib64/libart.so (mterp_op_invoke_virtual+20)
native: #20 pc 00000000006152b0 /system/framework/services.jar (com.android.server.power.PowerManagerService$SuspendBlockerImpl.acquire+52)
native: #21 pc 0000000000305b68 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute(art::Thread*, art::CodeItemDataAccessor const&, art::ShadowFrame&, art::JValue, bool, bool) (.llvm.10833873914857160001)+268)
native: #22 pc 0000000000669e48 /apex/com.android.art/lib64/libart.so (artQuickToInterpreterBridge+780)
native: #23 pc 000000000013cff8 /apex/com.android.art/lib64/libart.so (art_quick_to_interpreter_bridge+88)
native: #24 pc 00000000021f4bc4 /memfd:jit-cache (deleted) (offset 2000000) (com.android.server.power.PowerManagerService.updateSuspendBlockerLocked+228)
native: #25 pc 000000000201cf6c /memfd:jit-cache (deleted) (offset 2000000) (com.android.server.power.PowerManagerService.updatePowerStateLocked+988)
native: #26 pc 00000000021a3800 /memfd:jit-cache (deleted) (offset 2000000) (com.android.server.power.PowerManagerService.acquireWakeLockInternal+1712)
native: #27 pc 000000000205640c /memfd:jit-cache (deleted) (offset 2000000) (com.android.server.power.PowerManagerService$BinderService.acquireWakeLock+524)
native: #28 pc 0000000002040b64 /memfd:jit-cache (deleted) (offset 2000000) (android.os.IPowerManager$Stub.onTransact+8340)
native: #29 pc 00000000020c95a4 /memfd:jit-cache (deleted) (offset 2000000) (android.os.Binder.execTransactInternal+996)
native: #30 pc 00000000020b9a0c /memfd:jit-cache (deleted) (offset 2000000) (android.os.Binder.execTransact+284)
native: #31 pc 0000000000133564 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+548)
native: #32 pc 00000000001a8a78 /apex/com.android.art/lib64/libart.so (art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)+200)
native: #33 pc 0000000000553c70 /apex/com.android.art/lib64/libart.so (art::JValue art::InvokeVirtualOrInterfaceWithVarArgs(art::ScopedObjectAccessAlreadyRunnable const&, _jobject*, art::ArtMethod*, std::__va_list)+468)
native: #34 pc 0000000000553e10 /apex/com.android.art/lib64/libart.so (art::JValue art::InvokeVirtualOrInterfaceWithVarArgs(art::ScopedObjectAccessAlreadyRunnable const&, _jobject*, _jmethodID*, std::__va_list)+92)
native: #35 pc 00000000003a0920 /apex/com.android.art/lib64/libart.so (art::JNI::CallBooleanMethodV(_JNIEnv*, _jobject*, _jmethodID*, std::__va_list)+660)
native: #36 pc 000000000009c698 /system/lib64/libandroid_runtime.so (_JNIEnv::CallBooleanMethod(_jobject*, _jmethodID*, ...)+124)
native: #37 pc 0000000000124064 /system/lib64/libandroid_runtime.so (JavaBBinder::onTransact(unsigned int, android::Parcel const&, android::Parcel*, unsigned int)+156)
native: #38 pc 000000000004882c /system/lib64/libbinder.so (android::BBinder::transact(unsigned int, android::Parcel const&, android::Parcel*, unsigned int)+232)
native: #39 pc 0000000000051110 /system/lib64/libbinder.so (android::IPCThreadState::executeCommand(int)+1032)
native: #40 pc 0000000000050c58 /system/lib64/libbinder.so (android::IPCThreadState::getAndExecuteCommand()+156)
native: #41 pc 0000000000051490 /system/lib64/libbinder.so (android::IPCThreadState::joinThreadPool(bool)+60)
native: #42 pc 00000000000773e0 /system/lib64/libbinder.so (android::PoolThread::threadLoop()+24)
native: #43 pc 000000000001549c /system/lib64/libutils.so (android::Thread::_threadLoop(void*)+260)
native: #44 pc 00000000000a2590 /system/lib64/libandroid_runtime.so (android::AndroidRuntime::javaThreadShell(void*)+144)
native: #45 pc 0000000000014d60 /system/lib64/libutils.so (thread_data_t::trampoline(thread_data_t const*)+412)
native: #46 pc 00000000000af808 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+64)
native: #47 pc 000000000004fc88 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64)
at com.android.server.power.PowerManagerService.nativeAcquireSuspendBlocker(Native method)
at com.android.server.power.PowerManagerService.access$600(PowerManagerService.java:125)
at com.android.server.power.PowerManagerService$NativeWrapper.nativeAcquireSuspendBlocker(PowerManagerService.java:713)
at com.android.server.power.PowerManagerService$SuspendBlockerImpl.acquire(PowerManagerService.java:4643)
- locked (a com.android.server.power.PowerManagerService$SuspendBlockerImpl)
at com.android.server.power.PowerManagerService.updateSuspendBlockerLocked(PowerManagerService.java:3067)
at com.android.server.power.PowerManagerService.updatePowerStateLocked(PowerManagerService.java:1956)
at com.android.server.power.PowerManagerService.acquireWakeLockInternal(PowerManagerService.java:1320)
- locked (a java.lang.Object)
at com.android.server.power.PowerManagerService.access$4600(PowerManagerService.java:125)
at com.android.server.power.PowerManagerService$BinderService.acquireWakeLock(PowerManagerService.java:4780)
at android.os.IPowerManager$Stub.onTransact(IPowerManager.java:421)
at android.os.Binder.execTransactInternal(Binder.java:1154)
at android.os.Binder.execTransact(Binder.java:1123)
可以发现,native标记的调用栈里其实包含了java层的信息,因此java层的信息输出了两遍(信息冗余)。如果不了解栈回溯的具体原理,恐怕很多人都会好奇:为什么nativeAcquireSuspendBlocker的java方法会调用到#47的__start_thread?这其实并不是真实的调用路径,而只是因为当下的trace调用栈收集方案中有些缺陷。
- 当调用栈回溯碰到位于oat/odex文件里的JNI跳板函数时,则停止回溯。这种方案适用于大多数场景。可是如果函数调用呈现如下的交错情形,那么现行方案会丢失部分调用栈。
Java method A
↓(call)
Native method B
↓(call)
Java method C
↓(call)
Native method D
最终回溯出的整体调用栈信息中,Native method B将不见踪影,因为native层的回溯在碰到C时就已经结束了。以下是一个实际的案例。
"Binder:1540_2" prio=5 tid=9 Blocked
| group="main" sCount=1 dsCount=0 flags=1 obj=0x13700580 self=0x7e0c139800
| sysTid=1560 nice=-2 cgrp=default sched=0/0 handle=0x7df07474f0
| state=S schedstat=( 126689305075 80266662086 342299 ) utm=8978 stm=3690 core=0 HZ=100
| stack=0x7df064c000-0x7df064e000 stackSize=1009KB
| held mutexes=
at com.android.server.LocationManagerService.isProviderEnabledForUser(LocationManagerService.java:2813)
- waiting to lock (a java.lang.Object) held by thread 11
at android.location.ILocationManager$Stub.onTransact(ILocationManager.java:488)
at android.os.Binder.execTransact(Binder.java:726)
(---丢失了这中间的native调用栈---)
at android.os.BinderProxy.transactNative(Native method)
at android.os.BinderProxy.transact(BinderProxy.java:473)
at android.location.IGeocodeProvider$Stub$Proxy.getFromLocation(IGeocodeProvider.java:143)
at com.android.server.location.GeocoderProxy$1.run(GeocoderProxy.java:79)
at com.android.server.ServiceWatcher.runOnBinder(ServiceWatcher.java:425)
- locked (a java.lang.Object)
at com.android.server.location.GeocoderProxy.getFromLocation(GeocoderProxy.java:74)
at com.android.server.LocationManagerService.getFromLocation(LocationManagerService.java:3341)
at android.location.ILocationManager$Stub.onTransact(ILocationManager.java:217)
at android.os.Binder.execTransact(Binder.java:726)
该线程首先发起了一个通往对端进程的binder通信,对端进程在处理的过程中又给本进程发起了新的通信。基于binder transaction stack的设计,这个新的通信必然交给当初的线程。因此execTransact表明它正在处理这个通信。在transactNative和execTransact之间,其实漏掉了native层的帧。
这两个缺陷其实都是小问题,无伤大雅。跟Google工程师反馈沟通以后,他们表示大概率在T上修复这些问题。
结语
当我们在解决大多数APP问题时,调用栈都是最重要的分析素材。如果它总能完美地反映线程的执行逻辑,那么是否了解细节其实不重要。可现实并非如此。ANR的某些场景下,线程可能卡在GoToRunnable中;交错调用的情况下,中间的native方法可能丢失。等等。这些时候的调用栈会出现让人迷惑的信息,而只有了解回溯的细节,才能真正解惑。
Android高级开发系统进阶笔记、最新面试复习笔记PDF,我的GitHub
文末
您的点赞收藏就是对我最大的鼓励!
欢迎关注我的简书,分享Android干货,交流Android技术。
对文章有何见解,或者有何技术问题,欢迎在评论区一起留言讨论!
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
机房租用,北京机房租用,IDC机房托管, http://www.fwqtg.net