El aprendizaje automático está profundamente arraigado en varias áreas de la actividad humana: desde el reconocimiento del habla hasta el diagnóstico médico. La popularidad de este enfoque es tan grande que intentan usarlo siempre que sea posible. Algunos intentos de reemplazar los enfoques clásicos con redes neuronales no tienen tanto éxito. Echemos un vistazo al aprendizaje automático desde el punto de vista de la creación de analizadores de código estático eficaces para encontrar errores y vulnerabilidades potenciales.
A menudo se pregunta al equipo de PVS-Studio si queremos comenzar a utilizar el aprendizaje automático para encontrar errores en el código fuente de los programas. Respuesta corta: sí, pero muy limitada. Creemos que con el uso del aprendizaje automático en los problemas de análisis de código, existen muchas dificultades. En la segunda parte del artículo hablaremos sobre ellos. Comencemos con una revisión de nuevas soluciones e ideas.
Nuevos enfoques
Actualmente, ya hay muchas implementaciones de analizadores estáticos basados en el aprendizaje automático o que lo utilizan, incluido el aprendizaje profundo y la PNL para la detección de errores. No solo los entusiastas, sino también las grandes empresas, como Facebook, Amazon o Mozilla, llamaron la atención sobre el potencial del aprendizaje automático al buscar errores. Algunos proyectos no son analizadores estáticos completos, pero solo en el medio encuentran algunos errores específicos durante las confirmaciones.
Curiosamente, casi todos se posicionan como productos que cambian el juego que, con la ayuda de la inteligencia artificial, cambiarán el proceso de desarrollo.
Considere algunos ejemplos bien conocidos:
- Deepcode
- Inferir, Sapienz, SapFix
- Embold
- Fuente {d}
- Compromiso inteligente, asistente de compromiso
- CodeGuru
Deepcode
Deep Code es una herramienta de búsqueda de vulnerabilidades en el código de programas escritos en Java, JavaScript, TypeScript y Python, en los que el aprendizaje automático está presente como componente. Según Boris Paskalev, más de 250 mil reglas ya funcionan. Esta herramienta está capacitada en función de los cambios realizados por los desarrolladores en el código fuente de los proyectos abiertos (un millón de repositorios). La propia empresa dice que su proyecto es Grammarly para desarrolladores.
En esencia, este analizador compara su solución con su base de datos de proyectos y le ofrece la mejor solución estimada de la experiencia de otros desarrolladores.
En mayo de 2018, los desarrolladores escribieron que se estaba preparando el soporte para el lenguaje C ++, sin embargo, este lenguaje aún no es compatible. Aunque se indica en el sitio mismo que se puede agregar un nuevo idioma en cuestión de semanas, debido al hecho de que solo un paso depende del análisis del idioma.
También se publica en el sitio un grupo de publicaciones sobre los métodos en los que se basa el analizador.
Inferir
Facebook está intentando ampliamente introducir nuevos enfoques en sus productos. No pasaron por alto su atención y el aprendizaje automático. En 2013, compraron una startup que estaba desarrollando un analizador estático basado en máquina. Y en 2015, el código fuente del proyecto
se abrió .
Infer es un analizador estático para proyectos escritos en Java, C, C ++ y Objective-C, desarrollado por Facebook. Según el sitio, también se usa en Amazon Web Services, Oculus, Uber y otros proyectos populares.
Infer actualmente es capaz de detectar errores relacionados con la eliminación de referencias de un puntero nulo, pérdidas de memoria. Infer se basa en la lógica de Hoar, la lógica de separación y la bi-abducción, así como en la teoría de la interpretación abstracta. El uso de estos enfoques permite al analizador dividir el programa en pequeños bloques (fragmentos) y analizarlos independientemente uno del otro.
Puede intentar usar Infer en sus proyectos, sin embargo, los desarrolladores advierten que aunque en los proyectos de Facebook los resultados útiles representan el 80% de los resultados, en otros proyectos no se garantiza un bajo número de falsos positivos. Algunos de los errores que Infer aún no puede encontrar, pero los desarrolladores están trabajando para introducir dichos desencadenantes:
- saliendo de la matriz;
- excepciones de tipografía;
- fuga de datos no verificados;
- carrera condición de carrera.
Sapfix
SapFix es una herramienta de edición automatizada. Recibe información de Sapienz, una herramienta de automatización de pruebas y del analizador estático Infer, y basándose en los últimos cambios y mensajes, Infer elige una de varias estrategias para corregir errores.
En algunos casos, SapFix revierte todos o parte de los cambios. En otros casos, intenta resolver el problema generando un parche a partir de su conjunto de patrones de arreglos. Este conjunto se forma a partir de las plantillas de edición compiladas por los propios programadores a partir del conjunto de ediciones ya realizadas una vez. Si dicha plantilla no corrige el error, SapFix intenta ajustar la plantilla a la situación, haciendo pequeñas modificaciones en el árbol de sintaxis abstracta hasta que se encuentre una posible solución.
Pero una solución potencial no es suficiente, por lo que SapFix recopila varias soluciones que se seleccionan en base a tres preguntas: ¿hay algún error de compilación, hay un bloqueo, la edición introduce nuevos bloqueos? Una vez que las ediciones se prueban por completo, los parches se envían para su revisión al programador que decide cuál de las ediciones resuelve mejor el problema.
Embold
Embold es una plataforma de inicio para el análisis estático del código fuente de los programas, que antes del cambio de nombre se llamaba Gamma. El análisis estático se realiza sobre la base de nuestros propios diagnósticos, así como sobre la base de analizadores integrados como Cppheck, SpotBugs, SQL Check y otros.
Además de los diagnósticos en sí, el énfasis está en la capacidad de mostrar visualmente las infografías por la carga de la base del código y ver convenientemente los errores encontrados, así como buscar la posibilidad de refactorizar. Además, este analizador tiene un conjunto de antipatrones que le permiten detectar problemas en la estructura del código a nivel de clases y métodos, y varias métricas para calcular la calidad del sistema.
Una de las principales ventajas es la solución inteligente y el sistema de sugerencia de revisión, que, además de los diagnósticos habituales, verifica las revisiones basadas en información sobre cambios anteriores.
Utilizando NLP, Embold divide el código en partes y busca interconexiones y dependencias entre funciones y métodos entre ellas, lo que ahorra tiempo de refactorización.
Por lo tanto, Embold ofrece principalmente una visualización conveniente de los resultados de análisis de su código fuente por varios analizadores, así como sus propios diagnósticos, algunos de los cuales se basan en el aprendizaje automático.
Fuente {d}
La fuente {d} es la más abierta en términos de cómo implementarla de los analizadores que examinamos. También es una
solución de código abierto . En su sitio web puede (a cambio de su dirección de correo electrónico) obtener un folleto con una descripción de las tecnologías que utilizan. Además, contiene un
enlace a la base de publicaciones que han recopilado en relación con el uso del aprendizaje automático para el análisis de código, así como un
repositorio con un conjunto de datos para la capacitación en código. El producto en sí es una plataforma completa para analizar el código fuente y el producto de software, y se centra, más bien, no en los desarrolladores, sino en los administradores de enlaces. Entre sus capacidades hay una función para identificar el volumen de la deuda técnica, los cuellos de botella en el proceso de desarrollo y otras estadísticas globales sobre el proyecto.
Basan su enfoque del análisis de código asistido por máquina en la hipótesis natural, formulada en el artículo "
Sobre la naturalidad del software ".
"Los lenguajes de programación, en teoría, son complejos, flexibles y potentes, pero los programas que las personas reales escriben en realidad son en su mayoría simples y bastante repetitivos, y por lo tanto contienen propiedades estadísticas útiles y predecibles que pueden expresarse en estadística modelos de lenguaje y uso para tareas de desarrollo de software ".Según esta hipótesis, cuanto mayor sea la base del código para entrenar el analizador, más propiedades estadísticas destacarán y más precisas serán las métricas obtenidas a través del entrenamiento.
Para analizar el código, source {d} utiliza el servicio Babelfish, que puede analizar un archivo de código en cualquiera de los idiomas disponibles, obtener un árbol de sintaxis abstracta y convertirlo en un árbol de sintaxis universal.
Sin embargo, la fuente {d} no busca errores en el código. Basado en el árbol, usando el aprendizaje automático sobre la base de todo el proyecto, la fuente {d} revela cómo se formatea el código, qué estilo de codificación se usa en el proyecto y al confirmar, y si el nuevo código no coincide con el estilo del código del proyecto, realiza los cambios apropiados.
La capacitación se guía por varios elementos básicos: espacios, tabulaciones, saltos de línea, etc.
Puede leer más sobre esto en su publicación: "
STYLE-ANALYZER: arreglando inconsistencias de estilo de código con algoritmos interpretables no supervisados ".
En general, source {d} es una plataforma amplia para recopilar una amplia variedad de estadísticas sobre el código fuente y el proceso de desarrollo del proyecto, desde calcular la efectividad de los desarrolladores hasta identificar los costos de tiempo para las revisiones de código.
Compromiso inteligente
Clever-Commit es un analizador creado por Mozilla en colaboración con Ubisoft. Se basa en el estudio
CLEVER (combinación de niveles de prevención de errores y técnicas de resolución) de Ubisoft, y su asistente de compromiso basado en el producto, que identifica los compromisos sospechosos que probablemente contengan un error. Debido a que CLEVER se basa en la comparación de códigos, no solo indica un código peligroso, sino que también hace sugerencias sobre posibles correcciones. Según la descripción, en 60-70% de los casos Clever-Commit encuentra áreas problemáticas y con la misma frecuencia ofrece correcciones correctas para ellas. En general, hay poca información sobre este proyecto y sobre los errores que puede encontrar.
CodeGuru
Y más recientemente, la lista de analizadores que utilizan el aprendizaje automático se ha rellenado con un producto de Amazon llamado CodeGuru. Este servicio se basa en el aprendizaje automático, que le permite encontrar errores en el código, así como identificar secciones costosas en él. Hasta ahora, el análisis es solo para código Java, pero escriben sobre el soporte para otros idiomas en el futuro. Aunque se anunció recientemente, el CEO de AWS (Amazon Web Services), Andy Jassi, dice que lo ha estado utilizando durante mucho tiempo en la propia Amazonía.
El sitio dice que la capacitación se realizó en la base de código de Amazon, así como en más de 10,000 proyectos de código abierto.
En esencia, el servicio se divide en dos partes: CodeGuru Reviewer, capacitado buscando reglas asociativas y buscando errores en el código, y CodeGuru Profiler, que monitorea el rendimiento de la aplicación.
En general, no se ha publicado mucha información sobre este proyecto. El sitio dice que para aprender cómo detectar las desviaciones de las "mejores prácticas", Reviewer analiza las bases de código de Amazon y busca solicitudes de extracción que contengan llamadas de la API de AWS en ellas. Luego observa los cambios realizados y los compara con los datos de la documentación, que se analiza en paralelo. El resultado es un modelo de "mejores prácticas".
También se dice que las recomendaciones para el código personalizado mejoran después de recibir comentarios sobre las recomendaciones.
La lista de errores a la que responde Reviewer es bastante borrosa, ya que no se ha publicado documentación específica para errores:
- Mejores prácticas de AWS
- Concurrencia
- Fugas de recursos
- Fuga de información confidencial
- "Mejores prácticas" comunes para la codificación
Nuestro escepticismo
Ahora veamos el problema de encontrar errores a través de los ojos de nuestro equipo, que ha estado desarrollando analizadores estáticos durante muchos años. Vemos una serie de problemas de alto nivel de la aplicación de la capacitación, de los que queremos hablar. Pero al principio dividimos aproximadamente todos los enfoques de ML en dos tipos:
- Entrene manualmente un analizador estático para buscar varios problemas utilizando ejemplos de código sintético y real;
- Entrene los algoritmos en una gran cantidad de código fuente abierto (GitHub) y cambie el historial, después de lo cual el analizador comenzará a detectar errores e incluso sugerirá correcciones.
Hablaremos de cada dirección por separado, ya que tendrán varias deficiencias inherentes. Después de lo cual, creo, quedará claro para los lectores por qué no negamos la posibilidad de aprendizaje automático, sino que tampoco compartimos entusiasmo.
Nota Miramos desde la perspectiva del desarrollo de un analizador estático universal de uso general. Estamos enfocados en el desarrollo de un analizador que no se centre en una base de código específica, sino que cualquier equipo pueda usar en cualquier proyecto.
Manual de entrenamiento analizador estático
Supongamos que queremos usar ML para que el analizador comience a buscar anomalías de la siguiente forma en el código:
if (A == A)
Es extraño comparar una variable consigo misma. Podemos escribir muchos ejemplos de código correcto e incorrecto y capacitar al analizador para que busque dichos errores. Además, es posible agregar ejemplos reales de errores ya encontrados a las pruebas. La pregunta, por supuesto, es dónde obtener estos ejemplos. Pero consideraremos que es posible. Por ejemplo, hemos acumulado una serie de ejemplos de tales errores:
V501 ,
V3001 ,
V6001 .
Entonces, ¿es posible buscar tales defectos en el código utilizando algoritmos de aprendizaje automático? Usted puede ¡Pero no está claro por qué hacer esto!
Vea, para capacitar al analizador, debemos dedicar mucho esfuerzo a preparar ejemplos para la capacitación. O marque el código de aplicaciones reales, indicando dónde jurar y dónde no. En cualquier caso, habrá que hacer mucho trabajo, ya que debe haber miles de ejemplos de capacitación. O decenas de miles.
Después de todo, queremos buscar no solo casos (A == A), sino también:
- si (X && A == A)
- si (A + 1 == A + 1)
- si (A [i] == A [i])
- si ((A) == (A))
- Y así sucesivamente.
Ahora veamos cómo se implementaría un diagnóstico tan simple en PVS-Studio:
void RulePrototype_V501(VivaWalker &walker, const Ptree *left, const Ptree *right, const Ptree *operation) { if (SafeEq(operation, "==") && SafeEqual(left, right)) { walker.AddError(" , !", left, 501, Level_1, "CWE-571"); } }
Y eso es todo. ¡No se necesita una base de entrenamiento de muestra!
En el futuro, se debe enseñar a los diagnósticos a tener en cuenta una serie de excepciones y comprender que debe jurar en (A [0] == A [1-1]). Sin embargo, todo esto es muy fácil de programar. Pero solo con la base de ejemplos para el entrenamiento, todo será malo.
Tenga en cuenta que en ambos casos aún se requerirá un sistema de prueba, documentación escrita, etc. Sin embargo, el esfuerzo por crear un nuevo diagnóstico está claramente del lado del enfoque clásico, donde la regla simplemente está codificada en código.
Veamos ahora alguna otra regla. Por ejemplo, que se debe utilizar el resultado de algunas funciones. No tiene sentido llamarlos sin usar su resultado. Estas son algunas de estas características:
- malloc
- memcmp
- cadena :: vacío
En general, esto es lo que hacen los diagnósticos de
V530 implementados en PVS-Studio.
Por lo tanto, queremos buscar llamadas a tales funciones donde no se utiliza el resultado de su trabajo. Para hacer esto, puede generar muchas pruebas. Y creemos que todo funcionará bien. Pero nuevamente, no está claro por qué esto es necesario.
La implementación del diagnóstico V530 con todas las excepciones en el analizador PVS-Studio es de 258 líneas de código, de las cuales 64 líneas son comentarios. Además, hay una tabla con anotaciones de funciones, donde se observa que se debe usar su resultado. Reponer esta tabla es mucho más fácil que crear ejemplos sintéticos.
La situación será aún peor con diagnósticos que utilizan análisis de flujo de datos. Por ejemplo, el analizador PVS-Studio puede rastrear el valor de los punteros, lo que permite encontrar una pérdida de memoria:
uint32_t* BnNew() { uint32_t* result = new uint32_t[kBigIntSize]; memset(result, 0, kBigIntSize * sizeof(uint32_t)); return result; } std::string AndroidRSAPublicKey(crypto::RSAPrivateKey* key) { .... uint32_t* n = BnNew(); .... RSAPublicKey pkey; pkey.len = kRSANumWords; pkey.exponent = 65537;
Se toma un ejemplo del artículo "
Cromo: pérdidas de memoria ". Si
se cumple la condición
(pkey.n0inv == 0) , la función sale sin liberar el búfer, cuyo puntero se almacena en la variable
n .
Desde el punto de vista de PVS-Studio, no hay nada complicado. El analizador estudió la función
BnNew y recordó que devuelve un puntero a un bloque de memoria asignada. En otra función, notó que es posible una situación en la que el búfer no se libera, y el puntero hacia él se pierde cuando la función sale.
Un algoritmo de seguimiento de valor general funciona. No importa cómo se escriba el código. No importa qué más hay en la función que no esté relacionada con el trabajo con punteros. El algoritmo es universal y el diagnóstico V773 encuentra muchos errores en varios proyectos. ¡Vea cuán diferentes son los
fragmentos de código donde se detectan los errores!
No somos expertos en aprendizaje automático, pero parece que habrá grandes problemas. Hay una increíble cantidad de formas en que puede escribir código con pérdidas de memoria. Incluso si la máquina está entrenada para rastrear el valor de las variables, será necesario entrenarla para comprender que hay llamadas a funciones.
Existe la sospecha de que se necesitarán tantos ejemplos para la capacitación que la tarea se vuelve desalentadora. No decimos que sea irrealizable. Dudamos de que los costos de crear un analizador valgan la pena.
Analogia Una analogía viene a la mente con una calculadora, donde en lugar de diagnósticos es necesario programar operaciones aritméticas. Estamos seguros de que puede enseñarle a una calculadora basada en ML a sumar números bien introduciendo una base de conocimiento sobre el resultado de las operaciones 1 + 1 = 2, 1 + 2 = 3, 2 + 1 = 3, 100 + 200 = 300, y así sucesivamente. Como saben, la conveniencia de desarrollar una calculadora de este tipo es una gran pregunta (si no se asigna una subvención para ello :). Se puede escribir una calculadora mucho más simple, más rápida, más precisa y confiable utilizando la operación ordinaria "+" en el código.
Conclusión El método funcionará. Pero usarlo, en nuestra opinión, no tiene sentido práctico. El desarrollo llevará más tiempo y el resultado es menos confiable y preciso, especialmente si se trata de la implementación de diagnósticos complejos basados en el análisis del flujo de datos.
Aprendiendo de mucha fuente abierta
Bueno, descubrimos ejemplos sintéticos manuales, pero hay GitHub. Puede realizar un seguimiento del historial de confirmaciones y derivar patrones de cambios / correcciones de código. Luego puede señalar no solo secciones del código sospechoso, sino incluso sugerir una forma de solucionarlo.
Si se detiene en este nivel de detalle, entonces todo se ve bien. El diablo, como siempre, está en los detalles. Hablemos de estos detalles.
El primer matiz. Fuente de datosLas ediciones en GitHub son bastante caóticas y variadas. Las personas a menudo son demasiado flojas para realizar confirmaciones atómicas y hacer varios cambios al código a la vez. Usted mismo sabe cómo sucede: corrigieron el error y, al mismo tiempo, reestructuraron un poco ("Y aquí agregaré el procesamiento de tal caso al mismo tiempo ..."). Incluso entonces, puede no estar claro para una persona si estos cambios están relacionados entre sí o no.
El problema es cómo distinguir los errores reales de agregar nueva funcionalidad u otra cosa. Por supuesto, puede plantar 1,000 personas manualmente para marcar confirmaciones. La gente tendrá que indicar que corrigió el error aquí, refactorizando aquí, nueva funcionalidad aquí, requisitos modificados aquí, etc.
¿Es posible este marcado? Posible Pero preste atención a qué tan rápido ocurre el cambio. En lugar de "aprender el algoritmo en sí mismo sobre la base de GitHub", ya estamos discutiendo cómo confundir a cientos de personas durante mucho tiempo. Los costos laborales y el costo de crear una herramienta aumentan considerablemente.
Puede intentar identificar automáticamente dónde se solucionaron exactamente los errores. Para hacer esto, debe analizar los comentarios sobre las confirmaciones, prestar atención a las pequeñas ediciones locales, que, muy probablemente, son exactamente la revisión del error. Es difícil decir qué tan bien puede buscar automáticamente correcciones de errores. En cualquier caso, esta es una gran tarea que requiere investigación y programación por separado.
Entonces, todavía no hemos llegado a la capacitación, pero ya hay matices :).
El segundo matiz. Retraso en el desarrollo.Los analizadores que se entrenarán sobre la base de bases de datos como GitHub siempre estarán sujetos a un síndrome como "retraso mental". Esto se debe a que los lenguajes de programación cambian con el tiempo.
C # 8.0
introdujo los tipos de referencia anulables para ayudar a lidiar con las excepciones de referencia nula (NRE). JDK 12 presenta una nueva declaración de cambio (
JEP 325 ). En C ++ 17, se hizo posible ejecutar construcciones condicionales en la etapa de compilación (
constexpr si ). Y así sucesivamente.
Los lenguajes de programación están evolucionando. Además, como C ++ son muy rápidos y activos. En ellos aparecen nuevos diseños, se añaden nuevas funciones estándar, etc. Junto con las nuevas características, también aparecen nuevos patrones de error que también nos gustaría identificar mediante el análisis de código estático.
Y aquí el método de enseñanza en consideración tiene un problema: el patrón de error ya puede ser conocido, hay un deseo de identificarlo, pero no hay nada de qué aprender.
Veamos este problema con un ejemplo específico. El rango basado en el rango apareció en C ++ 11. Y puede escribir el siguiente código, iterando sobre todos los elementos en el contenedor:
std::vector<int> numbers; .... for (int num : numbers) foo(num);
El nuevo ciclo trajo consigo un nuevo patrón de error. Si el contenedor se cambia dentro del bucle, esto conducirá a la invalidación de los iteradores de "sombra".
Considere el siguiente código incorrecto:
for (int num : numbers) { numbers.push_back(num * 2); }
El compilador lo convertirá en algo como esto:
for (auto __begin = begin(numbers), __end = end(numbers); __begin != __end; ++__begin) { int num = *__begin; numbers.push_back(num * 2); }
Durante la operación
push_back , puede producirse la
invalidación de los iteradores
__begin y
__end si se produce una asignación de memoria dentro del vector. El resultado será un comportamiento indefinido del programa.
Por lo tanto, el patrón de error se conoce y describe desde hace tiempo en la literatura. El analizador PVS-Studio lo diagnostica utilizando los diagnósticos
V789 y ya ha encontrado
errores reales en proyectos abiertos.
¿Cuándo habrá suficiente código nuevo en GitHub para notar este patrón? Buena pregunta ... Debe comprender que si apareció el bucle for basado en rango, esto no significa que todos los programadores inmediatamente comenzaron a usarlo de forma masiva. Pueden pasar años antes de que aparezca mucho código utilizando un nuevo bucle. Además, deben cometerse muchos errores, y luego deben corregirse para que el algoritmo pueda notar el patrón en los cambios.
¿Cuántos años deberían pasar? Cinco? Diez?
Diez es demasiado, ¿y somos pesimistas? En absoluto Han pasado ocho años cuando se escribió este artículo, ya que el rango para loop apareció en C ++ 11. Pero hasta ahora, solo
tres casos de tal error se han escrito en nuestra base de datos. Tres errores no son mucho ni poco. No se debe sacar ninguna conclusión de su número. Lo principal es que puede confirmar que dicho patrón de error es real y tiene sentido detectarlo.
Ahora compare esta cantidad, por ejemplo, con este patrón de error: el
puntero se desreferencia antes de la verificación . En total, al verificar proyectos de código abierto, ya identificamos 1716 casos de este tipo.
¿Quizás no debería buscar errores de bucle basados en el rango? No Solo los programadores son inerciales, y este operador está ganando popularidad muy lentamente. Gradualmente, habrá una gran cantidad de código con su participación y, en consecuencia, también habrá más errores.
Lo más probable es que esto suceda solo después de 10-15 años desde el momento en que apareció C ++ 11. Y ahora una pregunta filosófica. ¿Ya conociendo el patrón de error, solo esperaremos muchos años hasta que se acumulen muchos errores en los proyectos abiertos?
Si la respuesta es "sí", entonces es posible diagnosticar razonablemente a todos los analizadores basados en ML el diagnóstico de "retraso mental".
Si la respuesta es no, ¿qué debo hacer? No hay ejemplos ¿Para escribirlos manualmente? Pero luego volvemos al capítulo anterior, donde consideramos escribirle a una persona muchos ejemplos para aprender.
Esto se puede hacer, pero nuevamente surge la cuestión de la conveniencia. La implementación del diagnóstico V789 con todas las excepciones en el analizador PVS-Studio es de solo 118 líneas de código, de las cuales 13 líneas son comentarios. Es decir Este es un diagnóstico muy simple que se puede tomar y programar fácilmente de una manera clásica.
Una situación similar será con cualquier otra innovación que aparezca en cualquier otro idioma. Como dicen, hay algo en qué pensar.
El tercer matiz. DocumentaciónUn componente importante de cualquier analizador estático es la documentación que describe cada diagnóstico. Sin él, usar el analizador será extremadamente difícil o incluso imposible. En la
documentación de PVS-Studio, tenemos una descripción de cada diagnóstico, que proporciona un ejemplo de un código erróneo y cómo solucionarlo. También hay un enlace a
CWE donde puede leer una descripción alternativa del problema. Y de todos modos, a veces algo es incomprensible para los usuarios y nos hacen preguntas aclaratorias.
En el caso de los analizadores estáticos, que se basan en algoritmos de aprendizaje automático, el problema de la documentación se oculta de alguna manera. Se supone que el analizador simplemente indica un lugar que le parece sospechoso y, tal vez, incluso sugiere cómo solucionarlo. La decisión de hacer un cambio o no permanece con la persona. Y aquí ... ejem ... No es fácil tomar una decisión, no poder leer, sobre la base de lo cual el analizador parece sospechar de uno u otro lugar en el código.
Por supuesto, en algunos casos todo será obvio. Supongamos que el analizador apunta a este código:
char *p = (char *)malloc(strlen(src + 1)); strcpy(p, src);
Y ofrecerá reemplazarlo con:
char *p = (char *)malloc(strlen(src) + 1); strcpy(p, src);
Está claro de inmediato que el programador selló y agregó 1 en el lugar equivocado. Como resultado, se asignará menos memoria.
Aquí, sin documentación, todo está claro. Sin embargo, este no siempre será el caso.
Imagine que el analizador señala silenciosamente este código:
char check(const uint8 *hash_stage2) { .... return memcmp(hash_stage2, hash_stage2_reassured, SHA1_HASH_SIZE); }
Y sugiere cambiar el tipo del valor de retorno de char a int:
int check(const uint8 *hash_stage2) { .... return memcmp(hash_stage2, hash_stage2_reassured, SHA1_HASH_SIZE); }
No hay documentación para la advertencia. Y aparentemente, el texto de la advertencia en sí, tal como lo entendemos, tampoco lo será, si estamos hablando de un analizador completamente independiente.
Que hacer Cual es la diferencia ¿Debo hacer tal reemplazo?
En principio, aquí puede arriesgarse y aceptar arreglar el código. Aunque acepta ediciones sin entenderlas, esta es una práctica
regular ... :) Puede ver la descripción de la función
memcmp y leer que la función devuelve valores
int : 0 que son mayores que cero y menores que cero. Pero de todos modos, puede que no esté claro por qué hacer cambios si el código ya funciona correctamente.
Ahora, si no sabe cuál es el objetivo de dicha edición, lea la descripción de los diagnósticos
V642 . De inmediato queda claro que esto es un verdadero error. Además, puede causar vulnerabilidad.
Quizás el ejemplo parecía poco convincente. Después de todo, el analizador propuso un código que probablemente sea mejor. Ok Veamos otro ejemplo de pseudocódigo, esta vez, para variar, en Java.
ObjectOutputStream out = new ObjectOutputStream(....); SerializedObject obj = new SerializedObject(); obj.state = 100; out.writeObject(obj); obj.state = 200; out.writeObject(obj); out.close();
Hay algún tipo de objeto. Está serializado. Luego, el estado del objeto cambia y se serializa nuevamente. Todo parece estar bien. Ahora imagine que al analizador, de repente, no le gusta este código, y sugiere reemplazarlo con:
ObjectOutputStream out = new ObjectOutputStream(....); SerializedObject obj = new SerializedObject(); obj.state = 100; out.writeObject(obj); obj = new SerializedObject();
En lugar de cambiar el objeto y volver a grabarlo, se crea un nuevo objeto y ya está serializado.
No hay una descripción del problema. Sin documentación El código se ha vuelto más largo. Por alguna razón, se agregó la creación de un nuevo objeto. ¿Estás listo para hacer tal edición en tu código?
Dirás que no está claro. De hecho, no está claro. Y será incomprensible todo el tiempo. Trabajar con un analizador "silencioso" será un estudio interminable en un intento por comprender por qué al analizador no le gusta algo.
Si hay documentación, entonces todo se vuelve transparente. La clase
java.io.ObjectOuputStream , que se utiliza para la serialización, almacena en caché los objetos que se pueden escribir. Esto significa que el mismo objeto no se serializará dos veces. Una vez que la clase serializa el objeto, y la segunda vez, simplemente escribe un enlace al mismo primer objeto en la secuencia. Leer más:
V6076 : la serialización recurrente utilizará el estado del objeto en caché desde la primera serialización.
Esperamos haber podido explicar la importancia de tener documentación. Y ahora la pregunta. ¿Cómo aparecerá la documentación para un analizador basado en ML?
Cuando se desarrolla un analizador de código clásico, todo es simple y claro. Hay un cierto patrón de errores. Lo describimos en la documentación e implementamos los diagnósticos.
En el caso de ML, lo contrario es cierto. Sí, el analizador puede notar una anomalía en el código y señalarlo. Pero él no sabe nada sobre la esencia del defecto. Él no entiende y no dirá por qué el código no se puede escribir así. Estas son abstracciones de alto nivel. Luego, el analizador también debe aprender a leer y
comprender la documentación de las funciones.
Como dije, dado que el tema de la documentación está cubierto en artículos sobre aprendizaje automático, no estamos listos para hablar más. Solo otro gran matiz que sacamos para su revisión.
Nota Se puede argumentar que la documentación es opcional. El analizador puede conducir a muchos ejemplos de soluciones en GitHub y la persona, observando los compromisos y comentarios sobre ellos, descubrirá qué es qué. Si lo es Pero la idea no parece atractiva. En lugar de un asistente, el analizador actúa como una herramienta que confundirá aún más al programador.
El cuarto matiz. Idiomas altamente especializados.El enfoque descrito no es aplicable para lenguajes altamente especializados para los que el análisis estático también puede ser extremadamente útil. La razón es que GitHub y otras fuentes simplemente no tienen una base de código fuente lo suficientemente grande como para proporcionar una capacitación efectiva.
Considere esto con un ejemplo específico. Para comenzar, vaya a GitHub y busque repositorios para el popular lenguaje Java.
Resultado: lenguaje: "Java":
3.128.884 resultados de repositorio disponibles
Ahora tomemos el lenguaje especializado "1C Enterprise" utilizado en aplicaciones de contabilidad emitidas por la compañía rusa
1C .
Resultado: idioma: "1C Enterprise":
551 resultados de repositorio disponibles
¿Quizás no se necesitan analizadores para este idioma? Son necesarios Existe una necesidad práctica para el análisis de tales programas, y los analizadores correspondientes ya existen. Por ejemplo, hay un complemento SonarQube 1C (BSL) fabricado por
Silver Bullet .
, - , .
. C, C++, #include .
, ML, , Java, JavaScript, Python. . C C++ - , .
, /, , C C++ . «» .
c/cpp- . , GitHub, - cpp- . , ML.
, . GitHub . , . , . , .cpp- .
. . . , , .
.
. :
bool Class::IsMagicWord() { return m_name == "ML"; }
:
bool Class::IsMagicWord() { return strcmp(m_name, "ML") == 0; }
,
(x == «y») strcmp(x, «y»)?
, ,
m_name . , , :
class Class { .... char *m_name; }; class Class { .... std::string m_name; };
, . , (
std::string ).
, , .h . , . , C C++.
- , , , C C++.
, . , , . , cpp-.
. (, , ). , . , , .
, GitHub . , , . - . - , . . « ». , , .cpp (.i) . .
, , , . , . . , - , , .
, . . C C++ , GitHub, . , .
Nota , . GitHub C++ , .cpp . :).
, C C++ .
. ., .
V789 , Range-based for loop. , , . , , , . , :
std::vector<int> numbers; .... for (int num : numbers) { if (num < 5) { numbers.push_back(0); break;
, . . PVS-Studio 26 .
, . , , , .
, . , , , ML. Es decir .
. ., . (, WinAPI, ..).
C,
strcmp , . GitHub, available code results:
- strcmp — 40,462,158
- stricmp — 1,256,053
, . , , , :
- , . .
- , NULL. .
- , . .
- Y así sucesivamente.
? No « ». « » . Top50 . , , , 100 , . , , , . , - Amazon.com , 130 « ».
. , . , :
- g_ascii_strncasecmp — 35,695
- lstrcmpiA — 27,512
- _wcsicmp_l — 5,737
- _strnicmp_l — 5,848
- _mbscmp_l — 2,458
- etc.
, , . . . , , . « ».
PVS-Studio . , C ++ 7200 . :
- WinAPI
- C,
- (STL),
- glibc (GNU C Library)
- Qt
- MFC
- zlib
- libpng
- OpenSSL
- etc.
, . . , .
. ML? , .
, , ML, . , . ,
strcmp malloc .
C . , . , , , .
,
_fread_nolock . , ,
fread . . , . , . :
int buffer[10]; size_t n = _fread_nolock(buffer, size_of(int), 100, stream);
PVS-Studio:
C_"size_t _fread_nolock" "(void * _DstBuf, size_t _ElementSize, size_t _Count, FILE * _File);" ADD(HAVE_STATE | RET_SKIP | F_MODIFY_PTR_1, nullptr, nullptr, "_fread_nolock", POINTER_1, BYTE_COUNT, COUNT, POINTER_2). Add_Read(from_2_3, to_return, buf_1). Add_DataSafetyStatusRelations(0, 3);
, , , , . , write-only . . .
ML. GitHub . 15000 . . :
#define fread_unlocked _fread_nolock
?
- . .
- , , . , , . .
- , , . , . ML :). .
, ML .
, ML, , , , . , , , .
. , , WinAPI. , , ? , Google, ,
. , .
_fread_nolock , . , , C++. , 20.
, . ,
memmove . - :
void *memmove (void *dest, const void *src, size_t len) { return __builtin___memmove_chk(dest, src, len, __builtin_object_size(dest, 0)); }
__builtin___memmove_chk ? intrinsic , . .
memmove - :
. , - .
Ok, . , . , ML , , .
. . . , , . , AI? , AI , . , . , 20 .
, , . . , .
- . , , . , - . . C++ auto_ptr . unique_ptr .
- . , C C++ , . , . , . , long Windows 32/64 32 . Linux 32/64 . , . -. , , . , ( ). , .
- . ML, , , . Es decir , — , , . , . , , — , . . , / , , , . . : " PVS-Studio: ". , , .
Conclusiones
, , . ML , , ( ) . , , ML .
, , ML. , , .
, ML . , . ML «» .
, , , , .
ML, .
PS
, - ,
ML, .
, . PVS-Studio. ML. , . , , , if- :). , :).
, -, .
. "
PVS-Studio ".

, : Andrey Karpov, Victoria Khanieva.
Machine Learning in Static Analysis of Program Source Code .