在我正在阅读的书中,写printf
有一个参数(不带转换说明符)已被弃用。建议替代
printf("Hello World!");
与
puts("Hello World!");
要么
printf("%s", "Hello World!");
有人可以告诉我为什么printf("Hello World!");
错了吗?它写在书中,其中包含漏洞。这些漏洞是什么?
printf
弃用的方式gets
与C99中弃用的方式不同,因此您可以考虑更精确地编辑问题。
在我正在阅读的书中,写printf
有一个参数(不带转换说明符)已被弃用。建议替代
printf("Hello World!");
与
puts("Hello World!");
要么
printf("%s", "Hello World!");
有人可以告诉我为什么printf("Hello World!");
错了吗?它写在书中,其中包含漏洞。这些漏洞是什么?
printf
弃用的方式gets
与C99中弃用的方式不同,因此您可以考虑更精确地编辑问题。
Answers:
printf("Hello World!");
恕我直言不易受攻击,但请考虑以下因素:
const char *str;
...
printf(str);
如果str
碰巧指向包含%s
格式说明符的字符串,则您的程序将表现出未定义的行为(通常是崩溃),而puts(str)
只会按原样显示该字符串。
例:
printf("%s"); //undefined behaviour (mostly crash)
puts("%s"); // displays "%s\n"
puts
可能是速度更快。
puts
“大概”更快,这可能是人们推荐它的另一个原因,但实际上并不快。"Hello, world!"
两种方式我都只打印了1,000,000次。用printf
了0.92秒。用puts
了0.93秒。在效率方面,有些事情值得担心,但是printf
vs. puts
并不是其中之一。
puts
更快” 的说法是假的也没关系,但它仍然是假的。
puts
为此担心。(但是,如果您想对此进行争论:如果能为任何puts
比printf
任何情况下都快得多的现代机器找到任何现代编译器,我会感到惊讶。)
printf("Hello world");
很好,没有安全漏洞。
问题在于:
printf(p);
其中p
是由用户控制的输入的指针。它很容易受到 格式字符串的攻击:用户可以插入转换规范来控制程序,例如%x
转储内存或%n
覆盖内存。
请注意,puts("Hello world")
是不是在行为等同printf("Hello world")
,但到printf("Hello world\n")
。编译器通常很聪明,可以优化后一个调用以将其替换为puts
。
printf(p,x)
,如果用户可以控制,同样会出现问题p
。因此,问题不在于仅使用printf
一个参数,而是使用用户控制的格式字符串。
printf(p)
,是因为他们没有意识到它是格式字符串,所以他们只是认为自己正在打印文字。
除了其他答案之外,printf("Hello world! I am 50% happy today")
还有一个容易犯的错误,有可能导致各种讨厌的内存问题(这是UB!)。
“要求”程序员在需要逐字字符串而不是其他任何东西时必须绝对清楚,这变得更加简单,容易和强大。
那就是printf("%s", "Hello world! I am 50% happy today")
让你得到的。完全是万无一失的。
(当然,史蒂夫printf("He has %d cherries\n", ncherries)
绝对不是同一回事;在这种情况下,程序员不是“普通字符串”思维方式的;她是“格式字符串”思维方式的。)
printf
”就像说“总是写if(NULL == p)
。这些规则可能对某些程序员有用,但并不是全部。在两种情况下(printf
格式不匹配和Yoda条件匹配),现代编译器无论如何都会警告错误,因此,人为规则甚至不那么重要
printf("%s", "hello")
将会比慢printf("hello")
,因此存在不利之处。很小,因为IO总是比这种简单的格式化慢得多,但是有一个缺点。
gcc -Wall -W -Werror
可以避免此类错误带来的严重后果。
我将在此处添加一些有关漏洞部分的信息。
据说由于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
请参阅此内容以获得更完整的说明和其他示例。
printf
使用文字格式的字符串进行调用既安全又高效,并且如果printf
使用用户提供的格式字符串进行的调用不安全,则存在一些工具可以自动警告您。
最严重的攻击是printf
利用%n
格式说明符。与其他所有格式说明符相反,例如%d
,%n
实际上将值写入格式参数之一中提供的内存地址。这意味着攻击者可以覆盖内存,从而有可能控制程序。维基百科
提供了更多细节。
如果您printf
使用文字格式的字符串进行调用,则攻击者无法将a潜入%n
您的格式字符串中,因此很安全。实际上,gcc会将您的呼叫更改printf
为puts
,从而将您的呼叫更改为,因此从根本上没有任何区别(通过运行进行测试gcc -O3 -S
)。
如果printf
使用用户提供的格式字符串进行调用,则攻击者可能会潜入%n
您的格式字符串中并控制程序。您的编译器通常会警告您他不安全,请参见
-Wformat-security
。还有一些更高级的工具可以确保printf
即使使用用户提供的格式字符串也可以安全地进行调用,并且它们甚至可以检查是否将正确的数字和参数类型传递给
printf
。例如,对于Java,有Google的Error Prone
和Checker Framework。
这是误导的建议。是的,如果您要打印运行时字符串,
printf(str);
非常危险,您应该始终使用
printf("%s", str);
相反,因为通常您永远不知道是否str
可能包含%
符号。但是,如果您有一个编译时常量字符串,则没有任何问题
printf("Hello, world!\n");
(在其他事情中,这是有史以来最经典的C程序,从字面上看是Genesis的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。如今,这里有大量的建议和编码风格,不仅不赞成使用,而且还很普通。
int
想到“永远不要使用简单” 的建议。)
由于没有人提及,因此我将添加有关其性能的注释。
在正常情况下,假设未使用编译器优化(即,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 > /dev/null
。以下是典型运行的结果(我已经运行了5次,所有结果都在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.2s):
real 0m12.726s
user 0m8.152s
sys 0m4.581s
注意:在检查了gcc生成的程序集之后,我意识到gcc 即使使用也会优化对fputs()
调用的fwrite()
调用-O0
。(printf()
调用保持不变。)我不确定这是否会使我的测试无效,因为编译器会在编译时计算字符串长度fwrite()
。
fputs()
(通常与字符串常量一起使用),并且优化机会是您想要进行的工作的一部分,也就是说,使用动态生成的字符串添加一个测试运行,fputs()
并且fprintf()
将是一个很好的补充数据点。
/dev/null
有点像玩具,因为通常在生成格式化输出时,您的目标是使输出到达某个地方,而不是被丢弃。一旦添加了“实际上不丢弃数据”的时间,它们将如何比较?
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
。
对于gcc,可以启用检查printf()
和警告的特定警告scanf()
。
gcc文档指出:
-Wformat
包含在中-Wall
。在过去的检查中,选择格式的某些方面更多的控制-Wformat-y2k
,-Wno-format-extra-args
,-Wno-format-zero-length
,-Wformat-nonliteral
,-Wformat-security
,和-Wformat=2
可用,但不包括在-Wall
。
将-Wformat
其在中启用-Wall
选项,不会使一些特殊的警告,帮助找到这些情况:
-Wformat-nonliteral
如果您没有传递字符串格式的格式说明符,则会发出警告。-Wformat-security
如果您传递的字符串可能包含危险的构造,则会发出警告。这是的子集-Wformat-nonliteral
。我必须承认,启用功能-Wformat-security
揭示了我们代码库中的几个错误(日志记录模块,错误处理模块,xml输出模块,所有这些都有一些函数,如果在参数中使用%字符调用它们,它们可能会执行未定义的操作。有关信息,我们的代码库现在已有20多年的历史了,即使我们意识到了这类问题,当我们启用这些警告时,我们仍然对代码库中仍然有多少个错误感到非常惊讶。
除了涵盖所有附带问题的其他解释清楚的答案之外,我还要对所提供的问题给出准确而简洁的答案。
为什么
printf
不推荐使用单个参数(不带转换说明符)?
一个printf
与一般的一个参数的函数调用时不剔除,也没有漏洞,当使用得当,你总是要编写。
从状态初学者到状态专家printf
,全世界的C用户都使用这种方式将简单的文本短语作为输出输出到控制台。
此外,必须区分一个唯一的参数是字符串文字还是指向字符串的指针,这是有效的,但通常不使用。对于后者,当然,当指针未正确设置为指向有效字符串时,可能会出现不便的输出或任何类型的未定义行为,但是如果格式说明符与给出的参数不匹配,则也会发生这些情况多个参数。
当然,作为唯一参数提供的字符串具有任何格式或转换说明符也是不正确的,因为不会进行转换。
就是说,给一个简单的字符串文字"Hello World!"
作为唯一的参数,而在字符串中没有任何格式说明符,就像您在问题中提供的那样:
printf("Hello World!");
是不是过时或者“不良做法 ”,也没有任何漏洞。
实际上,许多C程序员开始并开始使用该HelloWorld程序来学习和使用C甚至是编程语言。 printf
语句是同类中的第一条。
如果它们被弃用,那不是那样的。
在我正在阅读的书中,写
printf
有一个参数(不带转换说明符)已被弃用。
好吧,那我就把重点放在书或作者本身上。我认为,如果作者确实是在做这样的事情,那是不正确的断言,甚至在没有明确说明他/她这样做的原因的情况下教导他们(如果这些断言在书中字面上确实是等价的),我会认为这是一本不好的书。一个很好的书,而不是说,要解释为什么以避免某种编程方法或功能。
根据我在上面所说的,在任何情况下,不建议printf
仅使用一个参数(字符串文字)且不使用任何格式说明符的情况,也不认为这是“不良做法”。
您应该问作者,他的意思是什至更好,是什么意思,请介意澄清或纠正下一版或总体版本说明的相关部分。
printf("Hello World!");
是不是等同于puts("Hello World!");
无论如何,它告诉一些有关建议的作者。
printf("Hello World!")
与并不相同puts("Hello World!")
。puts()
附加一个'\n'
。取而代之的printf("abc")
是fputs("abc", stdout)