【问题标题】:What is the underlying difference between printf(s) and printf("%s", s)?printf(s) 和 printf("%s", s) 之间的根本区别是什么?
【发布时间】:2017-01-17 20:07:21
【问题描述】:

这个问题很简单,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) 之间的根本区别是什么?为什么我只会在一种情况下收到警告?

【问题讨论】:

  • 真的很惊喜,也很有趣。我确认了这种行为,可能会有解释,但在有人解释之前,我认为这是诊断中的错误。
  • @JoachimPileborg,所以您是说通过不提供字符串文字,编译器无法知道需要多少个参数?所以这就是为什么如果我不再提供参数,我会收到警告,但如果我至少提供一个,我不会。我在这个问题上又添加了一个例子,我想它支持我所说的。
  • 如果你也使用const char* const s,你可能会注意到不同。
  • 另一个区别是编译时分析和优化然后应用 - 或不能。
  • 根本区别在于printf(s) 是一个等待发生的错误(以及潜在的security hole),而printf("%s", s) 只是编写fputs(s, stdout) 的一种低效方式。

标签: c string printf compiler-warnings format-specifiers


【解决方案1】:

警告说明了一切。

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

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


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

目前,这会在调用printfscanf 函数时发出警告,其中格式字符串不是字符串文字,并且没有格式参数,如printf (foo);。如果格式字符串来自不受信任的输入并包含%n,这可能是一个安全漏洞。

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

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


更新:

关于第三个,excess 参数是提供的格式字符串所需要的

printf(s, 99, 50);

引自C11,第 7.21.6.1 章

[...] 如果格式已用尽而参数仍然存在,则多余的参数将 评估(一如既往),但在其他方面被忽略。 [...]

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

【讨论】:

  • printf(s)puts(s) 不等效。首先,当然,printf 处理格式说明符。其次,puts 增加了一个换行符。
  • @KeithThompson 我从来没有说过,它是替代品,先生。这是一个更安全的选择,如果字符串包含格式说明符,puts() 将忽略它们。这就是重点。
  • @KeithThompson 和关于 newline 的问题,还是有fputs()。 :)
  • @SouravGhosh 所以,如果程序员知道他在做什么,也就是说,字符串是“安全的”并且参数是正确的,那没有区别吧?但我认为,如果你有这个,你根本不需要一个字符串作为第一个参数,所以这样使用它没有意义吗?编辑 Jonathan Leffer 刚刚给出了以这种方式使用它的理由。我的观点仍然存在,如果我确定字符串是安全的,就没有问题,对吧?
  • @MikaelMello 只是为了详细说明,我对您添加的第三种情况添加了更多解释。如果你想看看。 :)
【解决方案2】:

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

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

第一个是“格式字符串”漏洞。您可以搜索有关该主题的更多信息。

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

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

来自cmets:

所以忽略警告,就好像我真的知道我在做什么并且不会有错误一样,使用一种或另一种更有效还是它们相同?兼顾空间和时间。

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

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

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

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

这些函数中的每一个都在格式的控制下转换、格式化和打印其参数。 format 是一个字符串,以它的初始移位状态开始和结束,如果有的话。 格式 由零个或多个指令组成:普通字符,它们被简单地复制到输出流,以及转换规范,每一个都将导致获取零个或多个参数。如果格式的参数不足,则结果未定义。如果格式已用尽而参数仍然存在,则应评估多余的参数,否则将被忽略。

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

【讨论】:

  • 我看不出前两段是如何解释这种行为的。在这两种情况下,格式字符串都可能来自用户。
  • 这是一个很好的答案!因此,忽略警告,就好像我真的知道自己在做什么并且不会有错误一样,使用一种或另一种更有效还是它们相同?兼顾空间和时间。
  • @MikaelMello 更有效的方法是使用puts。没有要解析的格式,只是简单的字符输出
  • @EugeneSh。是的。这是编译器必须绘制的一条线。如果它总是抱怨非文字格式字符串,它会使例如国际化消息使用起来很烦人。基于这种行为,我认为它只是假设没有其他参数,您将 printfputs 混为一谈,但有了这些参数,您似乎知道自己在做什么。
  • @MikaelMello,因为这是关于gcc,我认为它用puts("some string"); 替换了printf("some string\n");(启用了优化等)
【解决方案3】:

您的问题涉及两件事。

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

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

编辑: 我将进一步回答你的问题,作为上一个小肥皂盒的一种手段。

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

在您对其他答案的 cmets 中,您提到了“忽略警告...”。不要这样做。警告的存在是有原因的,应该加以解决(否则它们只是噪音,你会错过在所有不重要的东西中真正重要的警告。)

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

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

将解决警告,因为您现在使用的是 const 指针,并且没有 Jonathan 提到的任何危险。 (您也可以将其声明为const char* const s,但不必这样做。第一个const 很重要,因为它与printf 的声明相匹配,并且因为const char* s 意味着s 指向的字符可以'不改变,即字符串是文字。)

或者,更简单,就是这样做:

printf("abcdefghij\n");

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

【讨论】:

  • const char* vs char * const的问题,请参考stackoverflow.com/questions/1143262/…
  • 嗯。 “编译器无法检查您是否提供了正确数量的 [参数]”——但 gcc 可以:尝试使用 printf("%d\n");printf("%d\n", 123, 456);(-Wformat 和 -Wformat -extra-args 分别由 gcc 4.9.2 上的 -Wall 设置(至少在我的系统上)。
  • 另外,格式字符串的常量性并没有出现在其中...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] - 显然(如它所说)它只是检查格式字符串的内容是否是文字。如果我不得不猜测,这是一个实现细节(不必为此通过程序跟踪字符串的内容。)
【解决方案4】:

根本原因:printf 声明如下:

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

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

阅读更多关于__attribute__here的信息。

【讨论】:

  • "恕我直言,它必须是文字" -- 格式字符串不要求是文字(尽管这通常是个好主意)。
  • 当然。但这是对原始问题的直接回答:“那么printf(s)printf("%s", s) 之间的根本区别是什么,为什么我只会在一种情况下收到警告?”您会收到警告,因为 printf 是用 __attribute__((format(... 声明的,并且当第一个参数是文字时。它发生在编译时。在运行时printf 只看到一个指针,无论是以"%s" 还是s 的形式传递。
  • 呃,这至少令人困惑。格式字符串 have 不是文字,只是编译器只能检查它(这就是 __attribute__((format)) 告诉它做的事情),如果它 is文字。
  • 是的。正是所说的。
【解决方案5】:

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

"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)以及第三方库中的函数。

【讨论】:

    猜你喜欢
    • 2015-01-05
    • 2014-02-14
    • 2015-02-15
    • 2020-02-13
    • 2019-12-12
    • 2011-12-15
    • 2016-10-01
    • 1970-01-01
    相关资源
    最近更新 更多