%s的printf()的不同运行时检查

Different run-time checking of printf() for %s?

本文关键字:运行时 检查 printf      更新时间:2023-10-16

我知道,要正确使用printf(),我们需要根据const char* format中定义的内容,即%s/%f/%d...,进一步传入相同数量的值。

int printf( const char* format, ... );

我刚刚注意到,虽然不推荐使用,但如果我们不传递任何值,它将在没有任何运行时错误的情况下运行,如以下所示(当然,我们会得到意想不到的结果):

printf("%a"); // ok
printf("%c"); // ok
printf("%d"); // ok
printf("%e"); // ok
printf("%f"); // ok
printf("%g"); // ok
printf("%i"); // ok
printf("%o"); // ok
printf("%p"); // ok
printf("%u"); // ok
printf("%x"); // ok

如果这适用于所有格式,那么想到printf()不进行任何运行时检查,我不会感到惊讶。奇怪的是,它会给%s(似乎也是唯一一个)带来运行时错误。

printf("%s"); // run-time error: Access violation reading location

更有趣的是,它似乎只在运行时检查第一个或连续的%s。查看以下示例:

printf("%s%s", "xxx"); // run-time error: Access violation reading location
printf("%s%d%s", "xxx"); // ok

所以,我的问题是:

  • 与其他格式相比,printf()运行时对%s的检查是否不同?为什么
  • 为什么printf()只在运行时检查第一个或连续的%s

ps:我在VS2010、OS:Win7x64下测试了它。

在这种情况下,C++标准依赖于C99标准草案,该草案称这是7.19.6.1部分中未定义的行为。fprintf函数涵盖了printf的相关格式说明符,称:

[…]如果格式的参数不足,则行为为未定义。

因此,正如标准在未定义行为的定义中所说:

可能的未定义行为包括完全忽略情况和不可预测结果,在翻译或程序执行期间以环境特征的记录方式表现(有或没有发布诊断消息),终止翻译或执行(有诊断消息的发布)。

任何事情都有可能发生。

由于printf是一个可变函数,它必须依靠调用方来正确表示传入的数据。因此,运行时检查是不可能的。尽管有几个编译器确实提供了编译时检查,例如gcc甚至将这种检查作为一种属性格式(原型、字符串索引、第一个检查)提供,可以与您自己的函数一起使用。

没有运行时检查。printf将只是盲目地读取堆栈上的任何胡言乱语(或正在使用的任何var-arg机制)。在%s的情况下,它将胡言乱语解释为指针,然后尝试从该地址读取内容,这通常会导致访问违规*

本质上,您看到的是未定义的行为。


*好的,我想这是一种运行时检查,在操作系统/硬件级别

[ED:注意,这个问题最初被标记为C++,这个答案假设OP编译为C++]

尽管不推荐使用,但如果我们不传递任何值,

不,这会产生未定义的行为。任何事情都可能发生,包括您所期望的。这并不意味着可以。

printf不会在运行时检查您发送的任何垃圾,如果您发送了任何垃圾的话。有责任确保编写正确的代码。

顺便说一句,在您后来遇到"未处理的异常"错误的情况下,这不是sprintf进行运行时检查的结果。这是未定义行为的另一种表现。

所有这些奇怪而危险的行为都是因为sprintf是一个不安全、危险的函数。编写编译器很容易接受的代码,并且在测试甚至生产中运行一段时间似乎很好,但以某种微妙的方式表现出未定义的行为。这通常会在星期五你周末出门前一个小时咬你一口。

这里的人生教训是:不要在C++中使用sprintf。使用一些现代且类型安全的东西,比如溪流。

没有运行时检查,技术上很难做到。但也有可用的编译检查。

GCC是-Wall
使用Visual Studio,您需要运行它的静态分析工具(在更昂贵的版本中可用)。

这是未定义的行为,所以任何事情都可能发生。在实践中,您看到的差异可能是由于您使用的所有其他说明符都直接引用堆栈上的值,因此会显示垃圾,但不会崩溃。与%s不同的是,随机值被视为指针,您的程序访问内存的随机区域,因此很可能会崩溃。话虽如此,它仍然没有定义,不应该使用。

printf("%d"):试图从可能非法的内存地址读取4个字节。

printf("%s"):试图从可能非法的内存地址读取未知数量的字节,直到遇到值为0的字节。

在这两种情况下,是否会发生内存访问违规都是"运气问题"。

但正如你所理解的,在"%s"的情况下,这些机会要高得多。