Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(140)

Side by Side Diff: chrome/browser/cocoa/objc_zombie.mm

Issue 660411: [Mac] Implement NSObject zombies. (Closed)
Patch Set: Nits, and only enable in DEBUG for now. Created 10 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « chrome/browser/cocoa/objc_zombie.h ('k') | chrome/chrome_browser.gypi » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright (c) 2010 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #import "chrome/browser/cocoa/objc_zombie.h"
6
7 #include <dlfcn.h>
8 #include <mach-o/dyld.h>
9 #include <mach-o/nlist.h>
10
11 #import <objc/objc-class.h>
12
13 #include "base/lock.h"
14 #include "base/logging.h"
15 #import "chrome/app/breakpad_mac.h"
16 #import "chrome/browser/cocoa/objc_method_swizzle.h"
17
18 // Deallocated objects are re-classed as |CrZombie|. Overrides most
19 // |NSObject| methods to log fatal errors.
20 @interface CrZombie : NSObject {
21 }
22
23 @end
24
25 // Objects with enough space are made into "fat" zombies, which
26 // directly remember which class they were until reallocated.
27 @interface CrFatZombie : CrZombie {
28 @public
29 Class wasa;
30 }
31 @end
32
33 namespace {
34
35 // |object_cxxDestruct()| is an Objective-C runtime function which
36 // traverses the object's class tree for ".cxxdestruct" methods which
37 // are run to call C++ destructors as part of |-dealloc|. The
38 // function is not public, so must be looked up using nlist.
39 typedef void DestructFn(id obj);
40 DestructFn* g_object_cxxDestruct = NULL;
41
42 // The original implementation for |-[NSObject dealloc]|.
43 IMP g_originalDeallocIMP = NULL;
44
45 // Classes which freed objects become. |g_fatZombieSize| is the
46 // minimum object size which can be made into a fat zombie (which can
47 // remember which class it was before free, even after falling off the
48 // treadmill).
49 Class g_zombieClass = Nil; // cached [CrZombie class]
50 Class g_fatZombieClass = Nil; // cached [CrFatZombie class]
51 size_t g_fatZombieSize = 0;
52
53 // Whether to zombie all freed objects, or only those which return YES
54 // from |-shouldBecomeCrZombie|.
55 BOOL g_zombieAllObjects = NO;
56
57 // Protects |g_zombieCount|, |g_zombieIndex|, and |g_zombies|.
58 Lock lock_;
59
60 // How many zombies to keep before freeing, and the current head of
61 // the circular buffer.
62 size_t g_zombieCount = 0;
63 size_t g_zombieIndex = 0;
64
65 typedef struct {
66 id object; // The zombied object.
67 Class wasa; // Value of |object->isa| before we replaced it.
68 } ZombieRecord;
69
70 ZombieRecord* g_zombies = NULL;
71
72 // Lookup the private |object_cxxDestruct| function and return a
73 // pointer to it. Returns |NULL| on failure.
74 DestructFn* LookupObjectCxxDestruct() {
75 #if ARCH_CPU_64_BITS
76 // TODO(shess): Port to 64-bit. I believe using struct nlist_64
77 // will suffice. http://crbug.com/44021 .
78 NOTIMPLEMENTED();
79 return NULL;
80 #endif
81
82 struct nlist nl[3];
83 bzero(&nl, sizeof(nl));
84
85 nl[0].n_un.n_name = (char*)"_object_cxxDestruct";
86
87 // My ability to calculate the base for offsets is apparently poor.
88 // Use |class_addIvar| as a known reference point.
89 nl[1].n_un.n_name = (char*)"_class_addIvar";
90
91 if (nlist("/usr/lib/libobjc.dylib", nl) < 0 ||
92 nl[0].n_type == N_UNDF || nl[1].n_type == N_UNDF)
93 return NULL;
94
95 return (DestructFn*)((char*)&class_addIvar - nl[1].n_value + nl[0].n_value);
96 }
97
98 // Replace all methods in |refClass| which are not implemented in
99 // |aClass| with |anImp|.
100 void class_replaceUnimplementedWith(Class aClass, Class refClass, IMP anImp) {
101 unsigned int methodCount = 0;
102 Method* methodList = class_copyMethodList(refClass, &methodCount);
103 if (methodList) {
104 for (unsigned int i = 0; i < methodCount; ++i) {
105 const SEL name = method_getName(methodList[i]);
106 const char* types = method_getTypeEncoding(methodList[i]);
107
108 // Fails if the method already exists, which is fine.
109 class_addMethod(aClass, name, anImp, types);
110 }
111 free(methodList);
112 }
113 }
114
115 // Replacement |-dealloc| which turns objects into zombies and places
116 // them into |g_zombies| to be freed later.
117 void ZombieDealloc(id self, SEL _cmd) {
118 // This code should only be called when it is implementing |-dealloc|.
119 DCHECK_EQ(_cmd, @selector(dealloc));
120
121 // Use the original |-dealloc| if the object doesn't wish to be
122 // zombied.
123 if (!g_zombieAllObjects && ![self shouldBecomeCrZombie]) {
124 g_originalDeallocIMP(self, _cmd);
125 return;
126 }
127
128 // Use the original |-dealloc| if |object_cxxDestruct| was never
129 // initialized, because otherwise C++ destructors won't be called.
130 // This case should be impossible, but doing it wrong would cause
131 // terrible problems.
132 DCHECK(g_object_cxxDestruct);
133 if (!g_object_cxxDestruct) {
134 g_originalDeallocIMP(self, _cmd);
135 return;
136 }
137
138 Class wasa = object_getClass(self);
139 const size_t size = class_getInstanceSize(wasa);
140
141 // Destroy the instance by calling C++ destructors and clearing it
142 // to something unlikely to work well if someone references it.
143 (*g_object_cxxDestruct)(self);
144 memset(self, '!', size);
145
146 // If the instance is big enough, make it into a fat zombie and have
147 // it remember the old isa. Otherwise make it a regular zombie.
148 if (size >= g_fatZombieSize) {
149 object_setClass(self, g_fatZombieClass);
150 static_cast<CrFatZombie*>(self)->wasa = wasa;
151 } else {
152 object_setClass(self, g_zombieClass);
153 }
154
155 // The new record to swap into |g_zombies|. If |g_zombieCount| is
156 // zero, then |self| will be freed immediately.
157 ZombieRecord zombieToFree = {self, wasa};
158
159 {
160 AutoLock pin(lock_);
161 if (g_zombieCount > 0) {
162 // Put the current object on the treadmill and keep the previous
163 // occupant.
164 std::swap(zombieToFree, g_zombies[g_zombieIndex]);
165
166 // Bump the index forward.
167 g_zombieIndex = (g_zombieIndex + 1) % g_zombieCount;
168 }
169 }
170
171 // Do the free out here to prevent any chance of deadlock.
172 if (zombieToFree.object)
173 free(zombieToFree.object);
174 }
175
176 // Attempt to determine the original class of zombie |object|.
177 Class ZombieWasa(id object) {
178 // Fat zombies can hold onto their |wasa| past the point where the
179 // object was actually freed. Note that to arrive here at all,
180 // |object|'s memory must still be accessible.
181 if (object_getClass(object) == g_fatZombieClass)
182 return static_cast<CrFatZombie*>(object)->wasa;
183
184 // For instances which weren't big enough to store |wasa|, check if
185 // the object is still on the treadmill.
186 AutoLock pin(lock_);
187 for (size_t i=0; i < g_zombieCount; ++i) {
188 if (g_zombies[i].object == object)
189 return g_zombies[i].wasa;
190 }
191
192 return Nil;
193 }
194
195 // Log a message to a freed object. |wasa| is the object's original
196 // class. |aSelector| is the selector which the calling code was
197 // attempting to send. |viaSelector| is the selector of the
198 // dispatch-related method which is being invoked to send |aSelector|
199 // (for instance, -respondsToSelector:).
200 void LogAndDie(id object, SEL aSelector, SEL viaSelector) {
201 Class wasa = ZombieWasa(object);
202 const char* wasaName = (wasa ? class_getName(wasa) : "<unknown>");
203 NSString* aString =
204 [NSString stringWithFormat:@"Zombie <%s: %p> received -%s",
205 wasaName, object, sel_getName(aSelector)];
206 if (viaSelector) {
207 const char* viaName = sel_getName(viaSelector);
208 aString = [aString stringByAppendingFormat:@" (via -%s)", viaName];
209 }
210
211 // Set a value for breakpad to report, then crash.
212 SetCrashKeyValue(@"zombie", aString);
213 LOG(FATAL) << [aString UTF8String];
214 }
215
216 // Implements a method which will be used to override NSObject methods
217 // that zombies don't explicitly implement.
218 void DoesNotRecognize(id self, SEL _cmd) {
219 LogAndDie(self, _cmd, 0);
220 }
221
222 // Initialize our globals, returning YES on success.
223 BOOL ZombieInit() {
224 static BOOL initialized = NO;
225 if (initialized)
226 return YES;
227
228 Class rootClass = [NSObject class];
229
230 g_object_cxxDestruct = LookupObjectCxxDestruct();
231 g_originalDeallocIMP =
232 class_getMethodImplementation(rootClass, @selector(dealloc));
233 g_zombieClass = [CrZombie class];
234 g_fatZombieClass = [CrFatZombie class];
235 g_fatZombieSize = class_getInstanceSize(g_fatZombieClass);
236
237 if (!g_object_cxxDestruct || !g_originalDeallocIMP ||
238 !g_zombieClass || !g_fatZombieClass)
239 return NO;
240
241 // Override any inherited methods from |NSObject| with
242 // |DoesNotRecognize()|.
243 class_replaceUnimplementedWith(g_zombieClass, rootClass,
244 (IMP)DoesNotRecognize);
245
246 initialized = YES;
247 return YES;
248 }
249
250 } // namespace
251
252 @implementation CrZombie
253
254 // |DoesNotRecognize()| will log and crash for any selector send to
255 // instances of the class. Override a few methods related to message
256 // dispatch to provide more specific diagnostic information.
257 - (BOOL)respondsToSelector:(SEL)aSelector {
258 LogAndDie(self, aSelector, _cmd);
259 return NO;
260 }
261 - (id)forwardingTargetForSelector:(SEL)aSelector {
262 LogAndDie(self, aSelector, _cmd);
263 return nil;
264 }
265
266 @end
267
268 @implementation CrFatZombie
269
270 // This implementation intentionally left empty.
271
272 @end
273
274 @implementation NSObject (CrZombie)
275
276 - (BOOL)shouldBecomeCrZombie {
277 return NO;
278 }
279
280 @end
281
282 namespace ObjcEvilDoers {
283
284 BOOL ZombieEnable(BOOL zombieAllObjects,
285 size_t zombieCount) {
286 // Only allow enable/disable on the main thread, just to keep things
287 // simple.
288 CHECK([NSThread isMainThread]);
289
290 if (!ZombieInit())
291 return NO;
292
293 if (zombieCount < 0)
294 return NO;
295
296 g_zombieAllObjects = zombieAllObjects;
297
298 // Replace the implementation of -[NSObject dealloc].
299 Method m = class_getInstanceMethod([NSObject class], @selector(dealloc));
300 if (!m)
301 return NO;
302
303 const IMP prevDeallocIMP = method_setImplementation(m, (IMP)ZombieDealloc);
304 DCHECK(prevDeallocIMP == g_originalDeallocIMP ||
305 prevDeallocIMP == (IMP)ZombieDealloc);
306
307 // Grab the current set of zombies. This is thread-safe because
308 // only the main thread can change these.
309 const size_t oldCount = g_zombieCount;
310 ZombieRecord* oldZombies = g_zombies;
311
312 {
313 AutoLock pin(lock_);
314
315 // Save the old index in case zombies need to be transferred.
316 size_t oldIndex = g_zombieIndex;
317
318 // Create the new zombie treadmill, disabling zombies in case of
319 // failure.
320 g_zombieIndex = 0;
321 g_zombieCount = zombieCount;
322 g_zombies = NULL;
323 if (g_zombieCount) {
324 g_zombies =
325 static_cast<ZombieRecord*>(calloc(g_zombieCount, sizeof(*g_zombies)));
326 if (!g_zombies) {
327 NOTREACHED();
328 g_zombies = oldZombies;
329 g_zombieCount = oldCount;
330 g_zombieIndex = oldIndex;
331 ZombieDisable();
332 return NO;
333 }
334 }
335
336 // If the count is changing, allow some of the zombies to continue
337 // shambling forward.
338 const size_t sharedCount = std::min(oldCount, zombieCount);
339 if (sharedCount) {
340 // Get index of the first shared zombie.
341 oldIndex = (oldIndex + oldCount - sharedCount) % oldCount;
342
343 for (; g_zombieIndex < sharedCount; ++ g_zombieIndex) {
344 DCHECK_LT(g_zombieIndex, g_zombieCount);
345 DCHECK_LT(oldIndex, oldCount);
346 std::swap(g_zombies[g_zombieIndex], oldZombies[oldIndex]);
347 oldIndex = (oldIndex + 1) % oldCount;
348 }
349 g_zombieIndex %= g_zombieCount;
350 }
351 }
352
353 // Free the old treadmill and any remaining zombies.
354 if (oldZombies) {
355 for (size_t i = 0; i < oldCount; ++i) {
356 if (oldZombies[i].object)
357 free(oldZombies[i].object);
358 }
359 free(oldZombies);
360 }
361
362 return YES;
363 }
364
365 void ZombieDisable() {
366 // Only allow enable/disable on the main thread, just to keep things
367 // simple.
368 CHECK([NSThread isMainThread]);
369
370 // |ZombieInit()| was never called.
371 if (!g_originalDeallocIMP)
372 return;
373
374 // Put back the original implementation of -[NSObject dealloc].
375 Method m = class_getInstanceMethod([NSObject class], @selector(dealloc));
376 CHECK(m);
377 method_setImplementation(m, g_originalDeallocIMP);
378
379 // Can safely grab this because it only happens on the main thread.
380 const size_t oldCount = g_zombieCount;
381 ZombieRecord* oldZombies = g_zombies;
382
383 {
384 AutoLock pin(lock_); // In case any |-dealloc| are in-progress.
385 g_zombieCount = 0;
386 g_zombies = NULL;
387 }
388
389 // Free any remaining zombies.
390 if (oldZombies) {
391 for (size_t i = 0; i < oldCount; ++i) {
392 if (oldZombies[i].object)
393 free(oldZombies[i].object);
394 }
395 free(oldZombies);
396 }
397 }
398
399 } // namespace ObjcEvilDoers
OLDNEW
« no previous file with comments | « chrome/browser/cocoa/objc_zombie.h ('k') | chrome/chrome_browser.gypi » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698