摘要
项目中接入了Crasheye帮助收集崩溃信息,可最近测试总是反馈说刚刚崩溃的信息在crasheye上差不到,而本地却打印出了崩溃日志,这个比较郁闷,最终定位到,空指针的崩溃能收集到,check(UE_LOG)的崩溃收集不到,why?请教各位大神后,发现是信号的原因,在check(false)后,是不会触发crash sig的,而UE4则从call stack中拿信息写到本地日志中!哎,隐藏的好深啊,本篇文章就简单的记录一下UE4处理崩溃信息的大概思路。
一. 信号机制
![](https://img.haomeiwen.com/i2890076/6bf911f27dc40f12.png)
函数运行在用户态,当遇到系统调用、中断或是异常的情况时,程序会进入内核态。信号涉及到了这两种状态之间的转换,过程可以先看一下下面的示意图:
1.信号的接收
接收信号的任务是由内核代理的,当内核接收到信号后,会将其放到对应进程的信号队列中,同时向进程发送一个中断,使其陷入内核态。
注意,此时信号还只是在队列中,对进程来说暂时是不知道有信号到来的。
2.信号的检测
进程陷入内核态后,有两种场景会对信号进行检测:
进程从内核态返回到用户态前进行信号检测
进程在内核态中,从睡眠状态被唤醒的时候进行信号检测
当发现有新信号时,便会进入下一步,信号的处理。
3.信号的处理
信号处理函数是运行在用户态的,调用处理函数前,内核会将当前内核栈的内容备份拷贝到用户栈上,并且修改指令寄存器(eip)将其指向信号处理函数。
接下来进程返回到用户态中,执行相应的信号处理函数。
信号处理函数执行完成后,还需要返回内核态,检查是否还有其它信号未处理。如果所有信号都处理完成,就会将内核栈恢复(从用户栈的备份拷贝回来),同时恢复指令寄存器(eip)将其指向中断前的运行位置,最后回到用户态继续执行进程。
至此,一个完整的信号处理流程便结束了,如果同时有多个信号到达,上面的处理流程会在第2步和第3步骤间重复进行。
所以第一步就是要用信号处理函数捕获到native crash(SIGSEGV, SIGBUS等)。
二. UE4信号处理
//UE4中,使用SetCrashHandler注册信号处理器
void FAndroidMisc::SetCrashHandler(void(*CrashHandler)(const FGenericCrashContext& Context))
{
GCrashHandlerPointer = CrashHandler;
...
struct sigaction Action;
FMemory::Memzero(&Action, sizeof(struct sigaction));
Action.sa_sigaction = PlatformCrashHandler;
sigemptyset(&Action.sa_mask);
Action.sa_flags = SA_SIGINFO | SA_RESTART | SA_ONSTACK;
for (int32 i = 0; i < NumTargetSignals; ++i)
{
//sigaction回调函数的第三个参数是一个指向ucontext_t的指针,
//ucontext_t收集了寄存器数值(还有各种处理器特定的信息)。
sigaction(TargetSignals[i], &Action, &PrevActions[i]);
}
...
}
//当发生回调时,执行PlatformCrashHandler
void PlatformCrashHandler(int32 Signal, siginfo* Info, void* Context)
{
...
if (GCrashHandlerPointer)
{
//这里的函数指针指向崩溃处理函数,即下面的EngineCrashHandler
GCrashHandlerPointer(CrashContext);
}
else
{
// call default one
DefaultCrashHandler(CrashContext);
}
..
}
//注册崩溃信号处理函数EngineCrashHandler
JNIEXPORT jint JNI_OnLoad(JavaVM* InJavaVM, void* InReserved)
{
...
// hook signals
if (!FPlatformMisc::IsDebuggerPresent() || GAlwaysReportCrash)
{
#if !UE_BUILD_SHIPPING
FPlatformMisc::SetCrashHandler(EngineCrashHandler);
#endif
}
....
}
至此,UE4完成了崩溃信息的收集及处理流程,EngineCrashHandler中,会将崩溃信息写到本地log中。然而,对于手游项目而言,客户端本地的崩溃信息无法满足调试优化的需求(shipping版本还没有崩溃信息)。所以,我们需要将本地的崩溃信息上传到服务器,以供项目组调优。目前,很多第三方崩溃收集工具提供了这样的功能,我们使用的Crasheye(集成了google breakpad,通过注册crash sig来处理崩溃信息)可以帮助我们将崩溃信息上传的服务器并解析成可读信息。
三. UE4的特殊“崩溃”
特殊指的是check 或者 UE_LOG中的Error和Fatal,当触发这种逻辑的时候也会崩溃,准确的说,应该是退出游戏FGenericPlatformMisc::RequestExit(true),所以,这种情况是不会触发信号的。在非shipping下,它也会写本地log,但是,我们同样希望将这些日志发送到服务器,所以,就需要将写日志的逻辑替换为发送崩溃信息到crasheye了,最简单的方法就是在写日志的后面加一句调用发送日志的API,但是,我们不希望改动引擎的代码!
方法是,替换引擎自带的AndroidOutputDevice,自己写一个CrasheyeOutputDevice将崩溃信息发送到crasheye服务器
GError = CrasheyeOutputDevice.GetError();
需要特殊注意的是,上传日志的API是个比较耗时的函数,不要调用完毕后直接退出游戏,这样是上传不成功的。
网友评论