手把手教你怎么 Dump Native 线程栈和监听溃散信号
我在前面的文章 Android 怎么解读 Native 溃散栈信息 中介绍了怎么阅览 Native
的溃散栈信息,今日咱们来学习一下怎么 Dump 一个 Native
线程中的栈和怎么监听 Native
溃散信号,源码在这儿(假如觉得对你有协助,希望能够得到你的 Star),结合源码来阅览本篇文章理解更简单。
Dump Native 线程栈
在 Linux
中办法的栈帧在运行时时保存在 .eh_frame
的 Section
中,咱们需求经过某种办法把当时办法栈帧的调用序列拿到。在程序加载到内存中时 .text
的 Section
中存放了咱们的代码指令,咱们的进程会依据 ELF
文件的头信息,拿到入口函数在 .text
中的地址,然后从这个地址开端履行,一般这个入口函数便是 main()
函数,然后入口函数在履行过程中还会调用其他的函数,咱们假设它在 Addr1
的地址履行了 fun1()
函数的调用,同理,fun1()
中又在 Addr2
中履行了 fun2()
函数的调用,假设咱们在 fun2()
中的 Addr3
地址中去 Dump
当时栈信息,那咱们从 .eh_frame
拿到的栈信息便是,Addr3
-> Addr2
-> Addr1
,是的,咱们拿到的信息便是一个地址信息,咱们需求凭借 .strtab
来解析这个地址是归于哪个办法,假如不熟悉 .strtab
符号表的同学能够看看我前面的文章。所以咱们上面的地址解析后看到的大约便是这样 Addr3 (fun2 + 偏移量)
-> Addr2 (fun1 + 偏移量)
-> Addr1 (main + 偏移量)
,这个偏移量是当时的地址间隔办法开端时的地址的偏移。
在 Android
中默许集成了 unwind
库,他便是用来 Dump
线程栈用到的库,咱们也大致了解了 Dump
线程栈的原理,咱们就来看看源码要怎么完结。
首先需求经过 _Unwind_Reason_Code _Unwind_Backtrace(_Unwind_Trace_Fn, void *);
办法来获取栈帧中地址,也便是我上面举例的 Addr1
,Addr2
和 Addr3
。这个办法需求传两个参数,第一个便是处理栈帧信息的函数指针,第二个参数是咱们第一个参数中的函数的自定义参数。_Unwind_Trace_Fn
函数指针的定义是:typedef _Unwind_Reason_Code (*_Unwind_Trace_Fn)(struct _Unwind_Context *, void *);
,这个函数指针的第一个参数 _Unwind_Context
中存放了办法栈帧中的许多重要的信息,也包含咱们需求的指令的地址,第二个参数便是咱们上面说的自定义参数。
typedef struct {
void **pcStart;
void **pcEnd;
} DumpStackPcState;
static _Unwind_Reason_Code singleStackPcUnwind(_Unwind_Context *ctx, void *pcState) {
auto* state = static_cast<DumpStackPcState *> (pcState);
uintptr_t pc = _Unwind_GetIP(ctx);
int pcOffset = 0;
#if defined(__arm__)
pcOffset = -4;
#elif defined(__aarch64__)
pcOffset = -4;
#endif
if (pc) {
if (state->pcStart == state->pcEnd) {
return _URC_END_OF_STACK;
} else {
*state->pcStart++ = reinterpret_cast<void*>(pc + pcOffset);
}
}
return _URC_NO_REASON;
}
static void dumpStackPc(DumpStackPcState* state) {
_Unwind_Backtrace(singleStackPcUnwind, state);
}
我运用自定义参数 DumpStackPcState
来存放不同栈帧中的 pc
程序计数器的地址,存放的地址空间是 pcStart
到 pcEnd
,当超过 pcEnd
时就表明达到了设定的最大栈的数量。
以下是它的初始化:
void *pcBuffer[result->maxStackSize];
DumpStackPcState s = { pcBuffer, pcBuffer + result->maxStackSize };
咱们再来看看咱们定义的栈帧处理回调函数 singleStackPcUnwind()
,咱们经过 _Unwind_GetIP()
办法去拿 _Unwind_Context
中的 pc
地址 (当然这个 ctx
还有这个栈帧的其他有用的信息,咱们的需求只是要 pc
地址),我这儿依据不同的 CPU
架构做了一个偏移值,然后把这个值写入到咱们的自定义参数中,当超过咱们的最大栈数量时回来 _URC_END_OF_STACK
表明中止栈的回溯,假如回来 _URC_NO_REASON
表明持续回溯。
到这儿咱们就把栈帧的相关的 pc
信息存放在了咱们自定义的数据中了,接下来便是要解析 pc
中的信息。
以下便是解析 pc
信息的相关代码:
// ...
void* inst_addr_in_mem = pcBuffer[i];
int offset = result->maxSingleStackSize * indexInStack;
char *target_output = result->stacks + offset;
if (dladdr(inst_addr_in_mem, &dl_info)) {
const char *so_file_path = dl_info.dli_fname;
const char *method_name = dl_info.dli_sname;
long text_addr_in_mem = (long)dl_info.dli_fbase;
long method_addr_in_mem = (long)dl_info.dli_saddr;
long inst_addr_in_text = (long)inst_addr_in_mem - text_addr_in_mem;
long inst_offset = (long)inst_addr_in_mem - method_addr_in_mem;
if (!method_name) {
sprintf(target_output, "#%02d pc %016lx %s", indexInStack, inst_addr_in_text, so_file_path);
} else {
sprintf(target_output, "#%02d pc %016lx %s (%s+%ld)", indexInStack, inst_addr_in_text, so_file_path, method_name, inst_offset);
}
} else {
sprintf(target_output, "#%02d", indexInStack);
}
// ...
咱们需求经过 dladdr()
办法来解析上面经过 _Unwind_Backtrace
办法拿到的 pc
信息,解析的结果是存放在 Dl_info
中的,这儿面的信息需求单独描绘一下。
-
dli_fname
表明咱们的程序文件的途径。 -
dli_sname
表明咱们的pc
地点的办法所对应的姓名,因为办法的符号姓名是在符号表中查询的,假如没有符号表,这个值便是空的。 -
dli_fbase
表明.text
加载到内存中的开端的地址。 -
dli_saddr
表明当时调用的办法所对应的地址,和dli_sname
一样,假如没有符号表,这个值也是空的。
咱们理解了上面的参数表明的意思后,咱们就能计算咱们需求的信息了。补充一点 pc
表明当时履行的指令在内存中的地址。
指令在 .text Section 中的相对地址
= pc
– dli_fbase
。
指令在对应办法的偏移量
= pc
– dli_saddr
。
最终咱们的 Dump
信息经过格式化处理后写入到 target_output
中。
以下是我测试的数据:
// ...
#04 pc 000000000001768c /data/app/~~OT28_jT5HM9S3y-FvnD3BQ==/com.tans.stacktrace-Xjr_DLGp5lFseeHN9GRGUA==/base.apk!/lib/arm64-v8a/libstacktrace.so (_Z13add5DumpCrashi+16)
#05 pc 00000000000176a4 /data/app/~~OT28_jT5HM9S3y-FvnD3BQ==/com.tans.stacktrace-Xjr_DLGp5lFseeHN9GRGUA==/base.apk!/lib/arm64-v8a/libstacktrace.so (_Z14add10DumpCrashi+20)
#06 pc 00000000000176d4 /data/app/~~OT28_jT5HM9S3y-FvnD3BQ==/com.tans.stacktrace-Xjr_DLGp5lFseeHN9GRGUA==/base.apk!/lib/arm64-v8a/libstacktrace.so (Java_com_tans_stacktrace_MainActivity_testCrash+24)
#07 pc 000000000021a354 /apex/com.android.art/lib64/libart.so
#08 pc 000000000020a2b0 /apex/com.android.art/lib64/libart.so
#09 pc 0000000000209334 /apex/com.android.art/lib64/libart.so
// ...
看上去仍是和 Android
默许的溃散栈看上去差不多,哈哈。
监听 Native 溃散信号
在 Android
中的溃散信号主要有以下几种:
static SigActionInfo sigActionInfos[] = {
{.sig = SIGABRT},
{.sig = SIGBUS},
{.sig = SIGFPE},
{.sig = SIGILL},
{.sig = SIGSEGV},
{.sig = SIGTRAP},
{.sig = SIGSYS},
{.sig = SIGSTKFLT}
};
不同的信号表明不同的意思,咱们能够去找找别的文章,我这儿就不介绍了。
默许咱们阻拦信号的处理函数也是作业在本来的函数栈上的,在类似于栈溢出的溃散时,咱们的阻拦办法的可能会触发二次的溃散,所以咱们要经过以下的办法来让咱们的信号处理函数作业在一个新的栈上。
// ...
stack_t newStack;
newStack.ss_flags = 0;
int crashStackSize = 1024 * 128;
newStack.ss_size = crashStackSize;
newStack.ss_sp = malloc(crashStackSize);
sigaltstack(&newStack, nullptr);
// ...
咱们新建的栈的大小为 128K。
然后咱们要新创立一个新的信号处理:
// ...
sigaction_s sigAction{};
sigfillset(&sigAction.sa_mask);
sigAction.sa_flags = SA_RESTART | SA_SIGINFO | SA_ONSTACK;
sigAction.sa_sigaction = sigHandler;
int sigActionInfoSize = sizeof(sigActionInfos) / sizeof(SigActionInfo);
int ret = 1;
for (int i = 0; i < sigActionInfoSize; i ++) {
SigActionInfo info = sigActionInfos[i];
if (sigaction(info.sig, &sigAction, info.oldAct) == 0) {
ret = 0;
}
}
// ...
首先构建一个 sigaction
结构体,经过 sigfillset()
办法清空它的 sa_mask
,sa_sigaction
便是咱们的信号处理函数指针,这儿介绍一下 sa_flags
参数:
-
SA_RESTART
在处理信号时,体系处理睬暂停,在增加该参数后,处理信号完结后就会持续履行,否者不会持续履行。 -
SA_SIGINFO
在信号处理函数中会增加信号相关的信息。 -
SA_ONSTACK
反常信号的处理函数作业在新的栈上,也便是我上面的设置。
然后后续的代码便是经过 sigaction()
办法来替换本来的信号处理,旧的信号处理存放在 info.oldAct
中,当咱们自己的信号处理函数处理完结后,咱们再把信号再传递给本来的处理函数(后面会看到这部分代码)。
我这儿先直接展示完好的信号处理函数,我再渐渐解说:
static void sigHandler(int sig, siginfo_t *sig_info, void *uc) {
// auto * uctx = static_cast<ucontext *>(uc);
auto tid = gettid();
LOGE("Receive native crash: sig=%d, tid=%d", sig, tid);
auto oldAct = findOldAct(sig);
if (hasReceiveSig) {
LOGE("Skip handle signal.");
if (oldAct != nullptr) {
oldAct->sa_sigaction(sig, sig_info, uc);
}
return;
}
hasReceiveSig = true;
pid_t pid = fork();
if (pid == 0) {
// New process
LOGD("Dump process start");
DumpStackResult stackResult;
stackResult.stacks = static_cast<char *>(malloc(
stackResult.maxSingleStackSize * stackResult.maxStackSize));
dumpStack(&stackResult, 1);
printStackResult(&stackResult);
int cacheFd = open(cacheFile, O_WRONLY);
if (cacheFd > 0) {
int writeCount = 0;
writeCount += write(cacheFd, &stackResult.size, sizeof(stackResult.size));
writeCount += write(cacheFd, &stackResult.maxSingleStackSize, sizeof(stackResult.maxSingleStackSize));
writeCount += write(cacheFd, &stackResult.maxStackSize, sizeof(stackResult.maxStackSize));
writeCount += write(cacheFd, stackResult.stacks, stackResult.maxSingleStackSize * stackResult.maxStackSize);
close(cacheFd);
LOGD("Write to cache, writeSize=%d", writeCount);
} else {
LOGE("Open cache file: %s, fail: %d", cacheFile, cacheFd);
}
LOGD("Crash handle thread work finished.");
free(stackResult.stacks);
_Exit(0);
}
if (pid > 0) {
// Current process
LOGD("Waiting dump process: %d", pid);
int status;
waitpid(pid, &status, __WALL);
LOGD("Waiting crash handle thread.");
timeval tv{};
gettimeofday(&tv, nullptr);
long timeInMillis = tv.tv_sec * 1000L + tv.tv_usec / 1000L;
crashTid = tid;
crashSig = sig;
crashSigSub = sig_info->si_code;
crashTime = timeInMillis;
long data = 233;
write(crashNotifyFd, &data, sizeof(long));
if (crashNotifyFd > 0) {
close(crashNotifyFd);
crashNotifyFd = -1;
}
pthread_join(crashHandleThread, nullptr);
LOGD("Dump process finished: %d", status);
if (oldAct != nullptr) {
oldAct->sa_sigaction(sig, sig_info, uc);
}
}
}
我的这个函数顶用到了 fork()
来创立了一个子进程,所以我这儿有必要对 fork
做一个简单的介绍,fork
是在 Linux
中创立进程的一种常见的方式,fork()
办法也是十分奇特,它会调用一次回来两次,你可能听着有点懵,父进程中 fork()
函数会回来子进程的 pid
,而子进程中的 fork()
回来值为 0,在 fork()
函数调用后子进程也就创立成功了,子进程中他会复用父进程中本来的数据和状态,这部分数据也是父进程和它的每个子进程同享的,当子进程要尝试修正公用的数据时那么就会把这部分数据在写入到自己的进程中内存中,而父进程中的数据不会受影响,子进程中下次去那这个数据也不会从父进程中去拿了,而是拿自己的那部分数据,这这部分数据所占用的内存就被称为 private dirty
(子进程中新请求的内存也是归于 private dirty
),直接翻译过来便是私有脏数据。在 Android
中,咱们的进程都是由 Zygoty
fork
而来,Zygoty
中有十分多的同享数据都是由一切的进程同享的,在需求创立新的运用进程时,AMS
会经过 Socket
发送音讯给 Zygoty
进程,然后 Zegoty
进程会经过 fork
创立子进程,然后在子进程中会履行 ActivityThread
的 main()
函数,这便是咱们运用进程主线程开端的当地。
咱们再回到咱们上面的代码,为什么咱们这儿要用 fork
呢?按照网络上的描绘,在处理信号的过程中其间的办法栈信息可能被修正,经过 fork
后来用子线程来处理就能够避免这种问题。说实话我也不确定这个描绘的准确性,我自己不 fork
貌似得到的数据也是对的。像 xCrash
,Bugly
等等他们也都是 fork
一个子进程来 dump
栈信息,那咱们也按照通用的办法来吧。。。。。
咱们来看看子进程怎么 dump
栈信息:
// ...
if (pid == 0) {
// New process
LOGD("Dump process start");
DumpStackResult stackResult;
stackResult.stacks = static_cast<char *>(malloc(
stackResult.maxSingleStackSize * stackResult.maxStackSize));
dumpStack(&stackResult, 1);
printStackResult(&stackResult);
int cacheFd = open(cacheFile, O_WRONLY);
if (cacheFd > 0) {
int writeCount = 0;
writeCount += write(cacheFd, &stackResult.size, sizeof(stackResult.size));
writeCount += write(cacheFd, &stackResult.maxSingleStackSize, sizeof(stackResult.maxSingleStackSize));
writeCount += write(cacheFd, &stackResult.maxStackSize, sizeof(stackResult.maxStackSize));
writeCount += write(cacheFd, stackResult.stacks, stackResult.maxSingleStackSize * stackResult.maxStackSize);
close(cacheFd);
LOGD("Write to cache, writeSize=%d", writeCount);
} else {
LOGE("Open cache file: %s, fail: %d", cacheFile, cacheFd);
}
LOGD("Crash handle thread work finished.");
free(stackResult.stacks);
_Exit(0);
}
// ...
其实这部分代码就朴实无华了,就用咱们上一节描绘的办法来 dump
栈信息,然后把这个数据再写入到缓存文件中。
咱们再来看看主进程中:
// ...
if (pid > 0) {
// Current process
LOGD("Waiting dump process: %d", pid);
int status;
waitpid(pid, &status, __WALL);
LOGD("Waiting crash handle thread.");
timeval tv{};
gettimeofday(&tv, nullptr);
long timeInMillis = tv.tv_sec * 1000L + tv.tv_usec / 1000L;
crashTid = tid;
crashSig = sig;
crashSigSub = sig_info->si_code;
crashTime = timeInMillis;
long data = 233;
write(crashNotifyFd, &data, sizeof(long));
if (crashNotifyFd > 0) {
close(crashNotifyFd);
crashNotifyFd = -1;
}
pthread_join(crashHandleThread, nullptr);
LOGD("Dump process finished: %d", status);
if (oldAct != nullptr) {
oldAct->sa_sigaction(sig, sig_info, uc);
}
}
// ...
父进程中等待子进程中完结 dump
,然后通知 CrashHandleThread
去处理子进程 dump
好的数据,然后等待 CrashHandleThread
作业完结,然后调用本来旧信号处理函数的办法。这儿是经过向 fd
中写入了一个 233
来通知 CrashHandleThread
,在初始化时就会启动 CrashHandleThread
,它在 read
上面的 fd
,当有数据来时就会读取到一个 233
,然后他就处理 dump
数据。
咱们再来看看 CrashHandleThread
:
static void* crashHandleRoutine(void* args) {
auto *args_t = static_cast<CrashHandleThreadArgs *>(args);
auto *jniEnv = args_t->env;
LOGD("Crash handle thread started.");
long data;
if (crashNotifyFd > 0) {
JavaVMAttachArgs jvmAttachArgs {
.version = JNI_VERSION_1_6,
.name = "CrashHandleThread",
.group = nullptr
};
if (args_t->jvm->AttachCurrentThread(&jniEnv, &jvmAttachArgs) != JNI_OK) {
LOGE("Attach jvm thread fail.");
return nullptr;
}
while (true) {
read(crashNotifyFd, &data, sizeof(data));
LOGD("Crash handle read data: %ld", data);
if (data == 233L) {
LOGD("Crash handle thread receive crash, waiting crash handle thread: %ld", crashHandleThread);
close(crashNotifyFd);
crashNotifyFd = -1;
struct stat cacheStat {};
stat(cacheFile, &cacheStat);
long long cacheSize = cacheStat.st_size;
LOGD("Cache file size: %lld", cacheSize);
int cacheFd = 0;
if (cacheSize > 0) {
cacheFd = open(cacheFile, O_RDONLY);
}
if (cacheFd > 0) {
int crashStackSize, maxSingleStackSize, maxStackSize;
read(cacheFd, &crashStackSize, sizeof(int));
read(cacheFd, &maxSingleStackSize, sizeof(int));
read(cacheFd, &maxStackSize, sizeof(int));
LOGD("CrashStackSize=%d, MaxSingleStackSize=%d, MaxStackSize=%d", crashStackSize, maxSingleStackSize, maxStackSize);
if (crashStackSize > 0 && maxSingleStackSize > 0 && maxStackSize > 0) {
char *stacks = static_cast<char *>(malloc(crashStackSize * maxSingleStackSize));
read(cacheFd, stacks, crashStackSize * maxSingleStackSize);
LOGD("Read stacks: %s", stacks);
CrashData crashData {
.tid = crashTid,
.sig = crashSig,
.sigName = getSigName(crashSig),
.sigSub = crashSigSub,
.sigSubName = getSigSubName(crashSig, crashSigSub),
.time = crashTime,
.stackResult = DumpStackResult {
.size = crashStackSize,
.maxSingleStackSize = maxSingleStackSize,
.maxStackSize = maxStackSize,
.stacks = stacks
}
};
args_t->crashHandle(jniEnv, args_t->obj, &crashData);
free(stacks);
} else {
LOGE("Wrong size.");
}
close(cacheFd);
} else {
LOGE("Read cache file fail: %d", cacheFd);
}
break;
}
}
args_t->jvm->DetachCurrentThread();
} else {
LOGE("Crash handle fail: crash notify fd is invalid.");
}
free(args);
return nullptr;
}
这儿要注意因为后续涉及到调用 Java 的办法所以在调用前必须经过 AttachCurrentThread
办法增加到 JVM
中去,其间有用到的目标要经过 NewGlobalRef
办法增加一个强引证。否则会报错。
这儿便是经过读取子进程中写入的信息,然后把读取到的值经过 crashHandle
函数指针回调给 Java
层。
这儿还有一点特别坑,Java
中的 tid
和 Linux
中的 tid
是不一样的,Java
中的 tid
便是一个 int
值在 ++,咱们需求经过读 /proc/[tid]/comm
中去读线程的姓名,然后依据线程的姓名再去 Java
的线程中去找对应的线程。
下面是我的一个测试溃散数据:
Fatal signal 6 (SIGABRT), code -1 (SI_QUEUE) in tid 10485 (TestCrashThread), pid 10030 (com.tans.stacktrace)
#04 pc 000000000001768c /data/app/~~OT28_jT5HM9S3y-FvnD3BQ==/com.tans.stacktrace-Xjr_DLGp5lFseeHN9GRGUA==/base.apk!/lib/arm64-v8a/libstacktrace.so (_Z13add5DumpCrashi+16)
#05 pc 00000000000176a4 /data/app/~~OT28_jT5HM9S3y-FvnD3BQ==/com.tans.stacktrace-Xjr_DLGp5lFseeHN9GRGUA==/base.apk!/lib/arm64-v8a/libstacktrace.so (_Z14add10DumpCrashi+20)
#06 pc 00000000000176d4 /data/app/~~OT28_jT5HM9S3y-FvnD3BQ==/com.tans.stacktrace-Xjr_DLGp5lFseeHN9GRGUA==/base.apk!/lib/arm64-v8a/libstacktrace.so (Java_com_tans_stacktrace_MainActivity_testCrash+24)
#07 pc 000000000021a354 /apex/com.android.art/lib64/libart.so
#08 pc 000000000020a2b0 /apex/com.android.art/lib64/libart.so
#09 pc 0000000000209334 /apex/com.android.art/lib64/libart.so
#10 pc 0000000000209334 /apex/com.android.art/lib64/libart.so
#11 pc 000000000020b074 /apex/com.android.art/lib64/libart.so
#12 pc 000000000021096c /apex/com.android.art/lib64/libart.so
#13 pc 000000000027af68 /apex/com.android.art/lib64/libart.so (_ZN3art9ArtMethod6InvokeEPNS_6ThreadEPjjPNS_6JValueEPKc+184)
#14 pc 0000000000624cac /apex/com.android.art/lib64/libart.so (_ZN3art35InvokeVirtualOrInterfaceWithJValuesIPNS_9ArtMethodEEENS_6JValueERKNS_33ScopedObjectAccessAlreadyRunnableEP8_jobjectT_PK6jvalue+460)
#15 pc 000000000066de60 /apex/com.android.art/lib64/libart.so (_ZN3art6Thread14CreateCallbackEPv+1296)
#16 pc 00000000000eb720 /apex/com.android.runtime/lib64/bionic/libc.so
at com.tans.stacktrace.MainActivity.testCrash(Native Method)
at com.tans.stacktrace.MainActivity.onCreate$lambda$3$lambda$2(MainActivity.kt:49)
at com.tans.stacktrace.MainActivity.$r8$lambda$xuM_bd7L1pB3XG2xgdny9HZIqC0(Unknown Source:0)
at com.tans.stacktrace.MainActivity$$ExternalSyntheticLambda0.run(Unknown Source:2)
at java.lang.Thread.run(Thread.java:1012)
看上去也还行哈,而且我还把 Java
栈也打印了。
最终
我写的这些代码只是供学习运用,假如你想要用于线上环境需谨慎,许多机型都没有测试过,建议运用其他的稳定的开源的库,例如 xCrash
,在你读了我这个简单版的捕获 Native
溃散的代码后,再去阅览别的捕获 Native
反常库的代码会更简单理解,咱们的原理都是差不多的。