Commit f01ae981 authored by shess@chromium.org's avatar shess@chromium.org

[Mac] Capture -dealloc backtrace to log with CrZombie messages.

Logs a subset of the stack backtrace from the point of -dealloc into a
breakpad key to help debug messages to CrZombie objects.  This
productionizes the basic approach taken to debug a few recent zombie
object crashes.

BUG=none
TEST=monitor crash server.

Review URL: http://codereview.chromium.org/7766013

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@98858 0039d316-1c4b-4281-b951-d872f2087c98
parent acac6799
...@@ -4,9 +4,22 @@ ...@@ -4,9 +4,22 @@
#include "base/debug/stack_trace.h" #include "base/debug/stack_trace.h"
#include "base/basictypes.h"
#include <string.h>
#include <algorithm>
namespace base { namespace base {
namespace debug { namespace debug {
StackTrace::StackTrace(const void* const* trace, size_t count) {
count = std::min(count, arraysize(trace_));
if (count)
memcpy(trace_, trace, count * sizeof(trace_[0]));
count_ = static_cast<int>(count);
}
StackTrace::~StackTrace() { StackTrace::~StackTrace() {
} }
......
...@@ -26,6 +26,11 @@ class BASE_EXPORT StackTrace { ...@@ -26,6 +26,11 @@ class BASE_EXPORT StackTrace {
// Creates a stacktrace from the current location. // Creates a stacktrace from the current location.
StackTrace(); StackTrace();
// Creates a stacktrace from an existing array of instruction
// pointers (such as returned by Addresses()). |count| will be
// trimmed to |kMaxTraces|.
StackTrace(const void* const* trace, size_t count);
#if defined(OS_WIN) #if defined(OS_WIN)
// Creates a stacktrace for an exception. // Creates a stacktrace for an exception.
// Note: this function will throw an import not found (StackWalk64) exception // Note: this function will throw an import not found (StackWalk64) exception
......
...@@ -5,11 +5,16 @@ ...@@ -5,11 +5,16 @@
#import "chrome/browser/ui/cocoa/objc_zombie.h" #import "chrome/browser/ui/cocoa/objc_zombie.h"
#include <dlfcn.h> #include <dlfcn.h>
#include <execinfo.h>
#include <mach-o/dyld.h> #include <mach-o/dyld.h>
#include <mach-o/nlist.h> #include <mach-o/nlist.h>
#import <objc/objc-class.h> #import <objc/objc-class.h>
#include <algorithm>
#include <iostream>
#include "base/debug/stack_trace.h"
#include "base/logging.h" #include "base/logging.h"
#include "base/mac/mac_util.h" #include "base/mac/mac_util.h"
#include "base/metrics/histogram.h" #include "base/metrics/histogram.h"
...@@ -35,6 +40,15 @@ ...@@ -35,6 +40,15 @@
namespace { namespace {
// The depth of backtrace to store with zombies. This directly influences
// the amount of memory required to track zombies, so should be kept as
// small as is useful. Unfortunately, too small and it won't poke through
// deep autorelease and event loop stacks.
// NOTE(shess): Breakpad currently restricts values to 255 bytes. The
// trace is hex-encoded with "0x" prefix and " " separators, meaning
// the maximum number of 32-bit items which can be encoded is 23.
const size_t kBacktraceDepth = 20;
// Function which destroys the contents of an object without freeing // Function which destroys the contents of an object without freeing
// the object. On 10.5 this is |object_cxxDestruct()|, which // the object. On 10.5 this is |object_cxxDestruct()|, which
// traverses the class hierarchy to run the C++ destructors. On 10.6 // traverses the class hierarchy to run the C++ destructors. On 10.6
...@@ -71,6 +85,8 @@ size_t g_zombieIndex = 0; ...@@ -71,6 +85,8 @@ size_t g_zombieIndex = 0;
typedef struct { typedef struct {
id object; // The zombied object. id object; // The zombied object.
Class wasa; // Value of |object->isa| before we replaced it. Class wasa; // Value of |object->isa| before we replaced it.
void* trace[kBacktraceDepth]; // Backtrace at point of deallocation.
size_t traceDepth; // Actual depth of trace[].
} ZombieRecord; } ZombieRecord;
ZombieRecord* g_zombies = NULL; ZombieRecord* g_zombies = NULL;
...@@ -192,6 +208,8 @@ void ZombieDealloc(id self, SEL _cmd) { ...@@ -192,6 +208,8 @@ void ZombieDealloc(id self, SEL _cmd) {
// The new record to swap into |g_zombies|. If |g_zombieCount| is // The new record to swap into |g_zombies|. If |g_zombieCount| is
// zero, then |self| will be freed immediately. // zero, then |self| will be freed immediately.
ZombieRecord zombieToFree = {self, wasa}; ZombieRecord zombieToFree = {self, wasa};
zombieToFree.traceDepth =
std::max(backtrace(zombieToFree.trace, kBacktraceDepth), 0);
// Don't involve the lock when creating zombies without a treadmill. // Don't involve the lock when creating zombies without a treadmill.
if (g_zombieCount > 0) { if (g_zombieCount > 0) {
...@@ -213,23 +231,29 @@ void ZombieDealloc(id self, SEL _cmd) { ...@@ -213,23 +231,29 @@ void ZombieDealloc(id self, SEL _cmd) {
object_dispose(zombieToFree.object); object_dispose(zombieToFree.object);
} }
// Attempt to determine the original class of zombie |object|. // Search the treadmill for |object| and fill in |*record| if found.
Class ZombieWasa(id object) { // Returns YES if found.
// Fat zombies can hold onto their |wasa| past the point where the BOOL GetZombieRecord(id object, ZombieRecord* record) {
// object was actually freed. Note that to arrive here at all, // Holding the lock is reasonable because this should be fast, and
// |object|'s memory must still be accessible. // the process is going to crash presently anyhow.
if (object_getClass(object) == g_fatZombieClass)
return static_cast<CrFatZombie*>(object)->wasa;
// For instances which weren't big enough to store |wasa|, check if
// the object is still on the treadmill.
base::AutoLock pin(lock_); base::AutoLock pin(lock_);
for (size_t i=0; i < g_zombieCount; ++i) { for (size_t i = 0; i < g_zombieCount; ++i) {
if (g_zombies[i].object == object) if (g_zombies[i].object == object) {
return g_zombies[i].wasa; *record = g_zombies[i];
return YES;
}
} }
return NO;
}
// Dump the symbols. This is pulled out into a function to make it
// easy to use DCHECK to dump only in debug builds.
BOOL DumpDeallocTrace(const void* const* array, int size) {
// |cerr| because that's where PrintBacktrace() sends output.
std::cerr << "Backtrace from -dealloc:\n";
base::debug::StackTrace(array, size).PrintBacktrace();
return Nil; return YES;
} }
// Log a message to a freed object. |wasa| is the object's original // Log a message to a freed object. |wasa| is the object's original
...@@ -238,8 +262,19 @@ Class ZombieWasa(id object) { ...@@ -238,8 +262,19 @@ Class ZombieWasa(id object) {
// dispatch-related method which is being invoked to send |aSelector| // dispatch-related method which is being invoked to send |aSelector|
// (for instance, -respondsToSelector:). // (for instance, -respondsToSelector:).
void ZombieObjectCrash(id object, SEL aSelector, SEL viaSelector) { void ZombieObjectCrash(id object, SEL aSelector, SEL viaSelector) {
Class wasa = ZombieWasa(object); ZombieRecord record;
BOOL found = GetZombieRecord(object, &record);
// The object's class can be in the zombie record, but if that is
// not available it can also be in the object itself (in most cases).
Class wasa = Nil;
if (found) {
wasa = record.wasa;
} else if (object_getClass(object) == g_fatZombieClass) {
wasa = static_cast<CrFatZombie*>(object)->wasa;
}
const char* wasaName = (wasa ? class_getName(wasa) : "<unknown>"); const char* wasaName = (wasa ? class_getName(wasa) : "<unknown>");
NSString* aString = NSString* aString =
[NSString stringWithFormat:@"Zombie <%s: %p> received -%s", [NSString stringWithFormat:@"Zombie <%s: %p> received -%s",
wasaName, object, sel_getName(aSelector)]; wasaName, object, sel_getName(aSelector)];
...@@ -248,12 +283,36 @@ void ZombieObjectCrash(id object, SEL aSelector, SEL viaSelector) { ...@@ -248,12 +283,36 @@ void ZombieObjectCrash(id object, SEL aSelector, SEL viaSelector) {
aString = [aString stringByAppendingFormat:@" (via -%s)", viaName]; aString = [aString stringByAppendingFormat:@" (via -%s)", viaName];
} }
// Set a value for breakpad to report, then crash. // Set a value for breakpad to report.
SetCrashKeyValue(@"zombie", aString); SetCrashKeyValue(@"zombie", aString);
LOG(ERROR) << [aString UTF8String];
// Hex-encode the backtrace and tuck it into a breakpad key.
NSString* deallocTrace = @"<unknown>";
if (found && record.traceDepth) {
NSMutableArray* hexBacktrace =
[NSMutableArray arrayWithCapacity:record.traceDepth];
for (size_t i = 0; i < record.traceDepth; ++i) {
NSString* s = [NSString stringWithFormat:@"%p", record.trace[i]];
[hexBacktrace addObject:s];
}
deallocTrace = [hexBacktrace componentsJoinedByString:@" "];
// Warn someone if this exceeds the breakpad limits.
DCHECK_LE(strlen([deallocTrace UTF8String]), 255U);
}
SetCrashKeyValue(@"zombie_dealloc_bt", deallocTrace);
// Log -dealloc backtrace in debug builds then crash with a useful
// stack trace.
if (found && record.traceDepth) {
DCHECK(DumpDeallocTrace(record.trace, record.traceDepth));
} else {
DLOG(INFO) << "Unable to generate backtrace from -dealloc.";
}
DLOG(FATAL) << [aString UTF8String];
// This is how about:crash is implemented. Using instead of // This is how about:crash is implemented. Using instead of
// |baes::debug::BreakDebugger()| or |LOG(FATAL)| to make the top of // |base::debug::BreakDebugger()| or |LOG(FATAL)| to make the top of
// stack more immediately obvious in crash dumps. // stack more immediately obvious in crash dumps.
int* zero = NULL; int* zero = NULL;
*zero = 0; *zero = 0;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment