大灾变即将来临,静态分析和百吉饼

图片10

最有可能的是,从文章标题开始,您已经猜到焦点集中在源代码中的错误上。 但这不是本文将要讨论的唯一内容。 如果除了C ++和其他人的代码中的错误之外,您还喜欢上不寻常的游戏,并且您想知道这些“百吉饼”是什么以及它们与它们一起吃的东西,欢迎来到kat!

在寻找不寻常的游戏时,我偶然发现了《大灾变:黑暗的未来》游戏,该游戏不同于其他不寻常的图形:它是在黑色背景上使用多色ASCII字符实现的。

该游戏及其同类产品中引人注目的是多少东西都在其中实现。 具体地说,例如在《大地的裂变》中,即使要创建角色,我也要寻找向导,因为这里有数十种不同的参数,特征和初始情节,更不用说游戏本身的事件变化了。

这是一个开源游戏,也是用C ++编写的。 因此,无法通过PVS-Studio静态分析器来运行该项目,也不能运行该项目,我现在正在积极参与其中。 该项目本身对代码的高质量感到惊讶,但是,它仍然包含一些缺陷,在本文中我将讨论其中的一些缺陷。

迄今为止,已经使用PVS-Studio对许多游戏进行了测试。 例如,您可以阅读我们的其他文章“ 视频游戏行业的静态分析:十大软件错误 ”。

逻辑学


范例1:

以下示例是一个典型的复制错误。

V501在'||'的左侧和右侧有相同的子表达式 运算符:rng(2,7)<abs(z)|| rng(2,7)<abs(z)overmap.cpp 1503

bool overmap::generate_sub( const int z ) { .... if( rng( 2, 7 ) < abs( z ) || rng( 2, 7 ) < abs( z ) ) { .... } .... } 

在此,两次检查相同的条件。 最有可能的是,该表达式已被复制而忘记更改其中的某些内容。 我发现很难说这个错误是否很严重,但是检查并未按预期进行。

类似的警告:
  • V501在'&&'运算符的左侧和右侧有相同的子表达式'one_in(100000 / to_turns <int>(dur))。 player_hardcoded_effects.cpp 547

图片9

范例2:

V728过度检查可以简化。 '(A && B)|| (!A &&!B)'表达式等同于'bool(A)== bool(B)'表达式。 stock_ui.cpp 199

 bool inventory_selector_preset::sort_compare( .... ) const { .... const bool left_fav = g->u.inv.assigned.count( lhs.location->invlet ); const bool right_fav = g->u.inv.assigned.count( rhs.location->invlet ); if( ( left_fav && right_fav ) || ( !left_fav && !right_fav ) ) { return .... } .... } 

条件没有错误,但是不必要地复杂。 值得怜惜那些必须拆卸这种情况的人,并且如果if(left_fav == right_fav)更容易编写。

类似的警告:

  • V728过度检查可以简化。 '((A &&!B)|| (!A && B)'表达式等同于'bool(A)!= Bool(B)'表达式。 iuse_actor.cpp 2653

撤退我


事实证明,对我来说,今天被称为“百吉饼”的游戏只是流氓类游戏的旧流派的一小部分追随者。 一切始于1980年的Rogue游戏,后来成为榜样,并启发了许多学生和程序员来创建自己的游戏。 我认为DnD董事会角色扮演社区及其变体也带来了很多好处。

图片8

微优化


范例3:

下一组分析仪警告并不表示错误,而是可能对程序代码进行微优化。

V801性能下降 。 最好将第二个函数参数重新定义为引用。 考虑用“ const ...&type”替换“ const ... type”。 map.cpp 4644

 template <typename Stack> std::list<item> use_amount_stack( Stack stack, const itype_id type ) { std::list<item> ret; for( auto a = stack.begin(); a != stack.end() && quantity > 0; ) { if( a->use_amount( type, ret ) ) { a = stack.erase( a ); } else { ++a; } } return ret; } 

这里itdpe_id隐藏了std :: string 。 由于参数仍然传递常量,因此不允许对其进行更改,因此将变量引用传递给函数而不浪费资源来进行复制会更快。 并且尽管最有可能的是,该行将很小,但是没有明显原因的恒定复制是不必要的。 而且,此函数是从不同的地方调用的,其中许多地方依次从外部获取类型并进行复制。

类似警告:

  • V801性能下降。 最好重新定义第三个函数参数作为参考。 考虑用“ const ...&evt_filter”替换“ const ... evt_filter”。 输入.cpp 691
  • V801性能下降。 最好将第五个函数参数重新定义为参考。 考虑用“ const ...&color”替换“ const ... color”。 输出207
  • 分析仪总共生成了32个此类警告。

范例4:

V813性能下降 。 'str'参数可能应该呈现为常量引用。 catacharset.cpp 256

 std::string base64_encode( std::string str ) { if( str.length() > 0 && str[0] == '#' ) { return str; } int input_length = str.length(); std::string encoded_data( output_length, '\0' ); .... for( int i = 0, j = 0; i < input_length; ) { .... } for( int i = 0; i < mod_table[input_length % 3]; i++ ) { encoded_data[output_length - 1 - i] = '='; } return "#" + encoded_data; } 

在这种情况下,参数虽然不是常量,但在函数主体中不会更改。 因此,为了进行优化,最好将其通过恒定链接传递,而不是强制编译器创建本地副本。

这个警告也不是唯一,总共有26起此类案件。

图片7

类似警告:

  • V813性能下降。 'message'参数可能应该呈现为常量引用。 json.cpp 1452
  • V813性能下降。 “ s”自变量可能应呈现为常量引用。 catacharset.cpp 218
  • 依此类推...

撤退II


一些经典的流氓类游戏仍在开发中。 如果您访问GitHub Cataclysm DDA或NetHack存储库,则可以看到每天都在积极进行更改。 NetHack通常是仍在开发中的最古老的游戏:它于1987年7月发布,最新版本可追溯到2018年。

自2000年以来开发并于2006年首次发布的矮人要塞(Dwarf Fortress)是该类型游戏中最著名的游戏之一。 “输得开心”是游戏的座右铭,因为不可能赢得比赛,因此准确地反映了游戏的本质。 由于投票,该游戏于2007年获得年度最佳“类roguelike游戏”的称号,该游戏每年在ASCII GAMES网站上举行。

图片6

顺便说一下,那些对此游戏感兴趣的人可能会对以下新闻感兴趣。 《矮人要塞》将在Steam上发布,具有改进的32位图形。 通过更新的图像,两名经验丰富的游戏主持人正在工作,Dwarf Fortress的高级版将获得更多音乐曲目,并支持Steam Workshop。 但是,如果有的话,Dwarf Fortress付费版本的所有者将能够将更新后的图形更改为ASCII以前的格式。 更多细节

覆盖分配运算符


示例5、6:

还有一对有趣的类似警告。

V690'JsonObject'类实现了副本构造函数,但缺少'='运算符。 使用这样的类是危险的。 json.h 647

 class JsonObject { private: .... JsonIn *jsin; .... public: JsonObject( JsonIn &jsin ); JsonObject( const JsonObject &jsobj ); JsonObject() : positions(), start( 0 ), end( 0 ), jsin( NULL ) {} ~JsonObject() { finish(); } void finish(); // moves the stream to the end of the object .... void JsonObject::finish() { .... } .... } 

此类具有复制构造函数和析构函数,但是,它不会使赋值运算符重载。 这里的问题是自动生成的赋值运算符只能将指针分配给JsonIn 。 结果, JsonObject类的两个对象都指向相同的JsonIn 。 目前尚不知道这种情况是否会在某处出现,但是无论如何,这是某人迟早会踩到的耙子。

下一个类中存在类似的问题。

V690'JsonArray'类实现了复制构造函数,但缺少'='运算符。 使用这样的类是危险的。 json.h 820

 class JsonArray { private: .... JsonIn *jsin; .... public: JsonArray( JsonIn &jsin ); JsonArray( const JsonArray &jsarr ); JsonArray() : positions(), ...., jsin( NULL ) {}; ~JsonArray() { finish(); } void finish(); // move the stream position to the end of the array void JsonArray::finish() { .... } } 

您可以在“ 两大法则 ”(或本文的翻译“ C ++:两大法则 ”)中阅读有关复杂类的赋值运算符缺少重载的危险的更多信息。

示例7、8:

另一个示例与重载的赋值运算符有关,但是这次我们讨论的是其具体实现。

V794应该保护赋值运算符免受'this ==&other'的影响。 mattack_common.h 49

 class StringRef { public: .... private: friend struct StringRefTestAccess; char const* m_start; size_type m_size; char* m_data = nullptr; .... auto operator = ( StringRef const &other ) noexcept -> StringRef& { delete[] m_data; m_data = nullptr; m_start = other.m_start; m_size = other.m_size; return *this; } 

问题在于,这种实现没有受到保护,无法将对象分配给自己,这是不安全的做法。 也就是说,如果将对*的引用传递给该运算符,则可能会发生内存泄漏。

错误赋值运算符重载的类似示例,带有一个有趣的副作用:

V794应该保护赋值运算符免受'this ==&rhs'的影响。 player_activity.cpp 38

 player_activity &player_activity::operator=( const player_activity &rhs ) { type = rhs.type; .... targets.clear(); targets.reserve( rhs.targets.size() ); std::transform( rhs.targets.begin(), rhs.targets.end(), std::back_inserter( targets ), []( const item_location & e ) { return e.clone(); } ); return *this; } 

在这种情况下,就像不检查对象对其自身的分配一样。 但是此外,矢量会填充。 如果尝试通过这种重载将对象分配给自己,则在target字段中,我们将获得一个加倍的向量,其中某些元素已损坏。 但是,在转换之前有一个明确的方法可以清除对象的向量,并且数据将丢失。

图片16

撤退III


在2008年,百吉饼甚至获得了正式的定义,并被赋予史诗名称“柏林解释”。 根据此定义,此类游戏的主要功能是:

  • 随机产生的世界,可以增加重播价值;
  • Permadeath:如果您的角色死亡,他将永远死亡,所有物品都将丢失;
  • 分步操作:更改仅与玩家的动作一起发生,直到执行动作为止-时间停止;
  • 生存:资源极为有限。

好,也是最重要的:百吉饼的主要目的是探索和发现世界,寻找使用物体和穿越地牢的新方法。

大灾变DDA中的常见情况是:冻得饿死了,你被口渴折磨了,实际上你有六个触手而不是腿。

图片15

重要细节


范例9:

V1028可能的溢出。 考虑将“开始+较大”运算符的操作数强制转换为“ size_t”类型,而不是结果。 worldfactory.cpp 638

 void worldfactory::draw_mod_list( int &start, .... ) { .... int larger = ....; unsigned int iNum = ....; .... for( .... ) { if( iNum >= static_cast<size_t>( start ) && iNum < static_cast<size_t>( start + larger ) ) { .... } .... } .... } 

程序员似乎想避免溢出。 但是在这种情况下带来加法的结果是没有意义的,因为在加数时会发生溢出,并且对毫无意义的结果进行类型扩展。 为了避免这种情况,您只需要将一个参数转换为较大的类型: (static_cast <size_t>(start)+ large)

范例10:

V530需要使用功能“尺寸”的返回值。 worldfactory.cpp 1340

 bool worldfactory::world_need_lua_build( std::string world_name ) { #ifndef LUA .... #endif // Prevent unused var error when LUA and RELEASE enabled. world_name.size(); return false; } 

对于这种情况,有一个小技巧。 如果未使用该变量,则无需尝试调用任何方法,只需编写(void)world_name即可禁止编译器警告。

示例11:

V812性能下降 。 无效使用“计数”功能。 可以通过调用“ find”函数代替它。 播放器.cpp 9600

 bool player::read( int inventory_position, const bool continuous ) { .... player_activity activity; if( !continuous || !std::all_of( learners.begin(), learners.end(), [&]( std::pair<npc *, std::string> elem ) { return std::count( activity.values.begin(), activity.values.end(), elem.first->getID() ) != 0; } ) { .... } .... } 

从将计数结果与零进行比较的事实来看,这个想法是要了解活动中是否至少有一个必需的元素。 但是计数被迫遍历整个容器,因为它会计数元素的所有出现次数。 在这种情况下,使用find会更快,它将在找到第一个匹配项后停止。

示例12:

如果您知道一个细微之处,则很容易检测到以下错误。

V739 EOF不应与“ char”类型的值进行比较。 “ ch”应为“ int”类型。 json.cpp 762

 void JsonIn::skip_separator() { signed char ch; .... if (ch == ',') { if( ate_separator ) { .... } .... } else if (ch == EOF) { .... } 

图片3

如果您不知道EOF定义为-1,这就是很难注意到的错误之一。 因此,如果您尝试将其与具有符号char类型的变量进行比较,则该条件几乎总是false 。 唯一的例外是字符代码为0xFF(255)。 比较时,此类符号将变为-1,并且条件为true。

示例13:

下一个小错误有一天可能会变得很关键。 难怪它作为CWE-834出现在CWE列表中。 顺便说一句,其中有五个。

V663可能出现无限循环。 'cin.eof()'条件不足以使它脱离循环。 考虑将'cin.fail()'函数调用添加到条件表达式中。 动作.cpp 46

 void parse_keymap( std::istream &keymap_txt, .... ) { while( !keymap_txt.eof() ) { .... } } 

如警告中所述,仅在读取时检查是否到达文件末尾还不够,还必须检查cin.fail()读取错误。 更改代码以更安全地阅读:

 while( !keymap_txt.eof() ) { if(keymap_txt.fail()) { keymap_txt.clear(); keymap_txt.ignore(numeric_limits<streamsize>::max(),'\n'); break; } .... } 

如果从文件中读取错误, 需要keymap_txt.clear()来从流中删除错误状态(标志),否则无法进一步读取文本。 带有numeric_limits < streamsize > :: max()参数的keymap_txt.ignore和换行控件字符使您可以跳过其余行。

有一个更简单的方法来停止阅读:

 while( !keymap_txt ) { .... } 

在逻辑上下文中使用时,它将自身转换为等于true的值,直到达到EOF

撤退IV


现在,最流行的游戏是那些结合了类流氓游戏和其他类型的标志的游戏:平台游戏,策略等。此类游戏现在称为类流氓游戏或类流氓游戏。 这样的游戏包括诸如《不要挨饿》,《以撒的结合》,《 FTL:比光明快》,《黑暗地牢》甚至《暗黑破坏神》等著名游戏。

尽管有时流氓类和流氓类之间的差异是如此之小,以至于不清楚该游戏属于哪种流派。 有人认为矮人要塞不再像流氓一样,但对于某人而言,暗黑破坏神是经典的百吉饼。

图片1

结论


尽管整个项目是高质量代码的一个示例,并且不可能发现许多严重的错误,但这并不意味着使用静态分析对此是多余的。 关键不在于我们为了普及静态代码分析方法而进行的一次性检查,而是在于分析仪的常规使用。 这样一来,就可以在早期识别出许多错误,从而降低了纠正错误的成本。 计算示例

图片2

正在考虑的游戏上正在进行积极的工作,并且有活跃的Modders社区。 此外,它已移植到许多平台,包括iOS和Android。 因此,如果您对此游戏感兴趣,建议您尝试一下!

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


All Articles