【问题标题】:What is the behavior of printing NULL with printf's %s specifier?使用 printf 的 %s 说明符打印 NULL 的行为是什么?
【发布时间】:2012-07-20 07:40:52
【问题描述】:

遇到一个有趣的面试问题:

test 1:
printf("test %s\n", NULL);
printf("test %s\n", NULL);

prints:
test (null)
test (null)

test 2:
printf("%s\n", NULL);
printf("%s\n", NULL);
prints
Segmentation fault (core dumped)

虽然这可能在某些系统上运行良好,但至少我的系统抛出了分段错误。 这种行为的最佳解释是什么?上面的代码是 C 语言。

以下是我的 gcc 信息:

deep@deep:~$ gcc --version
gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3

【问题讨论】:

  • 在 VS2010 上都没有崩溃。它只是为空指针打印(null)
  • 是的,正确的。答案可能在很大程度上是特定于架构的。
  • 一开始我不知道你可以printf 一个空指针......我期待它们都崩溃。现在看看这是指定的、实现定义的、未定义的行为,还是只是编译器错误。
  • 未定义的行为意味着它可能会崩溃,它可能看起来成功,它可能会擦除您的硬盘驱动器,或者它可能会在您的屏幕上显示阳光和小狗。你永远不知道。
  • 根据 7.1.4:除非在随后的详细说明中另有明确说明,否则以下每个语句均适用: 如果函数的参数具有无效值(例如函数,或程序地址空间外的指针,或空指针,或指向不可修改存储的指针(当相应的参数不是 const 限定时)或具有变量的函数不期望的类型(提升后)参数数量,行为未定义。

标签: c linux language-lawyer compiler-bug


【解决方案1】:

第一件事:printf 期待一个有效的(即非 NULL) 其 %s 参数的指针,因此将其传递为 NULL 是正式的 不明确的。它可能会打印“(null)”或者它可能会删除您的所有文件 硬盘驱动器——就 ANSI 而言,两者都是正确的行为 (至少,Harbison 和 Steele 是这么告诉我的。)

话虽如此,是的,这确实是一种奇怪的行为。事实证明 发生的事情是,当您像这样执行简单的printf 时:

printf("%s\n", NULL);

gcc (ahem) 足够聪明,可以将其解构为对 puts。第一个printf,这个:

printf("test %s\n", NULL);

足够复杂,以至于 gcc 将改为发出对 real 的调用 printf.

(请注意 gcc 会发出有关您的无效 printf 参数的警告 编译时。那是因为它很久以前就开发了能力 解析*printf 格式字符串。)

您可以通过使用-save-temps 选项编译自己看到这一点 然后查看生成的.s 文件。

当我编译第一个例子时,我得到:

movl    $.LC0, %eax
movl    $0, %esi
movq    %rax, %rdi
movl    $0, %eax
call    printf      ; <-- Actually calls printf!

(评论是我添加的。)

但是第二个产生了这个代码:

movl    $0, %edi    ; Stores NULL in the puts argument list
call    puts        ; Calls puts

奇怪的是它不打印下面的换行符。 好像发现这会导致段错误 所以它不会打扰。 (它有——它在我编译时警告我 它。)

【讨论】:

  • 很好的答案(尽管 R. 比你早了几分钟)。但是换行符没有什么奇怪的。 puts() 将参数字符串打印到标准输出,后跟换行符。 (相比之下,fputs() 打印到指定文件并且添加换行符。)
  • 嗯。直到你告诉我,我才知道换行符。我想这是因为我一直只使用 fputs。
【解决方案2】:

就 C 语言而言,原因是您正在调用未定义的行为,任何事情都可能发生。

至于发生这种情况的机制,现代 gcc 将 printf("%s\n", x) 优化为 puts(x),而 puts 在看到空指针时没有愚蠢的代码来打印 (null),而常见的实现printf 有这种特殊情况。由于 gcc 不能像这样优化(通常)非平凡的格式字符串,所以当格式字符串中存在其他文本时,printf 实际上会被调用。

【讨论】:

  • 这就是我要找的。 +1
  • 这种行为是否在 C 标准的不同场景部分调用 puts 和 printf。或者编译器可以选择自己的机制。由于在 Visual c++ 上它工作正常。
  • 编译器可以进行任何不改变有效程序行为的转换。 (这里它改变了一个无效的行为。)
【解决方案3】:

第 7.1.4 节(C99 或 C11)说:

§7.1.4 库函数的使用

¶1 除非在详细说明中另有明确说明,否则以下每个陈述均适用 以下描述:如果函数的参数具有无效值(例如值 函数域外,或程序地址空间外的指针, 或者一个空指针,或者一个指向不可修改存储的指针,当对应的 参数不是 const 限定的)或函数不期望的类型(提升后) 使用可变数量的参数,行为是未定义的。

由于printf() 的规范没有说明当您将空指针传递给%s 说明符时会发生什么,因此该行为是明确未定义的。 (请注意,传递要由 %p 说明符打印的空指针不是未定义的行为。)

这是fprintf() 家庭行为的“章节和诗句”(C2011 - 它是 C1999 中的不同节号):

§7.21.6.1 fprintf 函数

s     如果不存在 l 长度修饰符,则参数应为指向初始值的指针 字符类型数组的元素。 [...]

     如果存在 l 长度修饰符,则参数应为指向初始值的指针 wchar_t 类型数组的元素。

p     参数应该是一个指向 void 的指针。指针的值为 在实现定义中转换为打印字符序列 方式。

s 转换说明符的规范排除了空指针有效的可能性,因为空指针不指向适当类型数组的初始元素。 p 转换说明符的规范不要求 void 指针特别指向任何东西,因此 NULL 是有效的。

许多实现在传递一个空指针时打印一个字符串,例如(null),这是一种危险的善意。未定义行为的美妙之处在于允许这样的响应,但不是必需的。同样,崩溃是允许的,但不是必需的(更遗憾的是,如果人们在一个宽容的系统上工作,然后移植到其他宽容度较低的系统,就会被咬)。

【讨论】:

  • 由于 printf() 的规范没有说明当您将空指针传递给 %s 说明符时会发生什么,因此该行为是明确未定义的。 实际上它说参数应该是一个指向字符类型数组的初始元素的指针,并且标准说违反出现在约束之外的shall是未定义的行为(C99 , 4.p2)。
  • @ouah:您所说的与第 7.1.4 节的引用部分所说的不同('空指针...行为未定义'),或者什么我说它说('空指针...行为明确未定义')?
  • 我将此添加为额外说明:4.p2 也可以与printf 的规范一起使用,以表明带有NULL 参数的调用是UB。在我的评论中,actually 这个词可能是多余的。
【解决方案4】:

NULL 指针不指向任何地址,尝试打印它会导致未定义的行为。未定义的意思是由你的编译器或 C 库决定当它尝试打印 NULL 时要做什么。

【讨论】:

    猜你喜欢
    • 2019-11-30
    • 2013-12-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-06-22
    • 2013-06-22
    相关资源
    最近更新 更多