为什么这不会崩溃?

9

我正在尝试将一个bug缩小到最小可重现的程度,并发现了一些奇怪的事情。

请看这段代码:

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方法吗?
第二个“static:”从哪里来?为什么这不会导致崩溃?
结果:
Kevin Ballard和Graham Lee都是正确的。 Graham之所以正确,是因为他意识到NSLog()没有调用-description(正如我错误地假设的那样),而Kevin几乎肯定是正确的,这是一个与复制格式字符串和va_list相关的奇怪堆栈问题。
1. NSLoggingNSString不会调用-description。 Graham很优雅地表现出了这一点,如果你跟踪执行日志记录的核心基础源代码,你会发现这是事实。任何起源于NSLog内部的回溯都表明它会调用NSLogv => _CFLogvEx => _CFStringCreateWithFormatAndArgumentsAux => _CFStringAppendFormatAndArgumentsAux_CFStringAppendFormatAndArgumentsAux()(第5365行)是发生所有魔法的地方。你可以看到它手动查找所有的%替换。只有当替换类型为CFFormatObjectType,描述函数非空,并且替换还没有被其他类型处理时,它才会最终调用描述副本函数。由于我们已经证明了描述未被复制,因此可以合理地假设一个NSString在此之前就被处理了(在这种情况下,它可能会执行原始字节复制),从而导致……
2. 正如Kevin所推测的那样,这里发生了堆栈错误。某种方式,最初指向自动释放字符串的指针被替换为不同的对象,这个对象恰好是一个NSString。因此,它不会崩溃。奇怪的是,如果我们将静态变量的类型更改为其他类型,例如NSArray,那么-description方法就会被调用,程序就会像预期的那样崩溃。
多么真正和完全的奇怪啊。 Kevin因为对行为的根本原因最正确而得分,而Graham则因纠正我的谬误思维而受到赞扬。 我希望我能接受两个答案……

你为什么期望它崩溃? - David Weiser
NSLog([NSString stringWithFormat:@"static: %@", staticString])确实会导致崩溃,因此这实际上是由于NSLog处理%@时的不同行为所引起的。 - Joost
Nitpicks: 我认为你在关于“Results:”和_CFStringAppendFormatAndArgumentsAux的分析是不正确的。对于每个%@_CFStringAppendFormatAndArgumentsAux将尝试1)通过参数传递的copyDesc函数;2)__CFCopyFormattingDescription,这对于ObjC对象会尝试_copyFormattingDescription:;最后3)CFCopyDescription,这对于ObjC对象会尝试_copyDescription,反汇编显示NSObject默认调用-description。因此,99%的情况下,%@将导致调用-description - johne
@johne Graham 明确表明,在记录 NSString 对象时,并没有调用 -description 方法,这意味着某些地方发生了短路。我曾认为它会被调用,而这种假设导致我期望出现崩溃。 - Dave DeLong
@Dave请看一下我对@Grahams答案的评论。-description肯定会被调用,只是被NSLog()“自动地”禁止了,因为(我猜测,可能与多线程锁有关)NSLog()会阻塞(即在当前处理NSLog()时立即返回而不执行任何工作)对NSLog()的其他调用。 - johne
显示剩余3条评论
5个回答

9
我最好的猜测是,NSLog()会复制格式字符串(可能是可变副本),然后解析参数。由于你已经dealloc了staticString,所以恰好发生的是,格式字符串的副本被放置到相同的位置。这就是你描述的“static: static:”输出的原因。当然,这种行为是未定义的 - 不能保证它总是使用相同的内存位置。
另一方面,你的NSLog(@"static: %s", [staticString UTF8String])在格式字符串复制发生之前访问了staticString,这意味着它正在访问垃圾内存。

我怀疑它没有复制字符串,但很可能使用-getCString:或类似方法,以便可以在内容上使用printf样式的函数。你看到的可能是这些字符。 - user23743
@Graham 实际上,这是一个自定义的 printf() 实现。我会编辑我的问题并分享我找到的内容。 - Dave DeLong
好的,我刚刚通过NSLog()进行了步进调试,它调用了CFStringCreateWithFormatAndArgumentsAux()函数来构建一个新的CFMutableString。实际上,这让我想到它不需要复制字符串,因为它没有进行任何原地修改。换句话说,它没有在格式化字符串上执行vsprintf()操作,这是我最初预期的。 - user23743
@Graham 如果使用了-getCString:,那么得到的值将是一个C字符串,如果传递给%@,程序将会崩溃。 - Lily Ballard
@Kevin 不是的,我的意思是它可能会在_format字符串_上使用-getCString:。任何此类函数的一种可能实现是从格式字符串获取C字符串,然后对其进行sprintf样式的替换。无论如何,它并没有这样做,尽管我见过另一个NSLog()的实现方式是这样做的(或者更确切地说,它调用了-[NSString initWithFormat:locale:arguments:],而_那个_执行了上述操作)。 - user23743
显示剩余3条评论

8

您的假设是错误的,NSLog() 不会在 NSString 实例上调用 -description 方法。我只是添加了这个分类:

@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() 不会在其参数中遇到的 NSString 上调用 -description 方法。当你错误地访问已释放的 staticString 变量时,你会两次获得静态字符串,这很可能是堆栈数据的一个怪癖。

这实际上与堆栈无关。请查看我的答案,了解我对情况的最佳猜测,但简而言之,NSLog很可能会复制其格式字符串,并且复制的字符串最终位于与原始的staticString相同的内存位置。 - Lily Ballard
关于-description,对于NSString来说,它实际上可能会回退到CFCopyDescription()的行为。或者它可能会对字符串进行智能处理,很难确定。 - Lily Ballard
实际上,你需要使用类似于 fprintf(stderr,"-description called on %s\n", [self UTF8String]); 而不是 NSLog()。当已经在使用 NSLog() 时,NSLog() 似乎会自动抑制其他输出。这就是为什么预期的 -description 没有显示出来的原因。当你使用这个注释中的 fprintf() 代码时,它确实会显示出来。 - johne

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()分配的内存。我认为,这可能是你的示例没有以你期望的方式崩溃的原因。

如果你真的想看到这个特定程序崩溃,你可以做以下任一操作:

  • 将环境变量CFZombieLevel设置为17(scribble + don't free)。
  • 将环境变量NSZombieEnabled设置为YES
  • 将环境变量DYLD_INSERT_LIBRARIES设置为/usr/lib/libgmalloc.dylib(参见man libgmalloc)。

1
我本来以为它会崩溃,因为我假设字符串上会调用“-description”方法。在Objective-C中,在已释放的对象上调用方法导致崩溃。然而,事实证明这不是正在发生的事情:字符串通过日志记录系统进行了特殊处理。随着这种假设的消失,“未定义行为”的论点就更有道理了。 - Dave DeLong
@Dave 相信我,这与“字符串通过日志系统进行特殊处理”无关。这只是一种“相关性”,而不是“因果关系”。尝试使用此行:staticString = [NSString stringWithString:[[NSArray arrayWithObjects:@"1", @"2", @"3", nil] componentsJoinedByString:@","]]; - johne
@Dave...这个代码“可行”:staticString = [NSString stringWithFormat:@"%@", [NSArray arrayWithObjects:@"1", @"2", @"3", nil]]; - johne

1

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


1
如果我在排水池之前放置一个日志,那么它会按预期记录。当我排水时,我希望NSString被释放,但静态指针仍然保持不变。因此,尝试发送-description方法以记录它应该导致EXC_BAD_ACCESS,因为访问已释放的内存。相反,它正在记录其他内容。但是,显式调用对象上的方法会导致崩溃。 - Dave DeLong
1
只有在包含该内存的VM页面不再有效时,才会发生EXC_BAD_ACCESS错误。释放对象只是在malloc的内部跟踪中标记其空间为空闲,但VM页面的其他部分可能仍被其他内存块使用。因此,除非该页面中的所有内容都被释放并且malloc将页面返回给内核以供重用,否则访问该内存后不会导致崩溃。 - Brian Webster

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

1
@"static: %@" 存储在二进制文件的 TEXT 部分,因为它是一个常量字符串。 - Dave DeLong

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接