概述
Breakpad
客户端库负责监控应用程序是否发生崩溃(异常)、在崩溃发生时通过生成转储来处理这些崩溃- 并提供将转储上传到崩溃报告服务器的方法
客户端
概述
Breakpad
客户端主要负责在应用程序崩溃时捕获崩溃信息,并生成minidump
文件- 该
minidump
文件包含了崩溃时的堆栈、寄存器状态和内存信息,可以用来分析崩溃原因
主要组件
Exception Handler
:- 其实就相当于一个管理器
- 溃处理程序,负责捕获异常并生成
minidump
文件
Minidump Writer
:- 负责将崩溃时的内存信息写入
minidump
文件
- 负责将崩溃时的内存信息写入
Client Interface
:- 应用程序与
Breakpad
的接口,用于初始化和配置Breakpad
- 应用程序与
设计与实现
- 初始化和配置:
- 在应用程序启动时,初始化
Breakpad
并设置崩溃处理程序 - 常见的初始化代码如下
- 在应用程序启动时,初始化
1 2 3 4 5 6 7 |
google_breakpad::ExceptionHandler* handler = new google_breakpad::ExceptionHandler( L".", /* Dump file path */ nullptr, /* Filter callback */ nullptr, /* Minidump callback */ nullptr, /* Callback context */ google_breakpad::ExceptionHandler::HANDLER_ALL /* Handler types */ ); |
- 捕获异常:
- 当应用程序发生崩溃时,
Windows
会调用注册的异常处理程序 Breakpad
的异常处理程序会生成一个minidump
文件
- 当应用程序发生崩溃时,
- 生成
Minidump
文件:- 崩溃处理程序调用
Minidump Writer
,将当前进程的内存信息写入minidump
文件 - 生成的
minidump
文件可以用来进行后续的崩溃分析
- 崩溃处理程序调用
客户端的细节
HandlerType
HANDLER_EXCEPTION = 1 << 0, // SetUnhandledExceptionFilter
HANDLER_INVALID_PARAMETER = 1 << 1, // _set_invalid_parameter_handler
HANDLER_PURECALL = 1 << 2, // _set_purecall_handler
HANDLER_ALL = HANDLER_EXCEPTION |HANDLER_INVALID_PARAMETER |HANDLER_PURECALL
ExceptionHandler
- 构造函数里调用了
Initialize
这个函数,它是用于初始化ExceptionHandler
对象的核心函数
重要的回调
typedef bool (*FilterCallback)(void* context, EXCEPTION_POINTERS* exinfo,MDRawAssertionInfo* assertion);
ExceptionHandler
构造函数的第二个参数Breakpad
对异常进行任何实质性处理之前运行的回调函数- 在写入小型转储之前会调用
FilterCallback
context
是创建处理程序时用户作为callback_context
提供的参数exinfo
指向异常记录(如果有)assertion
指向断言信息(如果有)- 如果
FilterCallback
返回true
,Breakpad
将继续处理,并尝试写入小型转储 - 如果
FilterCallback
返回false
,Breakpad
将立即报告异常未处理,而不会写入小型转储,从而允许另一个处理程序处理它
typedef bool (*MinidumpCallback)(const wchar_t* dump_path,const wchar_t* minidump_id,void* context,EXCEPTION_POINTERS* exinfo,MDRawAssertionInfo* assertion,bool succeeded);
- 写入
minidump
后运行的回调函数 minidump_id
是转储的唯一ID
,因此minidump
文件为<dump_path>\<minidump_id>.dmp
context
是创建处理程序时用户作为回调上下文提供的参数exinfo
指向异常记录,如果没有发生异常,则为NULL
successful
表示minidump
文件是否已成功写入assertion
如果处理程序由断言调用,则指向有关断言的信息- 如果发生异常并且回调返回
true
,则Breakpad
会将异常视为已完全处理,从而阻止任何其他处理程序收到异常通知 - 如果回调返回
false
,则Breakpad
将异常视为未处理,并允许另一个处理程序处理它 - 如果没有其他处理程序,
Breakpad
会将异常报告给系统,作为未处理,从而允许调试器或本机崩溃对话框有机会处理异常 - 大多数回调实现通常应返回
succeeded
的值,或者当它们希望不报告已处理的异常时,返回false
- 回调很少会直接返回
true
(除非succeeded
为true
)
- 回调很少会直接返回
- 对于进程外转储生成,转储路径和小型转储
ID
将始终为NULL
- 在进程外转储生成的情况下,转储路径和小型转储
ID
由服务器进程控制,不会传回崩溃进程
- 在进程外转储生成的情况下,转储路径和小型转储
- 写入
Initialize
- 参数
dump_path
:Minidump
文件的保存路径filter
: 过滤回调函数,用于决定是否生成minidump
文件callback
:Minidump
回调函数,用于在生成minidump
后执行自定义操作callback_context
: 传递给回调函数的上下文信息handler_types
: 要处理的异常类型dump_type
:Minidump
文件的类型pipe_name
: 用于out-of-process
崩溃处理的管道名称pipe_handle
: 用于out-of-process
崩溃处理的管道句柄crash_generation_client
: 崩溃生成客户端对象,用于out-of-process
崩溃处理custom_info
: 自定义客户端信息
InterlockedIncrement
- 增加实例计数
- 保存回调,初始化其他成员
- 配置 out-of-process 崩溃处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
scoped_ptr<CrashGenerationClient> client; if (crash_generation_client) { client.reset(crash_generation_client); } else if (pipe_name) { client.reset(new CrashGenerationClient(pipe_name, dump_type_, custom_info)); } else if (pipe_handle) { client.reset(new CrashGenerationClient(pipe_handle, dump_type_, custom_info)); } if (client.get() != NULL) { if (client->Register()) { crash_generation_client_.reset(client.release()); } } |
client->Register()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
bool CrashGenerationClient::Register() { if (IsRegistered()) { return true; } HANDLE pipe = ConnectToServer(); if (!pipe) { return false; } bool success = RegisterClient(pipe); CloseHandle(pipe); return success; } |
- 检查一下有没有成功配置外部进程
IsOutOfProcess
- 如果没有成功配置
out-of-process
崩溃处理,则设置in-process
崩溃处理InitializeCriticalSection
,临界区- 创建两个信号量,一个用于开始,一个 用于结束
- 创建线程
- 加载
dbghelp.dll
和rpcrt4.dll
- 设置指令内存信息
1 2 3 4 |
AppMemory instruction_memory; instruction_memory.ptr = NULL; instruction_memory.length = 0; app_memory_info_.push_back(instruction_memory); |
- 初始化关键段对象,设置异常处理程序
- 如果
handler_types
不为HANDLER_NONE
,则设置异常处理程序
- 如果
1 2 3 |
if (instance_count == 1) { InitializeCriticalSection(&handler_stack_critical_section_); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
if (handler_types != HANDLER_NONE) { EnterCriticalSection(&handler_stack_critical_section_); if (!handler_stack_) { handler_stack_ = new vector<ExceptionHandler*>(); } handler_stack_->push_back(this); if (handler_types & HANDLER_EXCEPTION) previous_filter_ = SetUnhandledExceptionFilter(HandleException); if (handler_types & HANDLER_INVALID_PARAMETER) previous_iph_ = _set_invalid_parameter_handler(HandleInvalidParameter); if (handler_types & HANDLER_PURECALL) previous_pch_ = _set_purecall_handler(HandlePureVirtualCall); LeaveCriticalSection(&handler_stack_critical_section_); } |
CrashGenerationClient
- 初始化
- 如果提供了
crash_generation_client
,就用提供的 - 如果提供了
pipe_name
或pipe_handle
,就用提供的信息去生成一个CrashGenerationClient
替换默认的 - 如果没有提供任何自定义信息,就用默认的
CrashGenerationClient
去注册 - 并且在注册成功后,把这个默认的
CrashGenerationClient
保存到管理器的成员变量中
- 如果提供了
- 所谓
Register
- 先判断一下已经有没有注册,没注册过才注册
- 具体是调用
ConnectToServer
函数,尝试通过命名管道连接到服务器并进行通信
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
scoped_ptr<CrashGenerationClient> client; if (crash_generation_client) { client.reset(crash_generation_client); } else if (pipe_name) { client.reset( new CrashGenerationClient(pipe_name, dump_type_, custom_info)); } else if (pipe_handle) { client.reset( new CrashGenerationClient(pipe_handle, dump_type_, custom_info)); } if (client.get() != NULL) { // If successful in registering with the monitoring process, // there is no need to setup in-process crash generation. if (client->Register()) { crash_generation_client_.reset(client.release()); } } |
- 总结就是再次检查
CrashGenerationClient
能不能用,能用就用它去发请求- 不能用就用别的手段去做事情
CrashGenerationClient
能用的情况,就是用它去通知服务端,真正最后做事情的,是在服务端
客户端和服务端的通信
客户端连接服务端
- 连接到具名管道(服务端)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
HANDLE CrashGenerationClient::ConnectToPipe(const wchar_t* pipe_name, DWORD pipe_access, DWORD flags_attrs) { if (pipe_handle_) { HANDLE t = pipe_handle_; pipe_handle_ = NULL; return t; } for (int i = 0; i < kPipeConnectMaxAttempts; ++i) { HANDLE pipe = CreateFile(pipe_name, pipe_access, 0, NULL, OPEN_EXISTING, flags_attrs, NULL); if (pipe != INVALID_HANDLE_VALUE) { return pipe; } // Cannot continue retrying if error is something other than // ERROR_PIPE_BUSY. if (GetLastError() != ERROR_PIPE_BUSY) { break; } // Cannot continue retrying if wait on pipe fails. if (!WaitNamedPipe(pipe_name, kPipeBusyWaitTimeoutMs)) { break; } } return NULL; } |
- 连接成功后,顺便设置管道的读取模式为消息模式
- 这种模式下,读取操作将读取完整的消息。这意味着每次读取操作都会读取一个完整的消息单元,确保消息的边界得到维护
- 相比之下,字节模式(
Byte Mode
)则是以字节流的方式读取数据,不保证消息边界
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
HANDLE CrashGenerationClient::ConnectToServer() { HANDLE pipe = ConnectToPipe(pipe_name_.c_str(), kPipeDesiredAccess, kPipeFlagsAndAttributes); if (!pipe) { return NULL; } DWORD mode = kPipeMode; if (!SetNamedPipeHandleState(pipe, &mode, NULL, NULL)) { CloseHandle(pipe); pipe = NULL; } return pipe; } |
- 成功连接到服务端后,向服务端注册客户端,并从服务端获取必要的同步对象和句柄,以便后续的崩溃处理
- 构建注册请求消息
- 通过命名管道发送注册请求并接收响应
- 验证响应消息
- 发送确认消息
- 存储从服务端获取的同步对象和句柄
- 返回注册结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
bool CrashGenerationClient::RegisterClient(HANDLE pipe) { ProtocolMessage msg(MESSAGE_TAG_REGISTRATION_REQUEST, GetCurrentProcessId(), dump_type_, &thread_id_, &exception_pointers_, &assert_info_, custom_info_, NULL, NULL, NULL); ProtocolMessage reply; DWORD bytes_count = 0; // The call to TransactNamedPipe below can be changed to a call // to TransactNamedPipeDebugHelper to help repro some scenarios. // For details see comments for TransactNamedPipeDebugHelper. if (!TransactNamedPipe(pipe, &msg, sizeof(msg), &reply, sizeof(ProtocolMessage), &bytes_count, NULL)) { return false; } if (!ValidateResponse(reply)) { return false; } ProtocolMessage ack_msg; ack_msg.tag = MESSAGE_TAG_REGISTRATION_ACK; if (!WriteFile(pipe, &ack_msg, sizeof(ack_msg), &bytes_count, NULL)) { return false; } crash_event_ = reply.dump_request_handle; crash_generated_ = reply.dump_generated_handle; server_alive_ = reply.server_alive_handle; server_process_id_ = reply.id; return true; } |
- 管理器在确认客户端初始化完成并连接到服务端之后设置一些额外的状态和处理程序
1 2 3 4 5 6 7 8 9 |
// 预留指令内存 // 预留一个指令内存的元素,AppMemory 是一个结构体,包含一个指向内存的指针和内存长度 // 用于后续的内存操作,确保 app_memory_info_ 向量至少有一个元素 // Reserve one element for the instruction memory AppMemory instruction_memory; instruction_memory.ptr = NULL; instruction_memory.length = 0; app_memory_info_.push_back(instruction_memory); |
1 2 3 4 5 6 7 |
// 竞争条件和关键段的延迟初始化 // 懒初始化 handler_stack_critical_section_,即只有在第一个实例初始化时才初始化这个关键段 // Lazy initialization of the handler_stack_critical_section_ if (instance_count == 1) { InitializeCriticalSection(&handler_stack_critical_section_); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// 根据 handler_types 设置不同的处理程序,并维护一个处理程序栈 if (handler_types != HANDLER_NONE) { EnterCriticalSection(&handler_stack_critical_section_); // The first time an ExceptionHandler that installs a handler is // created, set up the handler stack. if (!handler_stack_) { handler_stack_ = new vector<ExceptionHandler*>(); } handler_stack_->push_back(this); if (handler_types & HANDLER_EXCEPTION) previous_filter_ = SetUnhandledExceptionFilter(HandleException); #if _MSC_VER >= 1400 // MSVC 2005/8 if (handler_types & HANDLER_INVALID_PARAMETER) previous_iph_ = _set_invalid_parameter_handler(HandleInvalidParameter); #endif // _MSC_VER >= 1400 if (handler_types & HANDLER_PURECALL) previous_pch_ = _set_purecall_handler(HandlePureVirtualCall); LeaveCriticalSection(&handler_stack_critical_section_); } |
- 当需要生成
dump
时,管理器这边会调用相关写dump
函数,并通过客户端的RequestDump
函数去触发关键事件- 客户端这边触发关键事件消息后,等待服务端的回复
1 2 3 4 5 6 7 8 9 10 |
bool success = false; if (IsOutOfProcess()) { success = crash_generation_client_->RequestDump(exinfo, assertion); } else { success = WriteMinidumpWithExceptionForProcess(requesting_thread_id, exinfo, assertion, GetCurrentProcess(), true); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
bool CrashGenerationClient::RequestDump(EXCEPTION_POINTERS* ex_info, MDRawAssertionInfo* assert_info) { if (!IsRegistered()) { return false; } exception_pointers_ = ex_info; thread_id_ = GetCurrentThreadId(); if (assert_info) { memcpy(&assert_info_, assert_info, sizeof(assert_info_)); } else { memset(&assert_info_, 0, sizeof(assert_info_)); } return SignalCrashEventAndWait(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
bool CrashGenerationClient::SignalCrashEventAndWait() { assert(crash_event_); assert(crash_generated_); assert(server_alive_); // Reset the dump generated event before signaling the crash // event so that the server can set the dump generated event // once it is done generating the event. if (!ResetEvent(crash_generated_)) { return false; } if (!SetEvent(crash_event_)) { return false; } HANDLE wait_handles[kWaitEventCount] = {crash_generated_, server_alive_}; DWORD result = WaitForMultipleObjects(kWaitEventCount, wait_handles, FALSE, kWaitForServerTimeoutMs); // Crash dump was successfully generated only if the server // signaled the crash generated event. return result == WAIT_OBJECT_0; } |
- 服务器那边是先启动的
- 它在
Start
函数里创建了关键事件,以及具名管道 - 然后监听关键事件的触发,触发后在对应的回调函数
OnPipeConnected
里根据具体的状态进行处理 - 对于
IPC_SERVER_STATE_READ_DONE
这个状态,会通过RespondToClient
这个函数去响应,成功响应后会把客户端添加服务端的容器里 - 成功添加也意味着,为该客户端进行了两个注册,一个绑定了生成
dump
的回调OnDumpRequest
,一个绑定了客户端结束的回调OnClientEnd
- 它在
服务端收到消息并处理
关于客户端-服务端模式
- 在
breakpad
里面客户端负责根据应用程序的状况生成消息,并发送给服务端 - 服务端收到消息后,根据消息的类型进行对应的操作
关于客户端的问题
异常函数的注册
- 分程序内,程序外两种,后续再研究
客户端是怎么监控的
- 注册完异常函数后,当崩溃发生后,系统会回调这些异常函数,在这些异常函数里面客户端触发关键事件,服务端那边监听到事件触发后,就根据具体的状态去处理生成,后续再研究
服务端
- 先于客户端启动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
void CrashServerStart() { // Do not create another instance of the server. if (crash_server) { return; } std::wstring dump_path = L"C:\\Dumps\\"; if (_wmkdir(dump_path.c_str()) && (errno != EEXIST)) { MessageBoxW(NULL, L"Unable to create dump directory", L"Dumper", MB_OK); return; } crash_server = new CrashGenerationServer(kPipeName, NULL, ShowClientConnected, NULL, ShowClientCrashed, NULL, ShowClientExited, NULL, NULL, NULL, true, &dump_path); if (!crash_server->Start()) { MessageBoxW(NULL, L"Unable to start server", L"Dumper", MB_OK); delete crash_server; crash_server = NULL; } } |
Start
函数里面初始化了关键事件,互斥量,用于后续和客户端交互- 同时给对应的事件注册了回调函数
OnPipeConnected
- 同时给对应的事件注册了回调函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
bool CrashGenerationServer::Start() { if (server_state_ != IPC_SERVER_STATE_UNINITIALIZED) { return false; } server_state_ = IPC_SERVER_STATE_INITIAL; server_alive_handle_ = CreateMutex(NULL, TRUE, NULL); if (!server_alive_handle_) { return false; } // Event to signal the client connection and pipe reads and writes. overlapped_.hEvent = CreateEvent(NULL, // Security descriptor. TRUE, // Manual reset. FALSE, // Initially nonsignaled. NULL); // Name. if (!overlapped_.hEvent) { return false; } // Register a callback with the thread pool for the client connection. if (!RegisterWaitForSingleObject(&pipe_wait_handle_, overlapped_.hEvent, OnPipeConnected, this, INFINITE, kPipeIOThreadFlags)) { return false; } pipe_ = CreateNamedPipe(pipe_name_.c_str(), kPipeAttr, kPipeMode, 1, kOutBufferSize, kInBufferSize, 0, pipe_sec_attrs_); if (pipe_ == INVALID_HANDLE_VALUE) { return false; } // Kick-start the state machine. This will initiate an asynchronous wait // for client connections. if (!SetEvent(overlapped_.hEvent)) { server_state_ = IPC_SERVER_STATE_ERROR; return false; } // If we are in error state, it's because we failed to start listening. return true; } |
知识点
QueueUserWorkItem
QueueUserWorkItem
函数用于将一个工作项添加到系统的全局工作队列中,该队列由Windows
操作系统管理,并由操作系统维护的线程池处理- 函数签名
Function
: 一个指向工作函数的指针
在breakpad
的test
工程例子中,是AppendTextWorker
- `
Context
: 传递给工作函数的参数
在这个例子中,是text
Flags
: 指定工作项的执行选项
WT_EXECUTEDEFAULT
表示使用默认的执行选项
1 2 3 4 5 |
BOOL QueueUserWorkItem( LPTHREAD_START_ROUTINE Function, // 指向工作函数的指针 PVOID Context, // 工作函数的参数 ULONG Flags // 执行选项 ); |
- 工作流程
- 将工作项添加到工作队列:
QueueUserWorkItem
函数被调用时,会将AppendTextWorker
函数和text
参数添加到系统的全局工作队列中 - 线程池中的线程处理工作项:
系统的线程池会自动从工作队列中取出工作项,并在一个可用的线程中调用AppendTextWorker
函数
线程池中的线程会并行处理多个工作项,提高处理效率
- 将工作项添加到工作队列:
QueueUserWorkItem相关问题
- 这个系统全局工作队列什么时候创建的?
- 系统全局工作队列是在操作系统启动时由
Windows
内核创建的 - 这是操作系统的一部分,始终存在,并由操作系统负责管理和维护
- 系统全局工作队列是在操作系统启动时由
- 这个系统全局工作队列由谁管理的?
- 全局工作队列由
Windows
内核维护和管理 - 操作系统负责管理队列中的工作项,以及协调线程池线程来处理这些工作项
- 用户应用程序通过操作系统提供的
API
(如QueueUserWorkItem
)与该队列进行交互
- 全局工作队列由
- 系统线程池什么时候创建的?
- 系统线程池是在操作系统启动时由
Windows
内核创建的 - 线程池的创建和管理是操作系统的一部分,系统会根据需要动态调整线程池的大小和配置
- 系统线程池是在操作系统启动时由
- 系统线程池由谁管理的?
- 系统线程池由
Windows
操作系统创建和维护 Windows
提供了一个通用的线程池机制,允许操作系统高效地管理线程资源- 操作系统会根据工作队列的负载动态调整线程池的大小,以优化性能和资源利用率
- 系统线程池由
- 所有的应用程序都可以通过这个函数添加任务吗?
- 是的,所有用户模式的应用程序都可以使用
QueueUserWorkItem
函数将工作项添加到系统的全局工作队列中 - 这是一个公开的
API
,设计用于简化多线程编程,提供一种方便的方式来将工作项异步提交给操作系统管理的线程池
- 是的,所有用户模式的应用程序都可以使用
- 尽管所有应用程序都可以使用
QueueUserWorkItem
函数,但它们必须遵守以下约束和限制:- 权限:
应用程序需要有足够的权限来调用此函数
如果应用程序在受限环境中运行,可能会遇到权限问题 - 资源限制:
系统线程池和工作队列是共享资源
如果一个应用程序提交了大量的工作项,可能会影响系统的整体性能
操作系统会进行一些调度和资源管理,但开发者仍应小心管理提交的工作项数量 - 错误处理:
应用程序应正确处理QueueUserWorkItem
函数可能返回的错误,例如资源不足或其他系统限制
- 权限:
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ 51CTO:Linux C++网络编程三08/16
- ♥ CLion:配置C++下lua开发环境06/03
- ♥ Windows进程通信相关03/10
- ♥ STL_deque05/18
- ♥ Windows物理内存虚拟内存03/28
- ♥ 编译器扩展语法:一07/06