想扮演侦探吗? 在Midnight Commander中查找功能中的错误

虫子

在本文中,我们邀请您尝试从GNU Midnight Commander项目中的一个非常简单的函数中查找错误。 怎么了 没有特别的原因。 只是为了好玩。 好吧,这是一个谎言。 我们实际上想向您展示另一个错误,即人工审核者很难找到并且静态代码分析器PVS-Studio可以毫不费力地捕获。

某天某用户向我们发送了一封电子邮件,询问为什么他在功能EatWhitespace上收到警告(请参见下面的代码)。 这个问题看起来并不简单。 尝试自己弄清楚这段代码有什么问题。

static int EatWhitespace (FILE * InFile) /* ----------------------------------------------------------------------- ** * Scan past whitespace (see ctype(3C)) and return the first non-whitespace * character, or newline, or EOF. * * Input: InFile - Input source. * * Output: The next non-whitespace character in the input stream. * * Notes: Because the config files use a line-oriented grammar, we * explicitly exclude the newline character from the list of * whitespace characters. * - Note that both EOF (-1) and the nul character ('\0') are * considered end-of-file markers. * * ----------------------------------------------------------------------- ** */ { int c; for (c = getc (InFile); isspace (c) && ('\n' != c); c = getc (InFile)) ; return (c); } /* EatWhitespace */ 

如您所见, EatWhitespace是一个很小的函数; 它的身体比对它的评论还要小:)。 现在,让我们检查一些细节。

这是功能getc的描述:

 int getc ( FILE * stream ); 

返回指定流的内部文件位置指示符当前指向的字符。 然后,内部文件位置指示器将前进到下一个字符。 如果在调用时流位于文件末尾,则该函数返回EOF并设置流的文件末尾指示符。 如果发生读取错误,该函数将返回EOF并设置流的错误指示符(错误)。

这是功能isspace的描述:

 int isspace( int ch ); 

检查给定字符是否为按当前安装的C语言环境分类的空格字符。 在默认语言环境中,空格字符如下:

  • 空格(0x20,'');
  • 换页(0x0c,'\ f');
  • 换行符LF(0x0a,'\ n');
  • 回车CR(0x0d,'\ r');
  • 水平制表符(0x09,'\ t');
  • 垂直制表符(0x0b,'\ v')。

返回值 如果字符是空白字符,则为非零值;否则为0。 否则为零。

EatWhitespace函数应跳过换行符'\ n'以外的所有空白字符。 当遇到文件结尾(EOF)时,该功能还将停止从文件读取。

现在您已经知道了所有这些,请尝试查找错误!

下面的两个独角兽将确保您不会偶然看到该评论。

图1.错误搜索时间。独角兽正在等待。


图1.错误搜索时间。 独角兽正在等待。

还是没有运气吗?

好吧,您知道的是,因为我们对isspace撒了谎。 哇哈哈! 它根本不是一个标准函数-它是一个自定义宏。 是的,我们是坏人,我们让您感到困惑。

图2.独角兽使读者对isspace感到困惑。


图2.独角兽使读者对isspace感到困惑。

当然,这不是我们或我们的独角兽。 所有混乱的错在于GNU Midnight Commander项目的作者,他们在charset.h文件中实现了isspace的实现:

 #ifdef isspace #undef isspace #endif .... #define isspace(c) ((c)==' ' || (c) == '\t') 

使用此宏,作者使其他开发人员感到困惑。 该代码是在isspace是一个标准函数的假设下编写的,该函数将回车符(0x0d,'\ r')视为空格字符。

反过来,自定义宏仅将空格和制表符视为空白字符。 让我们替换该宏,看看会发生什么。

 for (c = getc (InFile); ((c)==' ' || (c) == '\t') && ('\n' != c); c = getc (InFile)) 

('\ n'!= C)子表达式是不必要的(冗余),因为它将始终为true。 这就是PVS-Studio通过输出警告来警告您的内容:

V560条件表达式的一部分始终为true:('\ n'!= C)。 c。第136章

为了清楚起见,让我们检查3种可能的结果:

  • 到达文件末尾。 EOF不是空格或制表符。 由于短路评估,未评估('\ n'!= C)子表达式。 循环终止。
  • 该函数读取的某些字符不是空格或制表符。 由于短路评估,未评估('\ n'!= C)子表达式。 循环终止。
  • 该功能已读取空格或水平制表符。 ('\ n'!= C)子表达式被求值,但其结果始终为true。

换句话说,上面的代码等效于以下代码:

 for (c = getc (InFile); c==' ' || c == '\t'; c = getc (InFile)) 

我们发现它无法按预期方式工作。 现在让我们看看其中的含义。

EatWhitespace函数的主体中编写了isspace调用的开发人员期望可以调用标准函数。 因此,他们添加了防止LF字符('\ n')被视为空白字符的条件。

这意味着,除了空格和水平制表符之外,他们还计划跳过换页和垂直制表符。

更值得注意的是,他们也希望跳过回车符(0x0d,'\ r')。 但是不会发生-遇到此字符时循环终止。 如果换行符由CR + LF序列表示,则该程序最终会出现意外行为,CR + LF序列是某些非UNIX系统(例如Microsoft Windows)中使用的类型。

有关使用LF或CR + LF作为换行符的历史原因的更多详细信息,请参见Wikipedia页面“ 换行符 ”。

EatWhitespace函数旨在以相同的方式处理文件,无论它们使用LF还是CR + LF作为换行符。 但是在CR + LF的情况下失败。 换句话说,如果您的文件来自Windows世界,那么您就有麻烦了:)。

尽管这可能不是一个严重的错误,尤其是考虑到在类似UNIX的操作系统中使用了GNU Midnight Commander,而LF(0x0a,'\ n')被用作换行符,但是像这样的琐事仍然容易让人讨厌在Linux和Windows上准备的数据的兼容性问题。

使这个错误变得有趣的是,您几乎可以肯定在执行标准代码审查时会忽略它。 宏的实现细节很容易忘记,有些项目作者可能根本不了解它们。 这是一个非常生动的例子,说明了静态代码分析如何有助于代码审查和其他错误检测技术。

覆盖标准功能是一个坏习惯。 顺便说一下,我们在最近的文章“ 赞赏静态代码分析 ”中讨论了#define sprintf std :: printf宏的类似情况。

更好的解决方案是为宏指定一个唯一的名称,例如is_space_or_tab 。 这本来可以避免所有的混乱。

也许标准的isspace函数太慢了,程序员创建了一个更快的版本,足以满足他们的需求。 但是他们仍然不应该那样做。 一个更安全的解决方案是定义isspace,以便您获得不可编译的代码,而所需的功能可以实现为具有唯一名称的宏。

感谢您的阅读。 不要犹豫, 下载 PVS-Studio并在您的项目中尝试一下。 提醒一下,我们现在也支持Java。

Source: https://habr.com/ru/post/zh-CN439372/


All Articles