Encuentre código muerto en un programa C mediante análisis de tiempo de ejecución

Tengo una aplicación grande escrita en C + POSIX, hay muchas funciones que nunca se llaman dentro de ella. Sin embargo, debido al tamaño del código, es difícil rastrearlos manualmente.
Algunas personas sugirieron usar gcc con -Wunusedy lto, pero no devolvió ninguna función utilizada, mientras que sigo encontrando y eliminando algunas manualmente.

Así que creo que necesito una herramienta de cobertura de código para analizar el programa en tiempo de ejecución. Peoples me sugirió gcov o valgrind, pero no pude encontrar cómo usarlos para imprimir una lista de funciones muertas. gcov solo reveló que solo se usa el 68% de las funciones compiladas, pero no tengo ninguna forma de enumerarlas.

Entonces, ¿alguien conoce una buena herramienta, y si es así, dígame exactamente cómo puedo usarla para ese propósito (un ejemplo de línea de comando sería bienvenido) ?

Eliminé todas las funciones que no se usan en el código fuente. Solo funciones como esta permanecen dentro del código fuente:

if(conditional statement) {
    some stuff;
    dead_function();
    some_stuff;
}

Where conditional statementnunca es verdadero en tiempo de ejecución, y la eliminación de dead_function()conduciría a la eliminación de la declaración para evitar errores indefinidos.

Entonces, lo que está buscando no son funciones no utilizadas ( dead_function se usa), sino código muerto. ¡Eso requiere técnicas completamente diferentes! Tenga en cuenta que el análisis de tiempo de ejecución solo encontrará código que no se haya ejecutado en una ejecución particular del programa; ese código puede estar vivo en diferentes circunstancias.
@Gilles: ese es un poco el problema de gcov... ¿Cómo combinar el resultado de diferentes lanzamientos? Tengo para el parámetro. Dicho esto, el programa solo hace una cosa: encontrar primos muy grandes, por lo que es fácil probar todos los casos. Esta es también la razón por la que trato de eliminar el código muerto (crítico para el rendimiento) .
Parece que no desea eliminar la función inactiva, sino eliminar el código condicional inactivo. ¿Tengo ese derecho?
@IraBaxter: No, porque actualmente no me importan las declaraciones no utilizadas dentro de las funciones. Solo quiero eliminar las funciones que son completamente inútiles (como las involucradas en el manejo de flotadores) .
Tenga en cuenta que el código que nunca se llama tiene muy poco impacto en el rendimiento aparte del tiempo de carga y la huella de memoria. Para ayudar con ambos, mueva la mayor cantidad de su código posible a muchas DLL pequeñas o bibliotecas compartidas .so: aquellas que nunca se usan realmente nunca se cargarán. Si tiene problemas graves de rendimiento, es probable que tenga un código al que se llama, pero los resultados nunca se usan; lint puede ayudarlo a encontrarlos.
Si bien estoy de acuerdo en que es desagradable tener código sin usar por ahí (incluido el código que está comentado), puede estar tranquilo sabiendo que el enlazador debería poder encargarse de esto por usted. Si encuentra una herramienta, asegúrese de que maneje los punteros de función correctamente. Por ejemplo, en mi línea de trabajo, nos basamos en en.wikipedia.org/wiki/Finite-state_machine que son controlados por en.wikipedia.org/wiki/Branch_table Para que nuestras funciones más importantes nunca se llamen directamente, solo indirectamente. Lo mismo podría ocurrir con las devoluciones de llamada.
@Mawg: el enlazador solo puede hacer esto si no hay llamadas de otras funciones. Pero en mi caso las llamadas existen pero nunca se usan cuando se ejecuta el programa. Reconozco que esto está relacionado con la eliminación de código condicional inactivo.
@SteveBarnes: el código que nunca se llama para un programa pequeño tiene un impacto debido a los cachés de la CPU. El uso de bibliotecas compartidas solo funcionaría para funciones que no tienen llamadas vinculadas dentro del programa, no para aquellas que nunca se llaman en tiempo de ejecución. Lo mismo con la solución del enlazador.
La única solución "gratuita" o casi gratuita que conozco para el caso de uso en el que la función es "utilizada" por el código de nivel superior pero nunca llamada en realidad es a) Escribir algunas pruebas que esté seguro cubren todos los casos de uso entonces b) en cada función agregue printf(__func__);como la primera línea, en gcc esto imprimirá el nombre de la función cada vez que se llame; ejecute su prueba capturando la salida, luego no se llamará cualquier nombre que no esté en la salida.
@SteveBarnes: Había muchas funciones que deseaba poder hacer automáticamente como gcov ya lo hace. Puede ser que olvidé una opción para enumerarlos, pero no puedo encontrarla.
Use su archivo de mapa para obtener una lista de todas las funciones que están en el código y gcov para enumerar todas las que se llaman, reste eso de la primera lista y tendrá una lista de funciones que no se llamaron. Un script de python corto o un poco de magia awk deberían hacer el truco.
Por supuesto, si usa gcovr, gcovr.com/guide.html , debería poder identificar rápidamente las líneas de código que nunca se ejecutan.
@SteveBarnes: seguí las guías incorrectamente y no puedo encontrar la opción relevante que permita enumerar las funciones que no forman parte del 68% utilizado en la salida de gcov.
@ user2284570: vea mi nueva respuesta.
No importa qué solución, habrá problemas si se usan tablas de salto (punteros de función)
@Mawg No uso tablas de salto directamente. Tal vez el compilador pueda generar algunos automáticamente, pero no los uso.
No muchos lo hacen. Tendemos a usarlos en gran medida incrustados, para el acoplamiento estado/par. Tenga cuidado también con las funciones de devolución de llamada.

Respuestas (3)

Dado que @user2284570 se encuentra en la cómoda situación de cubrir el 100 % de los casos de uso a través de pruebas, el análisis de código dinámico proporcionará la respuesta. En otros casos de destitución de funciones, sus convocatorias y condiciones requerirían una revisión minuciosa.

Cualquier herramienta de cobertura de código informará la cobertura de funciones de una forma u otra. La queja principal parece ser el informe de la ubicación exacta de las funciones no utilizadas (aquí: muertas). No puedo hablar por otras herramientas, pero nuestra empresa tiene una opción de informe de formato de texto que presenta marcadores de posición para el nombre del archivo de origen y los datos de línea. Dado que se solicitó un ejemplo de línea de comando concreto, aquí hay uno:

$ csgcc -o myapp mycode.c
$ ./myapp --run-all-tests
$ cmcsexeimport -m myapp.csmes -e myapp.csexe --title=mytests
$ cmreport --function-coverage -m myapp.csmes --format-unexecuted='%f:%l'

Esto imprimirá las ubicaciones de funciones muertas como:

mycode.c:101
mycode.c:213
mycode.c:1032

Un estudiante universitario local redactó instrucciones más detalladas para este enfoque.

Después de eliminar las funciones no utilizadas, también querrá analizar la cobertura de sucursales y eliminar las declaraciones if() superfluas y otras. Solo tenga cuidado con los efectos secundarios de las expresiones evaluadas. Pero afortunadamente, su cobertura de código perfecta detectará regresiones.

Debo decir que terminé usando la depuración manual paso a paso desde ese momento para eliminar el código muerto. Así que no probaré tu herramienta.

Puede usar splintcon la allusebandera para verificar funciones no utilizadas, pero personalmente lo usaría doxygenpara producir un mapa de llamadas : cualquier función que no tenga padres probablemente no se use, solo busque cualquier función que esté en las tablas de funciones que no se pueden llamar directamente pero cosas como las máquinas de estado pueden invocarse desde el índice de la tabla.

Doxygen es una herramienta invaluable para manejar grandes bases de código y vale la pena aprender a usarla en cualquier caso, es gratis y está disponible para múltiples plataformas, también tiende a fomentar la documentación de su código sobre la marcha.

En el caso de un código que se llama pero solo desde un código inalcanzable, tendrá que usar una herramienta de análisis estático completo como LDRA (costosa), que le indicará el código inalcanzable. En este caso, es mejor eliminar primero todo el código inalcanzable y luego buscar funciones no llamadas. Alternativamente, necesitará un conjunto de pruebas que esté seguro de que ejerce el 100% de la funcionalidad; luego, puede usar un generador de perfiles o una herramienta de cobertura como gcov en su programa mientras ejecuta su conjunto de pruebas. Si su prueba ha ejercido toda su funcionalidad y tiene partes con 0% de cobertura, entonces no se llaman , pero luego tendrá que encontrar las llamadas que no se pueden alcanzar y eliminar ese código para que el enlazador no se queje de todos modos.

" Puedes usar splint con el indicador alluse " Todavía no entiendo... ¿puedes proporcionar un ejemplo de línea de comando? Para doxygen, la respuesta es ¡No! Acabo de terminar de eliminar todas las funciones que no se han utilizado en el código. Solo quedan aquellos que se usan en el código pero donde algunas declaraciones condicionales hacen que nunca se llamen en tiempo de ejecución. y un mapa de llamadas no puede ayudar con cientos de funciones. Definitivamente necesito una salida legible con funciones no llamadas resueltas y con los nombres de archivo y números de línea del área en la que se declaran.
Steel no hay un ejemplo detallado... Así que no entiendo qué puedo hacer...
@ user2284570 Esta respuesta es correcta para la pregunta tal como la publicó originalmente. ¡No debe cambiar las preguntas de una manera que invalide las respuestas! Sería mejor si revirtiera su edición "Actualizar" a la pregunta y en su lugar hiciera una nueva pregunta.
Tenga cuidado, por supuesto, con las funciones a las que llama el puntero de función, como las devoluciones de llamada y las tablas de salto de estado/evento.
@Mawg no había tales funciones.

Para responder a su pregunta revisada, si compila todo su código con gcc -fprofile-arcs -ftest-coverageopciones establecidas y luego ejecuta un conjunto de pruebas que está seguro de que cubre toda la funcionalidad y todas las posibilidades (posiblemente en varias ejecuciones).

La gcovutilidad espera que usted haga parte del trabajo; no tiene simplemente una opción para "dime qué no se llamó", por lo que tendrá que encontrar esas funciones que no se llamaron.

Puede usargcov con la opción en cada archivo de origen , --function-summariesgenerará un conjunto de archivos de salida que incluirán resúmenes de funciones : busque cualquiera de los que incluyan nevero 0%para encontrar las funciones no ejecutadas.

Sugeriría agregar una función que sepa que nunca se llamará o conocer una que aún no eliminó; esto le permitirá ver cómo se ve la salida; luego puede usarla greppara encontrarlos todos.

Su próximo paso será usar grepo algo similar para ver la gcovsalida de todos los lugares donde esas funciones están presentes en su código; debería ver recuentos de ejecución de 0 para toda la rama que contiene la llamada , esto le dará un buen punto de partida. para extender sus pruebas, para casos de uso que se había perdido , o para eliminar código.

Entonces, no puede hacer exactamente lo que necesito: ordenar la función no utilizada y decir las líneas a lo largo de las rutas de los archivos. Es lo mismo que hice con kcachegrind antes de publicar esta pregunta. Ordenar la función manualmente es y greping para encontrarlos está bien, pero no lo es si tiene mucho.
¡Supongo que tendrás que contratar a alguien que pueda codificar para hacerlo entonces! Las herramientas no harán mucho por usted, de lo contrario sería redundante.
No necesito algo para la eliminación automática. Necesito algo que pueda enumerar funciones no utilizadas en tiempo de ejecución sin funciones utilizadas en esa lista. También necesito su ruta de declaración. También estoy haciendo esto por mí mismo. Así que espere que no escriba mi propia respuesta durante algunos años.
Steel no puede clasificar automáticamente las funciones no utilizadas de las llamadas... Mientras tanto, me di cuenta de los requisitos informáticos para generar números primos aleatorios seguros que se ajusten a 65Ko.