问题很简单,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)
之间的根本区别是什么?为什么我只会在一个案例中收到警告?
答案 0 :(得分:25)
在第一种情况下,非文字格式字符串可能来自用户代码或用户提供的(运行时)数据,在这种情况下,它可能包含%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 :(得分:6)
警告说明了一切。
首先,要讨论问题,根据签名,printf()
的第一个参数是格式字符串,可以包含格式说明符(< em>转换说明符)。如果 string 包含格式说明符且未提供相应的参数,则会调用undefined behavior。
因此,更清晰(或更安全)方法(打印不需要格式规范的字符串)将puts(s);
超过printf(s);
(前者不会为任何转换说明符处理s
,在后一种情况下删除可能的UB的原因)。如果您担心fputs()
中自动添加的结尾换行符,您可以选择puts()
。
也就是说,关于警告选项,-Wformat-security
from the online gcc manual
目前,这会警告调用
printf
和scanf
函数,其中格式字符串不是字符串文字,并且没有格式参数,如{{ 1}}。如果格式字符串来自不受信任的输入并包含printf (foo);
,则可能是安全漏洞。
在你的第一种情况下,只有一个参数提供给%n
,它不是字符串文字,而是变量,它可以在运行时很好地生成/填充,如果它包含意外的格式说明符,它可能会调用UB。编译器无法检查其中是否存在任何格式说明符。那是那里的安全问题。
在第二种情况下,提供了附带的参数,格式说明符不是传递给printf()
的唯一参数,因此第一个参数不需要验证。因此警告不存在。
更新
关于第三个,使用提供的格式字符串
所需的多余参数printf()
引自printf(s, 99, 50);
,章节§7.21.6.1
[...]如果参数保留时格式已用尽,则多余的参数为 评估(一如既往),否则被忽略。 [...]
因此,传递多余的参数根本不是问题(从编译器的角度来看)并且定义良好。没有任何警告的范围。
答案 2 :(得分: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指针,也不是问题。
答案 3 :(得分:2)
根本原因:printf
被声明为:
int printf(const char *fmt, ...) __attribute__ ((format(printf, 1, 2)));
这告诉gcc printf
是一个带有printf样式接口的函数,其中格式字符串首先出现。恕我直言,它必须是字面的;我不认为有一种方法可以告诉好的编译器s
实际上是一个它以前见过的文字字符串的指针。
详细了解__attribute__
here。
答案 4 :(得分:2)
那么printf(s)和printf(&#34;%s&#34;,s)之间的根本区别是什么
&#34; printf的(一个或多个)&#34;将s视为格式字符串。如果s包含格式说明符,那么printf将解释它们并继续查找varargs。由于实际上不存在varargs,因此可能会触发未定义的行为。
如果攻击者控制了&#34; s&#34;那么这可能是一个安全漏洞。
printf(&#34;%s&#34;,s)将只打印字符串中的内容。
为什么我只在一个案例中收到警告?
警告是在捕捉危险的愚蠢与不产生太多噪音之间取得平衡。
C程序员正处于使用printf和各种类似printf的函数*作为通用打印功能的习惯,即使他们实际上并不需要格式化。在这种环境下,如果有人在不考虑s来源的情况下编写printf(s)的错误,就很容易。由于没有任何数据来格式化printf(s)几乎没有合法用途,格式化是相当无用的。
另一方面,printf(s,format,arguments)表示程序员故意打算进行格式化。
默认情况下,在上游gcc中没有打开此警告,但是有些发行版正在将其打开,以减少安全漏洞。
*标准C函数如sprintf和fprintf以及第三方库中的函数。