为什么对添加到项目中的开源库进行静态分析很重要

PVS-Studio和Awesome仅标头C ++库


现代应用程序是从第三方库(如构建块)构建的。这是正常现象,并且是在合理的时间内以合理的预算完成项目的唯一选择。但是,乱扔所有砖块可能不是一个好主意。如果有多个选项,那么花时间分析开放库以选择质量最高的库将很有用。



集合“很棒的仅标头C ++库”



本书的故事始于Cppcast播客“跨平台移动电话”。从中,我了解了“ awesome-hpp ”列表的存在,该列表列出了大量仅由头文件组成的开放源C ++库。



此列表使我感兴趣,有两个原因。首先,这是一个补充项目基础的机会,以便以现代代码测试我们的PVS-Studio分析仪。许多项目是用C ++ 11,C ++ 14和C ++ 17编写的。其次,这是撰写有关检查这些项目的文章的机会。



这些项目很小,因此每个项目中几乎没有错误。另外,几乎没有警告,因为 如果在自定义代码中实例化了模板类或函数,则只能检测到某些错误。在使用这些类和函数之前,通常无法确定是否存在错误。尽管如此,总的来说还是有很多错误,我将在下一篇文章中介绍它们。本文不是关于错误,而是关于警告。



为什么要分析



通过使用第三方库,您将无条件地信任它们来执行某些工作和计算。危险在于,有时程序员选择一个库时根本不会想到错误不仅可能包含其代码,而且还可能包含该库本身的代码。结果,出现了一些不明显的,难以理解的错误,这些错误可能以最出乎意料的方式表现出来。



众所周知的开放库的代码经过了很好的调试,遇到错误的可能性比您自己编写的类似代码少。问题在于并非所有库都被广泛使用和调试。这就是评估其质量的地方。



为了更加清楚,让我们看一个例子。让我们来看看JSONCONS
JSONCONS是一个C ++,仅标头的库,用于构造JSON和类似CBOR的数据格式。
用于特定任务的特定库。它可能总体上运行良好,并且您将永远不会看到错误。但是上帝禁止您使用此重载运算符<< =



static constexpr uint64_t basic_type_bits = sizeof(uint64_t) * 8;
....
uint64_t* data() 
{
  return is_dynamic() ? dynamic_stor_.data_ : short_stor_.values_;
}
....
basic_bigint& operator<<=( uint64_t k )
{
  size_type q = (size_type)(k / basic_type_bits);
  if ( q ) // Increase common_stor_.length_ by q:
  {
    resize(length() + q);
    for (size_type i = length(); i-- > 0; )
      data()[i] = ( i < q ? 0 : data()[i - q]);
    k %= basic_type_bits;
  }
  if ( k )  // 0 < k < basic_type_bits:
  {
    uint64_t k1 = basic_type_bits - k;
    uint64_t mask = (1 << k) - 1;             // <=
    resize( length() + 1 );
    for (size_type i = length(); i-- > 0; )
    {
      data()[i] <<= k;
      if ( i > 0 )
        data()[i] |= (data()[i-1] >> k1) & mask;
      }
  }
  reduce();
  return *this;
}


PVS-Studio分析仪警告:V629考虑检查'1 << k'表达式。32位值的位移,随后扩展为64位类型。bigint.hpp 744



据我了解,该函数适用于大量存储为64位元素的数组。要使用某些位,您需要形成一个64位掩码:



uint64_t mask = (1 << k) - 1;


但是此蒙版未正确形成。由于数字文字1​​的类型为int,将其移位超过31位将导致未定义的行为。
从标准来看:

shift-expression <<加法表达式

...

2. E1 << E2的值是E1左移E2位的位置;空出的位为零。如果E1具有无符号类型,则结果的值为E1 * 2 ^ E2,比结果类型中可表示的最大值模减少1。否则,如果E1具有带符号的类型且非负值,并且E1 * 2 ^ E2在结果类型中可表示,则这是结果值;否则,行为是不确定的。
可变掩码可以是任何您想要的。是的,我知道,从理论上讲,由于UB,任何事情都会发生。但是实际上,我们很可能在谈论错误的表达结果。



因此,我们有一个无法使用的功能。相反,它仅对输入参数值的某些特殊情况有效。这是程序员可能陷入的潜在陷阱。该程序可以运行并通过各种测试,然后意外地拒绝用户使用其他输入文件。



您还可以在运算符>> =中看到另一个类似的错误



一个反问。我应该信任这个图书馆吗?



也许值得。毕竟,任何项目都存在错误。但是,值得考虑的是:如果存在这些错误,是否还有其他会导致讨厌的数据损坏的错误?如果有多个库,最好还是优先使用更受欢迎/经过测试的库?



一个令人信服的例子?好吧,让我们再来一个。让我们来看看通用数学库。期望该库提供对向量进行操作的能力。例如,将向量乘以标量值并除以。好的,让我们看看如何实现这些操作。乘法:



template<typename Scalar>
vector<Scalar> operator*(double scalar, const vector<Scalar>& v) {
  vector<Scalar> scaledVector(v);
  scaledVector *= scalar;
  return v;
}


PVS-Studio分析仪警告:V1001已分配“ scaledVector”变量,但在功能结束时未使用。vector.hpp 124



由于有错字,返回的不是新容器scaledVector,而是原始矢量。除法运算符中存在相同的错误。Facepalm。



同样,这些错误并不意味着分别。尽管没有,但这暗示着该库很少使用,并且很有可能存在其他严重的未发现的错误。



输出。如果几个库提供相同的功能,则有必要对其质量进行初步分析,并选择测试最可靠的库。



怎么分析



好的,我们想了解库代码的质量,但是该怎么做呢?是的,这不容易做到。您不能只是去看代码。相反,您可以查看某些内容,但信息很少。而且,这种审查不太可能有助于评估项目中错误的密度。



让我们回到前面提到的通用数学库。尝试在此函数的代码中查找错误。实际上,看到附带的评论,我无法绕过这个地方:)。



// subtract module using SUBTRACTOR: CURRENTLY BROKEN FOR UNKNOWN REASON


PVS-Studio Facepalm


template<size_t fbits, size_t abits>
void module_subtract_BROKEN(const value<fbits>& lhs, const value<fbits>& rhs,
                            value<abits + 1>& result) {
  if (lhs.isinf() || rhs.isinf()) {
    result.setinf();
    return;
  }
  int lhs_scale = lhs.scale(),
      rhs_scale = rhs.scale(),
      scale_of_result = std::max(lhs_scale, rhs_scale);

  // align the fractions
  bitblock<abits> r1 = lhs.template nshift<abits>(lhs_scale-scale_of_result+3);
  bitblock<abits> r2 = rhs.template nshift<abits>(rhs_scale-scale_of_result+3);
  bool r1_sign = lhs.sign(), r2_sign = rhs.sign();

  if (r1_sign) r1 = twos_complement(r1);
  if (r1_sign) r2 = twos_complement(r2);

  if (_trace_value_sub) {
    std::cout << (r1_sign ? "sign -1" : "sign  1") << " scale "
      << std::setw(3) << scale_of_result << " r1       " << r1 << std::endl;
    std::cout << (r2_sign ? "sign -1" : "sign  1") << " scale "
      << std::setw(3) << scale_of_result << " r2       " << r2 << std::endl;
  }

  bitblock<abits + 1> difference;
  const bool borrow = subtract_unsigned(r1, r2, difference);

  if (_trace_value_sub) std::cout << (r1_sign ? "sign -1" : "sign  1")
    << " borrow" << std::setw(3) << (borrow ? 1 : 0) << " diff    "
    << difference << std::endl;

  long shift = 0;
  if (borrow) {   // we have a negative value result
    difference = twos_complement(difference);
  }
  // find hidden bit
  for (int i = abits - 1; i >= 0 && difference[i]; i--) {
    shift++;
  }
  assert(shift >= -1);

  if (shift >= long(abits)) {            // we have actual 0 
    difference.reset();
    result.set(false, 0, difference, true, false, false);
    return;
  }

  scale_of_result -= shift;
  const int hpos = abits - 1 - shift;         // position of the hidden bit
  difference <<= abits - hpos + 1;
  if (_trace_value_sub) std::cout << (borrow ? "sign -1" : "sign  1")
    << " scale " << std::setw(3) << scale_of_result << " result  "
    << difference << std::endl;
  result.set(borrow, scale_of_result, difference, false, false, false);
}


我敢肯定,尽管事实上我建议此代码有错误,但要找到它并不容易。



如果找不到,那么就在这里。PVS-Studio警告V581彼此并排放置的'if'语句的条件表达式相同。检查行:789、790。value.hpp 790



if (r1_sign) r1 = twos_complement(r1);
if (r1_sign) r2 = twos_complement(r2);


一个经典的错字。在第二种情况下,应检查r2_sign变量



通常,您可以忘记“手动”代码审查。是的,这样的路径是可能的,但是却浪费时间。



我有什么建议?很简单。使用静态代码分析



检查您打算使用的库。开始查看报告,一切将很快变得清晰。



您甚至不需要深入,透彻的分析,也不需要过滤掉误报。您只需要浏览报告并检查警告。由于缺乏设置而导致的误报可能会很耐心,并且会专注于错误。



但是,也可以间接考虑误报。越多,代码越混乱。换句话说,代码中有许多技巧使分析仪感到困惑。他们还会使支持该项目的人员感到困惑,从而对项目的质量产生负面影响。



注意。不要忘记项目规模。一个大项目总是会有更多的错误。但是错误的数量与错误的密度完全不同。在进行不同规模的项目并进行调整时,请考虑这一点。



使用什么



许多静态代码分析工具。我自然建议使用PVS-Studio分析仪这对于一次评估代码质量以及定期搜索和修复错误都非常有用。



您可以使用C,C ++,C#和Java检查项目代码。该产品是专有的。但是,免费试用许可证将足以评估几个开源库的质量。



我还提醒您,有以下几种免费的分析仪许可选项:





结论



静态代码分析的方法仍应被许多程序员低估。造成这种情况的一个可能原因是使用简单的嘈杂的“线性”类工具,这些工具执行非常简单的(不幸的是,通常不是很有用)检查。



对于那些怀疑是否值得在开发过程中实现静态分析器的人,可以参考以下两个出版物:





感谢您的关注,并希望您的代码和所用库的代码中的错误都更少:)。





如果您想与讲英语的读者分享本文,请使用翻译链接:Andrey Karpov。为什么对添加到项目中的开放库应用静态分析很重要



All Articles