0%

记录程序的崩溃

Windows捕获Crash

打开regedit.exe在注册表中找到HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting
项,新建3个“字符串值”:配置DumpCount = 10DumpFolder = C:\XUranusDumpDumpType = 2如图所示:

其中DumpType代表的含义是:

  • 0 :Create a Custom Dump
  • 1 :Mini Dump
  • 2 :Full Dump

这里配置将会在程序崩溃时在C:\XUranusDump目录下生成完整的Dump信息,最多生成10个。生成的Dump信息以*.dmp文件形式存在:

有了*.dmp文件就可以用VS调试Dump。调试会依赖可执行程序及对应的*.pdb符号文件。如果是本地执行产生的*.dmp文件,VS可以直接从*.dmp文件拿到*.pdb的位置,如果是其他环境生成的*.dmp文件在本地调试,需要在VS中指定符号文件和源码
的路径,将源码、*.pdb*.dmp文件放在同一目录下。

*.pdb文件和可执行程序的构建是一一对应的,即:同样的源码两次构建产生的pdb不一样。因此必须在生成每一个Release后保存好对应的*.pdb文件以供日后调试。

在生产环境上,可以在用户程序中捕获异常并生成MiniDump文件。

Windows提供SetUnhandledExceptionFilter(LPTOP_LEVEL_EXCEPTION_FILTER* handler) API用于注册一个异常捕获函数:

// Register windows crash handler
static LONG ApplicationCrashCollector(EXCEPTION_POINTERS *pException)
{
// Handle Exception via pException ptr here ...
return EXCEPTION_EXECUTE_HANDLER;
}

通过EXCEPTION_POINTERS* pException可以拿到异常相关信息,并生成MiniDump文件。我们来实现一个DumpCollector来实现上述功能:

完整代码详见:MiniDump

class DumpCollector {
public:
// init collector, need to be called at main routine
static void Init();
#ifdef WIN32
// specify root path of dump file
static bool SetDumpFileRoot(const std::string& dumpFileRootPath);
#endif

#ifdef WIN32
// util method, to generate a dump file path contain current datetime
static std::string GenerateDumpFilePath();
// collect summary reason of win32 exception
static std::string GetWin32ExceptionInfo();
// generate detail info of stack trace (windows)
static std::vector<StackFrame> DumpWin32StackTrace();
// genterate a *.dmp file at target path, will return empty if failed
static std::string CreateDumpFile();
#endif

public:
#ifdef WIN32
// store ptr of EXCEPTION_POINTER of exception caught
static void* exceptionPtr;
// store the dump file root path specified
static std::string dumpFileRootPath;
// callback function (dump path, crash cause, stacktrace)
using WinCallbackType = void(const std::string&, const std::string&, const std::vector<StackFrame>& frames);
static std::function<WinCallbackType> Win32CrashHandler;
#endif
};

DumpCollector::Init中注册异常处理函数ApplicationCrashCollector,在ApplicationCrashCollector中处理EXCEPTION_POINTERS *pException,依次调用:

  1. DumpCollector::CreateDumpFile() 生成MiniDump文件
  2. DumpCollector::GetWin32ExceptionInfo()获取异常信息
  3. DumpCollector::DumpWin32StackTrace()获取当前堆栈
  4. DumpCollector::Win32CrashHandler()通过注册的回调函数返回MiniDump路径、异常原因、堆栈

    // Register windows crash handler
    static LONG ApplicationCrashCollector(EXCEPTION_POINTERS *pException)
    {
    // set exception pointer
    DumpCollector::exceptionPtr = static_cast<void*>(pException);
    // prepare to collect dump info
    std::string dumpFilePath = DumpCollector::CreateDumpFile();
    std::string exceptionCause = DumpCollector::GetWin32ExceptionInfo();
    std::vector<StackFrame> stacktrace = DumpCollector::DumpWin32StackTrace();
    if (DumpCollector::Win32CrashHandler == nullptr) {
    WRITE_ERROR("missing win32 crash handler");
    } else {
    DumpCollector::Win32CrashHandler(dumpFilePath, exceptionCause, stacktrace);
    }
    return EXCEPTION_EXECUTE_HANDLER;
    }

    void DumpCollector::Init()
    {
    #ifdef WIN32
    ::SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)ApplicationCrashCollector);
    #endif
    }
  5. MiniDumpWriteDump()EXCEPTION_POINTERS* pException生成MiniDump文件:

    // Create dump file from DumpCollector::exceptionPtr
    std::string DumpCollector::CreateDumpFile()
    {
    if (DumpCollector::exceptionPtr == nullptr) {
    WRITE_ERROR("failed to create dump file, exception ptr is null");
    return "";
    }
    std::string dumpFilePath = DumpCollector::GenerateDumpFilePath();
    EXCEPTION_POINTERS *pException = static_cast<EXCEPTION_POINTERS*>(DumpCollector::exceptionPtr);
    std::wstring wDumpFilePath = Utf8ToUtf16(dumpFilePath);
    HANDLE hDumpFile = ::CreateFileW(
    wDumpFilePath.c_str(),
    GENERIC_WRITE,
    0,
    nullptr,
    CREATE_ALWAYS,
    FILE_ATTRIBUTE_NORMAL,
    nullptr);
    if (hDumpFile == INVALID_HANDLE_VALUE) {
    return ""; // failed to create file
    }
    // fill dump information
    MINIDUMP_EXCEPTION_INFORMATION dumpInfo;
    dumpInfo.ExceptionPointers = pException;
    dumpInfo.ThreadId = ::GetCurrentThreadId();
    dumpInfo.ClientPointers = TRUE;
    // write dump information to file
    ::MiniDumpWriteDump(
    ::GetCurrentProcess(),
    ::GetCurrentProcessId(),
    hDumpFile,
    MiniDumpNormal,
    &dumpInfo,
    nullptr,
    nullptr);
    ::CloseHandle(hDumpFile);
    return dumpFilePath;
    }
  6. pException->ExceptionRecord->ExceptionCode枚举错误码,获取异常原因:

    #define CASE_RETURN(EXCEPTION) case EXCEPTION: { \
    return #EXCEPTION; \
    } \

    // obtain exception reason from pException
    std::string DumpCollector::GetWin32ExceptionInfo()
    {
    EXCEPTION_POINTERS* exceptionPtr = static_cast<EXCEPTION_POINTERS*>(DumpCollector::exceptionPtr);
    if (exceptionPtr == nullptr || exceptionPtr->ExceptionRecord == nullptr) {
    return "";
    }
    switch (exceptionPtr->ExceptionRecord->ExceptionCode) {
    CASE_RETURN(EXCEPTION_ACCESS_VIOLATION)
    CASE_RETURN(EXCEPTION_ARRAY_BOUNDS_EXCEEDED)
    CASE_RETURN(EXCEPTION_BREAKPOINT)
    CASE_RETURN(EXCEPTION_DATATYPE_MISALIGNMENT)
    CASE_RETURN(EXCEPTION_FLT_DENORMAL_OPERAND)
    CASE_RETURN(EXCEPTION_FLT_DIVIDE_BY_ZERO)
    CASE_RETURN(EXCEPTION_FLT_INEXACT_RESULT)
    CASE_RETURN(EXCEPTION_FLT_INVALID_OPERATION)
    CASE_RETURN(EXCEPTION_FLT_OVERFLOW)
    CASE_RETURN(EXCEPTION_FLT_STACK_CHECK)
    CASE_RETURN(EXCEPTION_FLT_UNDERFLOW)
    CASE_RETURN(EXCEPTION_ILLEGAL_INSTRUCTION)
    CASE_RETURN(EXCEPTION_IN_PAGE_ERROR)
    CASE_RETURN(EXCEPTION_INT_DIVIDE_BY_ZERO)
    CASE_RETURN(EXCEPTION_INT_OVERFLOW)
    CASE_RETURN(EXCEPTION_INVALID_DISPOSITION)
    CASE_RETURN(EXCEPTION_NONCONTINUABLE_EXCEPTION)
    CASE_RETURN(EXCEPTION_PRIV_INSTRUCTION)
    CASE_RETURN(EXCEPTION_SINGLE_STEP)
    CASE_RETURN(EXCEPTION_STACK_OVERFLOW)
    }
    return "Unknown Exception";
    }
  7. EXCEPTION_POINTERS* pException获取堆栈信息:

    struct StackFrame {
    std::string file;
    std::string module;
    std::string function;
    uint64_t address;
    uint64_t line;
    };

    // a util function to reduce complexity of DumpWin32StackTrace
    static std::vector<StackFrame> WalkStacks(DWORD machineType, HANDLE hProcess, HANDLE hThread, STACKFRAME* stackframePtr, CONTEXT* contextPtr)
    {
    bool first = true;
    std::vector<StackFrame> stackframes;
    while (::StackWalk(machineType, hProcess, hThread, stackframePtr, contextPtr, nullptr, SymFunctionTableAccess, SymGetModuleBase, nullptr)) {
    StackFrame f{};
    f.address = stackframePtr->AddrPC.Offset;

    #if _WIN64
    DWORD64 moduleBase = SymGetModuleBase(hProcess, stackframePtr->AddrPC.Offset);
    #else
    DWORD moduleBase = SymGetModuleBase(hProcess, stackframePtr->AddrPC.Offset);
    #endif
    char moduelBuff[MAX_PATH];
    if (moduleBase && GetModuleFileNameA((HINSTANCE)moduleBase, moduelBuff, MAX_PATH)) {
    f.module = moduelBuff;
    }
    #if _WIN64
    DWORD64 offset = 0;
    #else
    DWORD offset = 0;
    #endif
    char symbolBuffer[sizeof(IMAGEHLP_SYMBOL) + 255];
    PIMAGEHLP_SYMBOL symbol = (PIMAGEHLP_SYMBOL)symbolBuffer;
    symbol->SizeOfStruct = (sizeof IMAGEHLP_SYMBOL) + 255;
    symbol->MaxNameLength = 254;

    if (::SymGetSymFromAddr(hProcess, stackframePtr->AddrPC.Offset, &offset, symbol)) {
    f.function = symbol->Name;
    } // Failed to resolve address frame.AddrPC.Offset otherwise, default empty

    IMAGEHLP_LINE line;
    line.SizeOfStruct = sizeof(IMAGEHLP_LINE);

    DWORD offsetln = 0;
    if (::SymGetLineFromAddr(hProcess, stackframePtr->AddrPC.Offset, &offsetln, &line)) {
    f.file = line.FileName;
    f.line = line.LineNumber;
    } // Failed to resolve line for frame.AddrPC.Offset otherwise, default 0

    if (!first) {
    stackframes.push_back(f);
    }
    first = false;
    }
    return stackframes;
    }

    std::vector<StackFrame> DumpCollector::DumpWin32StackTrace()
    {
    #if _WIN64
    DWORD machine = IMAGE_FILE_MACHINE_AMD64;
    #else
    DWORD machine = IMAGE_FILE_MACHINE_I386;
    #endif
    HANDLE process = GetCurrentProcess();
    HANDLE thread = GetCurrentThread();

    if (!::SymInitialize(process, nullptr, TRUE)) {
    WRITE_ERROR("Failed to call SymInitialize");
    return std::vector<StackFrame>();
    }

    ::SymSetOptions(SYMOPT_LOAD_LINES);
    CONTEXT context = {};
    context.ContextFlags = CONTEXT_FULL;
    ::RtlCaptureContext(&context);

    STACKFRAME frame {};
    frame.AddrPC.Mode = AddrModeFlat;
    frame.AddrFrame.Mode = AddrModeFlat;
    frame.AddrStack.Mode = AddrModeFlat;
    #if _WIN64
    frame.AddrPC.Offset = context.Rip;
    frame.AddrFrame.Offset = context.Rbp;
    frame.AddrStack.Offset = context.Rsp;
    #else
    frame.AddrPC.Offset = context.Eip;
    frame.AddrFrame.Offset = context.Ebp;
    frame.AddrStack.Offset = context.Esp;
    #endif

    std::vector<StackFrame> stackframes = WalkStacks(machine, process, thread, &frame, &context);
    ::SymCleanup(process);
    return stackframes;
    }

执行DumpCollector::Init()并手动触发一次异常,观察异常捕获结果:

/* Coredump happend here */
void FuncCoredump()
{
int a = 10;
int b = 0;
int c = a/b;
std::cout << c << std::endl;
}

void Func2()
{
FuncCoredump();
}

void Func3()
{
Func2();
}

void Func4()
{
Func3();
}

void Func5()
{
Func4();
}

void TriggerCrash()
{
Func5();
}

int main(int argc, char** argv)
{
std::cout << "Init And Register Crash Handler" << std::endl;
DumpCollector::Init();

#ifdef WIN32
DumpCollector::SetDumpFileRoot("C:\\Test\\Coredump");
DumpCollector::Win32CrashHandler = Win32ApplicationCrashHandler;
#endif

std::cout << "Ready To Triger Crash" << std::endl;
TriggerCrash();
std::cout << "All Is Well, No Crash" << std::endl;
return 0;
}

编译执行,观察输出结果:

PS C:\Users\XUranus\source\repos\MiniDump\build64> .\Debug\dumpcollector.exe
Init And Register Crash Handler
Ready To Triger Crash
Dump File Generated At: C:\Test\Coredump\2023-03-21.22.17.28.dmp
Crash Reason: EXCEPTION_INT_DIVIDE_BY_ZERO
C:\Users\XUranus\source\repos\MiniDump\build64\Debug\dumpcollector.exe,ApplicationCrashCollector[0x7ff6e4a747cf]C:\Users\XUranus\source\repos\MiniDump\DumpCollector.:90
C:\WINDOWS\System32\KERNELBASE.dll,UnhandledExceptionFilter[0x7ffb0e635f0c]:0
C:\WINDOWS\SYSTEM32\ntdll.dll,RtlMoveMemory[0x7ffb10fd837d]:0
C:\WINDOWS\SYSTEM32\ntdll.dll,_C_specific_handler[0x7ffb10fbefa7]:0
C:\WINDOWS\SYSTEM32\ntdll.dll,_chkstk[0x7ffb10fd3cff]:0
C:\WINDOWS\SYSTEM32\ntdll.dll,RtlFindCharInUnicodeString[0x7ffb10f4e456]:0
C:\WINDOWS\SYSTEM32\ntdll.dll,KiUserExceptionDispatcher[0x7ffb10fd2cee]:0
C:\Users\XUranus\source\repos\MiniDump\build64\Debug\dumpcollector.exe,FuncCoredump[0x7ff6e4a7e92b]C:\Users\XUranus\source\repos\MiniDump\Demo.:11
C:\Users\XUranus\source\repos\MiniDump\build64\Debug\dumpcollector.exe,Func2[0x7ff6e4a7e96b]C:\Users\XUranus\source\repos\MiniDump\Demo.:18
C:\Users\XUranus\source\repos\MiniDump\build64\Debug\dumpcollector.exe,Func3[0x7ff6e4a7e98b]C:\Users\XUranus\source\repos\MiniDump\Demo.:23
C:\Users\XUranus\source\repos\MiniDump\build64\Debug\dumpcollector.exe,Func4[0x7ff6e4a7e9ab]C:\Users\XUranus\source\repos\MiniDump\Demo.:28
C:\Users\XUranus\source\repos\MiniDump\build64\Debug\dumpcollector.exe,Func5[0x7ff6e4a7e9cb]C:\Users\XUranus\source\repos\MiniDump\Demo.:33
C:\Users\XUranus\source\repos\MiniDump\build64\Debug\dumpcollector.exe,TriggerCrash[0x7ff6e4a7e9eb]C:\Users\XUranus\source\repos\MiniDump\Demo.:38
C:\Users\XUranus\source\repos\MiniDump\build64\Debug\dumpcollector.exe,main[0x7ff6e4a7ec77]C:\Users\XUranus\source\repos\MiniDump\Demo.:93
C:\Users\XUranus\source\repos\MiniDump\build64\Debug\dumpcollector.exe,invoke_main[0x7ff6e4a81929]D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:79
C:\Users\XUranus\source\repos\MiniDump\build64\Debug\dumpcollector.exe,__scrt_common_main_seh[0x7ff6e4a8180e]D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288
C:\Users\XUranus\source\repos\MiniDump\build64\Debug\dumpcollector.exe,__scrt_common_main[0x7ff6e4a816ce]D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:331
C:\Users\XUranus\source\repos\MiniDump\build64\Debug\dumpcollector.exe,mainCRTStartup[0x7ff6e4a819be]D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp:17
C:\WINDOWS\System32\KERNEL32.DLL,BaseThreadInitThunk[0x7ffb0f1e26bd]:0
C:\WINDOWS\SYSTEM32\ntdll.dll,RtlUserThreadStart[0x7ffb10f8a9f8]:0

可以看到异常原因为EXCEPTION_INT_DIVIDE_BY_ZERO,捕获到堆栈:

C:\Users\XUranus\source\repos\MiniDump\build64\Debug\dumpcollector.exe,FuncCoredump[0x7ff6e4a7e92b]C:\Users\XUranus\source\repos\MiniDump\Demo.:11
C:\Users\XUranus\source\repos\MiniDump\build64\Debug\dumpcollector.exe,Func2[0x7ff6e4a7e96b]C:\Users\XUranus\source\repos\MiniDump\Demo.:18
C:\Users\XUranus\source\repos\MiniDump\build64\Debug\dumpcollector.exe,Func3[0x7ff6e4a7e98b]C:\Users\XUranus\source\repos\MiniDump\Demo.:23
C:\Users\XUranus\source\repos\MiniDump\build64\Debug\dumpcollector.exe,Func4[0x7ff6e4a7e9ab]C:\Users\XUranus\source\repos\MiniDump\Demo.:28
C:\Users\XUranus\source\repos\MiniDump\build64\Debug\dumpcollector.exe,Func5[0x7ff6e4a7e9cb]C:\Users\XUranus\source\repos\MiniDump\Demo.:33
C:\Users\XUranus\source\repos\MiniDump\build64\Debug\dumpcollector.exe,TriggerCrash[0x7ff6e4a7e9eb]C:\Users\XUranus\source\repos\MiniDump\Demo.:38
C:\Users\XUranus\source\repos\MiniDump\build64\Debug\dumpcollector.exe,main[0x7ff6e4a7ec77]C:\Users\XUranus\source\repos\MiniDump\Demo.:93

注意:生成堆栈必须要在编译的时候保留符号表(*.pdb)文件,如果缺少符号表将不能获得函数名、文件名和行号等信息。如果开启了llvm编译后端优化,编译后的堆栈和符号表中的堆栈可能不一致,会导致收集到的堆栈信息和实际堆栈不一致,要获取实际堆栈信息还得用VS处理生成的MiniDump文件。上述程序中生成MiniDump文件需要对应体系结构(x86/x64)的imagehlp.lib库,如果system32中不包含imagehlp.dll则需要手动指定。

Linux捕获Crash

Linux通过信号机制抛出异常,Linux下可以通过捕获信号来捕获异常:

void DumpCollector::Init()
{
#ifdef WIN32
// ::SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)ApplicationCrashCollector);
#else
DumpCollector::SetPosixSignalHandler();
#endif
}

static void PosixApplicationSignalCollector(int sig, siginfo_t* siginfo, void* context)
{
std::lock_guard<std::mutex> lck(DumpCollector::lock);
DumpCollector::sig = sig;
DumpCollector::siginfo = siginfo;

std::string exceptionCause = DumpCollector::GetPosixExceptionInfo();
std::vector<StackFrame> stacktrace = DumpCollector::DumpPosixStackTrace();
if (DumpCollector::PosixCrashHandler == nullptr) {
WRITE_ERROR("missing posix crash handler");
} else {
DumpCollector::PosixCrashHandler(exceptionCause, stacktrace);
}
std::abort();
}

void DumpCollector::SetPosixSignalHandler()
{
static uint8_t alternateStack[SIGSTKSZ];
// setup alternate stack
stack_t ss {};
// malloc is usually used here, I'm not 100% sure my static allocation is valid but it seems to work just fine. */
ss.ss_sp = (void*)alternateStack;
ss.ss_size = SIGSTKSZ;
ss.ss_flags = 0;

if (::sigaltstack(&ss, NULL) != 0) {
WRITE_ERROR("call sigaltstack() error");
}

// register posix signal handlers
struct sigaction signalAction {};
signalAction.sa_sigaction = PosixApplicationSignalCollector;
::sigemptyset(&signalAction.sa_mask);

#ifdef __APPLE__
// for some reason we backtrace() doesn't work on osx when we use an alternate stack
signalAction.sa_flags = SA_SIGINFO;
#else
signalAction.sa_flags = SA_SIGINFO | SA_ONSTACK;
#endif

if (sigaction(SIGSEGV, &signalAction, nullptr) != 0) {
WRITE_ERROR("call sigaction(SIGSEGV) error");
}
if (sigaction(SIGFPE, &signalAction, nullptr) != 0) {
WRITE_ERROR("call sigaction(SIGFPE) error");
}
if (sigaction(SIGINT, &signalAction, nullptr) != 0) {
WRITE_ERROR("call sigaction(SIGINT) error");
}
if (sigaction(SIGILL, &signalAction, nullptr) != 0) {
WRITE_ERROR("call sigaction(SIGILL) error");
}
if (sigaction(SIGTERM, &signalAction, nullptr) != 0) {
WRITE_ERROR("call sigaction(SIGTERM) error");
}
// skip SIGABRT in case of infinite loop
}

从信号推测可能的异常原因:

std::string DumpCollector::GetPosixExceptionInfo()
{
int sig = DumpCollector::sig;
siginfo_t *siginfo = static_cast<siginfo_t*>(DumpCollector::siginfo);
switch (sig){
case SIGSEGV: return "Caught SIGSEGV: Segmentation Fault";
case SIGINT: return "Caught SIGINT: Interactive attention signal, (usually ctrl+c)";
case SIGFPE: {
switch (siginfo->si_code) {
case FPE_INTDIV: return "Caught SIGFPE: (integer divide by zero)";
case FPE_INTOVF: return "Caught SIGFPE: (integer overflow)";
case FPE_FLTDIV: return "Caught SIGFPE: (floating-point divide by zero)";
case FPE_FLTOVF: return "Caught SIGFPE: (floating-point overflow)";
case FPE_FLTUND: return "Caught SIGFPE: (floating-point underflow)";
case FPE_FLTRES: return "Caught SIGFPE: (floating-point inexact result)";
case FPE_FLTINV: return "Caught SIGFPE: (floating-point invalid operation)";
case FPE_FLTSUB: return "Caught SIGFPE: (subscript out of range)";
default: return "Caught SIGFPE: Arithmetic Exception";
}
}
case SIGILL: {
switch (siginfo->si_code) {
case ILL_ILLOPC: return "Caught SIGILL: (illegal opcode)";
case ILL_ILLOPN: return "Caught SIGILL: (illegal operand)";
case ILL_ILLADR: return "Caught SIGILL: (illegal addressing mode)";
case ILL_ILLTRP: return "Caught SIGILL: (illegal trap)";
case ILL_PRVOPC: return "Caught SIGILL: (privileged opcode)";
case ILL_PRVREG: return "Caught SIGILL: (privileged register)";
case ILL_COPROC: return "Caught SIGILL: (coprocessor error)";
case ILL_BADSTK: return "Caught SIGILL: (internal stack error)";
default: return "Caught SIGILL: Illegal Instruction";
}
}
case SIGTERM: return "Caught SIGTERM: a termination request was sent to the program";
case SIGABRT: return "Caught SIGABRT: usually caused by an abort() or assert()";
default: return "Unknown";
}
return "Unknown";
}

获取堆栈信息:

std::vector<StackFrame> DumpCollector::DumpPosixStackTrace()
{
std::vector<StackFrame> stacktrace;
static void* stacktraces[POSIX_MAX_STACK_FRAMES];
int traceSize = ::backtrace(stacktraces, POSIX_MAX_STACK_FRAMES);
char** messages = ::backtrace_symbols(stacktraces, traceSize);

for (int i = 0; i < traceSize; ++i) {
//if (::addr2line(icky_global_program_name, stacktraces[i]) != 0) {
// ::printf(" error determining line # for: %s\n", messages[i]);
//}
printf("[%d]%s\n", i, messages[i]);
}
if (messages) {
::free(messages);
}
return stacktrace;
}

参考资料

Disqus评论区没有正常加载,请使用科学上网