为啥不推荐使用带有单个参数(没有转换说明符)的 printf?

Posted

技术标签:

【中文标题】为啥不推荐使用带有单个参数(没有转换说明符)的 printf?【英文标题】:Why is printf with a single argument (without conversion specifiers) deprecated?为什么不推荐使用带有单个参数(没有转换说明符)的 printf? 【发布时间】:2015-09-26 06:09:03 【问题描述】:

在我正在阅读的一本书中,不推荐使用带有单个参数(没有转换说明符)的 printf。建议替换

printf("Hello World!");

puts("Hello World!");

printf("%s", "Hello World!");

有人能告诉我为什么printf("Hello World!"); 是错的吗?书中写到它包含漏洞。这些漏洞是什么?

【问题讨论】:

注意:printf("Hello World!") puts("Hello World!") 相同。 puts() 附加一个 '\n'。而是将printf("abc")fputs("abc", stdout) 进行比较 那本书是什么?我不认为 printf 的弃用方式与 C99 中弃用 gets 的方式相同,因此您可以考虑更准确地编辑您的问题。 听起来你正在读的书不是很好——一本好书不应该只是说这样的东西是“已弃用”(这实际上是错误的,除非作者使用这个词来描述他们的自己的观点)并且应该解释什么用法实际上是无效和危险的,而不是显示安全/有效的代码作为你“不应该做”的事情的例子。 你能认出这本书吗? 请注明书名、作者和参考页码。谢谢。 【参考方案1】:

printf("Hello World!"); 恕我直言不易受攻击,但请考虑一下:

const char *str;
...
printf(str);

如果str 恰好指向包含%s 格式说明符的字符串,您的程序将表现出未定义的行为(主要是崩溃),而puts(str) 将按原样显示字符串。

例子:

printf("%s");   //undefined behaviour (mostly crash)
puts("%s");     // displays "%s\n"

【讨论】:

除了导致程序崩溃之外,格式字符串还有许多其他可能的漏洞。请参阅此处了解更多信息:en.wikipedia.org/wiki/Uncontrolled_format_string 另一个原因是puts 可能会更快。 @black:puts“大概”更快,这可能是人们推荐它的另一个原因,但它实际上并不快。我刚刚打印了"Hello, world!" 1,000,000 次,双向。使用printf 需要 0.92 秒。使用 puts 需要 0.93 秒。在效率方面有些事情需要担心,但 printfputs 不是其中之一。 @KonstantinWeitz:但是(a)我没有使用 gcc,并且(b)没关系为什么声称“puts 更快”是错误的,它仍然是错误的。 gcc 自动将printf 转换为puts 当只有一个参数时,格式字符串不包含任何%-field,并以'\n' 结束。无需为此激活优化。看看gcc -S生成的汇编代码就知道了。【参考方案2】:

printf("Hello world");

很好,没有安全漏洞。

问题出在:

printf(p);

其中p 是指向由用户控制的输入的指针。容易出现format strings attacks:用户可以插入转换规范来控制程序,例如%x转储内存或%n覆盖内存。

请注意,puts("Hello world") 在行为上不等同于printf("Hello world"),而是等同于printf("Hello world\n")。编译器通常足够聪明,可以优化后一个调用以将其替换为puts

【讨论】:

当然,如果用户可以控制pprintf(p,x) 也会有问题。所以问题是不是只使用一个参数的printf,而是使用用户控制的格式字符串。 @HagenvonEitzen 这在技术上是正确的,但很少有人会故意使用用户提供的格式字符串。当人们写printf(p) 时,这是因为他们没有意识到这是一个格式字符串,他们只是认为他们正在打印一个文字。【参考方案3】:

除了其他答案之外,printf("Hello world! I am 50% happy today") 是一个容易犯的错误,可能会导致各种令人讨厌的内存问题(它是 UB!)。

“要求”程序员绝对清楚何时需要逐字字符串而不需要其他任何东西,这更简单、更容易、更健壮。

这就是printf("%s", "Hello world! I am 50% happy today") 带给你的。这完全是万无一失的。

(Steve,当然printf("He has %d cherries\n", ncherries) 绝对不是一回事;在这种情况下,程序员不是“逐字字符串”的心态;她是“格式字符串”的心态。)

【讨论】:

这不值得争论,我理解你所说的逐字与格式字符串的思维方式,但是,不是每个人都这么想,这是一个大小的原因 -千篇一律的规则会激怒。说“从不使用printf 打印常量字符串”几乎就像说“总是写if(NULL == p)”。这些规则可能对某些程序员有用,但不是全部。在这两种情况下(不匹配的printf 格式和Yoda 条件),现代编译器无论如何都会警告错误,因此人为规则就更不重要了。 @Steve 如果使用某些东西的好处完全为零,但有很多缺点,那么是的,真的没有理由使用它。另一方面,Yoda 条件 确实 的缺点是它们使代码更难阅读(你会直观地说“如果 p 为零”而不是“如果零是 p”)。 @Voo printf("%s", "hello") 会比 printf("hello") 慢,所以有一个缺点。一个小的,因为 IO 几乎总是比这种简单的格式化慢得多,但有一个缺点。 @Yakk 我怀疑这会更慢 gcc -Wall -W -Werror 将防止此类错误造成的不良后果。【参考方案4】:

我将在此处添加一些关于漏洞部分的信息。

据说由于 printf 字符串格式漏洞而易受攻击。在您的示例中,字符串是硬编码的,它是无害的(即使从未完全推荐这样的硬编码字符串)。但是指定参数的类型是一个好习惯。举个例子:

如果有人在您的 printf 中放入格式字符串字符而不是常规字符串(例如,如果您想打印程序 stdin),printf 将在堆栈中获取任何他能做到的。

它曾经(现在仍然)非常用于利用程序来探索堆栈以访问隐藏信息或绕过身份验证。

示例(C):

int main(int argc, char *argv[])

    printf(argv[argc - 1]); // takes the first argument if it exists

如果我输入这个程序"%08x %08x %08x %08x %08x\n"

printf ("%08x %08x %08x %08x %08x\n"); 

这指示 printf 函数从堆栈中检索五个参数并将它们显示为 8 位填充的十六进制数字。因此可能的输出可能如下所示:

40012980 080628c4 bffff7a4 00000005 08059c04

请参阅this 以获得更完整的说明和其他示例。

【讨论】:

【参考方案5】:

使用文字格式字符串调用printf 是安全有效的,而且 如果您使用用户调用printf,存在自动警告您的工具 提供的格式字符串是不安全的。

printf 最严重的攻击利用%n 格式 说明符。与所有其他格式说明符相反,例如%d, %n 实际上 将值写入格式参数之一中提供的内存地址。 这意味着攻击者可以覆盖内存,因此可能会占用 控制你的程序。 Wikipedia 提供更多细节。

如果您使用文字格式字符串调用printf,攻击者无法潜行 %n 到您的格式字符串中,因此您是安全的。实际上, gcc 会将您对printf 的呼叫更改为对puts 的呼叫,所以有一些 没有任何区别(通过运行gcc -O3 -S 进行测试)。

如果您使用用户提供的格式字符串调用printf,攻击者可以 可能将%n 潜入您的格式字符串,并控制您的 程序。您的编译器通常会警告您他的不安全,请参阅 -Wformat-security。还有更高级的工具可确保 即使使用用户提供的格式字符串,printf 的调用也是安全的,并且 他们甚至可能会检查您是否将正确数量和类型的参数传递给 printf。例如,对于 Java,有 Google's Error Prone 和Checker Framework。

【讨论】:

【参考方案6】:

这是错误的建议。是的,如果你有一个运行时字符串要打印,

printf(str);

很危险,你应该经常使用

printf("%s", str);

相反,因为通常您永远无法知道str 是否可能包含% 符号。但是,如果你有一个编译时 constant 字符串,那么

没有任何问题
printf("Hello, world!\n");

(除此之外,这是有史以来最经典的 C 程序,从字面上看是《创世纪》的 C 编程书籍。所以任何反对这种用法的人都是相当异端的,我对此会有些冒犯!)

【讨论】:

because printf's first argument is always a constant string 我不太清楚你的意思。 正如我所说,"He has %d cherries\n" 是一个常量字符串,这意味着它是一个编译时常量。但是,公平地说,作者的建议不是“不要将常量字符串作为printf 的第一个参数传递”,而是“不要将没有% 的字符串作为printf 的第一个参数传递”。 literally from the C programming book of Genesis. Anyone deprecating that usage is being quite offensively heretical - 近年来你实际上没有读过 K&R。那里有大量的建议和编码风格,它们不仅已被弃用,而且现在只是很糟糕的做法。 @Voo:好吧,我们只是说,并非所有被认为是不好的做法实际上都是不好的做法。 (我想到了“永远不要使用普通的int”的建议。) @Steve 我不知道你是从哪里听到的,但这肯定不是我们所说的那种坏(坏?)做法。不要误解我,因为当时代码非常好,但你真的不想看 k&r 太多,而是现在作为历史记录。如今,“它在 k&r 中”并不是质量好坏的指标,仅此而已【参考方案7】:

printf 的一个相当讨厌的方面是,即使在杂散内存读取只能造成有限(且可接受的)伤害的平台上,其中一个格式化字符 %n 会导致下一个参数被解释为指向可写整数的指针,并导致将迄今为止输出的字符数存储到由此标识的变量中。我自己从未使用过该功能,有时我会使用我编写的轻量级 printf 风格的方法,这些方法仅包含我实际使用的功能(并且不包括那个或类似的功能),但提供接收到的标准 printf 函数字符串来自不可靠来源的安全漏洞可能会暴露出无法读取任意存储空间的安全漏洞。

【讨论】:

【参考方案8】:

由于没有人提及,我会添加一个关于他们的表现的注释。

在正常情况下,假设没有使用编译器优化(即printf() 实际上调用printf() 而不是fputs()),我预计printf() 的执行效率会降低,尤其是对于长字符串。这是因为printf() 必须解析字符串以检查是否有任何转换说明符。

为了确认这一点,我进行了一些测试。测试是在 Ubuntu 14.04 上使用 gcc 4.8.4 进行的。我的机器使用 Intel i5 cpu。正在测试的程序如下:

#include <stdio.h>
int main() 
    int count = 10000000;
    while(count--) 
        // either
        printf("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM");
        // or
        fputs("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM", stdout);
    
    fflush(stdout);
    return 0;

两者都使用gcc -Wall -O0 编译。使用time ./a.out &gt; /dev/null 测量时间。以下是典型运行的结果(我已经运行了五次,所有结果都在 0.002 秒内)。

对于printf() 变体:

real    0m0.416s
user    0m0.384s
sys     0m0.033s

对于fputs() 变体:

real    0m0.297s
user    0m0.265s
sys     0m0.032s

如果你有一个非常长的字符串,这个效果会被放大。

#include <stdio.h>
#define STR "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM"
#define STR2 STR STR
#define STR4 STR2 STR2
#define STR8 STR4 STR4
#define STR16 STR8 STR8
#define STR32 STR16 STR16
#define STR64 STR32 STR32
#define STR128 STR64 STR64
#define STR256 STR128 STR128
#define STR512 STR256 STR256
#define STR1024 STR512 STR512
int main() 
    int count = 10000000;
    while(count--) 
        // either
        printf(STR1024);
        // or
        fputs(STR1024, stdout);
    
    fflush(stdout);
    return 0;

对于printf() 变体(运行 3 次,实际加/减 1.5 秒):

real    0m39.259s
user    0m34.445s
sys     0m4.839s

对于fputs() 变体(运行 3 次,实数正负 0.2 秒):

real    0m12.726s
user    0m8.152s
sys     0m4.581s

注意:检查 gcc 生成的程序集后,我意识到 gcc 将 fputs() 调用优化为 fwrite() 调用,即使使用 -O0 也是如此。 (printf() 调用保持不变。)我不确定这是否会使我的测试无效,因为编译器会在编译时计算 fwrite() 的字符串长度。

【讨论】:

它不会使您的测试无效,因为fputs() 通常与字符串常量一起使用,并且优化机会是您想要提出的重点的一部分。也就是说,添加一个动态的测试运行使用fputs()fprintf() 生成的字符串将是一个很好的补充数据点。 @PatrickSchlüter 使用动态生成的字符串进行测试似乎违背了这个问题的目的...... OP 似乎只对要打印的字符串文字感兴趣。 即使他的示例使用字符串文字,他也没有明确说明。事实上,我认为他对本书建议的困惑是由于示例中使用了字符串文字。对于字符串文字,书籍的建议有点可疑,对于动态字符串,这是很好的建议。 /dev/null 有点让它成为一个玩具,因为通常在生成格式化输出时,您的目标是输出到某个地方,而不是被丢弃。一旦你加上“实际上没有丢弃数据”的时间,它们如何比较?【参考方案9】:

对于 gcc,可以启用特定警告以检查 printf()scanf()

gcc 文档指出:

-Wformat 包含在-Wall 中。为了更好地控制某些方面 格式检查,选项-Wformat-y2k-Wno-format-extra-args, -Wno-format-zero-length, -Wformat-nonliteral-Wformat-security-Wformat=2 是 可用,但不包含在-Wall 中。

-Wall 选项中启用的-Wformat 不会启用有助于查找这些情况的几个特殊警告:

-Wformat-nonliteral 会在你不传递字符串作为格式说明符时发出警告。 -Wformat-security 将在您传递可能包含危险构造的字符串时发出警告。它是-Wformat-nonliteral 的子集。

我不得不承认,启用-Wformat-security 揭示了我们代码库中的几个错误(日志记录模块、错误处理模块、xml 输出模块,如果它们被调用时使用 % 字符在他们的参数。作为信息,我们的代码库现在已经有 20 年的历史了,即使我们意识到这些问题,当我们启用这些警告时,我们还是感到非常惊讶,有多少这些错误仍然存​​在于代码库中。

【讨论】:

【参考方案10】:
printf("Hello World\n")

自动编译成等价物

puts("Hello World")

你可以通过反汇编你的可执行文件来检查它:

push rbp
mov rbp,rsp
mov edi,str.Helloworld!
call dword imp.puts
mov eax,0x0
pop rbp
ret

使用

char *variable;
... 
printf(variable)

会导致安全问题,永远不要那样使用 printf!

所以你的书实际上是正确的,不推荐使用带有一个变量的 printf 但你仍然可以使用 printf("my string\n") 因为它会自动变成 puts

【讨论】:

这种行为实际上完全取决于编译器。 这是误导。你说A compiles to B,但实际上你的意思是A and B compile to C【参考方案11】:

除了包含任何附带问题的其他解释清楚的答案之外,我想对所提供的问题给出一个准确而简洁的答案。


为什么不推荐使用带有单个参数(没有转换说明符)的 printf

带有单个参数的printf 函数调用通常被弃用,并且正确使用时也没有漏洞,因为您总是应该编码。

全世界的C用户,从状态初学者到状态专家,都使用printf这种方式将简单的文本短语作为输出到控制台。

此外,有人必须区分这个唯一的参数是字符串文字还是指向字符串的指针,这是有效但通常不使用的。当然,对于后者,当指针未正确设置为指向有效字符串时,可能会出现不方便的输出或任何类型的Undefined Behavior,但如果格式说明符与相应的参数不匹配,这些事情也会发生给出多个参数。

当然,作为唯一参数提供的字符串具有任何格式或转换说明符也是不正确的,因为不会发生转换。

也就是说,提供一个简单的字符串文字,如 "Hello World!" 作为唯一参数,该字符串内没有任何格式说明符,就像您在问题中提供的那样:

printf("Hello World!");

完全没有被弃用或“不良做法”,也没有任何漏洞。

事实上,许多 C 程序员开始并开始学习和使用 C 甚至一般的编程语言,使用 HelloWorld 程序和 printf 语句是同类中的第一个。

如果它们被弃用,它们就不会那样了。

在我正在阅读的一本书中,写到不推荐使用带有单个参数(没有转换说明符)的 printf

好吧,那么我将重点放在书或作者本身上。如果作者真的这样做,在我看来,不正确断言,甚至在没有明确解释为什么他/她这样做的情况下教导他/她这样做(如果这些断言真的在字面上是等价的在那本书中提供),我会认为它是一本糟糕的书。一本的书,与此相反,应该解释为什么来避免某些类型的编程方法或功能。

根据我上面所说,使用printf 仅带有一个参数(字符串文字)并且没有任何格式说明符在任何情况下都不会被弃用或被视为“不好的做法".

您应该询问作者,他的意思是什么,或者更好的是,请提醒他澄清或更正下一版的相关部分或一般印记。

【讨论】:

您可能会补充说,printf("Hello World!"); 无论如何等同于 puts("Hello World!");,这说明了推荐的作者。

以上是关于为啥不推荐使用带有单个参数(没有转换说明符)的 printf?的主要内容,如果未能解决你的问题,请参考以下文章

为啥在一种情况下会收到带有字符串文字的不推荐使用的转换警告,而在另一种情况下却没有?

为啥 Java 中不推荐将 Optional 当做参数使用?

为啥 Java 中不推荐将 Optional 当做参数使用?

为啥不推荐使用 JSX 转换?与 JSX 转换相比,使用 Babel 有啥优势?

为啥发送单个参数的函数调用会突然收到整个元素?

为啥 std:set (带有单个冒号)可以编译?