【问题标题】:Why doesn't this crash?为什么这不会崩溃?
【发布时间】:2011-06-11 10:08:48
【问题描述】:

我正在尝试将错误范围缩小到最小可重现的情况,并发现了一些奇怪的地方。

考虑这段代码:

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()not 调用 -description (正如我错误地假设的那样),而 Kevin 几乎肯定是正确的,这是复制格式字符串和堆栈相关的奇怪问题va_list 左右。

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

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

【问题讨论】:

  • 你为什么期待它崩溃?
  • NSLog([NSString stringWithFormat:@"static: %@", staticString]) 确实会导致崩溃,所以这确实是由于NSLog在处理%@时的不同行为所致
  • Nitpicks:我相信您的分析 wrt/“结果:”和_CFStringAppendFormatAndArgumentsAux 是不正确的。对于每个%@_CFStringAppendFormatAndArgumentsAux 将尝试 1) 通过参数传入的 copyDesc 函数,2) __CFCopyFormattingDescription,这对于 ObjC 对象会导致尝试 _copyFormattingDescription:,最后是 3) CFCopyDescription ,对于 ObjC 对象会产生_copyDescription,反汇编显示NSObject 默认调用-description。因此,99% 的情况下,%@ 将导致 -description 被调用。
  • 当然,对于 NON-ObjC 对象(即 Core Foundation 对象),__CFCopyFormattingDescriptionCFCopyDescription 最终会执行特殊的 CF 操作。每个 CF 对象在 CFRuntimeBase 中都有一个用于 copyFormattingDesccopyDebugDesc 的条目(对应于前面提到的函数)。对于CFString 对象,这相当于调用__CFStringCopyFormattingDescription,只不过是字符串的CFStringCreateCopy
  • 因此,对于“大多数情况”,在 _CFStringAppendFormatAndArgumentsAux%@ 中对于“字符串”(实际上大部分是 CFString 对象)相当于调用 CFStringCreateCopy,结果这是CFStringAppend'ed 到正在构建的可变字符串结果。

标签: objective-c memory-management autorelease


【解决方案1】:

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

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

【讨论】:

  • 我怀疑它会复制字符串,但它可能会复制 -getCString: 或类似的东西,以便它可以在内容上使用 printf 样式的函数。可能是你看到的那些字符。
  • @Graham 实际上,它是printf() 的自定义实现。我将用我发现的内容编辑我的问题。
  • 好的,我刚刚完成了NSLog(),它调用了CFStringCreateWithFormatAndArgumentsAux(),它构建了一个新的CFMutableString。事实上,这让我认为它不需要复制字符串,因为它没有进行任何就地修改。换句话说,它没有在格式字符串上执行vsprintf(),这是我最初所期望的。
  • @Graham 如果它使用了-getCString:,那么结果值将是一个C字符串,如果传递给%@,它会崩溃。
  • @Graham 听起来真正发生的事情是它正在创建一个新的可变字符串,在第一个标记之前附加文本(例如 @"static:"),然后处理该标记。这个新的可变字符串与staticString 存在于同一内存位置,因此当它处理%@ 标记时,它会插入可变字符串的值,即@"static: ",这会将结果转换为@"static: static: "
【解决方案2】:

您假设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,它在其 args 中遇到。当您错误地访问已发布的staticString 变量时,为什么您两次获取静态字符串可能是堆栈上数据的怪癖。

【讨论】:

  • 这实际上与堆栈无关。请参阅我的答案,以了解我对发生了什么的最佳猜测,但简而言之,NSLog 很可能会复制其格式字符串,并且复制的字符串最终与原始 staticString 位于相同的内存位置。
  • 至于-description,对于NSString,它实际上可能会退回到CFCopyDescription() 行为。或者它可能对字符串很聪明,很难确定。
  • 实际上,您需要使用类似于fprintf(stderr,"-description called on %s\n", [self UTF8String]); 的东西而不是NSLog()。当NSLog() 已经在NSLoging 中间时,NSLog() 似乎“自动”抑制了通过NSLog() 的附加输出。这就是为什么预期的-description 没有出现的原因。当您使用此 cmets fprintf() 代码时,它会显示出来。
【解决方案3】:

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

【讨论】:

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

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

【讨论】:

  • @"static: %@" 存储在二进制文件的 TEXT 部分,因为它是一个常量字符串。
【解决方案5】:

这是“在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()'d 分配,直到他们不得不这样做。恕我直言,这可能就是您的示例没有按您预期的方式崩溃的原因。

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

  • 将环境变量CFZombieLevel 设置为17(涂鸦+不要免费)。
  • 将环境变量NSZombieEnabled设置为YES
  • 将环境变量DYLD_INSERT_LIBRARIES设置为/usr/lib/libgmalloc.dylib(见man libgmalloc)。

【讨论】:

  • 我预计它会崩溃,因为我假设将在字符串上调用 -description 方法。在释放的对象上调用方法导致Objective-C崩溃。然而,事实证明这不是正在发生的事情:字符串在日志系统中是特殊的。随着这个假设的消失,整个“未定义行为”的论点就更有意义了。
  • @Dave 相信我,这与“字符串在日志系统中的特殊情况”无关。这是一个相关,而不是因果。试试这条线:staticString = [NSString stringWithString:[[NSArray arrayWithObjects:@"1", @"2", @"3", nil] componentsJoinedByString:@","]];.
  • @Dave ... 而这个恰好“工作”:staticString = [NSString stringWithFormat:@"%@", [NSArray arrayWithObjects:@"1", @"2", @"3", nil]];
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-13
  • 2018-10-28
  • 2020-10-30
  • 2017-04-01
相关资源
最近更新 更多