printf(s)和printf("%s", s)之间的根本区别是什么?

28

问题很简单,s是一个字符串,我突然想尝试使用printf(s)看看是否能够正常工作,结果在一个情况下得到了警告,在另一个情况下则没有。

char* s = "abcdefghij\n";
printf(s);

// Warning raised with gcc -std=c11: 
// format not a string literal and no format arguments [-Wformat-security]

// On the other hand, if I use 

char* s = "abc %d efg\n";
printf(s, 99);

// I get no warning whatsoever, why is that?

// Update, I've tested this:
char* s = "random %d string\n";
printf(s, 99, 50);

// Results: no warning, output "random 99 string".

printf(s)和printf("%s", s)之间的本质区别是什么,为什么我只有在一个情况下会得到警告?


非常惊奇和有趣。我确认这种行为,可能有一个解释,但在有人解释之前,我认为这是诊断中的一个错误。 - Iharob Al Asimi
1
@JoachimPileborg,您是说如果不提供字符串字面值,编译器就无法知道需要多少个参数?所以这就是为什么如果我没有提供更多的参数,就会收到警告,但如果我至少提供一个参数,我就不会收到警告。我在问题中添加了另一个例子,我想这证明了我的说法。 - Mikael
2
如果您使用 const char* const s,您可能会注意到一些差异。 - aschepler
1
另一个区别是编译时的可分析性以及随后应用的优化 - 或者无法应用。 - chux - Reinstate Monica
6
基本区别在于printf(s)容易出现错误(并且可能存在安全漏洞),而printf("%s", s)只是一种低效的编写方式,等价于fputs(s, stdout) - Ilmari Karonen
显示剩余3条评论
5个回答

26
在第一种情况下,非字面量格式化字符串可能来自用户代码或用户提供的(运行时)数据,这种情况下它可能包含“%s”或其他转换规范,而您没有传递数据。这可能会导致各种阅读问题(如果字符串包括“%n”,还可能导致写作问题——请参见printf() 或者您的C库手册页)。
在第二种情况下,格式化字符串控制输出,无论要打印的任何字符串是否包含转换规范都没有关系(尽管所示的代码打印的是一个整数,而不是字符串)。编译器(问题中使用的是GCC或Clang)认为,由于在(非字面量)格式化字符串之后有参数,程序员知道他们要做什么。
第一种情况是“格式化字符串”漏洞。您可以搜索更多关于此主题的信息。
GCC知道大多数情况下带有非字面量格式化字符串的单个参数printf()很容易出问题。您可以改用puts()fputs()。它太危险了,以至于GCC生成警告的最低限度的挑衅。
非文字格式化字符串的更一般的问题也可能会有问题,如果您不小心的话——但是假设您小心使用,它也非常有用。要让GCC发出抱怨,您必须更加努力:它需要同时使用-Wformat-Wformat-nonliteral才能发出抱怨。
从评论中可以看到:
引用: “因此,忽略警告,就好像我真的知道自己在做什么并且不会出错,一种方法比另一种更有效还是它们相同?考虑空间和时间。”在三个`printf()`语句中,由于变量`s`在调用之前被赋值并被密切使用,因此没有实际问题。但是如果您省略了字符串中的换行符,则可以使用`puts(s)`,或者保持不变并使用`fputs(s, stdout)`,从而获得相同的结果,而无需通过`printf()`解析整个字符串以查找要打印的所有简单字符的开销。
第二个`printf()`语句所写的也是安全的;格式字符串与传递的数据匹配。直接传递格式字符串和传递它的字面量之间没有显着区别 - 除非编译器可以对字面量进行更多检查。运行时结果是相同的。
第三个`printf()`将比格式字符串需要的参数数量更多的数据参数传递,但这是良性的。虽然编译器可以更好地检查字面量格式字符串,但运行时效果几乎相同。
来自上述链接的`printf()`规范:
每个函数都在控制格式下转换、格式化和打印其参数。格式是一个字符串,在其初始换档状态(如果有)下开始和结束。格式由零个或多个指令组成:普通字符,只需简单地复制到输出流中,以及转换说明符,每个说明符应导致获取零个或多个参数。如果格式缺少参数,则结果是未定义的。如果格式在参数仍然存在的情况下耗尽,则将评估超额参数,但否则将被忽略。

在所有这些情况下,没有明显的迹象表明为什么格式字符串不是字面值。然而,想要一个非字面值的格式字符串的原因之一可能是有时您需要以%f表示法打印浮点数,有时需要以%e表示法打印,并且您需要在运行时选择其中一种方式。(如果只基于值,%g可能是适当的,但有时您需要显式控制-始终使用%e或始终使用%f)。


1
我不明白前两段是如何解释这种行为的。在这两种情况下,格式字符串都可以来自用户。 - Eugene Sh.
这是一个很好的答案!那么,忽略警告,仿佛我真的知道自己在做什么并且不会出现错误,是使用其中一种更有效还是两种都一样?考虑到空间和时间。 - Mikael
1
@MikaelMello,更高效的方式是使用puts。没有需要解析的格式,只需简单的字符输出。 - phuclv
2
@EugeneSh。是的。这是编译器必须划定的界限。如果它总是抱怨非文字格式字符串,那么例如国际化消息将很难使用。基于这种行为,我认为它只是假设没有其他参数,你混淆了printfputs,但有了这些参数,你似乎知道自己在做什么。 - ilkkachu
@MikaelMello,既然这是关于gcc的问题,我认为它会用puts("some string");(启用优化等)替换printf("some string\n"); - ilkkachu
@ilkkachu 不仅如此,它还将 printf("%s\n", some_string) 替换为 puts(some_string),如果您认为 %s 被定义为空指针,则这可能非常重要... - Antti Haapala -- Слава Україні

6

警告已经说明了一切。

首先,讨论一下这个问题,根据签名,printf()的第一个参数是一个格式字符串,它可以包含格式说明符(转换说明符)。如果一个字符串包含格式说明符而相应的参数没有提供,它会引发未定义行为

因此,更加简洁(或更安全)的方法(打印不需要格式说明的字符串)是使用puts(s);而不是printf(s);(前者不处理s中的任何转换说明符,消除后一种情况可能出现的UB的原因)。如果你担心puts()自动添加的结束换行符,则可以选择fputs()


关于警告选项,-Wformat-security来自在线gcc手册

目前,它会警告对 printfscanf 函数的调用,其中格式字符串不是字符串字面值,并且没有格式参数,例如 printf (foo);。如果格式字符串来自不受信任的输入并包含 %n,则可能存在安全漏洞。

在您的第一个案例中,仅提供了一个参数给 printf(),它不是一个字符串字面值,而是一个变量,可以在运行时非常好地生成/填充,如果其中包含意外的格式说明符,则可能会调用UB。编译器无法检查其中是否存在任何格式说明符。这就是安全问题所在。

在第二个案例中,提供了附带的参数,格式说明符不是传递给 printf()唯一参数,因此不需要验证第一个参数。因此,没有警告。


更新:

关于第三个问题,需要提供格式字符串所需的额外参数

printf(s, 99, 50);

引用自C11,第§7.21.6.1章节:

[...] 如果格式已经用完,而参数仍然存在,则多余的参数将被评估(如常)但是将被忽略。[...]

因此,从编译器的角度来看,传递多余的参数根本不是问题,并且它也是明确定义的。在这里没有任何警告的余地。


“printf(s)”和“puts(s)”并不等同。首先,“printf”会处理格式说明符。其次,“puts”会添加换行符。 - Keith Thompson
1
@KeithThompson我从未说过它是一个替代品,先生。这是一个更安全的选择,万一字符串包含格式说明符,它们将被puts()忽略。这就是重点。 - Sourav Ghosh
@KeithThompson 关于 _newline_,无论如何都有 fputs()。 :) - Sourav Ghosh
@MikaelMello 只因为你“能够”,并不意味着你必须使用某些东西并做出额外的假设。坚持使用为特定目的设计的函数/ API。你明白我的意思吗?为什么要把程序的安全留给“if,but,then”呢? - Sourav Ghosh
1
@MikaelMello 仅为了阐述,我在第三种情况中添加了更多的解释。如果你想看看的话,请看一下。 :) - Sourav Ghosh
显示剩余3条评论

5
你的问题涉及两个方面。
首先,正如Jonathan Leffler所简洁概括的那样——你收到的警告是因为该字符串不是文字常量,并且其中没有任何格式说明符。
另一个疑问是编译器为什么没有发出警告,指出参数数量与格式说明符数量不匹配。简短的回答是“因为它没有发出警告”,但更具体地说,printf是一个变参函数。在初始格式说明符之后,它可以接受任意数量的参数——从0开始。编译器无法检查您是否提供了正确的数量;这取决于printf函数本身,并导致Joachim在评论中提到的未定义行为。
编辑: 我将进一步回答你的问题,同时也是为了表达我的观点。 printf(s)和printf(“%s”,s)有什么区别?很简单——在后者中,你使用了printf声明的方式。"%s"是const char *,因此不会产生警告消息。
在回答其他问题的评论中,你提到了“忽略警告...”。不要这样做。警告是有原因的,应该加以解决(否则它们只是噪音,你会错过那些真正重要的警告,而被无用的警告淹没。)
你的问题可以通过几种方式解决。
const char* s = "abcdefghij\n";
printf(s);

这个警告会被解决,因为您现在使用的是const指针,并且没有Jonathan提到的任何危险。您也可以声明它为const char * const s,但不需要这样做。第一个const很重要,因为它与printf的声明相匹配,因为const char * s表示s所指向的字符不能改变,即该字符串是文字常量。

或者,更简单的方法,只需执行以下操作:

printf("abcdefghij\n");

这是一个隐式的常量指针,这也不是一个问题。


关于const char*char * const的问题,请参考https://dev59.com/0HM_5IYBdhLWcg3w-4dg。 - Scott Mermelstein
嗯,“编译器无法检查您是否提供了正确的参数数量” - 但是 gcc 可以:尝试使用 printf(“%d \ n”);printf(“%d \ n”,123,456); (分别使用-Wformat和-Wformat-extra-args,这两个都由gcc 4.9.2上的-Wall设置(至少在我的系统上是这样))。 - ilkkachu
此外,格式字符串的常量性并不影响它... const char* s = "abcdefghij\n"; printf(s); 会产生(使用 -Wformat-security):printf.c:8:2: warning: format not a string literal and no format arguments [-Wformat-security] -- 显然(正如它所说),只有在它是字面值时才检查格式字符串的内容。如果我必须猜测,这是一个实现细节(不需要在程序中跟随字符串的内容)。 - ilkkachu

3
“printf(s)”将把s作为格式字符串。如果s包含格式说明符,则printf将解释它们并寻找变量参数。由于实际上不存在变量参数,这可能会触发未定义的行为。如果攻击者控制“s”,那么这很可能是一个安全漏洞。printf(“%s”,s)只会打印字符串中的内容。
警告是在捕获危险的愚蠢和不产生太多噪音之间取得平衡。C程序员习惯使用printf和各种类似printf的函数*作为通用打印函数,即使他们实际上不需要格式化。在这种环境中,某人很容易犯错误,写出printf(s),而不考虑s来自哪里。由于没有数据进行格式化,格式化几乎没有任何用处,因此printf(s)几乎没有合法用途。另一方面,printf(s,format,arguments)表示程序员有意进行格式化。据我所知,在上游gcc中默认情况下未启用此警告,但一些发行版正在启用它作为减少安全漏洞的努力的一部分。*标准C函数(如sprintf和fprintf)和第三方库中的函数。

2
根本原因是:printf的声明如下:
int printf(const char *fmt, ...) __attribute__ ((format(printf, 1, 2)));

这里告诉gcc,printf是一个类似printf风格接口的函数,其中格式字符串首先出现。在我看来,它必须是字面值;我不认为有一种方法可以告诉好的编译器s实际上是指向它之前看到的一个字面字符串的指针。 在这里阅读更多有关 __attribute__ 的信息。

1
“IMHO它必须是字面值”--虽然通常是个好主意,但格式字符串不一定需要是字面值。 - Keith Thompson
当然。但这是对原始问题的直接回答:“printf(s)printf("%s", s)之间的根本区别是什么,为什么只有一个情况下会收到警告?”您会收到警告,因为printf被声明为__attribute__((format(...,并且第一个参数是文字。这全部发生在编译时。在运行时,printf只看到一个指针,无论是以"%s"还是s的形式传递的。 - Andreas Spindler
这个表述非常令人困惑。格式字符串不一定是字面值,只是编译器只能检查它(这就是__attribute__((format))的作用),如果它确实是字面值。 - ilkkachu
是的。正是我所说的。 - Andreas Spindler

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