美文网首页
UE4收集崩溃日志

UE4收集崩溃日志

作者: eca3ef0d3d2d | 来源:发表于2017-09-26 10:54 被阅读718次

    摘要

    项目中接入了Crasheye帮助收集崩溃信息,可最近测试总是反馈说刚刚崩溃的信息在crasheye上差不到,而本地却打印出了崩溃日志,这个比较郁闷,最终定位到,空指针的崩溃能收集到,check(UE_LOG)的崩溃收集不到,why?请教各位大神后,发现是信号的原因,在check(false)后,是不会触发crash sig的,而UE4则从call stack中拿信息写到本地日志中!哎,隐藏的好深啊,本篇文章就简单的记录一下UE4处理崩溃信息的大概思路。

    一. 信号机制

    信号处理

    函数运行在用户态,当遇到系统调用、中断或是异常的情况时,程序会进入内核态。信号涉及到了这两种状态之间的转换,过程可以先看一下下面的示意图:

    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是个比较耗时的函数,不要调用完毕后直接退出游戏,这样是上传不成功的。

    相关文章

      网友评论

          本文标题:UE4收集崩溃日志

          本文链接:https://www.haomeiwen.com/subject/icqsextx.html