使用printf的%s说明符打印NULL的行为是什么?

时间:2012-07-21 04:02:40

标签: c linux language-lawyer compiler-bug

遇到一个有趣的采访问题:

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

4 个答案:

答案 0 :(得分:52)

首先要做的事情是:printf期待有效(即非NULL) 它的%s参数的指针正式传递给NULL 未定义。它可能会打印“(null)”或者它可能会删除您的所有文件 硬盘 - 就ANSI而言,要么是正确的行为 (至少,这就是哈比森和斯蒂尔告诉我的。)

话虽如此,是的,这是非常奇怪的行为。事实证明 发生的事情就是当你像这样做一个简单的printf

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

gcc( ahem )非常聪明,可以将其解构为调用 puts。第一个printf,这个:

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

足够复杂,gcc会发出一个真实的调用 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

奇怪的是,它不会打印以下换行符。 就好像它已经发现这会导致一个段错误 所以它没有打扰。 (它有它 - 当我编译时它警告我 它)。

答案 1 :(得分:25)

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

至于为什么会发生这种情况的机制,现代gcc会优化printf("%s\n", x)puts(x),并且puts在看到(null)时没有打印printf的愚蠢代码一个空指针,而printf的常见实现有这种特殊情况。由于gcc无法优化(通常)像这样的非平凡格式字符串,因此当格式字符串中包含其他文本时,{{1}}实际上会被调用。

答案 2 :(得分:16)

第7.1.4节(C99或C11)说:

  

§7.1.4库函数的使用

     

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

由于printf()的规范没有说明当为%s说明符传递空指针时发生的事情,因此行为显然是未定义的。 (请注意,传递由%p说明符打印的空指针不是未定义的行为。)

以下是fprintf()家庭行为的“章节和经文”(C2011 - 它是C1999中的不同章节编号):

  

§7.21.6.1fprintf函数

     

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

     

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

     

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

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

许多实现在传递空指针时打印诸如(null)之类的字符串这一事实是一种很难依赖的善意。未定义行为的美妙之处在于允许这样的响应,但这不是必需的。类似地,允许崩溃,但不是必需的(更可惜的是 - 如果他们在宽容系统上工作然后移植到其他不那么宽容的系统,人们就会被咬伤。)

答案 3 :(得分:6)

NULL指针不指向任何地址,并且尝试打印它会导致未定义的行为。未定义意味着由您的编译器或C库决定在尝试打印NULL时要执行的操作。