Por qué es importante realizar un análisis estático de las bibliotecas de código abierto que agrega a su proyecto

PVS-Studio y bibliotecas de C ++ de solo encabezado Awesome


Las aplicaciones modernas se crean a partir de bibliotecas de terceros, como bloques de construcción. Esto es normal y la única opción para completar el proyecto en un tiempo razonable y con un presupuesto razonable. Sin embargo, tomar todos los ladrillos indiscriminadamente puede no ser una buena idea. Si hay varias opciones, es útil tomarse el tiempo para analizar las bibliotecas abiertas para elegir la de mayor calidad.



Colección "Geniales bibliotecas de C ++ solo de encabezado"



La historia de este escrito comenzó con el podcast de Cppcast " Cross Platform Mobile Telephony ". Gracias a él, me enteré de la existencia de la lista " awesome-hpp ", que enumera un gran número de bibliotecas C ++ de código abierto, que consta únicamente de archivos de encabezado.



Esta lista me interesó por dos razones. En primer lugar, es una oportunidad para reponer la base de proyectos para probar nuestro analizador PVS-Studio en código moderno. Muchos proyectos están escritos en C ++ 11, C ++ 14 y C ++ 17. En segundo lugar, es una oportunidad para escribir un artículo sobre cómo comprobar estos proyectos.



Los proyectos son pequeños, por lo que hay pocos errores en cada uno individualmente. Además, hay pocas advertencias, porque algunos errores solo pueden detectarse si las clases de plantilla o las funciones se instancian en código personalizado. Hasta que se utilicen estas clases y funciones, a menudo es imposible determinar si hay un error o no. Sin embargo, en total, hubo muchos errores, y escribiré sobre ellos en el próximo artículo. Este artículo no trata sobre errores, sino sobre una advertencia.



Por que analizar



Al utilizar bibliotecas de terceros, confía incondicionalmente en ellos para hacer parte del trabajo y los cálculos. El peligro es que a veces los programadores eligen una biblioteca sin siquiera pensar que los errores pueden contener no solo su código, sino también el código de la propia biblioteca. Como resultado, hay errores no obvios e incomprensibles que pueden manifestarse de la manera más inesperada.



El código de las bibliotecas de código abierto conocidas está bien depurado y la probabilidad de encontrar un error es mucho menor que en un código similar escrito por usted mismo. El problema es que no todas las bibliotecas se utilizan y depuran ampliamente. Y aquí es donde surge la cuestión de evaluar su calidad.



Para que quede más claro, veamos un ejemplo. Tomemos la biblioteca JSONCONS .
JSONCONS es una biblioteca de solo encabezado de C ++ para construir formatos de datos JSON y similares a JSON como CBOR.
Una biblioteca específica para tareas específicas. Puede que funcione bien en general y nunca verá errores en él. Pero Dios no lo quiera, necesita usar este operador sobrecargado << = .



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


Advertencia del analizador PVS-Studio: V629 Considere inspeccionar la expresión '1 << k'. Desplazamiento de bits del valor de 32 bits con una posterior expansión al tipo de 64 bits. bigint.hpp 744



Según tengo entendido, la función funciona con números grandes que se almacenan como una matriz de elementos de 64 bits. Para trabajar con ciertos bits, debe formar una máscara de 64 bits:



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


Pero esta máscara no está formada correctamente. Dado que el literal numérico 1 es de tipo int , cambiarlo en más de 31 bits dará como resultado un comportamiento indefinido.
Del estándar:

expresión-desplazamiento << expresión-aditiva

...

2. El valor de E1 << E2 son posiciones de bit E2 desplazadas a la izquierda de E1; los bits vacíos se rellenan con ceros. Si E1 tiene un tipo sin firmar, el valor del resultado es E1 * 2 ^ E2, módulo reducido uno más que el valor máximo representable en el tipo de resultado. De lo contrario, si E1 tiene un tipo con signo y un valor no negativo, y E1 * 2 ^ E2 es representable en el tipo de resultado, ese es el valor resultante; de lo contrario, el comportamiento no está definido.
La máscara variable puede ser cualquier cosa que desee. Sí, lo sé, teóricamente puede pasar cualquier cosa gracias a UB. Pero en la práctica, lo más probable es que estemos hablando de un resultado de expresión incorrecto.



Entonces, tenemos una función que no se puede usar. Más bien, funcionará solo para algunos casos especiales del valor del argumento de entrada. Esta es una trampa potencial en la que puede caer un programador. El programa puede ejecutar y aprobar varias pruebas y luego rechazar inesperadamente al usuario en otros archivos de entrada.



También puede ver otro error como este en el operador >> = .



Una pregunta retórica. ¿Debo confiar en esta biblioteca?



Quizás valga la pena. Después de todo, hay errores en cualquier proyecto. Sin embargo, vale la pena considerarlo: si existen estos errores, ¿hay otros que podrían provocar una corrupción de datos desagradable? ¿No sería mejor dar preferencia a la biblioteca más popular / probada si hay varias?



¿Un ejemplo poco convincente? Bien, consigamos otro. Tomemos la biblioteca de matemáticas universal . Se espera que la biblioteca proporcione la capacidad de operar con vectores. Por ejemplo, multiplique y divida un vector por un valor escalar. Ok, veamos cómo se implementan estas operaciones. Multiplicación:



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


Advertencia del analizador PVS-Studio: V1001 La variable 'scaledVector' está asignada pero no se utiliza al final de la función. vector.hpp 124



Debido a un error tipográfico, no se devuelve el nuevo contenedor scaledVector , sino el vector original. El mismo error está en el operador de división. Facepalm.



Nuevamente, estos errores no significan nada por separado. Aunque no, esto es un indicio de que esta biblioteca se usa poco y hay una alta probabilidad de que tenga otros errores graves no detectados.



Salida. Si varias bibliotecas brindan la misma funcionalidad, entonces vale la pena realizar un análisis preliminar de su calidad y elegir la más probada y confiable.



Cómo analizar



Ok, queremos entender la calidad del código de las bibliotecas, pero ¿cómo hacerlo? Sí, esto no es fácil de hacer. No puedes simplemente ir y ver el código. Más bien, puede mirar algo, pero le dará poca información. Además, es poco probable que dicha revisión ayude a evaluar la densidad de errores en el proyecto.



Volvamos a la biblioteca matemática universal mencionada anteriormente. Intente encontrar el error en el código de esta función. En realidad, al ver el comentario que lo acompaña, no puedo pasar de este lugar :).



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


Estoy seguro de que, a pesar de que sugerí que hay un error en este código, no es fácil encontrarlo.



Si no lo encuentra, aquí está. Advertencia de PVS-Studio: V581 Las expresiones condicionales de las declaraciones 'if' situadas una junto a la otra son idénticas. Verifique las líneas: 789, 790. value.hpp 790



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


Un error tipográfico clásico. En la segunda condición, se debe verificar la variable r2_sign .



En general, puede olvidarse de la revisión de código "manual". Sí, ese camino es posible, pero lleva demasiado tiempo.



¿Qué sugiero? Muy simple. Utilice análisis de código estático .



Verifique las bibliotecas que piensa usar. Comience a mirar los informes y todo se aclarará lo suficientemente rápido.



Ni siquiera necesita un análisis profundo y minucioso, y no necesita filtrar los falsos positivos. Solo tiene que revisar el informe y examinar las advertencias. Los falsos positivos debido a la falta de configuración pueden simplemente ser pacientes y concentrarse en los errores.



Sin embargo, los falsos positivos también se pueden tener en cuenta indirectamente. Cuanto más hay, más complicado es el código. En otras palabras, hay muchos trucos en el código que confunden al analizador. También confunden a las personas que apoyan el proyecto y, como resultado, afectan negativamente su calidad.



Nota. No se olvide del tamaño del proyecto. Siempre habrá más errores en un gran proyecto. Pero el número de errores no es en absoluto igual a la densidad de errores. Considere esto cuando tome proyectos de diferentes tamaños y realice ajustes.



Que usar



Hay muchas herramientas de análisis de código estático. Naturalmente, sugiero usar el analizador PVS-Studio . Es ideal tanto para una evaluación única de la calidad del código como para la búsqueda regular y la corrección de errores.



Puede consultar el código de proyectos en C, C ++, C # y Java. El producto es propietario. Sin embargo, una licencia de prueba gratuita será más que suficiente para evaluar la calidad de varias bibliotecas de código abierto.



También les recuerdo que existen varias opciones para la licencia gratuita del analizador para:





Conclusión



La metodología del análisis de código estático todavía es subestimada inmerecidamente por muchos programadores. Una posible razón de esto es la experiencia con herramientas sencillas y ruidosas de "linter", que realizan comprobaciones muy sencillas y, lamentablemente, a menudo no muy útiles.



Para aquellos que dudan de si vale la pena intentar implementar un analizador estático en el proceso de desarrollo, las siguientes dos publicaciones:





Gracias por su atención, y le deseo menos errores tanto en su código como en el código de las bibliotecas utilizadas :).





Si desea compartir este artículo con una audiencia de habla inglesa, utilice el enlace de traducción: Andrey Karpov. Por qué es importante aplicar el análisis estático para las bibliotecas abiertas que agrega a su proyecto .



All Articles