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

What is the underlying difference between printf(s) and printf("%s", s)?

问题简单明了,s是一个字符串,我突然想到用printf(s)试试看能不能行,有一次收到了警告, none在另一个

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) 之间的根本区别是什么?为什么我只在一种情况下收到警告?

警告说明了一切。

首先,讨论问题,根据签名,printf()的第一个参数是格式字符串,可以 包含格式说明符(转换说明符)。如果 string 包含格式说明符并且未提供相应的参数,它会调用 undefined behavior.

因此,cleaner或更安全)方法(打印不需要格式规范的字符串)将是puts(s); 超过 printf(s);前者不处理任何转换说明符的 s,消除了后一种情况下可能出现 UB 的原因 )。如果您担心 puts() 中自动添加的结尾换行符,您可以选择 fputs()


也就是说,关于警告选项,-Wformat-security from the online gcc manual

At present, this warns about calls to printf and scanf functions where the format string is not a string literal and there are no format arguments, as in printf (foo);. This may be a security hole if the format string came from untrusted input and contains %n.

在你的第一种情况下,只有一个参数提供给 printf(),它不是字符串文字,而是一个 变量,可以很好地生成/在 运行 时间填充,如果包含 unexpected 格式说明符,它可能会调用 UB。编译器无法检查其中是否存在任何格式说明符。那就是那里的安全问题。

在第二种情况下,提供了附带的参数,格式说明符不是传递给 printf() 只有 参数,因此第一个参数不需要是验证。因此警告不存在。


更新:

关于第三个,提供的格式字符串

需要excess参数
printf(s, 99, 50);

引自 C11,章节 §7.21.6.1

[...] If the format is exhausted while arguments remain, the excess arguments are evaluated (as always) but are otherwise ignored. [...]

因此,传递 excess 参数根本不是问题(从编译器的角度来看)并且定义明确。那里没有任何警告的范围。

在第一种情况下,非文字格式字符串可能来自用户代码或用户提供的(运行-时间)数据,在这种情况下它可能包含 %s 或其他转换规范,您尚未为其传递数据。这可能会导致各种阅读问题(如果字符串包含 %n,则可能会导致写入问题 — 请参阅 printf() 或您的 C 库的手册页)。

在第二种情况下,格式字符串控制输出,要打印的任何字符串是否包含转换规范都无关紧要(尽管显示的代码打印的是整数,而不是字符串)。编译器(问题中使用了 GCC 或 Clang)假定因为(非文字)格式字符串后有参数,所以程序员知道它们在做什么。

第一个是 'format string' 漏洞。您可以搜索有关该主题的更多信息。

GCC 知道大多数情况下,带有非文字格式字符串的单个参数 printf() 会招来麻烦。您可以改用 puts()fputs()。 GCC 以最少的挑衅生成警告就足够危险了。

如果您不小心,非文字格式字符串的更普遍问题也可能成为问题 — 但如果您小心的话,它会非常有用。你必须更加努力才能让 GCC 投诉:它需要 -Wformat-Wformat-nonliteral 才能得到投诉。

来自评论:

So ignoring the warning, as if I really know what I am doing and there will be no errors, is one or another more efficient to use or are they the same? Considering both space and time.

在你的三个 printf() 语句中,鉴于变量 s 是在调用上方立即分配的紧密上下文,没有实际问题。但是你可以使用 puts(s) 如果你从字符串中省略换行符或者 fputs(s, stdout) 原样并得到相同的结果,而不需要 printf() 解析整个字符串以找出它的开销都是要打印的简单字符。

第二个printf()语句也是安全的;格式字符串匹配传递的数据。这与简单地将格式字符串作为文字传递之间没有显着区别——除了编译器可以做更多检查格式字符串是否为文字。 运行次结果相同

第三个 printf() 传递的数据参数比格式字符串需要的多,但这是良性的。不过,这并不理想。同样,编译器可以更好地检查格式字符串是否为文字,但 运行 时间效果几乎相同。

来自顶部链接的 printf() 规范:

Each of these functions converts, formats, and prints its arguments under control of the format. The format is a character string, beginning and ending in its initial shift state, if any. The format is composed of zero or more directives: ordinary characters, which are simply copied to the output stream, and conversion specifications, each of which shall result in the fetching of zero or more arguments. The results are undefined if there are insufficient arguments for the format. If the format is exhausted while arguments remain, the excess arguments shall be evaluated but are otherwise ignored.

在所有这些情况下,没有明显的迹象表明为什么格式字符串不是文字。但是,需要非文字格式字符串的一个原因可能是有时您以 %f 表示法打印浮点数,有时以 %e 表示法打印浮点数,您需要在 运行-时间。 (如果它只是基于值,%g 可能是合适的,但有时您需要显式控制 — 总是 %e 或总是 %f。)

你的问题有两点在起作用。

第一个由 简洁地介绍 - 您收到的警告是因为字符串不是文字,并且其中没有任何格式说明符。

另一个谜团是编译器为什么不发出参数数量与说明符数量不匹配的警告。简短的回答是 "because it doesn't," 但更具体地说, printf 是一个可变参数函数。它在初始格式规范之后接受任意数量的参数——从 0 开始。编译器无法检查您是否提供了正确的数量;这取决于 printf 函数本身,并导致 Joachim 在评论中提到的未定义行为。

编辑: 我将进一步回答你的问题,作为获得小肥皂盒的一种方式。

printf(s)printf("%s", s) 有什么区别?简单 - 在后者中,您使用的是声明的 printf。 "%s" 是一个 const char *,它随后不会生成警告消息。

在您对其他答案的评论中,您提到了 "Ignoring the warning..."。不要这样做。警告的存在是有原因的,应该加以解决(否则它们只是噪音,你会错过所有警告中真正重要的警告。)

您的问题可以通过多种方式解决。

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

将解决警告,因为您现在使用的是 const 指针,并且存在 Jonathan 提到的 none 危险。 (您也可以将其声明为 const char* const s,但不必如此。第一个 const 很重要,因为它随后匹配 printf 的声明,并且因为 const char* s意味着 s 指向的字符不能改变,即字符串是文字。)

或者,更简单的做法是:

printf("abcdefghij\n");

这是隐含的 const 指针,也不是问题。

根本原因:printf 声明如下:

int printf(const char *fmt, ...) __attribute__ ((format(printf, 1, 2)));

这告诉 gcc printf 是一个带有 printf 风格接口的函数,格式字符串在前面。恕我直言,它必须是字面的;我认为没有办法告诉优秀的编译器 s 实际上是指向它之前见过的文字字符串的指针。

详细了解 __attribute__ here

So what's the underlying difference between printf(s) and printf("%s", s)

"printf(s)" 会将 s 视为格式字符串。如果 s 包含格式说明符,则 printf 将解释它们并寻找可变参数。由于实际不存在可变参数,这可能会触发未定义的行为。

如果攻击者控制 "s" 那么这很可能是一个安全漏洞。

printf("%s",s) 将只打印字符串中的内容。

and why do I get a warning in just one case?

警告是捕捉危险的愚蠢行为和不制造太多噪音之间的平衡。

C 程序员习惯于使用 printf 和各种类似 printf 的函数* 作为通用打印函数,即使它们实际上并不需要格式化。在这种环境下,有人很容易在不考虑 s 来自哪里的情况下犯下编写 printf(s) 的错误。由于没有任何数据格式化是非常无用的,因此 printf(s) 几乎没有合法用途。

printf(s,format,arguments) 另一方面表明程序员有意进行格式化。

Afaict 这个警告在上游 gcc 中默认没有打开,但是一些发行版正在打开它作为他们减少安全漏洞的努力的一部分。

* sprintf 和 fprintf 等标准 C 函数以及第三方库中的函数。