本文翻譯自debugInfo網站上一篇文章generating debug information with visual c++。因爲Chrome的Crash產生的Debug信息和這個有一些關係,所以作一些背景知識介紹html
簡介c++
當咱們採用一個調試器調試一個應用程序時,咱們老是但願能單步跟蹤代碼、設置斷點、查看變量值,哪怕變量是自定義的用戶類型。可是對於一個EXE程序來講,基本上就是一堆二進制數據(目前的Windows中EXE程序中還包含了一些頭部信息,用於系統執行程序)。當一個EXE程序運行時,系統將爲這些EXE分配一些額外的內存用於存儲運行時數據(Stack,Heap)。可是依舊沒有任何調試方面的信息。當程序Coredump時,像WinDBG這些調試器是如何定位到哪一行的呢,這些都是Visual Studio中的一些編譯特性。chrome
調試信息種類安全
在Intel X86指令體系下的Windows平臺,一個EXE或者DLL中主要有下面幾種調試信息:服務器
Debug類型數據結構 |
說明app |
Public functions and variableside |
主要包括了一些全局變量和全局函數信息。在Debug信息中主要存儲了他們的位置、大小、名字信息函數 |
Private functions and variablespost |
主要包含了非全局的變量和函數信息。 |
Source File and Line Information |
主要包含了每一行代碼在EXE中的對應位置信息 |
Type Information |
主要存儲了各類數據類型信息,包括用戶自定義的數據類型 |
FPO Information |
FPO(Frame Pointer Omission)。Frame Pointer 是一種用來在調用堆棧(Call stack)中找到下一個將要被調用的函數的數據結構源代碼的行序號(Source-line numbers);編譯器可針對這一特性作優化,Debug信息中依舊能夠存儲一些信息,用來查詢函數的棧區幀大小信息。 |
Edit and Continue Information |
主要包含了要實現用戶編輯後能夠繼續執行特性的相關信息。 |
表1 Windows平臺下調試信息分類
調試文件格式分類
在過去二十多年的時間裏,微軟採用了三種形式來存儲DEBUG信息:COFF,CodeView,Program Database。咱們從三個維度來對比分析一下這三種格式:
1. 每種格式中存儲了哪些調試信息?
2. 每種格式的調試信息存儲在哪裏?(包含在EXE中仍是單獨的調試信息文件)
3. 每種格式的設計文檔是否齊全?
這是最老的一種格式,只能存儲三種信息:Public functions and variables, source file and line information, FPO信息。COFF信息是存儲在EXE文件中的,不能單獨存儲。這種格式文檔有詳細的說明: Microsoft Portable Executable and Common Object File Format Specification。
這是在COFF基礎上推出的一個更爲複雜一些的格式。它能夠存儲表1中除了Edit and Continue Information外的其餘信息。CodeView信息一般存儲在EXE文件中,可是它也能夠存儲在單獨的文件(.DBG)中。CodeView的格式文檔在MSDN上有部分說明,不是很齊全。
這是微軟最新的格式。他能夠存儲表1中全部信息。另外,他還存儲了增量連接(increase Linking)信息。這在其餘格式中不可能存在的。
Program Database格式信息一般存儲在單獨的文件(.PDB)中。
Program Database格式微軟並無提供格式文檔說明。可是微軟提供了兩套SDK接口:DBGHelp和DIA供用戶調用。PDB有兩個版本,一個是PDB 2.0, 主要在VS6.0中使用。一個是PDB7.0,主要用在Visual Studio.NET以後的版本。DBGHelp是普通的API接口。而DIA提供的是COM接口。相對來講DBGHelp使用起來相對簡單一些,可是DIA提供的信息相對豐富一些。
下表是三種格式的對比:
格式 |
文檔齊全度 |
存儲 |
public function and variables |
Type information |
FPO information |
EnC information |
COFF |
齊全 |
EXE |
+ |
- |
+ |
- |
CodeView |
部分 |
EXE或者單獨文件(.DBG) |
+ |
+ |
+ |
- |
Program Database |
無 |
單獨文件(.PDB) |
+ |
+ |
+ |
+ |
表2:三種不一樣格式的對比
如何產生調試信息
在Windows下,一個EXE典型的生成過程主要分爲兩步:編譯(Compile)和連接(Link),能夠用下圖來描述:
若是咱們想產生DEBUG信息,一樣須要分爲兩步:咱們要求編譯器(Compiler)爲每個源文件產生相應的調試信息文件;而後由連接器(Linker)把各個調試文件合併成一個大的調試文件。能夠用下圖來描述:
在缺省狀況下,編譯器和連接器不會產生調試信息,咱們須要在編譯和連接選項中設置參數,告訴編譯器和連接器咱們須要生成DEBUG信息、生成什麼格式的調試信息、調試信息存儲在哪裏等。
下面咱們按照Visual C++6.0和Visual C++.NET兩種不一樣版本的IDE分別介紹。
主要包含了下面幾個選項:
/Zd 產生COFF格式調試信息,並保存在目標文件中。
/Z7 產生CodeView格式調試信息,並保證在目標文件中。
/Zi 產生Program Database格式調試信息,並單獨存儲在.PDB文件中。
/ZI 和Zi相似。並在Zi基礎上增長了Edit and Continue信息。
選項 |
格式 |
存儲格式 |
包含內容 |
/Zd |
COFF |
.obj |
|
/Z7 |
CodeView |
.obj |
|
/Zi |
Program Database |
.PDB |
|
/ZI |
Program Database |
.PDB |
|
主要包含了一下連接選項:
/debug 告訴Linker產生調試信息,若是該選項未設置,其它選項設置都不起做用。
/debugtype 告訴Linker採用哪一個格式的調試信息,主要包含了下面幾種:/debugtype:coff COFF格式; /debugtype:cv CodeView或者Program Database格式(依賴 /pdb 選項); /debugtype:both 同時產生COFF和CodeView/Program Database信息。
/pdb 告訴Linker到底採用CodeView仍是Program Database格式. /pdb:none 告訴Linker採用CodeView格式, /pdb:filename 告訴linker採用Program Database格式並且制定了PDB文件的名字.若是debugtype:coff 選項設置了, /pdb 選項不起做用.
/pdbtype選項主要用在有多個文件須要連接時,告訴連接器如何處理各個文件的調試信息。/pdbtype:sept表示Linker不會將各個文件的PDB文件合併到最後一個PDB文件中。若是要調試,須要準備各個PDB文件,而/pdbtype:con選項就是將各個PDB文件合併到一個PDB文件中。
/debugtype |
/pdb |
格式 |
存儲 |
coff |
無做用 |
COFF |
EXE |
coff |
無做用 |
COFF |
EXE |
cv |
/pdb:none |
CodeView |
EXE |
cv |
/pdb:filename |
Program Database |
.PDB |
both |
/pdb:none |
COFF and CodeView |
EXE |
both |
/pdb:filename |
COFF and Program Database |
COFF信息存儲在EXE中,Program Database存儲在單獨PDB文件中 |
表3 不一樣的Linker選項
主要包含了/Zd, /Z7, /Zi, /ZI。可是/Zd已經在Visual C++ 2005中不被支持了。
主要包含三個選項:
/debug 告訴Linker產生調試信息,若是該選項未設置,其它選項設置都不起做用。
/pdb:filename 告訴linker採用Program Database格式並且制定了PDB文件的名字.
/pdbstripped 告訴Linker產生單獨的PDB文件,只包含兩種信息:public functions and variables;FPO information.
在Visual C++.NET中,Linker已經不支持COFF和CODEVIEW兩種格式了。
靜態庫的調試信息
因爲靜態庫不須要Linker,所以靜態庫的調試信息相對來講就簡單多了,設置/Z*(Z7,Zd,Zi,ZI)選項就能夠產生相應的調試信息。
對於Z7和Zd選項,調試信息存儲在相應的.lib文件中,而Zi和ZI選項,調試信息存儲在獨立的.PDB文件中。
調試信息和可執行文件大小關係
調試信息是否影響最終EXE文件的大小,依賴於調試信息存儲的地方,說到底依賴於咱們選擇的格式。
當採用COFF和CodeView方式時,一般調試信息存儲在EXE文件中,將會致使EXE文件極度膨脹,基本上會翻倍。
當採用Program Database方式時,EXE文件就幾乎不受影響了。EXE文件僅僅增長了幾百個字節的頭域,用於定位相應的PDB文件信息。
選項 |
格式 |
存儲 |
內容 |
/Z7 |
CodeView |
.OBJ |
|
/Zi |
Program Database |
.PDB |
|
/ZI |
Program Database |
.PDB |
|
表4:Visual C++.NET下的編譯選項
上一篇中描述了在Windows平臺下產生Debug信息的一些背景知識,這一篇中咱們介紹一下Chrome的Crash Report服務上報了哪些信息。
按照咱們上篇所介紹的,若是應用程序比較複雜,堆棧比較深,一個異常產生的PDB文件也許是幾十MB,甚至上百MB,要把這麼大的文件上傳到服務器,不管從性能上、仍是可靠性上都是一個問題,若是用戶知道了,估計也不會買帳。
在Windows XP以後,Microsoft爲咱們提供了一個新的dump庫,稱爲minidumps庫,這個庫爲咱們提供了定製化的實現,咱們能夠根據本身的須要定製產生的dump內容。缺省設置下,已經能夠獲取到發生異常時的堆棧信息以及一些局部變量值,而相應產生的dump文件只有幾十到幾百KB級別。這個數量級的內容,傳輸起來就相對方便多了。
minidumps主要包含在DBGHelp.dll庫中,這個庫中包含了MiniDumpWriteDump 函數:
BOOL MiniDumpWriteDump(
HANDLE hProcess,
DWORD ProcessId,
HANDLE hFile,
MINIDUMP_TYPE DumpType,
PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
PMINIDUMP_CALLBACK_INFORMATION CallbackParam
);
其中 DumpType參數表示了dump的類型:
typedef enum _MINIDUMP_TYPE {
MiniDumpNormal = 0x00000000,
MiniDumpWithDataSegs = 0x00000001,
MiniDumpWithFullMemory = 0x00000002,
MiniDumpWithHandleData = 0x00000004,
MiniDumpFilterMemory = 0x00000008,
MiniDumpScanMemory = 0x00000010,
MiniDumpWithUnloadedModules = 0x00000020,
MiniDumpWithIndirectlyReferencedMemory = 0x00000040,
MiniDumpFilterModulePaths = 0x00000080,
MiniDumpWithProcessThreadData = 0x00000100,
MiniDumpWithPrivateReadWriteMemory = 0x00000200,
MiniDumpWithoutOptionalData = 0x00000400,
MiniDumpWithFullMemoryInfo = 0x00000800,
MiniDumpWithThreadInfo = 0x00001000,
MiniDumpWithCodeSegs = 0x00002000,
MiniDumpWithoutManagedState = 0x00004000,
} MINIDUMP_TYPE;
你們能夠觀察到可定製化的種類仍是挺多的。具體的參數意義和函數說明,請你們參考MSDN上的說明,亦能夠參考DebugInfo上的 effective minidumps 一文介紹。
Chrome上報的內容就是基於minidumps庫來實現的,Chrome在此基礎上稍微作了一些調整。
在Chrome中,Crash Report服務當程序Crash時,將會上報Dump信息到Google的一個URL(https://clients2.google.com/cr/report )中。
在下一篇中,咱們將進入正題,討論Chrome中是如何實現Crash信息採集和上報的。
1. Chrome是如何捕獲到異常的?
2. Chrome是如何在進程外實現dump文件的轉儲的?
3. Chrome是如何實現上傳的?
一個C++程序, 當發生異常時,好比內存訪問違例時,CPU硬件會發現此問題,併產生一個異常(你能夠把它理解爲中斷),而後CPU會把代碼流程切換到異常處理服務例程。操做系統異常處理服務例程會查看當前進程是否處於調試狀態,若是是,則通知調試器發生了異常,若是不是則操做系統會查看當前線程是否安裝了的異常幀鏈,若是安裝了SEH(try.... catch....),則調用SEH,並根據返回結果決定是否全局展開或者局部展開。若是異常鏈中全部的SEH都沒有處理此異常,並且此進程還處於調試狀態,則操做系統會再次通知調試器發生異常(二次異常)。若是還沒人處理,則調用操做系統的默認異常處理代碼UnhandledExceptionHandler,不過操做系統容許你Hook這個函數,就是經過 SetUnhandledExceptionFilter函數來設置。大部分異常經過此種方法都能捕獲。
不過在Visual C++ 2005以後, Microsoft 對 CRT ( C 運行時庫)的一些與安全相關的代碼作了些改動,典型的,例如增長了對緩衝溢出的檢查。新 CRT 版本在出現錯誤時強制把異常拋給默認的調試器(若是沒有配置的話,默認是 Dr.Watson ),而再也不通知應用程序設置的異常捕獲函數,這種行爲主要在如下兩種狀況出現。
(1) 遇到 _invalid_parameter 錯誤,而應用程序又沒有主動調用 _set_invalid_parameter_handler 設置錯誤捕獲函數。
(2) 虛函數調用錯誤, 而應用程序又沒有主動調用_set_purecall_handler設置捕獲函數。
在Chrome中對這兩種狀況也作了特殊處理。專門設置了兩個回調函數進行捕獲處理。
Chrome的Crash Report主要流程
在Chrome中,支持兩種不一樣模式的Dump。
進程外Dump :由獨立的Crash Handle Process處理Dump的生成過程,主進程產生異常時,經過IPC方式通知Crash Handle Process。由Crash Handle Process中的crash_generation_server負責寫Dump文件。大體流程以下:
上圖中,crash_generation_client和crash_generation_server之間是進程間通信(IPC)。crash_report_sender負責將dump信息發送到google的crash report server(https://clients2.google.com/cr/report)。
進程內Dump :與進程外方式相似,只不過在Browser進程中增長了一個crash_handle_thread線程,由此線程負責寫dump.基本流程以下:
crash_genration_client的實現
HANDLE server_alive_;
表示crash_handle_process是否活動的變量
HANDLE crash_event_;
表示crash_generation_client是否有exception事件發生的信號量。在crash_generation_client和crash_generation_server創建IPC通道後,crash_generation_server將等待這個信號量。
HANDLE crash_generated_;
表示crash_generation_server是否已寫完dump文件的信號量。由crash_generation_server在寫完dum文件後,設置該信號量。
CustomClientInfo custom_info_;
描述當前發生Exception的進程的一些信息,在這裏多是Browser進程,也多是Render進程。
EXCEPTION_POINTERS* exception_pointers_;
異常發生時,全部異常信息保存該指針指向的內存中。
MDRawAssertionInfo assert_info_;
Assert異常信息指針。
在crash_generation_client初始化時,將向crash_generation_server註冊,創建ICP通道,且把上面幾個地址發送給crash_generation_server,當後續crash_generation_client發生異常時,crash_generation_server將從這幾個地址中讀取信息,生成dump文件。(固然這是進程外模式,進程內模式由browser進程內的獨立線程完成這些工做。)
下面函數是
1. bool CrashGenerationClient::SignalCrashEventAndWait() {
2. assert(crash_event_);
3. assert(crash_generated_);
4. assert(server_alive_);
5.
6. // Reset the dump generated event before signaling the crash
7. // event so that the server can set the dump generated event
8. // once it is done generating the event.
9. if (!ResetEvent(crash_generated_)) {
10. return false ;
11. }
12.
13. if (!SetEvent(crash_event_)) {
14. return false ;
15. }
16.
17. HANDLE wait_handles[kWaitEventCount] = {crash_generated_, server_alive_};
18.
19. DWORD result = WaitForMultipleObjects(kWaitEventCount,
20. wait_handles,
21. FALSE,
22. kWaitForServerTimeoutMs);
23.
24. // Crash dump was successfully generated only if the server
25. // signaled the crash generated event.
26. return result == WAIT_OBJECT_0;
27. }
這個函數是crash_generation_client產生exception時,如何和服務器交互的。基本上在上面介紹變量時已經介紹到了。
crash_generation_client是如何捕獲異常的
在本文開始部分已經描述了原理。咱們能夠看一下實現。
1. void ExceptionHandler::Initialize(const wstring& dump_path,
2. FilterCallback filter,
3. MinidumpCallback callback,
4. void * callback_context,
5. int handler_types,
6. MINIDUMP_TYPE dump_type,
7. const wchar_t * pipe_name,
8. const CustomClientInfo* custom_info) {
9. LONG instance_count = InterlockedIncrement(&instance_count_);
10. filter_ = filter;
11. callback_ = callback;
12. callback_context_ = callback_context;
13. dump_path_c_ = NULL;
14. next_minidump_id_c_ = NULL;
15. next_minidump_path_c_ = NULL;
16. dbghelp_module_ = NULL;
17. minidump_write_dump_ = NULL;
18. dump_type_ = dump_type;
19. rpcrt4_module_ = NULL;
20. uuid_create_ = NULL;
21. handler_types_ = handler_types;
22. previous_filter_ = NULL;
23. #if _MSC_VER >= 1400 // MSVC 2005/8
24. previous_iph_ = NULL;
25. #endif // _MSC_VER >= 1400
26. previous_pch_ = NULL;
27. handler_thread_ = NULL;
28. is_shutdown_ = false ;
29. handler_start_semaphore_ = NULL;
30. handler_finish_semaphore_ = NULL;
31. requesting_thread_id_ = 0;
32. exception_info_ = NULL;
33. assertion_ = NULL;
34. handler_return_value_ = false ;
35. handle_debug_exceptions_ = false ;
36.
37. // Attempt to use out-of-process if user has specified pipe name.
38. if (pipe_name != NULL) {
39. scoped_ptr<CrashGenerationClient> client(
40. new CrashGenerationClient(pipe_name,
41. dump_type_,
42. custom_info));
43.
44. // If successful in registering with the monitoring process,
45. // there is no need to setup in-process crash generation.
46. if (client->Register()) {
47. crash_generation_client_.reset(client.release());
48. }
49. }
50.
51. if (!IsOutOfProcess()) {
52. // Either client did not ask for out-of-process crash generation
53. // or registration with the server process failed. In either case,
54. // setup to do in-process crash generation.
55.
56. // Set synchronization primitives and the handler thread. Each
57. // ExceptionHandler object gets its own handler thread because that's the
58. // only way to reliably guarantee sufficient stack space in an exception,
59. // and it allows an easy way to get a snapshot of the requesting thread's
60. // context outside of an exception.
61. InitializeCriticalSection(&handler_critical_section_);
62. handler_start_semaphore_ = CreateSemaphore(NULL, 0, 1, NULL);
63. assert(handler_start_semaphore_ != NULL);
64.
65. handler_finish_semaphore_ = CreateSemaphore(NULL, 0, 1, NULL);
66. assert(handler_finish_semaphore_ != NULL);
67.
68. // Don't attempt to create the thread if we could not create the semaphores.
69. if (handler_finish_semaphore_ != NULL && handler_start_semaphore_ != NULL) {
70. DWORD thread_id;
71. handler_thread_ = CreateThread(NULL, // lpThreadAttributes
72. kExceptionHandlerThreadInitialStackSize,
73. ExceptionHandlerThreadMain,
74. this , // lpParameter
75. 0, // dwCreationFlags
76. &thread_id);
77. assert(handler_thread_ != NULL);
78. }
79.
80. dbghelp_module_ = LoadLibrary(L"dbghelp.dll" );
81. if (dbghelp_module_) {
82. minidump_write_dump_ = reinterpret_cast <MiniDumpWriteDump_type>(
83. GetProcAddress(dbghelp_module_, "MiniDumpWriteDump" ));
84. }
85.
86. // Load this library dynamically to not affect existing projects. Most
87. // projects don't link against this directly, it's usually dynamically
88. // loaded by dependent code.
89. rpcrt4_module_ = LoadLibrary(L"rpcrt4.dll" );
90. if (rpcrt4_module_) {
91. uuid_create_ = reinterpret_cast <UuidCreate_type>(
92. GetProcAddress(rpcrt4_module_, "UuidCreate" ));
93. }
94.
95. // set_dump_path calls UpdateNextID. This sets up all of the path and id
96. // strings, and their equivalent c_str pointers.
97. set_dump_path(dump_path);
98. }
99.
100. // There is a race condition here. If the first instance has not yet
101. // initialized the critical section, the second (and later) instances may
102. // try to use uninitialized critical section object. The feature of multiple
103. // instances in one module is not used much, so leave it as is for now.
104. // One way to solve this in the current design (that is, keeping the static
105. // handler stack) is to use spin locks with volatile bools to synchronize
106. // the handler stack. This works only if the compiler guarantees to generate
107. // cache coherent code for volatile.
108. // TODO(munjal): Fix this in a better way by changing the design if possible.
109.
110. // Lazy initialization of the handler_stack_critical_section_
111. if (instance_count == 1) {
112. InitializeCriticalSection(&handler_stack_critical_section_);
113. }
114.
115. if (handler_types != HANDLER_NONE) {
116. EnterCriticalSection(&handler_stack_critical_section_);
117.
118. // The first time an ExceptionHandler that installs a handler is
119. // created, set up the handler stack.
120. if (!handler_stack_) {
121. handler_stack_ = new vector<ExceptionHandler*>();
122. }
123. handler_stack_->push_back(this );
124.
125. if (handler_types & HANDLER_EXCEPTION)
126. previous_filter_ = SetUnhandledExceptionFilter(HandleException);
127.
128. #if _MSC_VER >= 1400 // MSVC 2005/8
129. if (handler_types & HANDLER_INVALID_PARAMETER)
130. previous_iph_ = _set_invalid_parameter_handler(HandleInvalidParameter);
131. #endif // _MSC_VER >= 1400
132.
133. if (handler_types & HANDLER_PURECALL)
134. previous_pch_ = _set_purecall_handler(HandlePureVirtualCall);
135.
136. LeaveCriticalSection(&handler_stack_critical_section_);
137. }
138. }
在該函數的Line126中,調用了SetUnhandledExceptionFilter函數,設置了咱們要處理的回調函數。
另外針對invalid paramter和purecall兩種在VC2005中不支持的特性,作了特殊處理。
crash_generation_server 的實現
crash_generation_server基本上就是一個IPC Server。負責監聽各個crash_generation_client的請求。
crash_generation_server的關鍵函數也就是一個簡單的狀態機函數:
void CrashGenerationServer::HandleConnectionRequest() {
// If we are shutting doen then get into ERROR state, reset the event so more
// workers don't run and return immediately.
if (shutting_down_) {
server_state_ = IPC_SERVER_STATE_ERROR;
ResetEvent(overlapped_.hEvent);
return ;
}
switch (server_state_) {
case IPC_SERVER_STATE_ERROR:
HandleErrorState();
break ;
case IPC_SERVER_STATE_INITIAL:
HandleInitialState();
break ;
case IPC_SERVER_STATE_CONNECTING:
HandleConnectingState();
break ;
case IPC_SERVER_STATE_CONNECTED:
HandleConnectedState();
break ;
case IPC_SERVER_STATE_READING:
HandleReadingState();
break ;
case IPC_SERVER_STATE_READ_DONE:
HandleReadDoneState();
break ;
case IPC_SERVER_STATE_WRITING:
HandleWritingState();
break ;
case IPC_SERVER_STATE_WRITE_DONE:
HandleWriteDoneState();
break ;
case IPC_SERVER_STATE_READING_ACK:
HandleReadingAckState();
break ;
case IPC_SERVER_STATE_DISCONNECTING:
HandleDisconnectingState();
break ;
default :
assert(false );
// This indicates that we added one more state without
// adding handling code.
server_state_ = IPC_SERVER_STATE_ERROR;
break ;
}
}
這個函數負責維護IPC的各類鏈接狀態。並進行不一樣處理,至關直觀,無須贅述!
crash_report_sender的實現
這個實現很是簡單,模擬了一個表單的提交,將minidump信息封裝成一個MIME類型,經過HTTP方式提交到服務器上。估計google的crash report server(https://clients2.google.com/cr/report )也就是一個簡單的網頁處理腳本,徹底能夠認爲是經過一個表單提交上來的信息。
Browser如何使用crash report服務
首先,crash_handle process是一個獨立運行的程序,負責監聽chrome進程的請求。
其次,在Browser初始化時,生成crash_generation_client實例,
在chrome的主函數入口中包含了
// Initialize the crash reporter.
InitCrashReporterWithDllPath(dll_full_path);
這一行代碼,在這個函數中生成了一個全局變量
g_breakpad = new google_breakpad::ExceptionHandler(temp_dir, NULL, callback,
NULL, google_breakpad::ExceptionHandler::HANDLER_ALL,
dump_type, pipe_name.c_str(), info->custom_info);
其中ExceptionHandler類包含了CrashGenerationClient實例。
因爲Crash Report服務應該是越早啓動越好,所以咱們也能夠看到chrome初始化該變量的位置也是至關的靠前。
小節
Google的crash_report服務幾個關鍵點:1.Minidump的定製化處理機制。2.進程外dump寫機制。3.chrome是如何捕獲Exception的。