9

我试图将错误缩小到最小的可重现情况,并发现了一些奇怪的东西。

考虑这段代码:

static NSString *staticString = nil;
int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    if (staticString == nil) {
        staticString = [[NSArray arrayWithObjects:@"1", @"2", @"3", nil] componentsJoinedByString:@","];
    }   

    [pool drain];

    NSLog(@"static: %@", staticString);
    return 0;
}

期待这段代码崩溃。相反,它记录:

2011-01-18 14:41:06.311 EmptyFoundation[61419:a0f] static: static: 

但是,如果我将其更改NSLog()为:

NSLog(@"static: %s", [staticString UTF8String]);

然后它确实崩溃了。

编辑更多信息:

排干游泳池后:

NSLog(@"static: %@", staticString);  //this logs "static: static: "
NSLog(@"static: %@", [staticString description]); //this crashes

所以显然在字符串上调用一个方法足以让它崩溃。在那种情况下,为什么不直接记录字符串会导致它崩溃?不NSLog()应该调用该-description方法吗?

第二个“静态:”来自哪里?为什么这不崩溃?


结果:

凯文巴拉德和格雷厄姆李都是正确的。格雷厄姆正确地意识到这NSLog()不是调用(正如我错误地假设的那样-description),而凯文几乎肯定是正确的,这是复制格式字符串和va_list周围的一个奇怪的堆栈相关问题。

  1. NSLogging并且NSString不调用-description. Graham 优雅地展示了这一点,如果您跟踪进行日志记录的 Core Foundation 资源,您会发现情况确实如此。任何源自内部的回溯NSLog都表明它调用了NSLogv=> _CFLogvEx=> _CFStringCreateWithFormatAndArgumentsAux=> _CFStringAppendFormatAndArgumentsAux_CFStringAppendFormatAndArgumentsAux()(第 5365 行)是所有魔法发生的地方。您可以看到它正在手动查找所有%替换。CFFormatObjectType如果替换的类型是 a ,描述函数是非 nil 并且替换还没有被其他类型处理,它只会调用描述复制函数。由于我们已经证明描述没有被复制,因此可以合理地假设NSString得到更早的处理(在这种情况下,它可能会进行原始字节复制),这让我们相信......
  2. 正如凯文推测的那样,这里发生了堆栈错误。不知何故,指向自动释放字符串的指针替换为另一个对象,而该对象恰好是一个NSString. 所以,它不会崩溃。奇怪的。但是,如果我们将静态变量的类型更改为其他类型,例如 an NSArray,则该-description方法确实会被调用,并且程序确实会按预期崩溃。

多么真实和完全奇怪。凯文对行为根本原因的看法最正确,而格雷厄姆纠正了我的错误想法,我对此表示赞赏。我希望我能接受两个答案...

4

5 回答 5

9

我对您所看到的最好的猜测是 NSLog() 复制格式字符串(可能作为可变副本),然后解析参数。由于您已经 dealloc'd staticString,因此恰好将格式字符串的副本放置到同一位置。这导致您看到"static: static: "您描述的输出。当然,这种行为是未定义的——不能保证它总是为此使用相同的内存位置。

另一方面,您在格式字符串复制发生之前NSLog(@"static: %s", [staticString UTF8String])访问staticString,这意味着它正在访问垃圾内存。

于 2011-01-18T23:01:05.420 回答
8

NSLog()您调用实例的假设是错误-description的。NSString我刚刚添加了这个类别:

@implementation NSString (GLDescription)

- (NSString *)description {
  NSLog(@"-description called on %@", self);
  return self;
}

@end

它不会导致堆栈溢出,因为它不会被递归调用。不仅如此,如果我将该类别插入您问题的代码中,我会发现以下输出:

2011-01-18 23:04:11.653 LogString[3769:a0f] -description called on 1
2011-01-18 23:04:11.656 LogString[3769:a0f] -description called on 2
2011-01-18 23:04:11.657 LogString[3769:a0f] -description called on 3
2011-01-18 23:04:11.658 LogString[3769:a0f] static: static: 

所以我们得出结论,NSLog()它不会调用它在它的 args 中遇到的-description一个。当您错误地访问已释放的变量NSString时,为什么您两次获得静态字符串可能是堆栈上数据的怪癖。staticString

于 2011-01-18T23:07:43.413 回答
1

访问释放的内存不一定会导致崩溃。行为未定义。你期望太高了!

于 2011-01-18T22:48:11.347 回答
1

也许它与 @"static:" 存储在与 staticString 相同的内存位置有关。staticString 将被释放,并将 @"static: %@" 存储在回收的内存位置,因此 staticString 指针位于 "static: %@" 上,因此它最终为 static: static:。

于 2011-01-18T22:53:53.437 回答
1

这是“使用后free()”的情况。发生的是“未定义的行为”。您的示例实际上与以下内容没有什么不同:

char *stringPtr = NULL;
stringPtr = malloc(1024); // Example code, assumes this returns non-NULL.
strcpy(stringPtr, "Zippers!");
free(stringPtr);
printf("Pants: %s\n", stringPtr);

线上会发生什么printf?谁知道。任何从Pants: Zippers!Pants: (...garbage...) Core Dump

所有特定于 Objective-C 的东西实际上都是无关紧要的——事实上,你使用的内存指针不再有效是唯一重要的事情。你最好向墙上扔飞镖,而不是试图解释“为什么”它没有崩溃和打印static: static。出于性能原因,大多数malloc实现不会打扰“收获”free()分配,直到他们不得不这样做。恕我直言,这可能就是您的示例没有按照您期望的方式崩溃的原因。

如果您真的想看到这个特定的程序崩溃,您可以执行以下操作之一:

  • 将环境变量设置CFZombieLevel17(scribble + don't free)。
  • 将环境变量设置NSZombieEnabledYES.
  • 将环境变量设置DYLD_INSERT_LIBRARIES/usr/lib/libgmalloc.dylib(请参阅man libgmalloc)。
于 2011-01-20T01:13:57.127 回答