|
1 // Copyright (c) 2006, Google Inc. |
|
2 // All rights reserved. |
|
3 // |
|
4 // Redistribution and use in source and binary forms, with or without |
|
5 // modification, are permitted provided that the following conditions are |
|
6 // met: |
|
7 // |
|
8 // * Redistributions of source code must retain the above copyright |
|
9 // notice, this list of conditions and the following disclaimer. |
|
10 // * Redistributions in binary form must reproduce the above |
|
11 // copyright notice, this list of conditions and the following disclaimer |
|
12 // in the documentation and/or other materials provided with the |
|
13 // distribution. |
|
14 // * Neither the name of Google Inc. nor the names of its |
|
15 // contributors may be used to endorse or promote products derived from |
|
16 // this software without specific prior written permission. |
|
17 // |
|
18 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
|
19 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
|
20 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
|
21 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
|
22 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
23 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|
24 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
|
25 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
|
26 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
|
27 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
|
28 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
29 |
|
30 #include <ObjBase.h> |
|
31 |
|
32 #include <algorithm> |
|
33 #include <cassert> |
|
34 #include <cstdio> |
|
35 |
|
36 #include "common/windows/string_utils-inl.h" |
|
37 |
|
38 #include "client/windows/common/ipc_protocol.h" |
|
39 #include "client/windows/handler/exception_handler.h" |
|
40 #include "common/windows/guid_string.h" |
|
41 |
|
42 namespace google_breakpad { |
|
43 |
|
44 static const int kWaitForHandlerThreadMs = 60000; |
|
45 static const int kExceptionHandlerThreadInitialStackSize = 64 * 1024; |
|
46 |
|
47 // As documented on MSDN, on failure SuspendThread returns (DWORD) -1 |
|
48 static const DWORD kFailedToSuspendThread = static_cast<DWORD>(-1); |
|
49 |
|
50 // This is passed as the context to the MinidumpWriteDump callback. |
|
51 typedef struct { |
|
52 AppMemoryList::const_iterator iter; |
|
53 AppMemoryList::const_iterator end; |
|
54 } MinidumpCallbackContext; |
|
55 |
|
56 vector<ExceptionHandler*>* ExceptionHandler::handler_stack_ = NULL; |
|
57 LONG ExceptionHandler::handler_stack_index_ = 0; |
|
58 CRITICAL_SECTION ExceptionHandler::handler_stack_critical_section_; |
|
59 volatile LONG ExceptionHandler::instance_count_ = 0; |
|
60 |
|
61 ExceptionHandler::ExceptionHandler(const wstring& dump_path, |
|
62 FilterCallback filter, |
|
63 MinidumpCallback callback, |
|
64 void* callback_context, |
|
65 int handler_types, |
|
66 MINIDUMP_TYPE dump_type, |
|
67 const wchar_t* pipe_name, |
|
68 const CustomClientInfo* custom_info) { |
|
69 Initialize(dump_path, |
|
70 filter, |
|
71 callback, |
|
72 callback_context, |
|
73 handler_types, |
|
74 dump_type, |
|
75 pipe_name, |
|
76 NULL, |
|
77 custom_info); |
|
78 } |
|
79 |
|
80 ExceptionHandler::ExceptionHandler(const wstring& dump_path, |
|
81 FilterCallback filter, |
|
82 MinidumpCallback callback, |
|
83 void* callback_context, |
|
84 int handler_types, |
|
85 MINIDUMP_TYPE dump_type, |
|
86 HANDLE pipe_handle, |
|
87 const CustomClientInfo* custom_info) { |
|
88 Initialize(dump_path, |
|
89 filter, |
|
90 callback, |
|
91 callback_context, |
|
92 handler_types, |
|
93 dump_type, |
|
94 NULL, |
|
95 pipe_handle, |
|
96 custom_info); |
|
97 } |
|
98 |
|
99 ExceptionHandler::ExceptionHandler(const wstring &dump_path, |
|
100 FilterCallback filter, |
|
101 MinidumpCallback callback, |
|
102 void* callback_context, |
|
103 int handler_types) { |
|
104 Initialize(dump_path, |
|
105 filter, |
|
106 callback, |
|
107 callback_context, |
|
108 handler_types, |
|
109 MiniDumpNormal, |
|
110 NULL, |
|
111 NULL, |
|
112 NULL); |
|
113 } |
|
114 |
|
115 void ExceptionHandler::Initialize(const wstring& dump_path, |
|
116 FilterCallback filter, |
|
117 MinidumpCallback callback, |
|
118 void* callback_context, |
|
119 int handler_types, |
|
120 MINIDUMP_TYPE dump_type, |
|
121 const wchar_t* pipe_name, |
|
122 HANDLE pipe_handle, |
|
123 const CustomClientInfo* custom_info) { |
|
124 LONG instance_count = InterlockedIncrement(&instance_count_); |
|
125 filter_ = filter; |
|
126 callback_ = callback; |
|
127 callback_context_ = callback_context; |
|
128 dump_path_c_ = NULL; |
|
129 next_minidump_id_c_ = NULL; |
|
130 next_minidump_path_c_ = NULL; |
|
131 dbghelp_module_ = NULL; |
|
132 minidump_write_dump_ = NULL; |
|
133 dump_type_ = dump_type; |
|
134 rpcrt4_module_ = NULL; |
|
135 uuid_create_ = NULL; |
|
136 handler_types_ = handler_types; |
|
137 previous_filter_ = NULL; |
|
138 #if _MSC_VER >= 1400 // MSVC 2005/8 |
|
139 previous_iph_ = NULL; |
|
140 #endif // _MSC_VER >= 1400 |
|
141 previous_pch_ = NULL; |
|
142 handler_thread_ = NULL; |
|
143 is_shutdown_ = false; |
|
144 handler_start_semaphore_ = NULL; |
|
145 handler_finish_semaphore_ = NULL; |
|
146 requesting_thread_id_ = 0; |
|
147 exception_info_ = NULL; |
|
148 assertion_ = NULL; |
|
149 handler_return_value_ = false; |
|
150 handle_debug_exceptions_ = false; |
|
151 |
|
152 // Attempt to use out-of-process if user has specified a pipe. |
|
153 if (pipe_name != NULL || pipe_handle != NULL) { |
|
154 assert(!(pipe_name && pipe_handle)); |
|
155 |
|
156 scoped_ptr<CrashGenerationClient> client; |
|
157 if (pipe_name) { |
|
158 client.reset( |
|
159 new CrashGenerationClient(pipe_name, |
|
160 dump_type_, |
|
161 custom_info)); |
|
162 } else { |
|
163 client.reset( |
|
164 new CrashGenerationClient(pipe_handle, |
|
165 dump_type_, |
|
166 custom_info)); |
|
167 } |
|
168 |
|
169 // If successful in registering with the monitoring process, |
|
170 // there is no need to setup in-process crash generation. |
|
171 if (client->Register()) { |
|
172 crash_generation_client_.reset(client.release()); |
|
173 } |
|
174 } |
|
175 |
|
176 if (!IsOutOfProcess()) { |
|
177 // Either client did not ask for out-of-process crash generation |
|
178 // or registration with the server process failed. In either case, |
|
179 // setup to do in-process crash generation. |
|
180 |
|
181 // Set synchronization primitives and the handler thread. Each |
|
182 // ExceptionHandler object gets its own handler thread because that's the |
|
183 // only way to reliably guarantee sufficient stack space in an exception, |
|
184 // and it allows an easy way to get a snapshot of the requesting thread's |
|
185 // context outside of an exception. |
|
186 InitializeCriticalSection(&handler_critical_section_); |
|
187 handler_start_semaphore_ = CreateSemaphore(NULL, 0, 1, NULL); |
|
188 assert(handler_start_semaphore_ != NULL); |
|
189 |
|
190 handler_finish_semaphore_ = CreateSemaphore(NULL, 0, 1, NULL); |
|
191 assert(handler_finish_semaphore_ != NULL); |
|
192 |
|
193 // Don't attempt to create the thread if we could not create the semaphores. |
|
194 if (handler_finish_semaphore_ != NULL && handler_start_semaphore_ != NULL) { |
|
195 DWORD thread_id; |
|
196 handler_thread_ = CreateThread(NULL, // lpThreadAttributes |
|
197 kExceptionHandlerThreadInitialStackSize, |
|
198 ExceptionHandlerThreadMain, |
|
199 this, // lpParameter |
|
200 0, // dwCreationFlags |
|
201 &thread_id); |
|
202 assert(handler_thread_ != NULL); |
|
203 } |
|
204 |
|
205 dbghelp_module_ = LoadLibrary(L"dbghelp.dll"); |
|
206 if (dbghelp_module_) { |
|
207 minidump_write_dump_ = reinterpret_cast<MiniDumpWriteDump_type>( |
|
208 GetProcAddress(dbghelp_module_, "MiniDumpWriteDump")); |
|
209 } |
|
210 |
|
211 // Load this library dynamically to not affect existing projects. Most |
|
212 // projects don't link against this directly, it's usually dynamically |
|
213 // loaded by dependent code. |
|
214 rpcrt4_module_ = LoadLibrary(L"rpcrt4.dll"); |
|
215 if (rpcrt4_module_) { |
|
216 uuid_create_ = reinterpret_cast<UuidCreate_type>( |
|
217 GetProcAddress(rpcrt4_module_, "UuidCreate")); |
|
218 } |
|
219 |
|
220 // set_dump_path calls UpdateNextID. This sets up all of the path and id |
|
221 // strings, and their equivalent c_str pointers. |
|
222 set_dump_path(dump_path); |
|
223 } |
|
224 |
|
225 // Reserve one element for the instruction memory |
|
226 AppMemory instruction_memory; |
|
227 instruction_memory.ptr = NULL; |
|
228 instruction_memory.length = 0; |
|
229 app_memory_info_.push_back(instruction_memory); |
|
230 |
|
231 // There is a race condition here. If the first instance has not yet |
|
232 // initialized the critical section, the second (and later) instances may |
|
233 // try to use uninitialized critical section object. The feature of multiple |
|
234 // instances in one module is not used much, so leave it as is for now. |
|
235 // One way to solve this in the current design (that is, keeping the static |
|
236 // handler stack) is to use spin locks with volatile bools to synchronize |
|
237 // the handler stack. This works only if the compiler guarantees to generate |
|
238 // cache coherent code for volatile. |
|
239 // TODO(munjal): Fix this in a better way by changing the design if possible. |
|
240 |
|
241 // Lazy initialization of the handler_stack_critical_section_ |
|
242 if (instance_count == 1) { |
|
243 InitializeCriticalSection(&handler_stack_critical_section_); |
|
244 } |
|
245 |
|
246 if (handler_types != HANDLER_NONE) { |
|
247 EnterCriticalSection(&handler_stack_critical_section_); |
|
248 |
|
249 // The first time an ExceptionHandler that installs a handler is |
|
250 // created, set up the handler stack. |
|
251 if (!handler_stack_) { |
|
252 handler_stack_ = new vector<ExceptionHandler*>(); |
|
253 } |
|
254 handler_stack_->push_back(this); |
|
255 |
|
256 if (handler_types & HANDLER_EXCEPTION) |
|
257 previous_filter_ = SetUnhandledExceptionFilter(HandleException); |
|
258 |
|
259 #if _MSC_VER >= 1400 // MSVC 2005/8 |
|
260 if (handler_types & HANDLER_INVALID_PARAMETER) |
|
261 previous_iph_ = _set_invalid_parameter_handler(HandleInvalidParameter); |
|
262 #endif // _MSC_VER >= 1400 |
|
263 |
|
264 if (handler_types & HANDLER_PURECALL) |
|
265 previous_pch_ = _set_purecall_handler(HandlePureVirtualCall); |
|
266 |
|
267 LeaveCriticalSection(&handler_stack_critical_section_); |
|
268 } |
|
269 } |
|
270 |
|
271 ExceptionHandler::~ExceptionHandler() { |
|
272 if (dbghelp_module_) { |
|
273 FreeLibrary(dbghelp_module_); |
|
274 } |
|
275 |
|
276 if (rpcrt4_module_) { |
|
277 FreeLibrary(rpcrt4_module_); |
|
278 } |
|
279 |
|
280 if (handler_types_ != HANDLER_NONE) { |
|
281 EnterCriticalSection(&handler_stack_critical_section_); |
|
282 |
|
283 if (handler_types_ & HANDLER_EXCEPTION) |
|
284 SetUnhandledExceptionFilter(previous_filter_); |
|
285 |
|
286 #if _MSC_VER >= 1400 // MSVC 2005/8 |
|
287 if (handler_types_ & HANDLER_INVALID_PARAMETER) |
|
288 _set_invalid_parameter_handler(previous_iph_); |
|
289 #endif // _MSC_VER >= 1400 |
|
290 |
|
291 if (handler_types_ & HANDLER_PURECALL) |
|
292 _set_purecall_handler(previous_pch_); |
|
293 |
|
294 if (handler_stack_->back() == this) { |
|
295 handler_stack_->pop_back(); |
|
296 } else { |
|
297 // TODO(mmentovai): use advapi32!ReportEvent to log the warning to the |
|
298 // system's application event log. |
|
299 fprintf(stderr, "warning: removing Breakpad handler out of order\n"); |
|
300 vector<ExceptionHandler*>::iterator iterator = handler_stack_->begin(); |
|
301 while (iterator != handler_stack_->end()) { |
|
302 if (*iterator == this) { |
|
303 iterator = handler_stack_->erase(iterator); |
|
304 } else { |
|
305 ++iterator; |
|
306 } |
|
307 } |
|
308 } |
|
309 |
|
310 if (handler_stack_->empty()) { |
|
311 // When destroying the last ExceptionHandler that installed a handler, |
|
312 // clean up the handler stack. |
|
313 delete handler_stack_; |
|
314 handler_stack_ = NULL; |
|
315 } |
|
316 |
|
317 LeaveCriticalSection(&handler_stack_critical_section_); |
|
318 } |
|
319 |
|
320 // Some of the objects were only initialized if out of process |
|
321 // registration was not done. |
|
322 if (!IsOutOfProcess()) { |
|
323 #ifdef BREAKPAD_NO_TERMINATE_THREAD |
|
324 // Clean up the handler thread and synchronization primitives. The handler |
|
325 // thread is either waiting on the semaphore to handle a crash or it is |
|
326 // handling a crash. Coming out of the wait is fast but wait more in the |
|
327 // eventuality a crash is handled. This compilation option results in a |
|
328 // deadlock if the exception handler is destroyed while executing code |
|
329 // inside DllMain. |
|
330 is_shutdown_ = true; |
|
331 ReleaseSemaphore(handler_start_semaphore_, 1, NULL); |
|
332 WaitForSingleObject(handler_thread_, kWaitForHandlerThreadMs); |
|
333 #else |
|
334 TerminateThread(handler_thread_, 1); |
|
335 #endif // BREAKPAD_NO_TERMINATE_THREAD |
|
336 |
|
337 CloseHandle(handler_thread_); |
|
338 handler_thread_ = NULL; |
|
339 DeleteCriticalSection(&handler_critical_section_); |
|
340 CloseHandle(handler_start_semaphore_); |
|
341 CloseHandle(handler_finish_semaphore_); |
|
342 } |
|
343 |
|
344 // There is a race condition in the code below: if this instance is |
|
345 // deleting the static critical section and a new instance of the class |
|
346 // is created, then there is a possibility that the critical section be |
|
347 // initialized while the same critical section is being deleted. Given the |
|
348 // usage pattern for the code, this race condition is unlikely to hit, but it |
|
349 // is a race condition nonetheless. |
|
350 if (InterlockedDecrement(&instance_count_) == 0) { |
|
351 DeleteCriticalSection(&handler_stack_critical_section_); |
|
352 } |
|
353 } |
|
354 |
|
355 bool ExceptionHandler::RequestUpload(DWORD crash_id) { |
|
356 return crash_generation_client_->RequestUpload(crash_id); |
|
357 } |
|
358 |
|
359 // static |
|
360 DWORD ExceptionHandler::ExceptionHandlerThreadMain(void* lpParameter) { |
|
361 ExceptionHandler* self = reinterpret_cast<ExceptionHandler *>(lpParameter); |
|
362 assert(self); |
|
363 assert(self->handler_start_semaphore_ != NULL); |
|
364 assert(self->handler_finish_semaphore_ != NULL); |
|
365 |
|
366 while (true) { |
|
367 if (WaitForSingleObject(self->handler_start_semaphore_, INFINITE) == |
|
368 WAIT_OBJECT_0) { |
|
369 // Perform the requested action. |
|
370 if (self->is_shutdown_) { |
|
371 // The instance of the exception handler is being destroyed. |
|
372 break; |
|
373 } else { |
|
374 self->handler_return_value_ = |
|
375 self->WriteMinidumpWithException(self->requesting_thread_id_, |
|
376 self->exception_info_, |
|
377 self->assertion_); |
|
378 } |
|
379 |
|
380 // Allow the requesting thread to proceed. |
|
381 ReleaseSemaphore(self->handler_finish_semaphore_, 1, NULL); |
|
382 } |
|
383 } |
|
384 |
|
385 // This statement is not reached when the thread is unconditionally |
|
386 // terminated by the ExceptionHandler destructor. |
|
387 return 0; |
|
388 } |
|
389 |
|
390 // HandleException and HandleInvalidParameter must create an |
|
391 // AutoExceptionHandler object to maintain static state and to determine which |
|
392 // ExceptionHandler instance to use. The constructor locates the correct |
|
393 // instance, and makes it available through get_handler(). The destructor |
|
394 // restores the state in effect prior to allocating the AutoExceptionHandler. |
|
395 class AutoExceptionHandler { |
|
396 public: |
|
397 AutoExceptionHandler() { |
|
398 // Increment handler_stack_index_ so that if another Breakpad handler is |
|
399 // registered using this same HandleException function, and it needs to be |
|
400 // called while this handler is running (either because this handler |
|
401 // declines to handle the exception, or an exception occurs during |
|
402 // handling), HandleException will find the appropriate ExceptionHandler |
|
403 // object in handler_stack_ to deliver the exception to. |
|
404 // |
|
405 // Because handler_stack_ is addressed in reverse (as |size - index|), |
|
406 // preincrementing handler_stack_index_ avoids needing to subtract 1 from |
|
407 // the argument to |at|. |
|
408 // |
|
409 // The index is maintained instead of popping elements off of the handler |
|
410 // stack and pushing them at the end of this method. This avoids ruining |
|
411 // the order of elements in the stack in the event that some other thread |
|
412 // decides to manipulate the handler stack (such as creating a new |
|
413 // ExceptionHandler object) while an exception is being handled. |
|
414 EnterCriticalSection(&ExceptionHandler::handler_stack_critical_section_); |
|
415 handler_ = ExceptionHandler::handler_stack_->at( |
|
416 ExceptionHandler::handler_stack_->size() - |
|
417 ++ExceptionHandler::handler_stack_index_); |
|
418 |
|
419 // In case another exception occurs while this handler is doing its thing, |
|
420 // it should be delivered to the previous filter. |
|
421 SetUnhandledExceptionFilter(handler_->previous_filter_); |
|
422 #if _MSC_VER >= 1400 // MSVC 2005/8 |
|
423 _set_invalid_parameter_handler(handler_->previous_iph_); |
|
424 #endif // _MSC_VER >= 1400 |
|
425 _set_purecall_handler(handler_->previous_pch_); |
|
426 } |
|
427 |
|
428 ~AutoExceptionHandler() { |
|
429 // Put things back the way they were before entering this handler. |
|
430 SetUnhandledExceptionFilter(ExceptionHandler::HandleException); |
|
431 #if _MSC_VER >= 1400 // MSVC 2005/8 |
|
432 _set_invalid_parameter_handler(ExceptionHandler::HandleInvalidParameter); |
|
433 #endif // _MSC_VER >= 1400 |
|
434 _set_purecall_handler(ExceptionHandler::HandlePureVirtualCall); |
|
435 |
|
436 --ExceptionHandler::handler_stack_index_; |
|
437 LeaveCriticalSection(&ExceptionHandler::handler_stack_critical_section_); |
|
438 } |
|
439 |
|
440 ExceptionHandler* get_handler() const { return handler_; } |
|
441 |
|
442 private: |
|
443 ExceptionHandler* handler_; |
|
444 }; |
|
445 |
|
446 // static |
|
447 LONG ExceptionHandler::HandleException(EXCEPTION_POINTERS* exinfo) { |
|
448 AutoExceptionHandler auto_exception_handler; |
|
449 ExceptionHandler* current_handler = auto_exception_handler.get_handler(); |
|
450 |
|
451 // Ignore EXCEPTION_BREAKPOINT and EXCEPTION_SINGLE_STEP exceptions. This |
|
452 // logic will short-circuit before calling WriteMinidumpOnHandlerThread, |
|
453 // allowing something else to handle the breakpoint without incurring the |
|
454 // overhead transitioning to and from the handler thread. This behavior |
|
455 // can be overridden by calling ExceptionHandler::set_handle_debug_exceptions. |
|
456 DWORD code = exinfo->ExceptionRecord->ExceptionCode; |
|
457 LONG action; |
|
458 bool is_debug_exception = (code == EXCEPTION_BREAKPOINT) || |
|
459 (code == EXCEPTION_SINGLE_STEP); |
|
460 |
|
461 bool success = false; |
|
462 |
|
463 if (!is_debug_exception || |
|
464 current_handler->get_handle_debug_exceptions()) { |
|
465 // If out-of-proc crash handler client is available, we have to use that |
|
466 // to generate dump and we cannot fall back on in-proc dump generation |
|
467 // because we never prepared for an in-proc dump generation |
|
468 |
|
469 // In case of out-of-process dump generation, directly call |
|
470 // WriteMinidumpWithException since there is no separate thread running. |
|
471 if (current_handler->IsOutOfProcess()) { |
|
472 success = current_handler->WriteMinidumpWithException( |
|
473 GetCurrentThreadId(), |
|
474 exinfo, |
|
475 NULL); |
|
476 } else { |
|
477 success = current_handler->WriteMinidumpOnHandlerThread(exinfo, NULL); |
|
478 } |
|
479 } |
|
480 |
|
481 // The handler fully handled the exception. Returning |
|
482 // EXCEPTION_EXECUTE_HANDLER indicates this to the system, and usually |
|
483 // results in the application being terminated. |
|
484 // |
|
485 // Note: If the application was launched from within the Cygwin |
|
486 // environment, returning EXCEPTION_EXECUTE_HANDLER seems to cause the |
|
487 // application to be restarted. |
|
488 if (success) { |
|
489 action = EXCEPTION_EXECUTE_HANDLER; |
|
490 } else { |
|
491 // There was an exception, it was a breakpoint or something else ignored |
|
492 // above, or it was passed to the handler, which decided not to handle it. |
|
493 // This could be because the filter callback didn't want it, because |
|
494 // minidump writing failed for some reason, or because the post-minidump |
|
495 // callback function indicated failure. Give the previous handler a |
|
496 // chance to do something with the exception. If there is no previous |
|
497 // handler, return EXCEPTION_CONTINUE_SEARCH, which will allow a debugger |
|
498 // or native "crashed" dialog to handle the exception. |
|
499 if (current_handler->previous_filter_) { |
|
500 action = current_handler->previous_filter_(exinfo); |
|
501 } else { |
|
502 action = EXCEPTION_CONTINUE_SEARCH; |
|
503 } |
|
504 } |
|
505 |
|
506 return action; |
|
507 } |
|
508 |
|
509 #if _MSC_VER >= 1400 // MSVC 2005/8 |
|
510 // static |
|
511 void ExceptionHandler::HandleInvalidParameter(const wchar_t* expression, |
|
512 const wchar_t* function, |
|
513 const wchar_t* file, |
|
514 unsigned int line, |
|
515 uintptr_t reserved) { |
|
516 // This is an invalid parameter, not an exception. It's safe to play with |
|
517 // sprintf here. |
|
518 AutoExceptionHandler auto_exception_handler; |
|
519 ExceptionHandler* current_handler = auto_exception_handler.get_handler(); |
|
520 |
|
521 MDRawAssertionInfo assertion; |
|
522 memset(&assertion, 0, sizeof(assertion)); |
|
523 _snwprintf_s(reinterpret_cast<wchar_t*>(assertion.expression), |
|
524 sizeof(assertion.expression) / sizeof(assertion.expression[0]), |
|
525 _TRUNCATE, L"%s", expression); |
|
526 _snwprintf_s(reinterpret_cast<wchar_t*>(assertion.function), |
|
527 sizeof(assertion.function) / sizeof(assertion.function[0]), |
|
528 _TRUNCATE, L"%s", function); |
|
529 _snwprintf_s(reinterpret_cast<wchar_t*>(assertion.file), |
|
530 sizeof(assertion.file) / sizeof(assertion.file[0]), |
|
531 _TRUNCATE, L"%s", file); |
|
532 assertion.line = line; |
|
533 assertion.type = MD_ASSERTION_INFO_TYPE_INVALID_PARAMETER; |
|
534 |
|
535 // Make up an exception record for the current thread and CPU context |
|
536 // to make it possible for the crash processor to classify these |
|
537 // as do regular crashes, and to make it humane for developers to |
|
538 // analyze them. |
|
539 EXCEPTION_RECORD exception_record = {}; |
|
540 CONTEXT exception_context = {}; |
|
541 EXCEPTION_POINTERS exception_ptrs = { &exception_record, &exception_context }; |
|
542 |
|
543 ::RtlCaptureContext(&exception_context); |
|
544 |
|
545 exception_record.ExceptionCode = STATUS_INVALID_PARAMETER; |
|
546 |
|
547 // We store pointers to the the expression and function strings, |
|
548 // and the line as exception parameters to make them easy to |
|
549 // access by the developer on the far side. |
|
550 exception_record.NumberParameters = 3; |
|
551 exception_record.ExceptionInformation[0] = |
|
552 reinterpret_cast<ULONG_PTR>(&assertion.expression); |
|
553 exception_record.ExceptionInformation[1] = |
|
554 reinterpret_cast<ULONG_PTR>(&assertion.file); |
|
555 exception_record.ExceptionInformation[2] = assertion.line; |
|
556 |
|
557 bool success = false; |
|
558 // In case of out-of-process dump generation, directly call |
|
559 // WriteMinidumpWithException since there is no separate thread running. |
|
560 if (current_handler->IsOutOfProcess()) { |
|
561 success = current_handler->WriteMinidumpWithException( |
|
562 GetCurrentThreadId(), |
|
563 &exception_ptrs, |
|
564 &assertion); |
|
565 } else { |
|
566 success = current_handler->WriteMinidumpOnHandlerThread(&exception_ptrs, |
|
567 &assertion); |
|
568 } |
|
569 |
|
570 if (!success) { |
|
571 if (current_handler->previous_iph_) { |
|
572 // The handler didn't fully handle the exception. Give it to the |
|
573 // previous invalid parameter handler. |
|
574 current_handler->previous_iph_(expression, |
|
575 function, |
|
576 file, |
|
577 line, |
|
578 reserved); |
|
579 } else { |
|
580 // If there's no previous handler, pass the exception back in to the |
|
581 // invalid parameter handler's core. That's the routine that called this |
|
582 // function, but now, since this function is no longer registered (and in |
|
583 // fact, no function at all is registered), this will result in the |
|
584 // default code path being taken: _CRT_DEBUGGER_HOOK and _invoke_watson. |
|
585 // Use _invalid_parameter where it exists (in _DEBUG builds) as it passes |
|
586 // more information through. In non-debug builds, it is not available, |
|
587 // so fall back to using _invalid_parameter_noinfo. See invarg.c in the |
|
588 // CRT source. |
|
589 #ifdef _DEBUG |
|
590 _invalid_parameter(expression, function, file, line, reserved); |
|
591 #else // _DEBUG |
|
592 _invalid_parameter_noinfo(); |
|
593 #endif // _DEBUG |
|
594 } |
|
595 } |
|
596 |
|
597 // The handler either took care of the invalid parameter problem itself, |
|
598 // or passed it on to another handler. "Swallow" it by exiting, paralleling |
|
599 // the behavior of "swallowing" exceptions. |
|
600 exit(0); |
|
601 } |
|
602 #endif // _MSC_VER >= 1400 |
|
603 |
|
604 // static |
|
605 void ExceptionHandler::HandlePureVirtualCall() { |
|
606 // This is an pure virtual function call, not an exception. It's safe to |
|
607 // play with sprintf here. |
|
608 AutoExceptionHandler auto_exception_handler; |
|
609 ExceptionHandler* current_handler = auto_exception_handler.get_handler(); |
|
610 |
|
611 MDRawAssertionInfo assertion; |
|
612 memset(&assertion, 0, sizeof(assertion)); |
|
613 assertion.type = MD_ASSERTION_INFO_TYPE_PURE_VIRTUAL_CALL; |
|
614 |
|
615 // Make up an exception record for the current thread and CPU context |
|
616 // to make it possible for the crash processor to classify these |
|
617 // as do regular crashes, and to make it humane for developers to |
|
618 // analyze them. |
|
619 EXCEPTION_RECORD exception_record = {}; |
|
620 CONTEXT exception_context = {}; |
|
621 EXCEPTION_POINTERS exception_ptrs = { &exception_record, &exception_context }; |
|
622 |
|
623 ::RtlCaptureContext(&exception_context); |
|
624 |
|
625 exception_record.ExceptionCode = STATUS_NONCONTINUABLE_EXCEPTION; |
|
626 |
|
627 // We store pointers to the the expression and function strings, |
|
628 // and the line as exception parameters to make them easy to |
|
629 // access by the developer on the far side. |
|
630 exception_record.NumberParameters = 3; |
|
631 exception_record.ExceptionInformation[0] = |
|
632 reinterpret_cast<ULONG_PTR>(&assertion.expression); |
|
633 exception_record.ExceptionInformation[1] = |
|
634 reinterpret_cast<ULONG_PTR>(&assertion.file); |
|
635 exception_record.ExceptionInformation[2] = assertion.line; |
|
636 |
|
637 bool success = false; |
|
638 // In case of out-of-process dump generation, directly call |
|
639 // WriteMinidumpWithException since there is no separate thread running. |
|
640 |
|
641 if (current_handler->IsOutOfProcess()) { |
|
642 success = current_handler->WriteMinidumpWithException( |
|
643 GetCurrentThreadId(), |
|
644 &exception_ptrs, |
|
645 &assertion); |
|
646 } else { |
|
647 success = current_handler->WriteMinidumpOnHandlerThread(&exception_ptrs, |
|
648 &assertion); |
|
649 } |
|
650 |
|
651 if (!success) { |
|
652 if (current_handler->previous_pch_) { |
|
653 // The handler didn't fully handle the exception. Give it to the |
|
654 // previous purecall handler. |
|
655 current_handler->previous_pch_(); |
|
656 } else { |
|
657 // If there's no previous handler, return and let _purecall handle it. |
|
658 // This will just put up an assertion dialog. |
|
659 return; |
|
660 } |
|
661 } |
|
662 |
|
663 // The handler either took care of the invalid parameter problem itself, |
|
664 // or passed it on to another handler. "Swallow" it by exiting, paralleling |
|
665 // the behavior of "swallowing" exceptions. |
|
666 exit(0); |
|
667 } |
|
668 |
|
669 bool ExceptionHandler::WriteMinidumpOnHandlerThread( |
|
670 EXCEPTION_POINTERS* exinfo, MDRawAssertionInfo* assertion) { |
|
671 EnterCriticalSection(&handler_critical_section_); |
|
672 |
|
673 // There isn't much we can do if the handler thread |
|
674 // was not successfully created. |
|
675 if (handler_thread_ == NULL) { |
|
676 LeaveCriticalSection(&handler_critical_section_); |
|
677 return false; |
|
678 } |
|
679 |
|
680 // The handler thread should only be created when the semaphores are valid. |
|
681 assert(handler_start_semaphore_ != NULL); |
|
682 assert(handler_finish_semaphore_ != NULL); |
|
683 |
|
684 // Set up data to be passed in to the handler thread. |
|
685 requesting_thread_id_ = GetCurrentThreadId(); |
|
686 exception_info_ = exinfo; |
|
687 assertion_ = assertion; |
|
688 |
|
689 // This causes the handler thread to call WriteMinidumpWithException. |
|
690 ReleaseSemaphore(handler_start_semaphore_, 1, NULL); |
|
691 |
|
692 // Wait until WriteMinidumpWithException is done and collect its return value. |
|
693 WaitForSingleObject(handler_finish_semaphore_, INFINITE); |
|
694 bool status = handler_return_value_; |
|
695 |
|
696 // Clean up. |
|
697 requesting_thread_id_ = 0; |
|
698 exception_info_ = NULL; |
|
699 assertion_ = NULL; |
|
700 |
|
701 LeaveCriticalSection(&handler_critical_section_); |
|
702 |
|
703 return status; |
|
704 } |
|
705 |
|
706 bool ExceptionHandler::WriteMinidump() { |
|
707 // Make up an exception record for the current thread and CPU context |
|
708 // to make it possible for the crash processor to classify these |
|
709 // as do regular crashes, and to make it humane for developers to |
|
710 // analyze them. |
|
711 EXCEPTION_RECORD exception_record = {}; |
|
712 CONTEXT exception_context = {}; |
|
713 EXCEPTION_POINTERS exception_ptrs = { &exception_record, &exception_context }; |
|
714 |
|
715 ::RtlCaptureContext(&exception_context); |
|
716 exception_record.ExceptionCode = STATUS_NONCONTINUABLE_EXCEPTION; |
|
717 |
|
718 return WriteMinidumpForException(&exception_ptrs); |
|
719 } |
|
720 |
|
721 bool ExceptionHandler::WriteMinidumpForException(EXCEPTION_POINTERS* exinfo) { |
|
722 // In case of out-of-process dump generation, directly call |
|
723 // WriteMinidumpWithException since there is no separate thread running. |
|
724 if (IsOutOfProcess()) { |
|
725 return WriteMinidumpWithException(GetCurrentThreadId(), |
|
726 exinfo, |
|
727 NULL); |
|
728 } |
|
729 |
|
730 bool success = WriteMinidumpOnHandlerThread(exinfo, NULL); |
|
731 UpdateNextID(); |
|
732 return success; |
|
733 } |
|
734 |
|
735 // static |
|
736 bool ExceptionHandler::WriteMinidump(const wstring &dump_path, |
|
737 MinidumpCallback callback, |
|
738 void* callback_context) { |
|
739 ExceptionHandler handler(dump_path, NULL, callback, callback_context, |
|
740 HANDLER_NONE); |
|
741 return handler.WriteMinidump(); |
|
742 } |
|
743 |
|
744 // static |
|
745 bool ExceptionHandler::WriteMinidumpForChild(HANDLE child, |
|
746 DWORD child_blamed_thread, |
|
747 const wstring& dump_path, |
|
748 MinidumpCallback callback, |
|
749 void* callback_context) { |
|
750 EXCEPTION_RECORD ex; |
|
751 CONTEXT ctx; |
|
752 EXCEPTION_POINTERS exinfo = { NULL, NULL }; |
|
753 DWORD last_suspend_count = kFailedToSuspendThread; |
|
754 HANDLE child_thread_handle = OpenThread(THREAD_GET_CONTEXT | |
|
755 THREAD_QUERY_INFORMATION | |
|
756 THREAD_SUSPEND_RESUME, |
|
757 FALSE, |
|
758 child_blamed_thread); |
|
759 // This thread may have died already, so not opening the handle is a |
|
760 // non-fatal error. |
|
761 if (child_thread_handle != NULL) { |
|
762 last_suspend_count = SuspendThread(child_thread_handle); |
|
763 if (last_suspend_count != kFailedToSuspendThread) { |
|
764 ctx.ContextFlags = CONTEXT_ALL; |
|
765 if (GetThreadContext(child_thread_handle, &ctx)) { |
|
766 memset(&ex, 0, sizeof(ex)); |
|
767 ex.ExceptionCode = EXCEPTION_BREAKPOINT; |
|
768 #if defined(_M_IX86) |
|
769 ex.ExceptionAddress = reinterpret_cast<PVOID>(ctx.Eip); |
|
770 #elif defined(_M_X64) |
|
771 ex.ExceptionAddress = reinterpret_cast<PVOID>(ctx.Rip); |
|
772 #endif |
|
773 exinfo.ExceptionRecord = &ex; |
|
774 exinfo.ContextRecord = &ctx; |
|
775 } |
|
776 } |
|
777 } |
|
778 |
|
779 ExceptionHandler handler(dump_path, NULL, callback, callback_context, |
|
780 HANDLER_NONE); |
|
781 bool success = handler.WriteMinidumpWithExceptionForProcess( |
|
782 child_blamed_thread, |
|
783 exinfo.ExceptionRecord ? &exinfo : NULL, |
|
784 NULL, child, false); |
|
785 |
|
786 if (last_suspend_count != kFailedToSuspendThread) { |
|
787 ResumeThread(child_thread_handle); |
|
788 } |
|
789 |
|
790 CloseHandle(child_thread_handle); |
|
791 |
|
792 if (callback) { |
|
793 success = callback(handler.dump_path_c_, handler.next_minidump_id_c_, |
|
794 callback_context, NULL, NULL, success); |
|
795 } |
|
796 |
|
797 return success; |
|
798 } |
|
799 |
|
800 bool ExceptionHandler::WriteMinidumpWithException( |
|
801 DWORD requesting_thread_id, |
|
802 EXCEPTION_POINTERS* exinfo, |
|
803 MDRawAssertionInfo* assertion) { |
|
804 // Give user code a chance to approve or prevent writing a minidump. If the |
|
805 // filter returns false, don't handle the exception at all. If this method |
|
806 // was called as a result of an exception, returning false will cause |
|
807 // HandleException to call any previous handler or return |
|
808 // EXCEPTION_CONTINUE_SEARCH on the exception thread, allowing it to appear |
|
809 // as though this handler were not present at all. |
|
810 if (filter_ && !filter_(callback_context_, exinfo, assertion)) { |
|
811 return false; |
|
812 } |
|
813 |
|
814 bool success = false; |
|
815 if (IsOutOfProcess()) { |
|
816 success = crash_generation_client_->RequestDump(exinfo, assertion); |
|
817 } else { |
|
818 success = WriteMinidumpWithExceptionForProcess(requesting_thread_id, |
|
819 exinfo, |
|
820 assertion, |
|
821 GetCurrentProcess(), |
|
822 true); |
|
823 } |
|
824 |
|
825 if (callback_) { |
|
826 // TODO(munjal): In case of out-of-process dump generation, both |
|
827 // dump_path_c_ and next_minidump_id_ will be NULL. For out-of-process |
|
828 // scenario, the server process ends up creating the dump path and dump |
|
829 // id so they are not known to the client. |
|
830 success = callback_(dump_path_c_, next_minidump_id_c_, callback_context_, |
|
831 exinfo, assertion, success); |
|
832 } |
|
833 |
|
834 return success; |
|
835 } |
|
836 |
|
837 // static |
|
838 BOOL CALLBACK ExceptionHandler::MinidumpWriteDumpCallback( |
|
839 PVOID context, |
|
840 const PMINIDUMP_CALLBACK_INPUT callback_input, |
|
841 PMINIDUMP_CALLBACK_OUTPUT callback_output) { |
|
842 switch (callback_input->CallbackType) { |
|
843 case MemoryCallback: { |
|
844 MinidumpCallbackContext* callback_context = |
|
845 reinterpret_cast<MinidumpCallbackContext*>(context); |
|
846 if (callback_context->iter == callback_context->end) |
|
847 return FALSE; |
|
848 |
|
849 // Include the specified memory region. |
|
850 callback_output->MemoryBase = callback_context->iter->ptr; |
|
851 callback_output->MemorySize = callback_context->iter->length; |
|
852 callback_context->iter++; |
|
853 return TRUE; |
|
854 } |
|
855 |
|
856 // Include all modules. |
|
857 case IncludeModuleCallback: |
|
858 case ModuleCallback: |
|
859 return TRUE; |
|
860 |
|
861 // Include all threads. |
|
862 case IncludeThreadCallback: |
|
863 case ThreadCallback: |
|
864 return TRUE; |
|
865 |
|
866 // Stop receiving cancel callbacks. |
|
867 case CancelCallback: |
|
868 callback_output->CheckCancel = FALSE; |
|
869 callback_output->Cancel = FALSE; |
|
870 return TRUE; |
|
871 } |
|
872 // Ignore other callback types. |
|
873 return FALSE; |
|
874 } |
|
875 |
|
876 bool ExceptionHandler::WriteMinidumpWithExceptionForProcess( |
|
877 DWORD requesting_thread_id, |
|
878 EXCEPTION_POINTERS* exinfo, |
|
879 MDRawAssertionInfo* assertion, |
|
880 HANDLE process, |
|
881 bool write_requester_stream) { |
|
882 bool success = false; |
|
883 if (minidump_write_dump_) { |
|
884 HANDLE dump_file = CreateFile(next_minidump_path_c_, |
|
885 GENERIC_WRITE, |
|
886 0, // no sharing |
|
887 NULL, |
|
888 CREATE_NEW, // fail if exists |
|
889 FILE_ATTRIBUTE_NORMAL, |
|
890 NULL); |
|
891 if (dump_file != INVALID_HANDLE_VALUE) { |
|
892 MINIDUMP_EXCEPTION_INFORMATION except_info; |
|
893 except_info.ThreadId = requesting_thread_id; |
|
894 except_info.ExceptionPointers = exinfo; |
|
895 except_info.ClientPointers = FALSE; |
|
896 |
|
897 // Leave room in user_stream_array for possible breakpad and |
|
898 // assertion info streams. |
|
899 MINIDUMP_USER_STREAM user_stream_array[2]; |
|
900 MINIDUMP_USER_STREAM_INFORMATION user_streams; |
|
901 user_streams.UserStreamCount = 0; |
|
902 user_streams.UserStreamArray = user_stream_array; |
|
903 |
|
904 if (write_requester_stream) { |
|
905 // Add an MDRawBreakpadInfo stream to the minidump, to provide |
|
906 // additional information about the exception handler to the Breakpad |
|
907 // processor. The information will help the processor determine which |
|
908 // threads are relevant. The Breakpad processor does not require this |
|
909 // information but can function better with Breakpad-generated dumps |
|
910 // when it is present. The native debugger is not harmed by the |
|
911 // presence of this information. |
|
912 MDRawBreakpadInfo breakpad_info; |
|
913 breakpad_info.validity = MD_BREAKPAD_INFO_VALID_DUMP_THREAD_ID | |
|
914 MD_BREAKPAD_INFO_VALID_REQUESTING_THREAD_ID; |
|
915 breakpad_info.dump_thread_id = GetCurrentThreadId(); |
|
916 breakpad_info.requesting_thread_id = requesting_thread_id; |
|
917 |
|
918 int index = user_streams.UserStreamCount; |
|
919 user_stream_array[index].Type = MD_BREAKPAD_INFO_STREAM; |
|
920 user_stream_array[index].BufferSize = sizeof(breakpad_info); |
|
921 user_stream_array[index].Buffer = &breakpad_info; |
|
922 ++user_streams.UserStreamCount; |
|
923 } |
|
924 |
|
925 if (assertion) { |
|
926 int index = user_streams.UserStreamCount; |
|
927 user_stream_array[index].Type = MD_ASSERTION_INFO_STREAM; |
|
928 user_stream_array[index].BufferSize = sizeof(MDRawAssertionInfo); |
|
929 user_stream_array[index].Buffer = assertion; |
|
930 ++user_streams.UserStreamCount; |
|
931 } |
|
932 |
|
933 // Older versions of DbgHelp.dll don't correctly put the memory around |
|
934 // the faulting instruction pointer into the minidump. This |
|
935 // callback will ensure that it gets included. |
|
936 if (exinfo) { |
|
937 // Find a memory region of 256 bytes centered on the |
|
938 // faulting instruction pointer. |
|
939 const ULONG64 instruction_pointer = |
|
940 #if defined(_M_IX86) |
|
941 exinfo->ContextRecord->Eip; |
|
942 #elif defined(_M_AMD64) |
|
943 exinfo->ContextRecord->Rip; |
|
944 #else |
|
945 #error Unsupported platform |
|
946 #endif |
|
947 |
|
948 MEMORY_BASIC_INFORMATION info; |
|
949 if (VirtualQueryEx(process, |
|
950 reinterpret_cast<LPCVOID>(instruction_pointer), |
|
951 &info, |
|
952 sizeof(MEMORY_BASIC_INFORMATION)) != 0 && |
|
953 info.State == MEM_COMMIT) { |
|
954 // Attempt to get 128 bytes before and after the instruction |
|
955 // pointer, but settle for whatever's available up to the |
|
956 // boundaries of the memory region. |
|
957 const ULONG64 kIPMemorySize = 256; |
|
958 ULONG64 base = |
|
959 (std::max)(reinterpret_cast<ULONG64>(info.BaseAddress), |
|
960 instruction_pointer - (kIPMemorySize / 2)); |
|
961 ULONG64 end_of_range = |
|
962 (std::min)(instruction_pointer + (kIPMemorySize / 2), |
|
963 reinterpret_cast<ULONG64>(info.BaseAddress) |
|
964 + info.RegionSize); |
|
965 ULONG size = static_cast<ULONG>(end_of_range - base); |
|
966 |
|
967 AppMemory& elt = app_memory_info_.front(); |
|
968 elt.ptr = base; |
|
969 elt.length = size; |
|
970 } |
|
971 } |
|
972 |
|
973 MinidumpCallbackContext context; |
|
974 context.iter = app_memory_info_.begin(); |
|
975 context.end = app_memory_info_.end(); |
|
976 |
|
977 // Skip the reserved element if there was no instruction memory |
|
978 if (context.iter->ptr == 0) { |
|
979 context.iter++; |
|
980 } |
|
981 |
|
982 MINIDUMP_CALLBACK_INFORMATION callback; |
|
983 callback.CallbackRoutine = MinidumpWriteDumpCallback; |
|
984 callback.CallbackParam = reinterpret_cast<void*>(&context); |
|
985 |
|
986 // The explicit comparison to TRUE avoids a warning (C4800). |
|
987 success = (minidump_write_dump_(process, |
|
988 GetProcessId(process), |
|
989 dump_file, |
|
990 dump_type_, |
|
991 exinfo ? &except_info : NULL, |
|
992 &user_streams, |
|
993 &callback) == TRUE); |
|
994 |
|
995 CloseHandle(dump_file); |
|
996 } |
|
997 } |
|
998 |
|
999 return success; |
|
1000 } |
|
1001 |
|
1002 void ExceptionHandler::UpdateNextID() { |
|
1003 assert(uuid_create_); |
|
1004 UUID id = {0}; |
|
1005 if (uuid_create_) { |
|
1006 uuid_create_(&id); |
|
1007 } |
|
1008 next_minidump_id_ = GUIDString::GUIDToWString(&id); |
|
1009 next_minidump_id_c_ = next_minidump_id_.c_str(); |
|
1010 |
|
1011 wchar_t minidump_path[MAX_PATH]; |
|
1012 swprintf(minidump_path, MAX_PATH, L"%s\\%s.dmp", |
|
1013 dump_path_c_, next_minidump_id_c_); |
|
1014 |
|
1015 // remove when VC++7.1 is no longer supported |
|
1016 minidump_path[MAX_PATH - 1] = L'\0'; |
|
1017 |
|
1018 next_minidump_path_ = minidump_path; |
|
1019 next_minidump_path_c_ = next_minidump_path_.c_str(); |
|
1020 } |
|
1021 |
|
1022 void ExceptionHandler::RegisterAppMemory(void* ptr, size_t length) { |
|
1023 AppMemoryList::iterator iter = |
|
1024 std::find(app_memory_info_.begin(), app_memory_info_.end(), ptr); |
|
1025 if (iter != app_memory_info_.end()) { |
|
1026 // Don't allow registering the same pointer twice. |
|
1027 return; |
|
1028 } |
|
1029 |
|
1030 AppMemory app_memory; |
|
1031 app_memory.ptr = reinterpret_cast<ULONG64>(ptr); |
|
1032 app_memory.length = static_cast<ULONG>(length); |
|
1033 app_memory_info_.push_back(app_memory); |
|
1034 } |
|
1035 |
|
1036 void ExceptionHandler::UnregisterAppMemory(void* ptr) { |
|
1037 AppMemoryList::iterator iter = |
|
1038 std::find(app_memory_info_.begin(), app_memory_info_.end(), ptr); |
|
1039 if (iter != app_memory_info_.end()) { |
|
1040 app_memory_info_.erase(iter); |
|
1041 } |
|
1042 } |
|
1043 |
|
1044 } // namespace google_breakpad |