プロジェクトに追加するオープンソースライブラリの静的分析を実行することが重要である理由

PVS-StudioおよびAwesomeヘッダーのみのC ++ライブラリ


最新のアプリケーションは、ビルディングブロックなどのサードパーティライブラリから構築されています。これは正常であり、妥当な時間と妥当な予算でプロジェクトを完了するための唯一のオプションです。しかし、すべてのレンガを無差別に取るのはそれほど良い考えではないかもしれません。複数のオプションがある場合は、時間をかけて開いているライブラリを分析して、最高品質のライブラリを選択すると便利です。



コレクション「素晴らしいヘッダーのみのC ++ライブラリ」



この書き込みの物語はCppcastポッドキャスト「で始まったクロスプラットフォームの携帯電話」。それから、ヘッダーファイルのみで構成される多数のオープンC ++ライブラリをリストするawesome-hpp」リストの存在について学びました



このリストは2つの理由で私に興味を持った。まず、最新のコードでPVS-Studioアナライザーをテストするためのプロジェクトのベースを補充する機会です。多くのプロジェクトは、C ++ 11、C ++ 14、およびC ++ 17で記述されています。第二に、これらのプロジェクトのチェックに関する記事を書く機会です。



プロジェクトは小さいので、それぞれに個別のバグはほとんどありません。さらに、警告はほとんどありません。一部のエラーは、テンプレートクラスまたは関数がカスタムコードでインスタンス化されている場合にのみ検出できます。これらのクラスと関数が使用されるまで、エラーがあるかどうかを判断することはしばしば不可能です。とはいえ、全体としては間違いが多かったので、次の記事で書きます。この記事はエラーについてではなく、警告についてです。



分析する理由



サードパーティのライブラリを使用することにより、サードパーティのライブラリが作業や計算の一部を実行することを無条件に信頼できます。危険なのは、エラーにコードだけでなく、ライブラリ自体のコードも含まれている可能性があることを考えずに、プログラマがライブラリを選択する場合があることです。その結果、最も予期しない方法で現れる可能性のある、明白ではない、理解できないエラーがあります。



よく知られているオープンソースライブラリのコードは十分にデバッグされており、エラーが発生する可能性は、自分で作成した同様のコードよりもはるかに低くなります。問題は、すべてのライブラリが広く使用およびデバッグされているわけではないことです。そして、ここで品質を評価するという問題が発生します。



明確にするために、例を見てみましょう。JSONCONSライブラリを見てみましょう
JSONCONSは、JSONおよびCBORなどのJSONに似たデータ形式を構築するためのC ++のヘッダーのみのライブラリです。
特定のタスクのための特定のライブラリ。全体的にはうまく機能する可能性があり、バグが発生することはありません。しかし、神はあなたがこのオーバーロードされた演算子<< =を使用する必要があることを禁じています



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 << additive-expression

...

2. E1 << E2の値は、E1の左シフトE2ビット位置です。空になったビットはゼロで埋められます。E1に符号なしタイプがある場合、結果の値はE1 * 2 ^ E2であり、結果タイプで表現可能な最大値より1を法として減少します。それ以外の場合、E1に符号付きタイプと非負の値があり、E1 * 2 ^ E2が結果タイプで表現可能である場合、それが結果の値です。それ以外の場合、動作は定義されていません。
可変マスクの値は任意です。はい、私は知っています、理論的にはUBのために何でも起こり得ます。しかし実際には、おそらく、誤った表現結果について話しているのです。



そのため、使用できない機能があります。むしろ、入力引数の値のいくつかの特別な場合にのみ機能します。これは、プログラマーが陥る可能性のある潜在的な罠です。プログラムはさまざまなテストを実行して合格し、その後、他の入力ファイルでユーザーを予期せず拒否する可能性があります。



そして、もう1つのエラーが演算子>> =に見られます。



修辞的な質問。このライブラリを信頼する必要がありますか?



おそらくそれだけの価値があります。結局のところ、どのプロジェクトにも間違いがあります。ただし、検討する価値があります。これらのエラーが存在する場合、厄介なデータ破損につながる可能性のある他のエラーはありますか?複数ある場合は、より人気のある/テスト済みのライブラリを優先する方がよいのではないでしょうか。



説得力のない例?さて、別のものを取得しましょう。ユニバーサル数学ライブラリを見てみましょう。ライブラリは、ベクトルを操作する機能を提供することが期待されます。たとえば、ベクトルをスカラー値で乗算および除算します。では、これらの操作がどのように実装されているか見てみましょう。乗算:



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);


古典的なタイプミス。 2番目の条件では、r2_sign変数をチェックする必要があります



一般に、「手動」のコードレビューは忘れることができます。はい、そのようなパスは可能ですが、それは不当に時間がかかります。



私は何を提案しますか?とてもシンプルです。静的コード分析を使用します



使用するライブラリを確認してください。レポートを見始めると、すべてがすぐに明らかになります。



深く徹底的な分析も必要ありません。また、誤検知を除外する必要もありません。レポートに目を通し、警告を確認するだけです。設定の欠如による誤検知は、単に辛抱強く、間違いに集中する可能性があります。



ただし、誤検知は間接的に考慮することもできます。存在するほど、コードは乱雑になります。言い換えれば、アナライザーを混乱させるコードには多くのトリックがあります。また、プロジェクトをサポートする人々を混乱させ、その結果、プロジェクトの品質に悪影響を及ぼします。



注意。プロジェクトのサイズを忘れないでください。大きなプロジェクトでは常に間違いが多くなります。ただし、エラーの数はエラー密度とまったく同じではありません。さまざまなサイズのプロジェクトを取り、調整する場合は、これを考慮してください。



何を使うか



多くの静的コード分析ツール があります。当然、PVS-Studioアナライザーの使用をお勧めしますこれは、コードの品質の1回限りの評価と、定期的な検索およびバグの修正の両方に最適です。



プロジェクトのコードは、C、C ++、C#、およびJavaで確認できます。この製品は独自のものです。ただし、いくつかのオープンソースライブラリの品質を評価するには、無料の試用ライセンスで十分です。



また、次のようなアナライザーの無料ライセンスにはいくつかのオプションがあることを思い出してください。





結論



静的コード分析の方法論は、依然として多くのプログラマーによって不当に過小評価されています。これの考えられる理由は、非常に単純で、残念ながら、あまり役に立たないチェックを実行する、単純なノイズの多い「リンター」ツールの使用経験です。



開発プロセスで静的アナライザーを実装する価値があるかどうか疑問がある人のために、次の2つの出版物があります。





ご清聴ありがとうございました。コードと使用するライブラリのコードの両方でバグが少なくなることを願っています:)。





この記事を英語を話す聴衆と共有したい場合は、翻訳リンクを使用してください:AndreyKarpov。プロジェクトに追加するオープンライブラリに静的分析を適用することが重要である理由



All Articles