Dee*_*dar 48 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)
Run Code Online (Sandbox Code Playgroud)
虽然这可能在某些系统上运行良好,但至少我的方法正在抛出一个分段错误.这种行为最好的解释是什么?以上代码在C中.
以下是我的gcc信息:
deep@deep:~$ gcc --version
gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3
Run Code Online (Sandbox Code Playgroud)
小智 57
首先要做的事情printf是:期望有一个有效的(即非NULL)指针用于其%s参数,因此将其传递给NULL是正式未定义的.它可能会打印"(null)"或者它可能会删除硬盘上的所有文件 - 就ANSI而言,这是正确的行为(至少,这就是Harbison和Steele告诉我的.)
话虽如此,是的,这是非常奇怪的行为.事实证明,当你做这样简单的事情时printf:
printf("%s\n", NULL);
Run Code Online (Sandbox Code Playgroud)
gcc((ahem)足够聪明,可以将其解构为一个调用
puts.第一个printf,这个:
printf("test %s\n", NULL);
Run Code Online (Sandbox Code Playgroud)
很复杂,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!
Run Code Online (Sandbox Code Playgroud)
(评论是我添加的.)
但第二个产生了这个代码:
movl $0, %edi ; Stores NULL in the puts argument list
call puts ; Calls puts
Run Code Online (Sandbox Code Playgroud)
奇怪的是,它不会打印以下换行符.好像它已经发现这会引起一个段错误,所以它不会打扰.(它有它 - 当我编译它时它警告我.)
R..*_*R.. 28
就C语言而言,原因是你正在调用未定义的行为,任何事情都可能发生.
至于为什么会发生这种情况的机制,现代gcc优化printf("%s\n", x)到puts(x),并且在看到空指针时没有puts打印的愚蠢代码(null),而常见的实现printf具有这种特殊情况.由于gcc无法优化(通常)像这样的非平凡格式字符串,因此printf当格式字符串中包含其他文本时,实际上会调用它.
Jon*_*ler 18
第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)传递空指针时,是一种很难依赖的善意.未定义行为的美妙之处在于允许这样的响应,但这不是必需的.类似地,允许崩溃,但不是必需的(更可惜的是 - 如果他们在宽容系统上工作然后移植到其他不太宽容的系统,人们就会被咬伤).