对于那些想扮演侦探的人:在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 ); 

该函数根据当前语言环境的分类检查字符是否为空格。 在标准语言环境中,以下字符为空格:

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

返回值 非零值,如果字符为空格,则为零。

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')不应被视为空白字符的条件。

因此,程序员计划除了空格和水平制表符之外,还将跳过诸如页面更改和垂直制表符之类的字符。

值得注意的是,还计划跳过CR回车符(0x0d,'\ r')。 这不会发生,并且在遇到该符号时循环会停止。 如果文件中的行分隔符是某些非UNIX系统(例如Microsoft Windows)上使用的CR + LF序列,这将导致不愉快的意外。

对于那些想了解更多使用LF或CR + LF作为行分隔符的历史原因的人,这里是Wikipedia文章“ 换行 ”。

EatWhitespace函数应该以相同的方式处理文件,其中LF和CR + LF都用作分隔符。 对于CR + LF,情况并非如此。 换句话说,如果您的文件来自Windows世界,那么您就不走运了:)。

也许这不是一个严重的错误,特别是因为GNU Midnight Commander在类似UNIX的操作系统上很常见,在该操作系统上,LF字符(0x0a,'\ n')用于转换行。 然而,由于这些琐事,在Linux和Windows系统中准备的数据不兼容的各种烦人的问题出现了。

所描述的错误很有趣,因为用经典的代码审查几乎不可能检测到。 并非所有的项目开发人员都能知道宏的复杂性,而忘记它们很容易。 这是一个很好的示例,其中静态代码分析补充了代码审查和其他错误查找技术。

覆盖标准功能是不好的做法。 顺便说一下,最近在“ 爱静态代码分析 ”一文中,使用#define sprintf std :: printf宏考虑了类似的情况。

更好的解决方案是为宏指定唯一的名称,例如is_space_or_tab 。 这样就不会造成混乱。

创建宏的原因可能是标准isspace函数的运行缓慢程序员创建了一个更快的版本,足以解决所有必要的任务。 但是,这个决定是错误的。 定义isspace以便获得未编译的代码会更加可靠 。 并在具有唯一名称的宏中实现必要的功能。

谢谢您的关注。 我们邀请您下载并尝试使用PVS-Studio分析仪来测试您的项目。 另外,我们提醒您,分析器最近增加了对Java语言的支持。



如果您想与说英语的读者分享这篇文章,请使用以下链接:Andrey Karpov。 想扮演侦探吗? 在Midnight Commander中找到函数中的错误

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


All Articles