Windows端使用crashpad来抓取dump因为使用了MT而踩到坑了

(一)前言

windows端抓取dump,有一个现成的谷歌官方提供的工具:crashpad。

具体可以参考官方资料:GitHub - chromium/crashpad: A crash-reporting system · GitHub

这里编译crashpad使用的是MT的编译模式,因为不想把依赖chromium中的base库等基础库。

这样很方便多个项目很方便的使用。

crashpad的架构如下:

简单总结就是有一个CrashpadHander.exe的进程和一个CrashpadClient.dll的动态库,接入方就是Client Process,加载动态库CrashpadClient.dll,会有这个动态库启动进程CrashpadHander.exe。

但是在实际使用过程中遇到了一些问题,也就是有些类型的崩溃没有抓取到。

(二)遇到的问题

在windows平台下,抓取dump一般比较常用的方式是使用SetUnhandledExceptionFilter 设置未处理异常的回调函数,在回调函数里抓取dump。

但这种方式只能抓取到SEH可以处理的异常,比如违规访问(比如空指针),除零异常等。

还有一些异常处理不了。比如调用了纯虚函数,抛出了c++异常等。

详细的讲解在这个文章里有讲:

https://www.codeproject.com/Articles/207464/Exception-Handling-in-Visual-Cplusplus

总结下来就是这四类:

_set_purecall_handler(pureVirtualCall);

_set_invalid_parameter_handler(InvalidParameterHandler);

_set_thread_local_invalid_parameter_handler(localThreadInvalidParameterHandler);

signal(SIGABRT, signalHandler);

这四类都是在CRT里的函数,也就是C语言运行时库。

我当时遇到了一个崩溃,大概的代码可以简化如下:

cpp 复制代码
struct Base
{
    virtual void doWork() { }
    std::string a = "he";
};

struct Sub : Base
{
    std::string b = "xx";
};
void*                  pVoid = (void*)malloc(0);
std::shared_ptr<Base>& base = *(std::shared_ptr<Base>*)pVoid;
auto                   sub = std::dynamic_pointer_cast<Sub>(base);

这个代码会在dynamic_pointer_cast执行过程中崩溃,具体来说就会触发std::abort的调用,而这个行为的捕获就是要依赖 signal(SIGABRT, signalHandler); 这个函数注册的 signalHandler 来处理。

问题是我在CrashpadClient.dll里已经注册过了SIGABRT的处理函数,还是没有抓取到dump,但是我模拟一个空指针崩溃就可以抓到dump。

(三)如何解决

通过询问AI(不得不说AI对软件工程师来说,帮助很大,效率提高了不少)和自己的实际代码验证,确认了问题的根源:

我编译的CrashpadClient.dll是使用MT编译的。

/MT/MD 是 MSVC 编译器中指定 ‌C/C++ 运行时库(CRT)链接方式 ‌的核心选项,本质区别在于‌静态链接 ‌与‌动态链接‌。‌‌

核心区别

  • ‌**/MT (Multi-threaded, Static)** ‌:‌静态链接 ‌。将运行时库代码(如 libcmt.lib)直接编译进可执行文件。程序‌独立 ‌,无需外部 DLL,但文件体积较大;若项目含多个模块,需确保所有模块均用 /MT,否则易引发"跨堆内存释放"崩溃。
  • ‌**/MD (Multi-threaded DLL, Dynamic)** ‌:‌动态链接 ‌。程序仅导入 msvcrt.lib,实际代码依赖系统对应的运行时 DLL(如 msvcr140.dll)。文件体积小,所有模块共用同一堆内存,但部署时需保证目标机器存在对应版本的 CRT DLL。‌‌

由于CrashpadClient.dll是用/MT编译的,所以虽然调用了signal(SIGABRT, signalHandler)设置了SIGABRT异常回调函数,但是当ClientProcess(也就是接入crashpad的进程)异常的时候,他只会在自己的依赖的CRT库里查找对应的SIGABRT处理函数,这个处理函数一定不会是CrashpadClient.dll使用的。

项目刚好用的CEF(比如工程是Main.exe),所以项目都是用的/MT编译出来的,如果想要抓取CRT异常相关的崩溃,需要在Main.exe中设置上述的异常处理函数。

如果还有其他/MT编译的模块,也需要在模块的初始化相关代码中,设置一次CRT相关异常处理函数。

总结下来就是:

每个/MT的模块(无论exe还是dll)都需要设置一遍CRT相关的异常处理函数。

所有使用/MD编译的模块,只需要设置一次CRTCRT相关的异常处理函数即可,因为/MD的是动态链接的,所有的模块公用一个CRT库。

(四)总结

crashpad使用了/MT编译之后。外部如果需要使用,就需要注意使用方使用的是/MT还是/MD编译的。

如果是/MT编译的:每个/MT的模块(无论exe还是dll)都需要设置一遍CRT相关的异常处理函数。

如果是/MD编译的:所有使用/MD编译的模块,只需要设置一次CRTCRT相关的异常处理函数即可,因为/MD的是动态链接的,所有的模块公用一个CRT库。

备注:

windows的SEH是支持跨模块的,也就是说和/MT、/MD没有关系,也不论异常发生在哪个模块。

chromium代码和cef源码中,已经自动设置了上述三个handler:

_set_purecall_handler(pureVirtualCall);

_set_invalid_parameter_handler(InvalidParameterHandler);

signal(SIGABRT, signalHandler);

这个invalid_parameter_handler是和线程相关的,也就是说每个线程都需要设置,我们自己开发的项目一般也只设置一个主线程。

_set_thread_local_invalid_parameter_handler(localThreadInvalidParameterHandler);

最开始我使用_set_purecall_handler的时候,我在想如何才能拿触发这个purecall,查了下官网:

https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/get-purecall-handler-set-purecall-handler?view=msvc-170

有现成的代码:

cpp 复制代码
// _set_purecall_handler.cpp
// compile with: /W1
#include <tchar.h>
#include <stdio.h>
#include <stdlib.h>

class CDerived;
class CBase
{
public:
   CBase(CDerived *derived): m_pDerived(derived) {}
   ~CBase();
   virtual void function(void) = 0;

   CDerived * m_pDerived;
};

class CDerived : public CBase
{
public:
   CDerived() : CBase(this) {}   // C4355
   virtual void function(void) {}
};

CBase::~CBase()
{
   m_pDerived -> function();
}

void myPurecallHandler(void)
{
   printf("In _purecall_handler.");
   exit(0);
}

int _tmain(int argc, _TCHAR* argv[])
{
   _set_purecall_handler(myPurecallHandler);
   CDerived myDerived;
}

(五)号外号外,招人环节,招聘环境

我们公司在招聘c++开发工程,需要找工作的同学可以找我内推。这个岗位是我所在部门。

可以在评论区留言留下联系方式,或者私信我吧。

base:上海

薪资open,看面试情况和个人技术水平来定。界内TOP水平。大厂。

其他各种岗位也可以帮忙内推:

Java、AI工程师,算法工程师、数据库、前端、客户端(android, iOS, PC,鸿蒙等)。

产品经理,运营经理,实习生,应届生都可以。